@cjhyy/code-shell-capability-coding 0.9.44 → 0.9.47

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.
Files changed (58) hide show
  1. package/dist/capability-runtime.js +4 -1
  2. package/dist/cc-orchestrator/codex-session-history.d.ts +3 -1
  3. package/dist/cc-orchestrator/codex-session-history.js +11 -4
  4. package/dist/cc-orchestrator/external-agent-changes.js +7 -65
  5. package/dist/cc-orchestrator/external-agent-driver.js +61 -3
  6. package/dist/cc-orchestrator/session-discovery.d.ts +4 -2
  7. package/dist/cc-orchestrator/session-discovery.js +63 -6
  8. package/dist/cc-orchestrator/session-history.js +5 -0
  9. package/dist/external-runtimes/claude-code/event-translator.d.ts +7 -0
  10. package/dist/external-runtimes/claude-code/event-translator.js +58 -9
  11. package/dist/external-runtimes/claude-code/mcp-config.d.ts +8 -0
  12. package/dist/external-runtimes/claude-code/mcp-config.js +16 -3
  13. package/dist/external-runtimes/claude-code/permissions.d.ts +69 -0
  14. package/dist/external-runtimes/claude-code/permissions.js +120 -0
  15. package/dist/external-runtimes/claude-code/runtime.d.ts +43 -4
  16. package/dist/external-runtimes/claude-code/runtime.js +155 -39
  17. package/dist/external-runtimes/claude-code/text-inference.d.ts +22 -0
  18. package/dist/external-runtimes/claude-code/text-inference.js +240 -0
  19. package/dist/external-runtimes/codex/app-server-client.d.ts +15 -0
  20. package/dist/external-runtimes/codex/app-server-client.js +69 -6
  21. package/dist/external-runtimes/codex/event-translator.js +16 -2
  22. package/dist/external-runtimes/codex/runtime.d.ts +14 -0
  23. package/dist/external-runtimes/codex/runtime.js +152 -16
  24. package/dist/external-runtimes/index.d.ts +4 -0
  25. package/dist/external-runtimes/index.js +2 -0
  26. package/dist/external-runtimes/session-factory.d.ts +16 -0
  27. package/dist/external-runtimes/session-factory.js +43 -1
  28. package/dist/external-runtimes/shared/mcp-bridge.d.ts +36 -1
  29. package/dist/external-runtimes/shared/mcp-bridge.js +93 -13
  30. package/dist/git/parse-log.d.ts +6 -2
  31. package/dist/git/parse-log.js +7 -3
  32. package/dist/git/utils.d.ts +4 -0
  33. package/dist/git/utils.js +29 -10
  34. package/dist/git/worktree/crud.d.ts +2 -0
  35. package/dist/git/worktree/crud.js +29 -1
  36. package/dist/git/worktree/diff.js +57 -29
  37. package/dist/index.capability.js +14 -1
  38. package/dist/index.git.d.ts +1 -0
  39. package/dist/index.git.js +1 -0
  40. package/dist/lsp/client.d.ts +3 -0
  41. package/dist/lsp/client.js +96 -25
  42. package/dist/lsp/manager.d.ts +2 -0
  43. package/dist/lsp/manager.js +56 -15
  44. package/dist/prompt/coding.md +1 -0
  45. package/dist/quota/index.d.ts +2 -1
  46. package/dist/quota/index.js +27 -7
  47. package/dist/quota/types.d.ts +5 -2
  48. package/dist/review/review-prompt.d.ts +2 -0
  49. package/dist/review/review-prompt.js +70 -29
  50. package/dist/review/review-target.d.ts +29 -0
  51. package/dist/review/review-target.js +353 -0
  52. package/dist/tools/apply-patch/applier.js +64 -39
  53. package/dist/tools/apply-patch/backup-targets.d.ts +8 -0
  54. package/dist/tools/apply-patch/backup-targets.js +24 -0
  55. package/dist/tools/apply-patch/parser.js +4 -2
  56. package/dist/tools/check-quota.js +3 -1
  57. package/dist/tools/lsp.js +36 -5
  58. package/package.json +2 -2
