open-claude-p 1.1.0 → 1.1.1

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/CHANGELOG.md CHANGED
@@ -7,6 +7,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.1.1] — 2026-05-19
11
+
12
+ Post-1.1.0 fixes driven by Cosmica integration testing. Several of
13
+ these are essentially the 1.1.0 release "actually working" — the
14
+ 1.1.0 surface was correct on paper but a handful of subtle parser
15
+ and default-value bugs kept the new behaviour from landing in
16
+ practice.
17
+
18
+ ### Fixed
19
+
20
+ - **Trust dialog auto-accept regressed silently for two distinct
21
+ reasons.**
22
+ - The driver's `❯` chevron pattern matched both the real input
23
+ box and the trust dialog's "currently-selected" row
24
+ (`❯ 1. Yes, I trust this folder`). `prompt-box-shown` fired
25
+ on the dialog row, the await chain proceeded to the prompt
26
+ write, and the trailing `\r` confirmed whichever option was
27
+ highlighted — silently dropping the user message. Pattern is
28
+ now `/^[❯›❮‹](?:$|\s(?!\d+\.\s))/` and a write-time guard
29
+ refuses to send when `dialogState` is in a blocked state.
30
+ - The trust dialog text is rendered with cursor-positioning
31
+ escapes between words rather than real space bytes; after the
32
+ ANSI strip the buffer reads `Quicksafetycheck` (no spaces) and
33
+ the `/Quick safety check/i` pattern never matched. Result: the
34
+ dialog rendered in ~250 ms but the watcher never fired, the
35
+ driver fell back to writing the prompt after
36
+ `OCP_PROMPT_BOX_WAIT_MS`, and the trailing CR confirmed "Yes"
37
+ in the still-open dialog. The watcher now strips ANSI before
38
+ pattern-matching and the patterns accept `\s*` between every
39
+ word.
40
+ - **`OCP_PROMPT_BOX_WAIT_MS` default 6 s → 30 s.** Environments
41
+ with many MCP servers or plugins need 8–25 s before the input
42
+ box renders. 6 s was tripping the timeout before claude had
43
+ even shown the trust dialog. Short-circuited the moment
44
+ `prompt-box-shown` fires, so the common-case latency is
45
+ unchanged.
46
+ - **`dangerouslySkipPermissions` is now ON by default across
47
+ EVERY surface** — not just the CLI. The 1.1.0 commit only
48
+ flipped the CLI's default and left `createDriver().runOneShot()`
49
+ and `createChatClient()` on the pre-1.1 `false`, so anything
50
+ using the SDK directly (the bundled sample chat, third-party
51
+ wrappers) still hung on the first tool call. Sample server's
52
+ redundant env-gate is dropped accordingly.
53
+ - **End-of-reply marker no longer flagged as
54
+ "prompt-injection".** The 1.0/1.1 instruction text used the
55
+ word "token": `Append the literal token ⟦OCP_END:xxx⟧ ...`.
56
+ The model's safety classifier treated "include this token in
57
+ your reply" as an access-token / API-key exfiltration attempt
58
+ and sometimes appended a `prompt injection attempt detected`
59
+ warning to the user-facing reply. Reworded to "end-of-reply
60
+ marker" and framed as `ocp wrapper's plumbing` so claude
61
+ recognises it as harness infrastructure rather than hostile
62
+ injection.
63
+
64
+ ### Changed
65
+
66
+ - **postinstall skips the shebang rewrite when running inside a
67
+ git checkout** (detected by the presence of `.git/` next to
68
+ `package.json`). The rewrite is meant for end-user installs from
69
+ the npm tarball; in a dev `git clone + npm link` setup it
70
+ otherwise overwrote `#!/usr/bin/env node` with the developer's
71
+ absolute node path on every `npm install` / `npm test`, showing
72
+ up as a noisy working-tree diff. Consumer installs unaffected.
73
+
74
+ ---
75
+
10
76
  ## [1.1.0] — 2026-05-18
