@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.
- package/dist/capability-runtime.js +4 -1
- package/dist/cc-orchestrator/codex-session-history.d.ts +3 -1
- package/dist/cc-orchestrator/codex-session-history.js +11 -4
- package/dist/cc-orchestrator/external-agent-changes.js +7 -65
- package/dist/cc-orchestrator/external-agent-driver.js +61 -3
- package/dist/cc-orchestrator/session-discovery.d.ts +4 -2
- package/dist/cc-orchestrator/session-discovery.js +63 -6
- package/dist/cc-orchestrator/session-history.js +5 -0
- package/dist/external-runtimes/claude-code/event-translator.d.ts +7 -0
- package/dist/external-runtimes/claude-code/event-translator.js +58 -9
- package/dist/external-runtimes/claude-code/mcp-config.d.ts +8 -0
- package/dist/external-runtimes/claude-code/mcp-config.js +16 -3
- package/dist/external-runtimes/claude-code/permissions.d.ts +69 -0
- package/dist/external-runtimes/claude-code/permissions.js +120 -0
- package/dist/external-runtimes/claude-code/runtime.d.ts +43 -4
- package/dist/external-runtimes/claude-code/runtime.js +155 -39
- package/dist/external-runtimes/claude-code/text-inference.d.ts +22 -0
- package/dist/external-runtimes/claude-code/text-inference.js +240 -0
- package/dist/external-runtimes/codex/app-server-client.d.ts +15 -0
- package/dist/external-runtimes/codex/app-server-client.js +69 -6
- package/dist/external-runtimes/codex/event-translator.js +16 -2
- package/dist/external-runtimes/codex/runtime.d.ts +14 -0
- package/dist/external-runtimes/codex/runtime.js +152 -16
- package/dist/external-runtimes/index.d.ts +4 -0
- package/dist/external-runtimes/index.js +2 -0
- package/dist/external-runtimes/session-factory.d.ts +16 -0
- package/dist/external-runtimes/session-factory.js +43 -1
- package/dist/external-runtimes/shared/mcp-bridge.d.ts +36 -1
- package/dist/external-runtimes/shared/mcp-bridge.js +93 -13
- package/dist/git/parse-log.d.ts +6 -2
- package/dist/git/parse-log.js +7 -3
- package/dist/git/utils.d.ts +4 -0
- package/dist/git/utils.js +29 -10
- package/dist/git/worktree/crud.d.ts +2 -0
- package/dist/git/worktree/crud.js +29 -1
- package/dist/git/worktree/diff.js +57 -29
- package/dist/index.capability.js +14 -1
- package/dist/index.git.d.ts +1 -0
- package/dist/index.git.js +1 -0
- package/dist/lsp/client.d.ts +3 -0
- package/dist/lsp/client.js +96 -25
- package/dist/lsp/manager.d.ts +2 -0
- package/dist/lsp/manager.js +56 -15
- package/dist/prompt/coding.md +1 -0
- package/dist/quota/index.d.ts +2 -1
- package/dist/quota/index.js +27 -7
- package/dist/quota/types.d.ts +5 -2
- package/dist/review/review-prompt.d.ts +2 -0
- package/dist/review/review-prompt.js +70 -29
- package/dist/review/review-target.d.ts +29 -0
- package/dist/review/review-target.js +353 -0
- package/dist/tools/apply-patch/applier.js +64 -39
- package/dist/tools/apply-patch/backup-targets.d.ts +8 -0
- package/dist/tools/apply-patch/backup-targets.js +24 -0
- package/dist/tools/apply-patch/parser.js +4 -2
- package/dist/tools/check-quota.js +3 -1
- package/dist/tools/lsp.js +36 -5
- 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
|
|
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
|
-
*
|
|
55
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
*
|
|
177
|
-
*
|
|
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
|
|
181
|
-
if (!
|
|
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
|
-
|
|
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
|
+
}>;
|