@@ -0,0 +1,120 @@
1
+ /** Values passed to `claude --permission-mode`. */
2
+ export const CLAUDE_PERMISSION_MODES = [
3
+ "default",
4
+ "acceptEdits",
5
+ "plan",
6
+ "dontAsk",
7
+ "bypassPermissions",
8
+ ];
9
+ /**
10
+ * Map a CodeShell permission selection onto Claude's.
11
+ *
12
+ * Never escalates: Plan wins over everything; full access maps to
13
+ * `bypassPermissions` only when CodeShell itself is in `bypassPermissions`; and
14
+ * anything without an exact Claude equivalent — notably CodeShell's legacy
15
+ * `auto`, which in Claude would mean a model-classifier auto-approval — becomes
16
+ * `default`.
17
+ */
18
+ export function claudePermissionModeFor(selection) {
19
+ if (selection.planMode || selection.permissionMode === "plan")
20
+ return "plan";
21
+ switch (selection.permissionMode) {
22
+ case "acceptEdits":
23
+ case "dontAsk":
24
+ case "bypassPermissions":
25
+ return selection.permissionMode;
26
+ default:
27
+ return "default";
28
+ }
29
+ }
30
+ /**
31
+ * Whether native asks should be routed to the host in this mode. Plan stays
32
+ * read-only (an approved ExitPlanMode would otherwise leave it); `dontAsk` means
33
+ * "do not ask"; `bypassPermissions` never asks.
34
+ */
35
+ export function claudeModeUsesPermissionPrompt(mode) {
36
+ return mode === "default" || mode === "acceptEdits";
37
+ }
38
+ /** Plain CodeShell-side name; the bridge qualifies it as `mcp__<server>__…`. */
39
+ export const CLAUDE_PERMISSION_PROMPT_TOOL = "CodeShellPermissionPrompt";
40
+ function asRecord(value) {
41
+ return value && typeof value === "object" && !Array.isArray(value)
42
+ ? value
43
+ : undefined;
44
+ }
45
+ function deny(message) {
46
+ // The prompt tool's verdict is its TEXT result; an MCP error result would be
47
+ // treated by the CLI as a failed prompt rather than as a decision.
48
+ return { result: JSON.stringify({ behavior: "deny", message }) };
49
+ }
50
+ /**
51
+ * Add the permission-prompt tool to a session's bridge surface.
52
+ *
53
+ * Answers, in order:
54
+ * - the prompt tool itself → deny. It is visible to the model like any MCP
55
+ * tool; a direct call must not raise a host approval dialog.
56
+ * - this server's own tools → allow without asking. `ToolExecutor` authorizes
57
+ * them; asking here too would approve one operation twice (the same rule the
58
+ * Codex adapter applies to its MCP elicitations).
59
+ * - `ExitPlanMode` → deny. The mode is CodeShell's selection, not the model's.
60
+ * - everything else → `decide`, failing closed on any error or abort.
61
+ */
62
+ export function withClaudePermissionPrompt(host, decide, serverName) {
63
+ const ownPrefix = `mcp__${serverName}__`;
64
+ const qualifiedSelf = `${ownPrefix}${CLAUDE_PERMISSION_PROMPT_TOOL}`;
65
+ return {
66
+ businessSessionId: host.businessSessionId,
67
+ listTools: () => [
68
+ ...host.listTools(),
69
+ {
70
+ name: CLAUDE_PERMISSION_PROMPT_TOOL,
71
+ description: "Internal: Claude Code's permission prompt for its native tools, answered by " +
72
+ "the CodeShell user. Not for direct use.",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ tool_name: { type: "string" },
77
+ input: { type: "object" },
78
+ tool_use_id: { type: "string" },
79
+ },
80
+ required: ["tool_name", "input"],
81
+ },
82
+ },
83
+ ],
84
+ execute: async (call, signal) => {
85
+ if (call.name !== CLAUDE_PERMISSION_PROMPT_TOOL)
86
+ return host.execute(call, signal);
87
+ const params = asRecord(call.input);
88
+ const toolName = typeof params?.tool_name === "string" ? params.tool_name : "";
89
+ const input = asRecord(params?.input);
90
+ if (!toolName || !input)
91
+ return deny("Malformed permission request.");
92
+ if (toolName === qualifiedSelf || toolName === CLAUDE_PERMISSION_PROMPT_TOOL) {
93
+ return deny("The permission prompt tool cannot be called directly.");
94
+ }
95
+ if (toolName.startsWith(ownPrefix)) {
96
+ return { result: JSON.stringify({ behavior: "allow", updatedInput: input }) };
97
+ }
98
+ if (toolName === "ExitPlanMode") {
99
+ return deny("The permission mode is set by the CodeShell user and cannot be changed here.");
100
+ }
101
+ try {
102
+ if (signal?.aborted)
103
+ return deny("The permission request was cancelled.");
104
+ const decision = await decide({
105
+ toolName,
106
+ input,
107
+ ...(typeof params?.tool_use_id === "string" ? { toolUseId: params.tool_use_id } : {}),
108
+ }, signal);
109
+ if (signal?.aborted)
110
+ return deny("The permission request was cancelled.");
111
+ return decision.approved
112
+ ? { result: JSON.stringify({ behavior: "allow", updatedInput: input }) }
113
+ : deny(decision.reason || "The CodeShell user denied this tool call.");
114
+ }
115
+ catch {
116
+ return deny("The CodeShell approval request failed.");
117
+ }
118
+ },
119
+ };
120
+ }
@@ -1,4 +1,5 @@
1
1
  import type { StreamEvent } from "@cjhyy/code-shell-core/extension";