11
77
 
12
78
  `ocp` 1.1 reshapes the CLI defaults around the assumption that
@@ -64,12 +130,18 @@ extraction path so PTY chrome can no longer leak into responses.
64
130
 
65
131
  ### Changed
66
132
 
67
- - **`--dangerously-skip-permissions` is now the CLI default.**
68
- Rationale: `ocp` is a non-interactive PTY automation surface. A
69
- permission prompt that wants a human y/n hangs forever — every
70
- Bash / Edit / Write / MCP call breaks without this flag. Library
71
- callers (`createDriver` / `createChatClient`) keep the
72
- conservative `false` default; only the `ocp` binary flips.
133
+ - **`--dangerously-skip-permissions` / `dangerouslySkipPermissions`
134
+ is now the default ON across EVERY surface** — `ocp` CLI,
135
+ `createDriver().runOneShot()`, `createChatClient()`, and the
136
+ `sample/` server. Rationale: `ocp` is a PTY automation library; an
137
+ interactive permission prompt that wants a human y/n hangs forever
138
+ every Bash / Edit / Write / MCP / WebSearch call breaks without
139
+ this flag. The pre-1.1 library default of `false` was the cause
140
+ of nearly every "the SDK silently hangs on tool use" report.
141
+ Opt out per surface:
142
+ - CLI: `OCP_NO_SKIP_PERMS=1` (env)
143
+ - `createChatClient({ dangerouslySkipPermissions: false })`
144
+ - `runOneShot({ dangerouslySkipPermissions: false })`
73
145
  Pre-1.1 users with `OCP_DEFAULT_SKIP_PERMS=1` exported see no
74
146
  change; the env is now a no-op.
75
147
  - **Folder-trust dialog is auto-accepted by default.** Same
@@ -183,5 +255,6 @@ Initial release.
183
255
  `OCP_NO_DEFAULT_PROMPT=1`).
184
256
  - Localised READMEs: Korean, Japanese, Chinese.
185
257
 
258
+ [1.1.1]: https://github.com/empty-user77/open-claude-p/releases/tag/v1.1.1
186
259
  [1.1.0]: https://github.com/empty-user77/open-claude-p/releases/tag/v1.1.0
187
260
  [1.0.0]: https://github.com/empty-user77/open-claude-p/releases/tag/v1.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-claude-p",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "A PTY-backed compatibility shim for `claude -p` (Claude Code headless/print mode). Drives the interactive `claude` CLI via node-pty and exposes the same option surface and stream-json contract. Ships as both a library and an `ocp` CLI binary.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -79,6 +79,23 @@ if (!fixedAny) {
79
79
  // and the file extension makes shebang lines a no-op.
