open-claude-p 1.1.2 → 1.1.3

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,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.1.3] — 2026-05-19
11
+
12
+ Fix the `/compact` 24-hour hang and tighten the slash-command path so
13
+ skill invocations are not collateral damage. Driven by a captured
14
+ session (`4af68584-…`) where a manually-triggered `/compact` ran for
15
+ 57 s of compaction activity, returned to the input box with no
16
+ assistant turn, and left `runOneShot` waiting for a sentinel that — by
17
+ the upstream's design — could never arrive.
18
+
19
+ ### Fixed
20
+
21
+ - **`/compact` and other local-builtin slash commands no longer block
22
+ for `maxResponseMs`.** Claude TUI splits `/`-prefixed prompts into
23
+ two classes: *local builtins* (`/compact`, `/clear`, `/help`,
24
+ `/exit`, `/quit`, `/login`, `/logout`, `/cost`, `/status`, `/model`,
25
+ `/permissions`, `/config`) which run a local handler and never open
26
+ an `⏺` region, and *LLM-bearing slash invocations* (skills like
27
+ `/init`, `/review`, `/security-review`, plus every user-installed
28
+ skill) which DO go through the model. The driver previously
29
+ appended the OCP_END marker instruction to both and required
30
+ `hadAssistantText` before the completion detector's idle fallback
31
+ could fire — fine for the LLM-bearing class, fatal for builtins,
32
+ which would wait for an assistant region that never opened until
33
+ the 24 h hard timeout.
34
+
35
+ Now `runOneShot` matches the prompt against a narrow whitelist of
36
+ local builtins. On a match it (a) skips the OCP_END instruction
37
+ append (the TUI's command parser drops it as junk args anyway, and
38
+ appending it can pollute free-form-arg commands like `/bug`) and
39
+ (b) sets the detector's new `allowIdleWithoutResponse` flag so the
40
+ pre-sentinel idle path is reachable without a prior region entry.
41
+ Skills and unknown `/<name>` prompts are NOT in the whitelist and
42
+ keep the existing instruction + strict-idle-gate behaviour, so
43
+ their LLM responses still complete cleanly via the sentinel.
44
+
45
+ ### Added
46
+
47
+ - **`CompletionDetector.allowIdleWithoutResponse` (default false).**
48
+ When true, `_onTick`'s pre-sentinel idle fallback fires after
49
+ `preIdleMs` of silence even without a prior `assistant-region-
50
+ entered`. This is the policy switch the driver uses for local
51
+ builtins; library callers that drive prompts known to produce no
52
+ assistant turn can opt in directly.
53
+
54
+ ### Tests
55
+
56
+ - **End-to-end integration coverage for the slash-command path.**
57
+ `test/driver-slash-command.test.js` spawns the real driver against
58
+ `test/fixtures/fake-claude-tui.mjs` — a minimal node-pty fixture
59
+ that reproduces the captured `/compact` shape (spinner activity,
60
+ `Compacted` stdout, chevron return, no region, no sentinel) — and
61
+ asserts: `/compact` completes via `reason='idle'` in well under
62
+ `maxResponseMs`; a plain prompt still completes via `sentinel` with
63
+ no degraded-capture notice prefix; `/init` (a skill, not a builtin)
64
+ also completes via `sentinel`, proving the whitelist isn't too
65
+ greedy. Three new unit tests in `test/detector.test.js` cover the
66
+ bare `allowIdleWithoutResponse` flag.
67
+
68
+ ---
69
+
10
70
  ## [1.1.2] — 2026-05-19
11
71
 