2
+ import { type ClaudePermissionMode, type ClaudePermissionPromptDecider } from "./permissions.js";
2
3
  import type { McpBridgeHandle } from "../shared/mcp-bridge.js";
3
4
  import { type ExternalRuntimeTurnInput } from "../turn-input.js";
4
5
  export interface ClaudeRuntimeOptions {
@@ -15,11 +16,40 @@ export interface ClaudeRuntimeOptions {
15
16
  model?: string;
16
17
  resumeRuntimeSessionId?: string;
17
18
  initialContext?: string;
19
+ /**
20
+ * Stable host guidance, appended to Claude's system prompt on EVERY turn —
21
+ * new and resumed alike, because each turn is a new process. Contrast
22
+ * `initialContext`, a one-time transcript replay for a fresh conversation.
23
+ */
24
+ developerInstructions?: string;
25
+ /**
26
+ * `--permission-mode` for Claude's NATIVE tools. Derive it with
27
+ * `claudePermissionModeFor`; see `permissions.ts` for why it is always sent.
28
+ */
29
+ permissionMode?: ClaudePermissionMode;
30
+ /**
31
+ * Plain name of the bridge tool that answers Claude's permission prompts
32
+ * (`withClaudePermissionPrompt`). Qualified here as `mcp__<server>__<name>`.
33
+ */
34
+ permissionPromptTool?: string;
18
35
  serverName?: string;
36
+ /**
37
+ * How long `interrupt()`/`close()` wait after SIGTERM before SIGKILL.
38
+ * Defaults to {@link DEFAULT_INTERRUPT_GRACE_MS}.
39
+ */
40
+ interruptGraceMs?: number;
19
41
  log?: (event: string, data: Record<string, unknown>) => void;
20
42
  }
43
+ /** SIGTERM grace before escalating to SIGKILL. */
44
+ export declare const DEFAULT_INTERRUPT_GRACE_MS = 3000;
21
45
  export interface ClaudeRuntimeHooks {
22
46
  onEvent?: (event: StreamEvent) => void;
47
+ /**
48
+ * Answer Claude's permission prompts for its NATIVE tools (Bash, Edit, …).
49
+ * Wired by the session factory through the bridge's permission-prompt tool;
50
+ * without it every native ask is denied. CodeShell Host Tools never reach it.
51
+ */
52
+ onClaudePermissionPrompt?: ClaudePermissionPromptDecider;
23
53
  }
24
54
  export interface ClaudeTurnHandle {
25
55
  readonly done: Promise<void>;
@@ -29,14 +59,17 @@ export declare class ClaudeCodeRuntime {
29
59
  private readonly hooks;
30
60
  readonly kind: "claude-code";
31
61
  private readonly log;
32
- private child?;
62
+ private active?;
33
63
  private claudeSessionId?;
64
+ private reportedModel?;
34
65
  private closed;
35
66
  private firstTurn;
36
67
  private turnCount;
37
68
  constructor(options: ClaudeRuntimeOptions, hooks?: ClaudeRuntimeHooks);
38
69
  /** Claude session id, once the first turn has reported it. Resume key only. */
39
70
  get runtimeSessionId(): string | undefined;
71
+ /** CLI-reported model, falling back to the requested model until reported. */
72
+ get resolvedModel(): string | undefined;
40
73
  /**
41
74
  * Run one turn. Resolves when the process exits, which is also when the turn is
42
75
  * terminal — `claude -p` is one process per turn.
@@ -49,12 +82,18 @@ export declare class ClaudeCodeRuntime {
49
82
  private onLine;
50
83
  private emit;
51
84
  /**
52
- * Interrupt the active turn.
85
+ * Interrupt the active turn and wait until its process is gone.
86
+ *
87
+ * `claude -p` has no interrupt RPC, so this is a signal: SIGTERM, then SIGKILL
88
+ * after the grace period. Returning while the process still runs would let a
89
+ * CLI that ignores SIGTERM keep working — and keep the turn pending — after
90
+ * the host believes it stopped. The turn settles with `aborted_streaming`.
53
91
  *
54
- * `claude -p` has no interrupt RPC, so this is a signal. As with Codex, it
55
- * cannot retract side effects already dispatched — it only stops further steps.
92
+ * As with Codex, it cannot retract side effects already dispatched — it only
93
+ * stops further steps.
56
94
  */
57
95
  interrupt(): Promise<void>;
96
+ private terminate;
58
97
  close(): Promise<void>;
59
98
  /** Start a fresh conversation on the next `send()`, discarding resume state. */
60
99
  resetConversation(): void;
@@ -17,22 +17,50 @@
17
17
  import { spawn } from "node:child_process";
18
18
  import { createInterface } from "node:readline";
19
19
  import { ClaudeEventTranslator } from "./event-translator.js";
20
- import { claudeBridgeArgs, CLAUDE_MCP_SERVER_NAME } from "./mcp-config.js";
20
+ import { claudeBridgeArgs, CLAUDE_MCP_SERVER_NAME, writeClaudeSystemPromptFile, } from "./mcp-config.js";
21
21
  import { buildRuntimeSpawnEnv } from "../shared/spawn-env.js";
22
+ import { CLAUDE_PERMISSION_MODES, } from "./permissions.js";
22
23
  import { textWithAttachmentReferences } from "../turn-input.js";
24
+ /** SIGTERM grace before escalating to SIGKILL. */
25
+ export const DEFAULT_INTERRUPT_GRACE_MS = 3_000;
26
+ /**
27
+ * Bound on waiting for `exit` after SIGKILL. SIGKILL cannot be ignored, so this
28
+ * only guards against a lost `exit` event; the turn is settled regardless.
29
+ */
30
+ const KILL_SETTLE_MS = 2_000;
31
+ function settlesWithin(promise, ms) {
32
+ return new Promise((resolve) => {
33
+ const timer = setTimeout(() => resolve(false), ms);
34
+ void promise.then(() => {
35
+ clearTimeout(timer);
36
+ resolve(true);
37
+ });
38
+ });
39
+ }
23
40
  export class ClaudeCodeRuntime {
24
41
  options;
25
42
  hooks;
26
43
  kind = "claude-code";
27
44
  log;
28
- child;
45
+ active;
29
46
  claudeSessionId;
47
+ reportedModel;
30
48
  closed = false;
31
49
  firstTurn = true;
32
50
  turnCount = 0;
33
51
  constructor(options, hooks = {}) {
34
52
  this.options = options;
35
53
  this.hooks = hooks;
54
+ // Typed, but also reachable from untyped callers: an unknown mode must not
55
+ // reach the CLI, where e.g. `auto` would mean classifier auto-approval.
56
+ if (options.permissionMode !== undefined &&
57
+ !CLAUDE_PERMISSION_MODES.includes(options.permissionMode)) {
58
+ throw new Error(`Unsupported Claude permission mode: ${String(options.permissionMode)}`);
59
+ }
60
+ if (options.permissionPromptTool !== undefined &&
61
+ !/^[A-Za-z][A-Za-z0-9_]*$/.test(options.permissionPromptTool)) {
62
+ throw new Error("permissionPromptTool must be a plain bridge tool name");
63
+ }
36
64
  this.log = options.log ?? (() => { });
37
65
  this.claudeSessionId = options.resumeRuntimeSessionId;
38
66
  }
@@ -40,6 +68,10 @@ export class ClaudeCodeRuntime {
40
68
  get runtimeSessionId() {
41
69
  return this.claudeSessionId;
42
70
  }
71
+ /** CLI-reported model, falling back to the requested model until reported. */
72
+ get resolvedModel() {
73
+ return this.reportedModel ?? this.options.model;
74
+ }
43
75
  /**
44
76
  * Run one turn. Resolves when the process exits, which is also when the turn is
45
77
  * terminal — `claude -p` is one process per turn.
@@ -51,7 +83,7 @@ export class ClaudeCodeRuntime {
51
83
  async send(input, onEvent) {
52
84
  if (this.closed)
53
85
  throw new Error("ClaudeCodeRuntime is closed");
54
- if (this.child)
86
+ if (this.active)
55
87
  throw new Error("a turn is already running");
56
88
  let terminalSeen = false;
57
89
  const turnNumber = ++this.turnCount;
@@ -65,6 +97,15 @@ export class ClaudeCodeRuntime {
65
97
  exposedToolNames: this.options.exposedToolNames,
66
98
  ...(this.options.serverName ? { serverName: this.options.serverName } : {}),
67
99
  });
100
+ // Per turn: each `claude -p` is a new process, and `--resume` does not carry a
101
+ // previous invocation's appended system prompt forward.
102
+ const systemPrompt = this.options.developerInstructions?.trim()
103
+ ? writeClaudeSystemPromptFile(this.options.developerInstructions)
104
+ : undefined;
105
+ const cleanupTurnFiles = () => {
106
+ wiring.cleanup();
107
+ systemPrompt?.cleanup();
108
+ };
68
109
  const args = [
69
110
  "-p",
70
111
  "--output-format",
@@ -76,21 +117,36 @@ export class ClaudeCodeRuntime {
76
117
  // Continuity across turns: a fresh process per turn means the session id is
77
118
  // the only thread of memory.
78
119
  ...(this.claudeSessionId ? ["--resume", this.claudeSessionId] : []),
120
+ ...(systemPrompt ? ["--append-system-prompt-file", systemPrompt.path] : []),
121
+ ...(this.options.permissionMode ? ["--permission-mode", this.options.permissionMode] : []),
122
+ ...(this.options.permissionPromptTool
123
+ ? [
124
+ "--permission-prompt-tool",
125
+ `mcp__${this.options.serverName ?? CLAUDE_MCP_SERVER_NAME}__${this.options.permissionPromptTool}`,
126
+ ]
127
+ : []),
79
128
  ...(this.options.extraArgs ?? []),
80
129
  ...wiring.args,
81
130
  ];
82
- const child = spawn(this.options.command ?? "claude", args, {
83
- cwd: this.options.cwd,
84
- env: buildRuntimeSpawnEnv({
85
- bridgeToken: {
86
- name: this.options.bridge.tokenEnvVar,
87
- value: this.options.bridge.token,
88
- },
89
- }),
90
- stdio: ["pipe", "pipe", "pipe"],
91
- shell: false,
92
- });
93
- this.child = child;
131
+ let child;
132
+ try {
133
+ child = spawn(this.options.command ?? "claude", args, {
134
+ cwd: this.options.cwd,
135
+ env: buildRuntimeSpawnEnv({
136
+ bridgeToken: {
137
+ name: this.options.bridge.tokenEnvVar,
138
+ value: this.options.bridge.token,
139
+ },
140
+ }),
141
+ stdio: ["pipe", "pipe", "pipe"],
142
+ shell: false,
143
+ });
144
+ }
145
+ catch (error) {
146
+ // A synchronous spawn failure never reaches the exit/error handlers below.
147
+ cleanupTurnFiles();
148
+ throw error;
149
+ }
94
150
  child.stdout.setEncoding("utf8");
95
151
  const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
96
152
  lines.on("line", (line) => {
@@ -112,31 +168,46 @@ export class ClaudeCodeRuntime {
112
168
  }
113
169
  this.firstTurn = false;
114
170
  child.stdin.end(text);
115
- const done = new Promise((resolve) => {
116
- let finished = false;
117
- const finish = (code, error) => {
171
+ let resolveSettled;
172
+ const settled = new Promise((resolve) => (resolveSettled = resolve));
173
+ let finished = false;
174
+ const active = {
175
+ child,
176
+ settled,
177
+ interrupted: false,
178
+ finish: (code, error) => {
118
179
  if (finished)
119
180
  return;
120
181
  finished = true;
121
182
  lines.close();
122
- // Clean up the config file that carried the bearer token.
123
- wiring.cleanup();
124
- this.child = undefined;
183
+ // Clean up the config file that carried the bearer token, and the
184
+ // system-prompt file.
185
+ cleanupTurnFiles();
186
+ if (this.active === active)
187
+ this.active = undefined;
125
188
  if (!terminalSeen) {
126
- const detail = error?.message ??
127
- `Claude Code exited without a terminal result (code ${code ?? "unknown"})`;
128
- this.emit({ type: "error", error: detail }, onEvent);
129
- this.emit({ type: "turn_complete", reason: "model_error" }, onEvent);
189
+ if (active.interrupted) {
190
+ // A stop the user asked for is not a model failure. Same terminal
191
+ // reason Codex reports for an interrupted turn.
192
+ this.emit({ type: "turn_complete", reason: "aborted_streaming" }, onEvent);
193
+ }
194
+ else {
195
+ const detail = error?.message ??
196
+ `Claude Code exited without a terminal result (code ${code ?? "unknown"})`;
197
+ this.emit({ type: "error", error: detail }, onEvent);
198
+ this.emit({ type: "turn_complete", reason: "model_error" }, onEvent);
199
+ }
130
200
  }
131
- resolve();
132
- };
133
- child.once("exit", (code) => finish(code));
134
- child.once("error", (error) => {
135
- this.log("claude.spawn_failed", { error: error.message.slice(0, 200) });
136
- finish(undefined, error);
137
- });
201
+ resolveSettled();
202
+ },
203
+ };
204
+ this.active = active;
205
+ child.once("exit", (code) => active.finish(code));
206
+ child.once("error", (error) => {
207
+ this.log("claude.spawn_failed", { error: error.message.slice(0, 200) });
208
+ active.finish(undefined, error);
138
209
  });
139
- return { done };
210
+ return { done: settled };
140
211
  }
141
212
  onLine(line, translator, emit) {
142
213
  if (!line.trim())
@@ -150,6 +221,26 @@ export class ClaudeCodeRuntime {
150
221
  this.log("claude.unparsable_line", { bytes: line.length });
151
222
  return;
152
223
  }
224
+ // Capture identity before publishing terminal output so post-turn work sees
225
+ // the model this process actually used, including aliases resolved by Claude.
226
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
227
+ const message = parsed;
228
+ // Sub-agent assistant output can name a different model from the root CLI.
229
+ if (message.parent_tool_use_id == null) {
230
+ const model = message.type === "system" && message.subtype === "init"
231
+ ? message.model
232
+ : message.type === "assistant"
233
+ ? message.message?.model
234
+ : message.type === "stream_event" && message.event?.type === "message_start"
235
+ ? message.event.message?.model
236
+ : undefined;
237
+ if (typeof model === "string") {
238
+ const identity = model.trim();
239
+ if (identity && identity !== "<synthetic>")
240
+ this.reportedModel = identity;
241
+ }
242
+ }
243
+ }
153
244
  for (const event of translator.translate(parsed))
154
245
  emit(event);
155
246
  if (!this.claudeSessionId && translator.runtimeSessionId) {
@@ -171,16 +262,41 @@ export class ClaudeCodeRuntime {
171
262
  }
172
263
  }
173
264
  /**
174
- * Interrupt the active turn.
265
+ * Interrupt the active turn and wait until its process is gone.
266
+ *
267
+ * `claude -p` has no interrupt RPC, so this is a signal: SIGTERM, then SIGKILL
268
+ * after the grace period. Returning while the process still runs would let a
269
+ * CLI that ignores SIGTERM keep working — and keep the turn pending — after
270
+ * the host believes it stopped. The turn settles with `aborted_streaming`.
175
271
  *
176
- * `claude -p` has no interrupt RPC, so this is a signal. As with Codex, it
177
- * cannot retract side effects already dispatched — it only stops further steps.
272
+ * As with Codex, it cannot retract side effects already dispatched — it only
273
+ * stops further steps.
178
274
  */
179
275
  async interrupt() {
180
- const child = this.child;
181
- if (!child)
276
+ const active = this.active;
277
+ if (!active)
278
+ return;
279
+ active.interrupted = true;
280
+ active.stopping ??= this.terminate(active);
281
+ await active.stopping;
282
+ }
283
+ async terminate(active) {
284
+ const { child } = active;
285
+ const running = () => child.exitCode === null && child.signalCode === null;
286
+ if (running())
287
+ child.kill("SIGTERM");
288
+ const grace = this.options.interruptGraceMs ?? DEFAULT_INTERRUPT_GRACE_MS;
289
+ if (await settlesWithin(active.settled, grace))
290
+ return;
291
+ this.log("claude.sigkill", { graceMs: grace });
292
+ if (running())
293
+ child.kill("SIGKILL");
294
+ if (await settlesWithin(active.settled, KILL_SETTLE_MS))
182
295
  return;
183
- child.kill("SIGTERM");
296
+ // No `exit` event even after SIGKILL (or the spawn never produced a process).
297
+ // Settle the turn anyway so neither the caller nor the session hangs.
298
+ this.log("claude.exit_not_observed", {});
299
+ active.finish(null);
184
300
  }
185
301
  async close() {
186
302
  this.closed = true;
@@ -0,0 +1,22 @@
1
+ import { spawn } from "node:child_process";
2
+ import { type TokenUsage } from "@cjhyy/code-shell-core/extension";
3
+ export interface ClaudeTextInferenceOptions {
4
+ systemPrompt: string;
5
+ userMessage: string;
6
+ /** Trusted foreground directory, preserving the CLI's project auth/provider settings. */
7
+ cwd?: string;
8
+ model?: string;
9
+ signal?: AbortSignal;
10
+ maxTokens?: number;
11
+ command?: string;
12
+ env?: NodeJS.ProcessEnv;
13
+ timeoutMs?: number;
14
+ }
15
+ /** Fresh CLI inference, with no conversation, Host tools or user customizations. */
16
+ export declare function runClaudeTextInference(options: ClaudeTextInferenceOptions, dependencies?: {
17
+ spawn?: typeof spawn;
18
+ }): Promise<{
19
+ text: string;
20
+ usage?: TokenUsage;
21
+ stopReason?: string;
22
+ }>;