diff --git a/.gitignore b/.gitignore index 4327703..d34ad54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ node_modules/ dist/ -audit.jsonl -.claude-cli-session +.claude/ diff --git a/README.md b/README.md index 41bc4e1..a9902f6 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ Proof of concept — functional and actively used for development. - Home/End, Ctrl+Home/End cursor navigation - Waiting indicator with elapsed time during SDK calls - Session resumption (`/session` to view, `/session ` to switch) -- Auto-resume — persists session ID to `.claude-cli-session`, automatically resumes on restart +- Auto-resume — persists session ID to `.claude/cli-session`, automatically resumes on restart - Session portability — sessions created by the official CLI or VS Code extension can be resumed (same cwd required) - `/quit` / `/exit` - Auto-approve edits — Edit/Write tools for files inside `cwd` are auto-approved, files outside prompt for confirmation - Coloured diff display — Edit tool calls show a unified diff instead of raw JSON - Permission queue with timeout — concurrent tool permission requests are queued, 5 minute timeout per prompt -- Audit log — all SDK events written to `audit.jsonl` for debugging (`tail -f audit.jsonl` in another tmux pane) +- Audit log — all SDK events written to `.claude/audit.jsonl` for debugging (`tail -f .claude/audit.jsonl` in another tmux pane) - Cost/turns/duration display on result messages ### Terminal Setup @@ -85,7 +85,7 @@ The SDK emits `stream_event` messages containing Anthropic API streaming events #### Audit Log as Context Transfer -The audit log (`audit.jsonl`) is a complete record of all SDK events — assistant messages, tool calls, tool results, system events. This makes it useful beyond debugging: +The audit log (`.claude/audit.jsonl`) is a complete record of all SDK events — assistant messages, tool calls, tool results, system events. This makes it useful beyond debugging: - Paste relevant entries to another Claude for instant context on what happened - Replay/review sessions after the fact @@ -303,7 +303,7 @@ Sessions are keyed by `sessionId + cwd`. A session created from one directory ca - **Raw stdin** — no TUI framework, plain escape sequence rendering - **`@anthropic-ai/claude-agent-sdk`** — session management, tool orchestration, compaction - **`@anthropic-ai/claude-code`** — provides the claude executable -- **`audit.jsonl`** — all SDK events logged for debugging, viewable via `tail -f` in a separate pane +- **`.claude/audit.jsonl`** — all SDK events logged for debugging, viewable via `tail -f` in a separate pane - **`config.ts`** — in-memory config with `autoApproveEdits` (to be backed by a file later) ## Development diff --git a/src/files.ts b/src/files.ts new file mode 100644 index 0000000..352248e --- /dev/null +++ b/src/files.ts @@ -0,0 +1,36 @@ +import { accessSync, mkdirSync, writeFileSync, constants } from 'node:fs'; +import { resolve } from 'node:path'; + +export interface CliPaths { + claudeDir: string; + auditFile: string; + sessionFile: string; +} + +export function initFiles(): CliPaths { + const claudeDir = resolve(process.cwd(), '.claude'); + const auditFile = resolve(claudeDir, 'audit.jsonl'); + const sessionFile = resolve(claudeDir, 'cli-session'); + + try { + mkdirSync(claudeDir, { recursive: true }); + } catch (err) { + console.error(`FATAL: Cannot create directory ${claudeDir}: ${err}`); + process.exit(1); + } + + // Ensure audit file exists and is writable + try { + try { + accessSync(auditFile, constants.W_OK); + } catch { + writeFileSync(auditFile, ''); + } + accessSync(auditFile, constants.W_OK); + } catch (err) { + console.error(`FATAL: Cannot write to audit log at ${auditFile}: ${err}`); + process.exit(1); + } + + return { claudeDir, auditFile, sessionFile }; +} diff --git a/src/index.ts b/src/index.ts index 60b68b9..e3ad427 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,8 +21,8 @@ import { type EditorState, } from './editor.js'; import { existsSync, readFileSync, writeFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { initAudit, writeAuditEntry } from './audit.js'; +import { initFiles } from './files.js'; import { getConfig, isInsideCwd, isSafeBashCommand } from './config.js'; import { formatDiff } from './diff.js'; import { parseKey } from './input.js'; @@ -30,15 +30,15 @@ import { render, createRenderState, type RenderState } from './renderer.js'; import { QuerySession } from './session.js'; import { Terminal } from './terminal.js'; -const SESSION_FILE = resolve(process.cwd(), '.claude-cli-session'); +let sessionFile = ''; function loadSession(log: (msg: string) => void): string | undefined { - if (!existsSync(SESSION_FILE)) { - log(`No session file found at ${SESSION_FILE}`); + if (!existsSync(sessionFile)) { + log(`No session file found at ${sessionFile}`); return undefined; } try { - const content = readFileSync(SESSION_FILE, 'utf8').trim(); + const content = readFileSync(sessionFile, 'utf8').trim(); if (!content) { log('Session file exists but is empty'); return undefined; @@ -52,7 +52,7 @@ function loadSession(log: (msg: string) => void): string | undefined { } function saveSession(id: string): void { - writeFileSync(SESSION_FILE, id); + writeFileSync(sessionFile, id); } const term = new Terminal(); @@ -524,12 +524,14 @@ function cleanup(): void { } function start(): void { + const paths = initFiles(); const auditPath = initAudit(); + sessionFile = paths.sessionFile; term.info('claude-cli v0.0.3'); term.info(`cwd: ${process.cwd()}`); term.info(`audit: ${auditPath}`); - term.info(`session file: ${SESSION_FILE}`); + term.info(`session file: ${paths.sessionFile}`); const savedSession = loadSession((msg) => term.info(msg)); if (savedSession) {