open-claude-p 1.1.0 → 1.1.2
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 +118 -6
- package/package.json +1 -1
- package/scripts/postinstall.js +17 -0
- package/src/chat/index.js +12 -3
- package/src/index.js +134 -20
- package/src/parsers/tui-frame.js +15 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,111 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.1.2] — 2026-05-19
|
|
11
|
+
|
|
12
|
+
Recovery-path fixes for the sentinel-missing case. When the end-of-reply
|
|
13
|
+
marker fails to land — usually because claude omitted it or the PTY
|
|
14
|
+
frames were truncated — the driver's text extraction was returning
|
|
15
|
+
either an empty string or, worse, the entire re-rendered transcript of
|
|
16
|
+
a `--resume` session. Two changes correct that.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **`extractAssistantText` no longer slices from the FIRST `⏺`** when
|
|
21
|
+
the sentinel is missing. On a `--resume` run the buffer carries old
|
|
22
|
+
history regions before the current response; anchoring on the first
|
|
23
|
+
`⏺` returned the oldest history turn plus everything after it,
|
|
24
|
+
silently leaking prior turns into `result.text`. The fallback now
|
|
25
|
+
uses `lastIndexOf('⏺')` so the slice always covers the current
|
|
26
|
+
turn's response. Fresh (non-resumed) sessions are unaffected because
|
|
27
|
+
the buffer only has one region marker, so first == last.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Degraded-capture notice prefix.** When `completionReason` is
|
|
32
|
+
`'idle'` (sentinel never seen, idle-fallback completed) or
|
|
33
|
+
`'jsonl-recovered'` (a stall was rescued by reading the upstream
|
|
34
|
+
JSONL session log), `result.text` is now prefixed with:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
[ocp] Streaming capture not detected — showing last result from {source}.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
where `{source}` is either `local session log` (JSONL had the
|
|
41
|
+
response) or `terminal buffer (best effort)` (JSONL was unavailable
|
|
42
|
+
and we fell back to the PTY slice above). Callers and chat UIs can
|
|
43
|
+
now distinguish a normal capture from a post-hoc recovery without
|
|
44
|
+
inspecting `completionReason`. The default success path
|
|
45
|
+
(`reason='sentinel'`) is unchanged — no prefix is added.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## [1.1.1] — 2026-05-19
|
|
50
|
+
|
|
51
|
+
Post-1.1.0 fixes driven by Cosmica integration testing. Several of
|
|
52
|
+
these are essentially the 1.1.0 release "actually working" — the
|
|
53
|
+
1.1.0 surface was correct on paper but a handful of subtle parser
|
|
54
|
+
and default-value bugs kept the new behaviour from landing in
|
|
55
|
+
practice.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **Trust dialog auto-accept regressed silently for two distinct
|
|
60
|
+
reasons.**
|
|
61
|
+
- The driver's `❯` chevron pattern matched both the real input
|
|
62
|
+
box and the trust dialog's "currently-selected" row
|
|
63
|
+
(`❯ 1. Yes, I trust this folder`). `prompt-box-shown` fired
|
|
64
|
+
on the dialog row, the await chain proceeded to the prompt
|
|
65
|
+
write, and the trailing `\r` confirmed whichever option was
|
|
66
|
+
highlighted — silently dropping the user message. Pattern is
|
|
67
|
+
now `/^[❯›❮‹](?:$|\s(?!\d+\.\s))/` and a write-time guard
|
|
68
|
+
refuses to send when `dialogState` is in a blocked state.
|
|
69
|
+
- The trust dialog text is rendered with cursor-positioning
|
|
70
|
+
escapes between words rather than real space bytes; after the
|
|
71
|
+
ANSI strip the buffer reads `Quicksafetycheck` (no spaces) and
|
|
72
|
+
the `/Quick safety check/i` pattern never matched. Result: the
|
|
73
|
+
dialog rendered in ~250 ms but the watcher never fired, the
|
|
74
|
+
driver fell back to writing the prompt after
|
|
75
|
+
`OCP_PROMPT_BOX_WAIT_MS`, and the trailing CR confirmed "Yes"
|
|
76
|
+
in the still-open dialog. The watcher now strips ANSI before
|
|
77
|
+
pattern-matching and the patterns accept `\s*` between every
|
|
78
|
+
word.
|
|
79
|
+
- **`OCP_PROMPT_BOX_WAIT_MS` default 6 s → 30 s.** Environments
|
|
80
|
+
with many MCP servers or plugins need 8–25 s before the input
|
|
81
|
+
box renders. 6 s was tripping the timeout before claude had
|
|
82
|
+
even shown the trust dialog. Short-circuited the moment
|
|
83
|
+
`prompt-box-shown` fires, so the common-case latency is
|
|
84
|
+
unchanged.
|
|
85
|
+
- **`dangerouslySkipPermissions` is now ON by default across
|
|
86
|
+
EVERY surface** — not just the CLI. The 1.1.0 commit only
|
|
87
|
+
flipped the CLI's default and left `createDriver().runOneShot()`
|
|
88
|
+
and `createChatClient()` on the pre-1.1 `false`, so anything
|
|
89
|
+
using the SDK directly (the bundled sample chat, third-party
|
|
90
|
+
wrappers) still hung on the first tool call. Sample server's
|
|
91
|
+
redundant env-gate is dropped accordingly.
|
|
92
|
+
- **End-of-reply marker no longer flagged as
|
|
93
|
+
"prompt-injection".** The 1.0/1.1 instruction text used the
|
|
94
|
+
word "token": `Append the literal token ⟦OCP_END:xxx⟧ ...`.
|
|
95
|
+
The model's safety classifier treated "include this token in
|
|
96
|
+
your reply" as an access-token / API-key exfiltration attempt
|
|
97
|
+
and sometimes appended a `prompt injection attempt detected`
|
|
98
|
+
warning to the user-facing reply. Reworded to "end-of-reply
|
|
99
|
+
marker" and framed as `ocp wrapper's plumbing` so claude
|
|
100
|
+
recognises it as harness infrastructure rather than hostile
|
|
101
|
+
injection.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- **postinstall skips the shebang rewrite when running inside a
|
|
106
|
+
git checkout** (detected by the presence of `.git/` next to
|
|
107
|
+
`package.json`). The rewrite is meant for end-user installs from
|
|
108
|
+
the npm tarball; in a dev `git clone + npm link` setup it
|
|
109
|
+
otherwise overwrote `#!/usr/bin/env node` with the developer's
|
|
110
|
+
absolute node path on every `npm install` / `npm test`, showing
|
|
111
|
+
up as a noisy working-tree diff. Consumer installs unaffected.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
10
115
|
## [1.1.0] — 2026-05-18
|
|
11
116
|
|
|
12
117
|
`ocp` 1.1 reshapes the CLI defaults around the assumption that
|
|
@@ -64,12 +169,18 @@ extraction path so PTY chrome can no longer leak into responses.
|
|
|
64
169
|
|
|
65
170
|
### Changed
|
|
66
171
|
|
|
67
|
-
- **`--dangerously-skip-permissions`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
172
|
+
- **`--dangerously-skip-permissions` / `dangerouslySkipPermissions`
|
|
173
|
+
is now the default ON across EVERY surface** — `ocp` CLI,
|
|
174
|
+
`createDriver().runOneShot()`, `createChatClient()`, and the
|
|
175
|
+
`sample/` server. Rationale: `ocp` is a PTY automation library; an
|
|
176
|
+
interactive permission prompt that wants a human y/n hangs forever
|
|
177
|
+
— every Bash / Edit / Write / MCP / WebSearch call breaks without
|
|
178
|
+
this flag. The pre-1.1 library default of `false` was the cause
|
|
179
|
+
of nearly every "the SDK silently hangs on tool use" report.
|
|
180
|
+
Opt out per surface:
|
|
181
|
+
- CLI: `OCP_NO_SKIP_PERMS=1` (env)
|
|
182
|
+
- `createChatClient({ dangerouslySkipPermissions: false })`
|
|
183
|
+
- `runOneShot({ dangerouslySkipPermissions: false })`
|
|
73
184
|
Pre-1.1 users with `OCP_DEFAULT_SKIP_PERMS=1` exported see no
|
|
74
185
|
change; the env is now a no-op.
|
|
75
186
|
- **Folder-trust dialog is auto-accepted by default.** Same
|
|
@@ -183,5 +294,6 @@ Initial release.
|
|
|
183
294
|
`OCP_NO_DEFAULT_PROMPT=1`).
|
|
184
295
|
- Localised READMEs: Korean, Japanese, Chinese.
|
|
185
296
|
|
|
297
|
+
[1.1.1]: https://github.com/empty-user77/open-claude-p/releases/tag/v1.1.1
|
|
186
298
|
[1.1.0]: https://github.com/empty-user77/open-claude-p/releases/tag/v1.1.0
|
|
187
299
|
[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.
|
|
3
|
+
"version": "1.1.2",
|
|
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",
|
package/scripts/postinstall.js
CHANGED
|
@@ -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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
431
|
-
/Is
|
|
432
|
-
/trust
|
|
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
|
-
|
|
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
|
-
//
|
|
520
|
-
// can tell when the reply is done.
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
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
|
-
` (
|
|
529
|
-
'
|
|
530
|
-
'
|
|
531
|
-
'
|
|
600
|
+
` (End your reply with the literal text ${sentinel} on its own line.` +
|
|
601
|
+
' This is the ocp wrapper\'s end-of-reply marker — programmatic 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
|
-
|
|
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`);
|
|
@@ -686,6 +775,28 @@ class Driver {
|
|
|
686
775
|
);
|
|
687
776
|
}
|
|
688
777
|
|
|
778
|
+
// Degraded-capture notice. When the per-turn sentinel never landed
|
|
779
|
+
// (`reason='idle'`) or a stalled completion was rescued via JSONL
|
|
780
|
+
// (`reason='jsonl-recovered'`), the streamed PTY frames did NOT
|
|
781
|
+
// produce a clean end-of-reply boundary. The text we are returning
|
|
782
|
+
// is a post-hoc recovery — usually from the upstream JSONL session
|
|
783
|
+
// log, or as a best-effort tail of the PTY buffer when JSONL was
|
|
784
|
+
// unavailable. Prepend a one-line notice so the caller (and any
|
|
785
|
+
// chat UI rendering it) can tell this apart from a normal capture.
|
|
786
|
+
// The notice is intentionally a literal English prefix and only
|
|
787
|
+
// appears on the degraded path — the default success path
|
|
788
|
+
// (`reason='sentinel'`) is unchanged.
|
|
789
|
+
const captureFailed =
|
|
790
|
+
effectiveCompletion === 'idle' || effectiveCompletion === 'jsonl-recovered';
|
|
791
|
+
if (captureFailed && text) {
|
|
792
|
+
const source = jsonlExtraction?.text
|
|
793
|
+
? 'local session log'
|
|
794
|
+
: 'terminal buffer (best effort)';
|
|
795
|
+
text =
|
|
796
|
+
`[ocp] Streaming capture not detected — showing last result from ${source}.\n\n` +
|
|
797
|
+
text;
|
|
798
|
+
}
|
|
799
|
+
|
|
689
800
|
// For stall-style failures, capture the tail of the stripped PTY
|
|
690
801
|
// buffer so the caller can see exactly what claude was rendering
|
|
691
802
|
// when we gave up — that is usually the dialog or error that the
|
|
@@ -1055,9 +1166,12 @@ function extractAssistantText(stripped, nonce) {
|
|
|
1055
1166
|
sentinelIdx = i;
|
|
1056
1167
|
}
|
|
1057
1168
|
if (sentinelIdx === -1) {
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1060
|
-
|
|
1169
|
+
// Sentinel-missing fallback. On a resumed session the buffer carries
|
|
1170
|
+
// history regions before the current response, so anchoring on the
|
|
1171
|
+
// FIRST `⏺` would slice from the oldest history turn and return the
|
|
1172
|
+
// entire re-rendered transcript. Anchor on the LAST `⏺` instead —
|
|
1173
|
+
// that's the region marker for the current turn even after `--resume`.
|
|
1174
|
+
const ri = stripped.lastIndexOf(marker);
|
|
1061
1175
|
return ri === -1 ? '' : stripped.slice(ri + marker.length).trim();
|
|
1062
1176
|
}
|
|
1063
1177
|
|
package/src/parsers/tui-frame.js
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|