open-claude-p 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,850 @@
1
+ **English** · [한국어](README.ko.md) · [中文](README.zh.md) · [日本語](README.ja.md)
2
+
3
+ [![npm](https://img.shields.io/npm/v/open-claude-p.svg)](https://www.npmjs.com/package/open-claude-p)
4
+ [![downloads](https://img.shields.io/npm/dm/open-claude-p.svg)](https://www.npmjs.com/package/open-claude-p)
5
+ [![stars](https://img.shields.io/github/stars/empty-user77/open-claude-p.svg)](https://github.com/empty-user77/open-claude-p/stargazers)
6
+ [![License](https://img.shields.io/npm/l/open-claude-p.svg)](https://github.com/empty-user77/open-claude-p/blob/main/LICENSE)
7
+ [![Sponsor](https://img.shields.io/badge/Sponsor-%E2%9D%A4-ec1c8a?logo=github)](https://github.com/sponsors/empty-user77)
8
+
9
+ ---
10
+
11
+ # open-claude-p (ocp)
12
+
13
+ A PTY-based compatibility layer that drives the interactive `claude` CLI via **node-pty**, providing the same functionality as `claude -p` (headless print mode) in environments where it is unavailable.
14
+
15
+ > **Key difference**: `claude -p` is Claude Code's non-interactive mode that operates through an internal API, but it is not available on certain plans/environments. `open-claude-p` runs the actual TUI client through a PTY and parses the output stream to achieve the same result.
16
+
17
+ ---
18
+
19
+ ## Table of Contents
20
+
21
+ - [Installation](#installation)
22
+ - [Recommended one-time setup](#recommended-one-time-setup)
23
+ - [CLI Usage](#cli-usage) — full reference in [docs/cli-reference.md](./docs/cli-reference.md)
24
+ - [Daemon (Session Persistence)](#daemon-session-persistence)
25
+ - [SDK Usage](#sdk-usage) — full reference in [docs/sdk-reference.md](./docs/sdk-reference.md)
26
+ - [High-level: createChatClient](#high-level-createchatclient)
27
+ - [Low-level: createDriver](#low-level-createdriver)
28
+ - [Session Management](#session-management)
29
+ - [Working with JSONL Session Files](#working-with-jsonl-session-files)
30
+ - [About Output Parsing](#about-output-parsing)
31
+ - [Environment Variables](#environment-variables)
32
+ - [Full Option Reference](#full-option-reference)
33
+ - [Sample App](#sample-app)
34
+
35
+ ---
36
+
37
+ ## Installation
38
+
39
+ ### npm
40
+
41
+ ```bash
42
+ # Install as a project dependency
43
+ npm install open-claude-p
44
+
45
+ # Or install globally to use the `ocp` CLI anywhere
46
+ npm install -g open-claude-p
47
+ ```
48
+
49
+ ### From source (development)
50
+
51
+ ```bash
52
+ # Clone and symlink from the project root
53
+ git clone https://github.com/empty-user77/open-claude-p.git
54
+ cd open-claude-p
55
+ npm link
56
+
57
+ # Or install via a local path from another project
58
+ npm install /path/to/open-claude-p
59
+ ```
60
+
61
+ **Prerequisite**: The `claude` CLI must be installed and available on `PATH`.
62
+
63
+ ```bash
64
+ # Verify Claude Code CLI installation
65
+ claude --version
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Recommended one-time setup
71
+
72
+ `ocp` is non-interactive automation — set these once in `~/.zshrc` /
73
+ `~/.bashrc` so prompts that need WebSearch / Bash / file tools "just
74
+ work" without prompts you cannot answer:
75
+
76
+ ```bash
77
+ export OCP_AUTO_ACCEPT_TRUST=1 # auto-accept "do you trust this folder?" on first use
78
+ export OCP_DEFAULT_SKIP_PERMS=1 # default --dangerously-skip-permissions for the CLI
79
+ ```
80
+
81
+ Without `OCP_DEFAULT_SKIP_PERMS`, claude declines tool use with
82
+ "I don't have access to that tool" (the permission prompt would need a
83
+ human to click "Yes").
84
+
85
+ > ⚠️ With `OCP_DEFAULT_SKIP_PERMS=1`, every `ocp "…"` invocation can
86
+ > execute Bash, Write, Edit, and other tools on whatever prompt you
87
+ > feed it, with no per-tool confirmation. Use this on a personal
88
+ > workstation. **Never** set it in CI, in a shared shell, or in a
89
+ > project `.envrc` you might clone from an untrusted source.
90
+
91
+ See [docs/cli-reference.md](./docs/cli-reference.md) for the full env var list.
92
+
93
+ ---
94
+
95
+ ## CLI Usage
96
+
97
+ The package installs a single binary: **`ocp`**.
98
+
99
+ When stderr is an interactive terminal, ocp shows a live spinner while
100
+ working and prints a one-line meta footer after the response:
101
+
102
+ ```
103
+ ⏱ 42.8s · ↑41.2K ↓864 tok · $0.0287 · 🔧 Web Search, ToolSearch, WebSearch
104
+ ```
105
+
106
+ Hide it with `--no-meta` / `OCP_NO_META=1`. In pipe / redirect mode
107
+ both spinner and meta are suppressed automatically.
108
+
109
+ ```bash
110
+ # Basic usage
111
+ ocp "Hello"
112
+
113
+ # Supports the same argv format as claude -p (-p flag is ignored for compatibility)
114
+ ocp -p "Hello"
115
+
116
+ # Read prompt from stdin
117
+ echo "What is the weather in Seoul?" | ocp
118
+
119
+ # Specify output format
120
+ ocp --output-format json "Answer in one word: apple"
121
+ ocp --output-format stream-json "Hi"
122
+
123
+ # Specify model
124
+ ocp --model sonnet "Complex question..."
125
+ ocp --model claude-opus-4-7 "Architecture review..."
126
+
127
+ # Append system prompt
128
+ ocp --append-system-prompt "Always reply in English" "what's the weather?"
129
+
130
+ # Resume a session — sessionId is printed to stderr
131
+ SID=$(ocp "Say only kiwi" 2>&1 >/dev/null | grep sessionId | grep -oE '[0-9a-f-]{36}')
132
+ ocp --resume "$SID" "What did you just say?"
133
+
134
+ # Or automatically continue the most recent session
135
+ ocp --continue "What did you just say?"
136
+
137
+ # Skip permission checks (for automation)
138
+ ocp --dangerously-skip-permissions "Read and analyze the file"
139
+ ```
140
+
141
+ ### Output Formats
142
+
143
+ #### `text` (default)
144
+
145
+ ```
146
+ Hello! How can I help you today?
147
+ ```
148
+
149
+ #### `json`
150
+
151
+ ```json
152
+ {
153
+ "result": "Hello! How can I help you today?",
154
+ "session_id": "a1b2c3d4-...",
155
+ "is_error": false,
156
+ "cost_usd": null,
157
+ "duration_ms": 4200,
158
+ "num_turns": 1
159
+ }
160
+ ```
161
+
162
+ #### `stream-json` (NDJSON)
163
+
164
+ Response is streamed line by line:
165
+
166
+ ```jsonl
167
+ {"type":"system","subtype":"init","session_id":"a1b2c3d4-...","tools":[],"mcp_servers":[]}
168
+ {"type":"assistant","session_id":"a1b2c3d4-...","message":{"role":"assistant","content":[{"type":"text","text":"Hello!"}]}}
169
+ {"type":"result","subtype":"success","session_id":"a1b2c3d4-...","is_error":false,"duration_ms":4200}
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Daemon (Session Persistence)
175
+
176
+ The `ocp` CLI keeps PTY sessions alive through a **background daemon** by default.
177
+ This means repeated calls in the same directory skip the 2.5-second warmup wait, and conversation context is automatically preserved.
178
+
179
+ ```
180
+ ocp "First question" → starts a new daemon if none exists, reuses if one does
181
+ ocp "Second question" → connects to the same daemon, context is preserved
182
+ ```
183
+
184
+ Daemon sockets are stored under `~/.ocp/`, **one per working directory**.
185
+
186
+ ### Disabling the Daemon
187
+
188
+ ```bash
189
+ OCP_NO_DAEMON=1 ocp "Run just once" # spawns a PTY directly, no daemon
190
+ ```
191
+
192
+ When you should skip the daemon:
193
+ - When using `--resume`, `--continue`, or `--fork-session` flags (automatically switches to direct mode)
194
+ - When using `--input-format=stream-json`
195
+ - For single isolated runs in CI/CD environments
196
+
197
+ ### Daemon Environment Variables
198
+
199
+ | Variable | Description | Default |
200
+ |----------|-------------|---------|
201
+ | `OCP_NO_DAEMON` | Set to `1` to disable the daemon | — |
202
+ | `OCP_DAEMON_IDLE_MS` | Auto-terminate daemon after this much idle time | `600000` (10 min) |
203
+ | `OCP_MAX_DAEMONS` | Maximum number of daemons to keep alive simultaneously | `30` |
204
+ | `OCP_DAEMON_MAX_PENDING` | Max queued requests per daemon socket; over → `busy` reply | `16` |
205
+ | `OCP_DAEMON_MAX_REQ_BYTES` | Per-request body cap | `4194304` (4 MiB) |
206
+ | `OCP_DAEMON_SOCKET_TIMEOUT_MS` | Idle timeout for a single daemon socket | `30000` |
207
+ | `OCP_DAEMON_SOCKET_MAX_LIFETIME_MS` | Hard cap on a single socket's total lifetime (slow-loris guard) | `300000` (5 min) |
208
+ | `OCP_DAEMON_MAX_RESPONSE_BYTES` | Client-side cap on bytes buffered from a single daemon reply | `67108864` (64 MiB) |
209
+ | `OCP_DAEMON_MAX_PARALLEL` | Max warm PTYs the daemon keeps for concurrent fresh requests | `8` |
210
+ | `OCP_NO_DEFAULT_PROMPT` | Set to `1` to suppress the CLI's default tool-use system prompt | — |
211
+ | `OCP_NO_DEFAULT_TOOLS` | Set to `1` to NOT pre-approve `WebSearch`/`WebFetch` | — |
212
+ | `OCP_DIR` | Override the daemon state directory | `~/.ocp` |
213
+
214
+ ### Driver / Pool Environment Variables
215
+
216
+ | Variable | Description | Default |
217
+ |----------|-------------|---------|
218
+ | `OCP_CLAUDE_BIN` | Path to the upstream `claude` binary | `claude` (PATH lookup) |
219
+ | `OCP_POOL_SIZE` | Default `poolSize` for `createDriver` | `0` (off) |
220
+ | `OCP_POOL_MAX_AGE_MS` | Max age of a pooled PTY before forced refresh | `600000` |
221
+ | `OCP_WARMUP_MS` / `OCP_REUSE_WARMUP_MS` | Per-spawn / reuse warmup wait | tuned defaults |
222
+ | `OCP_IDLE_MS` / `OCP_PRE_IDLE_MS` | Completion idle thresholds (post-sentinel / silence-only) | `1500` / `8000` |
223
+ | `OCP_FIRST_RESPONSE_MS` | Max wait for first byte after prompt submit | `120000` |
224
+ | `OCP_MAX_RESPONSE_MS` | Per-turn hard ceiling in ms. Default is intentionally long because in-flight idle / pre-idle silence detectors already abort genuinely-stuck runs much earlier. | `86400000` (24 h) |
225
+ | `OCP_TRUST_SETTLE_MS` | Pause after dismissing the trust prompt | tuned |
226
+ | `OCP_PROMPT_BOX_WAIT_MS` / `OCP_PROMPT_BOX_SETTLE_MS` | Wait for prompt box / settle after type | tuned |
227
+ | `OCP_AUTO_ACCEPT_TRUST` | Auto-accept the "trust this folder" dialog | `0` |
228
+ | `OCP_DEFAULT_SKIP_PERMS` | Auto-pass `--dangerously-skip-permissions` | `0` |
229
+ | `OCP_NO_LIVE` | Suppress live spinner / progress in CLI | `0` |
230
+ | `OCP_NO_META` | Suppress the post-turn meta line in CLI | `0` |
231
+ | `OCP_NO_WARN` | Mute non-fatal driver warnings to stderr | `0` |
232
+ | `OCP_DEBUG` | Verbose debug logging (path takes precedence over flag) | `0` |
233
+ | `OCP_PRINT_MODE` | Force print-mode codepath | `0` |
234
+ | `OCP_END` | Sentinel string the driver appends to detect completion | internal |
235
+ | `OCP_ALLOW_UNSAFE_ARGV` | **Security-sensitive.** Disable argv sanitizer (control-char / `--` stripping). Daemon refuses this var; CLI prints a warning. Only set for trusted, ephemeral invocations. | `0` |
236
+
237
+ ### Chat SDK Environment Variables
238
+
239
+ | Variable | Description | Default |
240
+ |----------|-------------|---------|
241
+ | `OCP_MAX_CONVERSATIONS` | Hard cap on conversations kept in the JSON store | `500` |
242
+ | `OCP_MAX_MESSAGES_PER_CONV` | Hard cap on messages per conversation | `500` |
243
+ | `OCP_MAX_MESSAGE_CHARS` | Reject `chat.send` messages larger than this | `262144` (256 KiB) |
244
+ | `OCP_MAX_JSONL_BYTES` | Tail-read cap for `~/.claude/projects/.../*.jsonl` | `8388608` (8 MiB) |
245
+ | `OCP_MAX_STDIN_BYTES` | CLI stdin payload cap | `262144` |
246
+ | `OCP_LOCK_SWEEP_AGE_MS` | Drop sentinel files older than this from the lock dir | `86400000` (24 h) |
247
+
248
+ ---
249
+
250
+ ## SDK Usage
251
+
252
+ Two layers ship in the same `open-claude-p` package — pick whichever
253
+ matches your altitude:
254
+
255
+ | Layer | Import | For |
256
+ |-------|--------|-----|
257
+ | **High-level chat client** | `open-claude-p/chat` | Build a chat UI / app. Brings conversation persistence, skill loading, JSONL clean-text, cost calc. |
258
+ | **Low-level driver** | `open-claude-p` | One-shot PTY runs with full control. |
259
+
260
+ Full API: [docs/sdk-reference.md](./docs/sdk-reference.md).
261
+
262
+ ### High-level: `createChatClient`
263
+
264
+ The same SDK the bundled sample chat server uses. One import gives you
265
+ conversation state, multi-turn `--resume`, skill loading, JSONL clean
266
+ markdown, and per-turn cost / token / tool tracking.
267
+
268
+ ```js
269
+ import { createChatClient } from 'open-claude-p/chat';
270
+
271
+ const chat = createChatClient({
272
+ // dbPath: './conversations.json', // default: <cwd>/conversations.json
273
+ // skillsDir: '~/.claude/skills', // default: ~/.claude/skills
274
+ dangerouslySkipPermissions: true, // let claude actually use its tools
275
+ // appendSystemPrompt: 'extra rules', // appended on top of SDK base default
276
+ // // (null to opt the base out entirely)
277
+ });
278
+
279
+ // New conversation
280
+ const r1 = await chat.send({
281
+ message: 'What is the weather in Seoul right now?',
282
+ onEvent(ev) {
283
+ if (ev.type === 'spinner') process.stderr.write(`[…] ${ev.label}\n`);
284
+ if (ev.type === 'assistant-text') process.stdout.write(ev.text);
285
+ },
286
+ });
287
+ console.log(r1.text); // clean markdown
288
+ console.log(r1.meta); // { elapsedMs, inputTokens, outputTokens, costUsd, tools }
289
+
290
+ // Continue the same conversation
291
+ const r2 = await chat.send({
292
+ conversationId: r1.conversationId,
293
+ message: 'And tomorrow?',
294
+ });
295
+
296
+ // CRUD
297
+ await chat.listConversations(); // [{ id, title, ... }]
298
+ await chat.getConversation(r1.conversationId);
299
+ await chat.deleteConversation(r1.conversationId);
300
+ await chat.listSkills(); // [{ name, description }]
301
+
302
+ await chat.close();
303
+ ```
304
+
305
+ Also exported from the same module: `readSessionText` (read JSONL clean
306
+ markdown), `cleanResponse` (TUI chrome strip), `cleanSpinnerLabel`,
307
+ `isAssistantTextNoise`, `extractToolName`. See the
308
+ [SDK reference](./docs/sdk-reference.md#standalone-helpers-also-exported-from-open-claude-pchat).
309
+
310
+ ### Low-level: `createDriver`
311
+
312
+ For one-shot PTY runs with no conversation state or higher-level helpers.
313
+
314
+ ### `createDriver(opts?)`
315
+
316
+ Creates a driver. Share a single driver instance across your entire application.
317
+
318
+ ```js
319
+ import { createDriver } from 'open-claude-p';
320
+
321
+ const driver = createDriver({
322
+ claudeBin: 'claude', // path to claude binary (default: claude on PATH)
323
+ warmupMs: 2500, // PTY initialization wait time (ms)
324
+ reuseWarmupMs: 200, // wait time when reusing from pool (ms)
325
+ idleMs: 1500, // silence wait after response completes (ms)
326
+ preIdleMs: 8000, // minimum wait before sentinel matching (ms)
327
+ maxResponseMs: 86_400_000, // hard timeout (ms), default 24 h (idleMs/preIdleMs abort earlier)
328
+ poolSize: 0, // PTY pool size (0=disabled, N>0=keep N warmed up)
329
+ poolMaxAgeMs: 600_000, // maximum pool session lifetime (ms)
330
+ cwd: process.cwd(), // working directory
331
+ env: {}, // additional environment variables
332
+ debug: false, // print debug logs to stderr
333
+ });
334
+ ```
335
+
336
+ ### `runOneShot(req)`
337
+
338
+ Sends a single prompt to Claude and waits for the response.
339
+
340
+ ```js
341
+ const result = await driver.runOneShot({
342
+ prompt: 'What is the current weather in Seoul?',
343
+
344
+ // ── Model / Behavior ──────────────────────────
345
+ model: 'sonnet', // model name
346
+ effort: 'high', // 'low' | 'medium' | 'high' | 'max'
347
+ thinking: 'adaptive', // 'enabled' | 'adaptive' | 'disabled'
348
+ maxTurns: 5, // max agent turns (shim-enforced)
349
+
350
+ // ── System Prompt ─────────────────────────────
351
+ systemPrompt: 'You are a weather expert', // replace entire system prompt
352
+ appendSystemPrompt: 'Always reply in English', // append to default prompt
353
+
354
+ // ── Permissions / Tools ───────────────────────
355
+ dangerouslySkipPermissions: true, // skip permission checks
356
+ allowedTools: ['WebSearch', 'Read'], // tool whitelist
357
+ disallowedTools: ['Bash'], // tool blacklist
358
+
359
+ // ── Session ───────────────────────────────────
360
+ resume: 'a1b2c3d4-...', // resume from previous session UUID
361
+ continue: false, // continue most recent session
362
+ forkSession: false, // create a new session ID on resume
363
+
364
+ // ── Working Directory ─────────────────────────
365
+ cwd: '/path/to/project',
366
+
367
+ // ── Cancellation ──────────────────────────────
368
+ abortSignal: controller.signal,
369
+
370
+ // ── Real-time Event Callback ──────────────────
371
+ onEvent(ev) {
372
+ // called in real time as the response is generated
373
+ // see "Event Types" section below
374
+ if (ev.type === 'assistant-text') {
375
+ process.stdout.write(ev.text);
376
+ }
377
+ },
378
+ });
379
+ ```
380
+
381
+ ### Return Value: OneShotResult
382
+
383
+ When `runOneShot()` resolves, it returns an object with the following structure:
384
+
385
+ ```ts
386
+ {
387
+ // ── Core Result ────────────────────────────────────────────────────
388
+ text: string,
389
+ // Claude's final response text (TUI artifacts removed).
390
+ // Raw text as Claude produced it — markdown, HTML, code blocks, etc.
391
+ // Rendering/parsing is the caller's responsibility.
392
+
393
+ sessionId: string | null,
394
+ // Claude session UUID for this request.
395
+ // Use with --resume <sessionId> to continue the conversation.
396
+ // Falls back to filesystem scan of ~/.claude/projects/ if banner capture fails.
397
+
398
+ isError: boolean,
399
+ // true = completed with error or timeout
400
+
401
+ completionReason: string,
402
+ // How the request completed:
403
+ // 'sentinel' normal completion (sentinel string detected)
404
+ // 'idle' silence timeout after response
405
+ // 'prompt-box' TUI input box reappeared
406
+ // 'timeout' maxResponseMs exceeded
407
+ // 'max-turns' maxTurns limit reached
408
+ // 'upstream-exited' claude process exited first
409
+ // 'write-failed' PTY write failed
410
+ // 'cancelled' cancelled via AbortSignal
411
+
412
+ exitCode: number,
413
+ // 0 = success, 1 = error
414
+
415
+ // ── Event Array ───────────────────────────────────────────────────
416
+ events: Array<object>,
417
+ // All events produced by the pipeline (same objects as onEvent callback).
418
+ // See "Event Types" section below.
419
+
420
+ // ── Performance Metrics ───────────────────────────────────────────
421
+ durationMs: number,
422
+ // Total elapsed time (ms)
423
+
424
+ cost: { totalUsd: number | null, numTurns: number | null },
425
+ // Currently null (cost info is not directly available from PTY).
426
+ // For accurate token/cost data, read the JSONL session file (see below).
427
+
428
+ diagnostics: { rawBytes: number, strippedBytes: number },
429
+ // Raw bytes received from PTY / bytes after ANSI stripping
430
+ }
431
+ ```
432
+
433
+ ### Event Types (onEvent callback)
434
+
435
+ The `onEvent` callback and `result.events` array contain events of the following types:
436
+
437
+ ```ts
438
+ // When Claude starts responding (⏺ marker detected)
439
+ { type: 'assistant-region-entered', n: number }
440
+
441
+ // When the response region closes (hr or sentinel detected)
442
+ { type: 'assistant-region-exited', n: number }
443
+
444
+ // One line of response text (real-time streaming)
445
+ {
446
+ type: 'assistant-text',
447
+ text: string, // one line of text (raw markdown)
448
+ region: number // which response region (higher = later; useful when resuming)
449
+ }
450
+
451
+ // Claude session UUID detected (from banner or exit message)
452
+ { type: 'session-id', id: string }
453
+
454
+ // TUI spinner (indicates Claude is working)
455
+ // label: "Searching the web...", "Reading file...", "Cogitated for 25s", etc.
456
+ { type: 'spinner', label: string }
457
+
458
+ // TUI input box appeared on screen (one of the completion signals)
459
+ { type: 'prompt-box-shown' }
460
+
461
+ // Sentinel string detected (normal completion)
462
+ { type: 'sentinel' }
463
+ ```
464
+
465
+ #### Event Usage Example
466
+
467
+ ```js
468
+ const result = await driver.runOneShot({
469
+ prompt: 'Analyze this long document',
470
+ onEvent(ev) {
471
+ switch (ev.type) {
472
+ case 'assistant-text':
473
+ // real-time streaming — print line by line
474
+ process.stdout.write(ev.text + '\n');
475
+ break;
476
+
477
+ case 'spinner':
478
+ // spinner label — shown while a tool is in use (e.g. "Searching the web...")
479
+ process.stderr.write(`\r⏳ ${ev.label} `);
480
+ break;
481
+
482
+ case 'session-id':
483
+ // save session ID early so you can resume even if a timeout occurs
484
+ saveSessionId(ev.id);
485
+ break;
486
+ }
487
+ },
488
+ });
489
+
490
+ // Full text can also be reconstructed from events
491
+ const lines = result.events
492
+ .filter(e => e.type === 'assistant-text' && e.region === Math.max(...result.events.filter(e => e.type === 'assistant-text').map(e => e.region)))
493
+ .map(e => e.text);
494
+ ```
495
+
496
+ ---
497
+
498
+ ## Session Management
499
+
500
+ Claude identifies each session with a UUID, which you can use to resume previous conversations.
501
+
502
+ ```js
503
+ // 1. First request — start a new session
504
+ const result1 = await driver.runOneShot({
505
+ prompt: 'Implement Fibonacci in Python',
506
+ });
507
+ console.log('Session ID:', result1.sessionId);
508
+ // → "a1b2c3d4-5678-..."
509
+
510
+ // 2. Resume session — previous conversation context is preserved
511
+ const result2 = await driver.runOneShot({
512
+ prompt: 'Now rewrite that without recursion using iteration',
513
+ resume: result1.sessionId,
514
+ });
515
+
516
+ // 3. Fork session — explore a different direction while preserving the original
517
+ const result3 = await driver.runOneShot({
518
+ prompt: 'Instead, make a generator version',
519
+ resume: result1.sessionId,
520
+ forkSession: true, // assigns a new UUID, original session is preserved
521
+ });
522
+ ```
523
+
524
+ ---
525
+
526
+ ## Working with JSONL Session Files
527
+
528
+ The Claude CLI saves each session as a JSONL file at:
529
+
530
+ ```
531
+ ~/.claude/projects/<cwd-encoded-as-path>/<session-uuid>.jsonl
532
+ ```
533
+
534
+ For example, if `cwd` is `/Users/alice/myproject`:
535
+ → `~/.claude/projects/-Users-alice-myproject/<uuid>.jsonl`
536
+
537
+ These files contain **token usage, cost, and tool usage metadata** that is not available in the PTY output.
538
+
539
+ ```js
540
+ import { readFile } from 'node:fs/promises';
541
+ import path from 'node:path';
542
+ import os from 'node:os';
543
+
544
+ async function readSessionMeta(sessionId, cwd = process.cwd()) {
545
+ const key = path.resolve(cwd).replace(/\//g, '-');
546
+ const filePath = path.join(os.homedir(), '.claude', 'projects', key, `${sessionId}.jsonl`);
547
+ const lines = (await readFile(filePath, 'utf8')).split('\n').filter(Boolean);
548
+
549
+ // Extract usage from the last assistant message
550
+ for (let i = lines.length - 1; i >= 0; i--) {
551
+ try {
552
+ const ev = JSON.parse(lines[i]);
553
+ if (ev.message?.role === 'assistant') {
554
+ const textBlock = ev.message.content?.find(c => c.type === 'text');
555
+ return {
556
+ text: textBlock?.text, // clean markdown text (no TUI artifacts)
557
+ usage: ev.message.usage, // { input_tokens, output_tokens, cache_read_input_tokens, ... }
558
+ timestamp: ev.timestamp,
559
+ };
560
+ }
561
+ } catch {}
562
+ }
563
+ return null;
564
+ }
565
+
566
+ const meta = await readSessionMeta(result.sessionId);
567
+ // meta.usage.input_tokens → input tokens
568
+ // meta.usage.output_tokens → output tokens
569
+ // meta.usage.cache_read_input_tokens → cache read tokens
570
+ // meta.usage.server_tool_use.web_search_requests → web search count
571
+ ```
572
+
573
+ ### What You Can Get from JSONL
574
+
575
+ | Item | PTY result.text | JSONL |
576
+ |------|----------------|-------|
577
+ | Response text | ✅ (may have TUI artifacts) | ✅ (clean markdown) |
578
+ | Input token count | ❌ | ✅ |
579
+ | Output token count | ❌ | ✅ |
580
+ | Cache token count | ❌ | ✅ |
581
+ | Cost calculation | ❌ | ✅ (tokens × unit price) |
582
+ | Web search count | ❌ | ✅ |
583
+ | Timestamps | ❌ | ✅ |
584
+ | Tool usage details | Partial (events) | ✅ |
585
+
586
+ ---
587
+
588
+ ## About Output Parsing
589
+
590
+ **`result.text` is the raw markdown/text produced by Claude.**
591
+ It is an open format — rendering, parsing, and display are your responsibility.
592
+
593
+ ```
594
+ result.text example:
595
+ ─────────────────────────────────────
596
+ # Fibonacci Sequence
597
+
598
+ Here is how to implement Fibonacci in Python:
599
+
600
+ ```python
601
+ def fib(n):
602
+ a, b = 0, 1
603
+ for _ in range(n):
604
+ a, b = b, a + b
605
+ return a
606
+ ```
607
+
608
+ - Time complexity: O(n)
609
+ - Space complexity: O(1)
610
+ ─────────────────────────────────────
611
+ ```
612
+
613
+ ### Parsing Implementation Reference
614
+
615
+ The `renderMarkdown()` function in `sample/public/app.js` is a parsing example for web UIs.
616
+ Implement your own based on your target environment:
617
+
618
+ ```js
619
+ // Web UI → HTML rendering (example)
620
+ import { marked } from 'marked';
621
+ const html = marked.parse(result.text);
622
+
623
+ // Terminal → ANSI color rendering (example)
624
+ import { renderMarkdown } from 'cli-markdown';
625
+ console.log(renderMarkdown(result.text));
626
+
627
+ // Pass to another LLM → use as-is
628
+ const nextPrompt = `Previous response: ${result.text}\n\nNow proceed to the next step`;
629
+ ```
630
+
631
+ ### On TUI Artifacts
632
+
633
+ `result.text` has TUI rendering residue removed as much as possible by ocp, but it may not be perfect.
634
+ If you need cleaner text, reading from the **JSONL session file** is recommended (see above).
635
+
636
+ ---
637
+
638
+ ## Environment Variables
639
+
640
+ Most-used:
641
+
642
+ | Variable | What |
643
+ |----------|------|
644
+ | `OCP_AUTO_ACCEPT_TRUST=1` | Auto-accept first-use "Do you trust this folder?" dialog |
645
+ | `OCP_DEFAULT_SKIP_PERMS=1` | Default `--dangerously-skip-permissions` for the CLI (tools just work) |
646
+ | `OCP_NO_LIVE=1` | Disable the live spinner on stderr |
647
+ | `OCP_NO_META=1` | Hide the trailing meta footer (`⏱ … · 🔧 …`) |
648
+ | `OCP_NO_DAEMON=1` | Fresh PTY for every call (no warm daemon) |
649
+ | `OCP_MAX_RESPONSE_MS` | Hard timeout in ms, default `86400000` (24 h) |
650
+ | `OCP_FIRST_RESPONSE_MS` | Fail-fast if no progress within N ms after prompt, default `20000` |
651
+ | `OCP_PROMPT_BOX_WAIT_MS` | Max wait for the input chevron, default `15000` (raise for heavy hook/MCP loading) |
652
+ | `OCP_CLAUDE_BIN` | Path to upstream `claude` binary, default `'claude'` |
653
+
654
+ Full table (timeouts, pool, daemon, sanitizer escape hatch, etc.):
655
+ [docs/cli-reference.md#environment-variables](./docs/cli-reference.md#environment-variables).
656
+
657
+ ```bash
658
+ # Tighten per-turn timeout to 10 minutes (default is 24 h)
659
+ OCP_MAX_RESPONSE_MS=600000 ocp "Complex task..."
660
+
661
+ # Single run without daemon
662
+ OCP_NO_DAEMON=1 ocp "Run just once"
663
+ ```
664
+
665
+ ---
666
+
667
+ ## Full Option Reference
668
+
669
+ Quick mapping between `runOneShot(req)` request fields and CLI flags
670
+ appears below; the canonical, fully-described table is in
671
+ [docs/cli-reference.md#options](./docs/cli-reference.md#options).
672
+
673
+ | req field | CLI flag | Type | Description |
674
+ |-----------|----------|------|-------------|
675
+ | `model` | `--model` | string | Model name (e.g. `sonnet`, `claude-sonnet-4-6`) |
676
+ | `systemPrompt` | `--system-prompt` | string | Replace entire system prompt |
677
+ | `appendSystemPrompt` | `--append-system-prompt` | string | Append to default system prompt |
678
+ | `dangerouslySkipPermissions` | `--dangerously-skip-permissions` | boolean | Skip permission checks |
679
+ | `allowedTools` | `--allowed-tools` | string[] | Tool whitelist |
680
+ | `disallowedTools` | `--disallowed-tools` | string[] | Tool blacklist |
681
+ | `resume` | `--resume` / `-r` | string | Resume from session UUID |
682
+ | `continue` | `--continue` / `-c` | boolean | Continue most recent session |
683
+ | `forkSession` | `--fork-session` | boolean | Create new session ID on resume |
684
+ | `sessionId` | `--session-id` | string | Assign a specific UUID to the new session |
685
+ | `noSessionPersistence` | `--no-session-persistence` | boolean | Disable session saving |
686
+ | `effort` | `--effort` | enum | `low` \| `medium` \| `high` \| `max` |
687
+ | `thinking` | `--thinking` | enum | `enabled` \| `adaptive` \| `disabled` |
688
+ | `maxTurns` | `--max-turns` | number | Maximum agent turns |
689
+ | `fallbackModel` | `--fallback-model` | string | Fallback when primary model is overloaded |
690
+ | `permissionMode` | `--permission-mode` | string | `default` \| `plan` \| `acceptEdits` \| `bypassPermissions` |
691
+ | `mcpConfig` | `--mcp-config` | string[] | MCP config paths |
692
+ | `addDir` | `--add-dir` | string[] | Additional directories tools can access |
693
+ | `bare` | `--bare` | boolean | Minimal mode (disables hooks, LSP, plugins, etc.) |
694
+ | `debug` | `--debug` | boolean | Print debug logs to stderr |
695
+ | `verbose` | `--verbose` | boolean | Verbose output |
696
+ | `cwd` | `--cwd` | string | PTY process working directory |
697
+ | `abortSignal` | — | AbortSignal | Cancellation signal |
698
+ | `onEvent` | — | function | Real-time event callback |
699
+ | `passThroughArgv` | — | string[] | Additional argv passed directly to claude |
700
+
701
+ ---
702
+
703
+ ## Sample App
704
+
705
+ The `sample/` directory contains a web-based chat UI built with ocp.
706
+
707
+ ### Running
708
+
709
+ ```bash
710
+ cd sample
711
+ node server.js
712
+ # → http://localhost:3000
713
+ ```
714
+
715
+ ### Sample App Structure
716
+
717
+ ```
718
+ sample/
719
+ server.js Express server — wraps ocp driver, SSE streaming
720
+ data/
721
+ conversations.json Conversation history (auto-generated)
722
+ public/
723
+ index.html Chat UI
724
+ app.js Client-side JavaScript
725
+ style.css Stylesheet
726
+ ```
727
+
728
+ ### Sample Server API
729
+
730
+ | Endpoint | Method | Description |
731
+ |----------|--------|-------------|
732
+ | `/api/conversations` | GET | List conversations |
733
+ | `/api/conversations/:id` | GET | Conversation detail (all messages) |
734
+ | `/api/conversations/:id` | DELETE | Delete conversation |
735
+ | `/api/chat` | POST | Send message (SSE streaming) |
736
+ | `/api/monitor` | GET | PTY event monitor (SSE) |
737
+ | `/api/skills` | GET | Skill list from `~/.claude/skills/` |
738
+ | `/api/processes` | GET | In-flight request list (`id`, `prompt`, `elapsedMs`) |
739
+ | `/api/processes/:id` | DELETE | Abort a specific request (`all` to abort all) |
740
+
741
+ ### `/api/chat` SSE Events
742
+
743
+ Chat requests (`POST /api/chat`) stream responses as Server-Sent Events:
744
+
745
+ ```js
746
+ // Client request
747
+ const resp = await fetch('/api/chat', {
748
+ method: 'POST',
749
+ headers: { 'Content-Type': 'application/json' },
750
+ body: JSON.stringify({
751
+ message: 'What is the weather in Seoul?',
752
+ conversationId: null, // null to start a new conversation
753
+ skillName: 'my-skill', // optional: skill name from ~/.claude/skills/
754
+ }),
755
+ });
756
+
757
+ // SSE event types
758
+ { type: 'spinner', label: 'Searching the web...' } // working status
759
+ { type: 'text', text: 'Hello...' } // streaming text (chunk)
760
+ { type: 'error', error: 'error message' } // error
761
+ {
762
+ type: 'done',
763
+ conversationId: 'uuid', // conversation ID (saved)
764
+ text: 'full final response', // complete final text (clean markdown from JSONL)
765
+ isNew: true, // whether this is a new conversation
766
+ meta: {
767
+ elapsedMs: 4200, // elapsed time (ms)
768
+ inputTokens: 1500, // input tokens (including cache)
769
+ outputTokens: 320, // output tokens
770
+ costUsd: 0.0042, // cost (USD)
771
+ tools: ['WebSearch'], // tools used
772
+ }
773
+ }
774
+ ```
775
+
776
+ ### Markdown Parsing in the Sample
777
+
778
+ The sample app (`sample/public/app.js`) converts `result.text` to HTML via a `renderMarkdown()` function.
779
+
780
+ **This parsing code is sample-only.** For real projects, use:
781
+ - Web: `marked`, `markdown-it`, etc.
782
+ - Terminal: `cli-markdown`, `terminal-link`, etc.
783
+ - React: `react-markdown`
784
+ - LLM input: use as-is
785
+
786
+ ### Process Manager (`ocp-ps`)
787
+
788
+ The sample app ships a CLI tool that uses the `/api/processes` API to list and cancel in-flight requests.
789
+
790
+ ```bash
791
+ cd sample
792
+
793
+ node ocp-ps.js # list in-flight requests
794
+ node ocp-ps.js kill <id> # abort a specific request
795
+ node ocp-ps.js kill all # abort all
796
+ node ocp-ps.js watch # auto-refresh every second
797
+ ```
798
+
799
+ > **Note**: `ocp-ps` is a sample implementation that uses the sample app's HTTP API (`/api/processes`).
800
+ > When building your own server with the ocp library, you can implement the same process management pattern.
801
+
802
+ ### Skill Invocation (`/skillname`)
803
+
804
+ Typing `/` in the chat input shows a dropdown of skills from `~/.claude/skills/`.
805
+
806
+ ```
807
+ User input: /my-skill Analyze this PRD and find related repos
808
+
809
+ Server: injects SKILL.md content as appendSystemPrompt
810
+
811
+ Claude: executes following skill instructions
812
+ ```
813
+
814
+ ---
815
+
816
+ ## Module Structure
817
+
818
+ ```
819
+ src/
820
+ index.js Library public API (createDriver, runOneShot)
821
+ options/
822
+ spec.js All option definitions (single source of truth)
823
+ parse-argv.js CLI argv parser
824
+ validate.js Cross-option validation
825
+ parsers/
826
+ ansi-strip.js ANSI escape removal
827
+ tui-frame.js TUI frame parser (event generation)
828
+ sentinel.js Completion sentinel detection
829
+ pipeline.js Parser pipeline composition
830
+ output/
831
+ text.js --output-format text adapter
832
+ json.js --output-format json adapter
833
+ stream-json.js --output-format stream-json adapter
834
+ pty/
835
+ session.js Single PTY session lifecycle
836
+ pool.js Warmed-up PTY pool
837
+ completion/
838
+ detector.js Completion detection (sentinel + idle + prompt-box)
839
+ bin/
840
+ cli.js ocp CLI entry point
841
+ sample/
842
+ server.js Example web server
843
+ public/ Chat UI
844
+ ```
845
+
846
+ ---
847
+
848
+ ## License
849
+
850
+ MIT