80
80
  if (process.platform !== 'win32') {
81
81
  const here = path.dirname(fileURLToPath(import.meta.url));
82
+ const repoRoot = path.resolve(here, '..');
83
+ // Skip the shebang rewrite when this install is a git checkout
84
+ // (dev environment via `git clone + npm link`, or a contributor
85
+ // running `npm install` inside the repo). The rewrite would
86
+ // otherwise overwrite the portable `#!/usr/bin/env node` with the
87
+ // developer's absolute node path, which then shows up as a noisy
88
+ // working-tree diff on every commit and risks getting committed
89
+ // by mistake. Consumers installing from the npm tarball never
90
+ // have a `.git/` so they still get the GUI-launch fix.
91
+ try {
92
+ const st = await lstat(path.join(repoRoot, '.git'));
93
+ if (st.isDirectory() || st.isFile()) {
94
+ process.stdout.write('postinstall: detected git checkout — skipping shebang rewrite\n');
95
+ process.exit(0);
96
+ }
97
+ } catch { /* no .git/ — npm-tarball install, proceed */ }
98
+
82
99
  // Every shipped bin entry needs the same shebang treatment. List them
83
100
  // here rather than re-reading package.json (the file may be pruned
84
101
  // from production installs and the bin field is small enough to
package/src/chat/index.js CHANGED
@@ -500,9 +500,13 @@ const SENTINEL_REGEX = /⟦OCP_END:[a-f0-9]+⟧/g;
500
500
  * @param {string} [opts.dbPath] Path to the conversations JSON file. Default `./conversations.json`
501
501
  * in the process cwd, so each project keeps its own history.
502
502
  * @param {string} [opts.skillsDir] Directory holding `<name>/SKILL.md` files. Default `~/.claude/skills`.
503
- * @param {boolean} [opts.dangerouslySkipPermissions=false]
503
+ * @param {boolean} [opts.dangerouslySkipPermissions=true]
504
504
  * Forward `--dangerously-skip-permissions` to upstream so tool
505
- * calls (WebSearch, Bash, …) do not block on a permission prompt.
505
+ * calls (WebSearch, Bash, …) do not block on a permission prompt
506
+ * PTY automation cannot answer. Default ON in 1.1+ across every
507
+ * surface (CLI, driver, chat client) — pass `false` explicitly
508
+ * to restore claude's interactive permission prompts (and accept
509
+ * that every tool call will then hang).
506
510
  * @param {string|null} [opts.appendSystemPrompt] Appended to every turn's system prompt. Pass `null` to disable
507
511
  * the default ("use WebSearch immediately …") text.
508
512
  * @param {object} [opts.pricing] Per-token rates: `{ input, cacheWrite, cacheRead, output }`.