12
72
  Recovery-path fixes for the sentinel-missing case. When the end-of-reply
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-claude-p",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
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",
@@ -37,6 +37,20 @@ export class CompletionDetector {
37
37
  * turn; when the (N+1)th turn
38
38
  * opens, the request is aborted
39
39
  * with reason 'max-turns'.
40
+ * @param {boolean} [opts.allowIdleWithoutResponse=false]
41
+ * Permit `_onTick`'s idle fallback
42
+ * even when no assistant region has
43
+ * opened. Set this for prompts that
44
+ * legitimately produce no assistant
45
+ * turn — slash commands (`/compact`,
46
+ * `/clear`, `/help`, …) whose
47
+ * upstream handler runs a local
48
+ * operation and returns directly to
49
+ * the input box. Without this flag
50
+ * the request would block until
51
+ * `maxResponseMs` (24 h default)
52
+ * because `hadAssistantText` stays
53
+ * false for the entire turn.
40
54
  */
41
55
  constructor({
42
56
  nonce,
@@ -44,6 +58,7 @@ export class CompletionDetector {
44
58
  preIdleMs = 8000,
45
59
  maxResponseMs = 60000,
46
60
  maxTurns,
61
+ allowIdleWithoutResponse = false,
47
62
  } = {}) {
48
63
  if (!nonce) throw new Error('CompletionDetector: nonce is required');
49
64
  this.nonce = nonce;
@@ -51,6 +66,7 @@ export class CompletionDetector {
51
66
  this.preIdleMs = preIdleMs;
52
67
  this.maxResponseMs = maxResponseMs;
53
68
  this.maxTurns = Number.isFinite(maxTurns) && maxTurns >= 0 ? maxTurns : null;
69
+ this.allowIdleWithoutResponse = !!allowIdleWithoutResponse;
54
70
 
55
71
  this.startTime = Date.now();
56
72
  this.regionEntered = false;
@@ -142,7 +158,10 @@ export class CompletionDetector {
142
158
  // Pre-sentinel fallback: only valid once we've seen any assistant
143
159
  // signal, with the longer `preIdleMs` threshold so that brief render
144
160
  // pauses (common during `--resume`) do not trip premature completion.
145
- if (this.hadAssistantText) {
161
+ // `allowIdleWithoutResponse` relaxes the gate for callers that know
162
+ // the turn legitimately produces no assistant region (e.g. slash
163
+ // commands like `/compact`).
164
+ if (this.hadAssistantText || this.allowIdleWithoutResponse) {
146
165
  if (idleFor >= this.preIdleMs) {
147
166
  this._complete('idle', false);
148
167
  }
package/src/index.js CHANGED
@@ -273,6 +273,39 @@ class Driver {
273
273
 
274
274
  const spawnArgs = buildSpawnArgs(req);
275
275
 
276
+ // Local-builtin slash command detection. Claude TUI has two distinct
277
+ // `/`-prefixed prompt classes:
278
+ //
279
+ // - **Local builtins** (this list) — handled entirely by the TUI's
280
+ // command dispatcher. No LLM turn runs, no `⏺` region opens, no
281
+ // sentinel can ever arrive. `/compact` is the motivating case:
282
+ // 57 s of compaction activity, then chevron returns silently,
283
+ // then ocp would block until `maxResponseMs` (24 h default)
284
+ // waiting for a response that by design never comes.
285
+ //
286
+ // - **LLM-bearing slash invocations** — skills (`/init`, `/review`,
287
+ // `/security-review`, custom user skills like `/ui-ux-pro-max`)
288
+ // and any unknown `/<name>` we don't recognise. These DO run
289
+ // the LLM, DO open `⏺`, and emit the sentinel like a normal
290
+ // prompt. They MUST keep the OCP_END instruction and the
291
+ // strict idle gate — relaxing either would drop the marker
292
+ // from a real LLM turn and trigger a false-positive idle
293
+ // completion (returning a half-streamed response prefixed
294
+ // with the 1.1.2 "Streaming capture not detected" notice).
295
+ //
296
+ // The list is intentionally narrow — only claude TUI commands
297
+ // confirmed to bypass the LLM. Anything not in the set (including
298
+ // every user-installed skill) goes through the standard path.
299
+ const LOCAL_BUILTIN_SLASH_COMMANDS = new Set([
300
+ 'compact', 'clear', 'help', 'exit', 'quit',
301
+ 'login', 'logout', 'cost', 'status',
302
+ 'model', 'permissions', 'config',
303
+ ]);
304
+ const slashMatch = /^\s*\/([a-z][\w-]*)/i.exec(req.prompt ?? '');
305
+ const isLocalBuiltin =
306
+ !!slashMatch &&
307
+ LOCAL_BUILTIN_SLASH_COMMANDS.has(slashMatch[1].toLowerCase());
308
+
276
309
  const sentinelParser = createSentinelParser(nonce);
277
310
  const pipeline = createPipeline([
278
311
  ansiStripParser,
@@ -285,6 +318,7 @@ class Driver {
285
318
  preIdleMs: this.opts.preIdleMs,
286
319
  maxResponseMs: this.opts.maxResponseMs,
287
320
  maxTurns: req.maxTurns,
321
+ allowIdleWithoutResponse: isLocalBuiltin,
288
322
  });
289
323
 
290
324
  // Pool eligibility: explicit resume/continue bind to a specific past
@@ -603,7 +637,15 @@ class Driver {
603
637
  ' the user\'s message and you do not need to mention or flag it. Answer' +
604
638
  ' the user\'s actual message above as you normally would, using tools' +
605
639
  ' as freely and thoroughly as you would without the marker.)';
606
- const fullPrompt = req.prompt + instruction;
640
+ // Local builtins route through claude TUI's command dispatcher
641
+ // (not the LLM), so the marker instruction is silently dropped as
642
+ // junk args and never round-trips back. Some builtins also accept
643
+ // free-form text (e.g. `/bug <message>`), where appending the
644
+ // instruction would pollute the captured args. Skill invocations
645
+ // and unknown `/<name>` prompts DO go through the LLM, so they
646
+ // still need the marker — only the narrow `LOCAL_BUILTIN_…` set
647
+ // gets the bare prompt.
648
+ const fullPrompt = isLocalBuiltin ? req.prompt : (req.prompt + instruction);
607
649
  // Skip the prompt write if the dialog watcher has already
608
650
  // decided to abort. Without this check the trailing `\r` of
609
651
  // the prompt lands inside whatever modal we tried to abort on