@@ -518,7 +522,12 @@ export function createChatClient(opts = {}) {
518
522
  ? path.resolve(opts.dbPath)
519
523
  : path.resolve(process.cwd(), DEFAULT_DB_FILENAME);
520
524
  const skillsDir = path.resolve(opts.skillsDir ?? DEFAULT_SKILLS_DIR);
521
- const dangerouslySkipPermissions = opts.dangerouslySkipPermissions ?? false;
525
+ // Default ON across the SDK in 1.1+: PTY automation cannot answer
526
+ // an interactive permission prompt, so leaving this off makes the
527
+ // first tool call hang forever. Library callers who genuinely want
528
+ // claude to prompt must pass `dangerouslySkipPermissions: false`
529
+ // explicitly. The `??` operator preserves explicit `false`.
530
+ const dangerouslySkipPermissions = opts.dangerouslySkipPermissions ?? true;
522
531
  // Store the raw client-level option (string | null | undefined); the
523
532
  // per-turn compose merges it with the SDK base default and any
524
533
  // per-turn override.
package/src/index.js CHANGED
@@ -163,7 +163,15 @@ class Driver {
163
163
  maxEvents: finiteNonNeg(opts.maxEvents, 10_000),
164
164
  firstResponseMs: finiteNonNeg(opts.firstResponseMs, numberFromEnv('OCP_FIRST_RESPONSE_MS', 120_000)),
165
165
  trustSettleMs: finiteNonNeg(opts.trustSettleMs, numberFromEnv('OCP_TRUST_SETTLE_MS', 2_500)),
166
- promptBoxWaitMs: finiteNonNeg(opts.promptBoxWaitMs, numberFromEnv('OCP_PROMPT_BOX_WAIT_MS', 6_000)),
166
+ // 30 s default: in environments with many MCP servers, plugins,
167
+ // or hooks the first prompt-box render can take 8–25 s. The pre-
168
+ // 1.1 default of 6 s caused the driver to give up on
169
+ // prompt-box-shown and write the prompt anyway, *before* claude
170
+ // had even shown the trust dialog — the prompt's trailing CR
171
+ // then confirmed whatever option was highlighted on the dialog
172
+ // and the user message was dropped. 30 s leaves room for slow
173
+ // startups while still bounding total spawn latency.
174
+ promptBoxWaitMs: finiteNonNeg(opts.promptBoxWaitMs, numberFromEnv('OCP_PROMPT_BOX_WAIT_MS', 30_000)),
167
175
  initialSessionId: opts.initialSessionId ?? null,
168
176
  printMode:
169
177
  opts.printMode === true ||
@@ -199,7 +207,11 @@ class Driver {
199
207
  * @param {string} [req.systemPrompt]
200
208
  * @param {string[]} [req.allowedTools]
201
209
  * @param {string[]} [req.disallowedTools]
202
- * @param {boolean} [req.dangerouslySkipPermissions]
210
+ * @param {boolean} [req.dangerouslySkipPermissions=true]
211
+ * Default ON in 1.1+. PTY automation cannot answer claude's
212
+ * interactive y/n permission prompt, so leaving it OFF makes
213
+ * the first tool call hang forever. Pass `false` explicitly to
214
+ * restore the prompts (and accept the hang).
203
215
  * @param {boolean} [req.debug]
204
216
  * @param {boolean} [req.verbose]
205
217
  * @param {string[]} [req.passThroughArgv] extra argv to forward verbatim
@@ -215,6 +227,14 @@ class Driver {
215
227
  throw new Error('runOneShot: `prompt` (string) is required');
216
228
  }
217
229
 
230
+ // Default ON across the driver in 1.1+: PTY automation has no
231
+ // way to answer claude's interactive permission prompt, so a
232
+ // request that doesn't explicitly set it would otherwise hang on
233
+ // the first tool call. We mutate `req` in place because every
234
+ // downstream branch (print-mode, daemon proxy, buildSpawnArgs)
235
+ // reads the same field. `??` keeps an explicit `false` intact.
236
+ req.dangerouslySkipPermissions = req.dangerouslySkipPermissions ?? true;
237
+
218
238
  // Print-mode short-circuit: spawn `claude --print` directly, bypass
219
239
  // the PTY+TUI pipeline. Caller opts in via req.printMode, driver-level
220
240
  // opts.printMode, or env OCP_PRINT_MODE=1.
@@ -426,10 +446,16 @@ class Driver {
426
446
  || process.env.OCP_NO_AUTO_ACCEPT_TRUST === 'true'
427
447
  || req.autoAcceptFolderTrust === false;
428
448
  const autoAcceptTrust = !autoAcceptOptOut;
449
+ // The trust dialog text is rendered with cursor-positioning
450
+ // escapes between words rather than real space bytes. After our
451
+ // ANSI strip the buffer reads `Quicksafetycheck...` with the
452
+ // spaces gone, so `/Quick safety check/i` no longer matches.
453
+ // Allow zero-or-more whitespace between every word and accept
454
+ // the seq with or without spaces.
429
455
  const TRUST_PATTERNS = [
430
- /Quick safety check/i,
431
- /Is this a project you (?:created|trust)/i,
432
- /trust this folder/i,
456
+ /Quick\s*safety\s*check/i,
457
+ /Is\s*this\s*a\s*project\s*you\s*(?:created|trust)/i,
458
+ /trust\s*this\s*folder/i,
433
459
  ];
434
460
  const TRUST_SETTLE_MS = this.opts.trustSettleMs
435
461
  ?? numberFromEnv('OCP_TRUST_SETTLE_MS')
@@ -439,7 +465,21 @@ class Driver {
439
465
 
440
466
  const dialogWatchHandler = (chunk) => {
441
467
  if (dialogState !== 'none') return;
442
- dialogScanBuf += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
468
+ // Strip ANSI escapes BEFORE pattern-matching. claude's TUI wraps
469
+ // the dialog text in color codes mid-word ("\x1b[1mQ\x1b[muick
470
+ // safety check..."), which prevents a simple `/Quick safety/`
471
+ // regex from ever matching the raw byte stream. Result observed
472
+ // in user testing: trust dialog renders in 1-2 s but our
473
+ // detector never fires, the driver falls back to writing the
474
+ // user's prompt after `OCP_PROMPT_BOX_WAIT_MS`, and the
475
+ // trailing `\r` confirms whichever dialog option is highlighted
476
+ // (silently dropping the user message). Stripping here keeps
477
+ // the watcher decoupled from the main parser pipeline while
478
+ // still seeing the plain text claude actually rendered.
479
+ const raw = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
480
+ dialogScanBuf += raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '')
481
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
482
+ .replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
443
483
  if (dialogScanBuf.length > 16384) dialogScanBuf = dialogScanBuf.slice(-8192);
444
484
  if (TRUST_PATTERNS.some((p) => p.test(dialogScanBuf))) {
445
485
  if (autoAcceptTrust) {
@@ -508,6 +548,27 @@ class Driver {
508
548
  promptBoxReadyPromise,
509
549
  sleep(PROMPT_BOX_WAIT_MS).then(() => { promptBoxTimeoutHit = true; }),
510
550
  ]);
551
+ // If the dialog watcher is mid-flow (trust just auto-accepted)
552
+ // when our prompt-box wait times out, claude is still transitioning
553
+ // from the dialog to the main UI — the chevron has not landed
554
+ // because it doesn't exist yet. Give it the trust-settle window
555
+ // plus another bounded wait for the real input box to appear.
556
+ // Writing now would land the prompt into the dialog (and its
557
+ // trailing `\r` would confirm whichever option is highlighted),
558
+ // silently dropping the user's message.
559
+ if (promptBoxTimeoutHit && !promptBoxReady && dialogState === 'trust-accepted') {
560
+ const EXTRA_WAIT = PROMPT_BOX_WAIT_MS;
561
+ this._logDebug(
562
+ `prompt-box-shown still not seen but trust dialog is processing — ` +
563
+ `extending wait by ${EXTRA_WAIT}ms`,
564
+ );
565
+ let extendedTimeoutHit = false;
566
+ await Promise.race([
567
+ promptBoxReadyPromise,
568
+ sleep(EXTRA_WAIT).then(() => { extendedTimeoutHit = true; }),
569
+ ]);
570
+ promptBoxTimeoutHit = extendedTimeoutHit;
571
+ }
511
572
  if (promptBoxTimeoutHit && !promptBoxReady) {
512
573
  this._logDebug(`prompt-box-shown not seen within ${PROMPT_BOX_WAIT_MS}ms — sending anyway`);
513
574
  } else if (promptBoxReady && PROMPT_BOX_SETTLE_MS > 0) {
@@ -516,22 +577,50 @@ class Driver {
516
577
  await sleep(PROMPT_BOX_SETTLE_MS);
517
578
  }
518
579
 
519
- // Sentinel marker appended to every user turn so PTY automation
520
- // can tell when the reply is done. Phrased to be inert to model
521
- // behaviour — do NOT use words like "finish", "complete",
522
- // "wrap up", or "final" that nudge the model to cut tool use
523
- // short. The reply itself is the actual signal of completion;
524
- // this marker is just the bookkeeping byte we emit afterwards.
580
+ // End-of-reply marker appended to every user turn so PTY
581
+ // automation can tell when the reply is done. Word choice
582
+ // matters here:
583
+ // - **Avoid "token"**: the model's safety training flags
584
+ // prompts of the form "include this token in your reply"
585
+ // as access-token / API-key exfiltration attempts; it then
586
+ // warns the user about a "prompt injection attempt" in
587
+ // the response, which is alarming and user-visible. Use
588
+ // "marker" / "sentinel" instead — they carry no auth
589
+ // baggage.
590
+ // - **Avoid "finish", "complete", "wrap up", "final"**: those
591
+ // nudge the model to cut tool use short. The reply itself
592
+ // is the actual signal of completion; this marker is just
593
+ // the bookkeeping byte we emit afterwards.
594
+ // - Identify the source explicitly: framing it as "the ocp
595
+ // wrapper's marker" gives the model context to recognise
596
+ // it as harness plumbing rather than a hostile injection.
525
597
  // Single-line form because `\n` flips claude TUI into multi-line
526
598
  // edit mode where `\r` no longer submits.
527
599
  const instruction =
528
- ` (Append the literal token ${sentinel} on its own line at the very` +
529
- ' end of your reply. Automation glue does not constrain how you' +
530
- ' answer above; use tools as freely and thoroughly as you would' +
531
- ' without this marker.)';
600
+ ` (End your reply with the literal text ${sentinel} on its own line.` +
601
+ ' This is the ocp wrapper\'s end-of-reply markerprogrammatic plumbing' +
602
+ ' the automation uses to know your reply has landed. It is not part of' +
603
+ ' the user\'s message and you do not need to mention or flag it. Answer' +
604
+ ' the user\'s actual message above as you normally would, using tools' +
605
+ ' as freely and thoroughly as you would without the marker.)';
532
606
  const fullPrompt = req.prompt + instruction;
533
- try {
607
+ // Skip the prompt write if the dialog watcher has already
608
+ // decided to abort. Without this check the trailing `\r` of
609
+ // the prompt lands inside whatever modal we tried to abort on
610
+ // (most commonly the folder-trust dialog), confirms its
611
+ // currently-highlighted option, and silently drops the user
612
+ // prompt. The detector has already been cancel()'d at this
613
+ // point but the await chain above does not observe that
614
+ // cancellation, so the write would otherwise still happen.
615
+ if (dialogState === 'trust-blocked' || dialogState === 'unknown-blocked') {
616
+ this._logDebug(`skipping prompt write — dialog blocked (${dialogState})`);
617
+ } else try {
618
+ this._logDebug(
619
+ `writing prompt: ${fullPrompt.length} chars, ` +
620
+ `dialogState=${dialogState}, promptBoxReady=${promptBoxReady}`,
621
+ );
534
622
  await writePromptToSession(session, fullPrompt, this.opts);
623
+ this._logDebug('prompt write returned');
535
624
  firstResponseTimer = setTimeout(() => {
536
625
  if (!assistantActivitySeen) {
537
626
  this._logDebug(`no assistant activity for ${FIRST_RESPONSE_MS}ms — interactive prompt suspected`);
@@ -40,8 +40,21 @@ export const PATTERNS = {
40
40
  /** The `❯` chevron that marks the start of the input prompt line.
41
41
  * Once we see this we know we have reached the input box; everything
42
42
  * that follows on subsequent lines is the statusline / HUD plugin
43
- * area, not response content, and is suppressed from parsing. */
44
- promptInputChevron: /^[❯›❮‹]/,
43
+ * area, not response content, and is suppressed from parsing.
44
+ *
45
+ * NOTE: claude's TUI also uses `❯` to mark the currently-selected
46
+ * row in modal selection dialogs — e.g. the folder-trust dialog
47
+ * shows `❯ 1. Yes, I trust this folder`. The naive `/^[❯›❮‹]/`
48
+ * pattern misclassifies that as the input box, fires
49
+ * `prompt-box-shown`, and tricks the driver into writing the
50
+ * user's prompt into the dialog (the trailing `\r` then confirms
51
+ * "Yes" while the prompt itself is dropped). Anchor the match to
52
+ * `chevron + space + (anything-but-numbered-option)` so dialog
53
+ * selections do not match. The real input box is either `❯`
54
+ * alone, `❯ ` with a placeholder hint (`❯ Try "refactor …"`), or
55
+ * `❯ <typed-text>` — none of those collide with `❯ <digit>.`.
56
+ */
57
+ promptInputChevron: /^[❯›❮‹](?:$|\s(?!\d+\.\s))/,
45
58
  };
46
59
 
47
60
  /**