@cjhyy/code-shell-capability-coding 0.9.46 → 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/external-runtimes/claude-code/runtime.d.ts +3 -0
- package/dist/external-runtimes/claude-code/runtime.js +25 -0
- 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 +3 -0
- package/dist/external-runtimes/codex/app-server-client.js +53 -6
- package/dist/external-runtimes/codex/event-translator.js +16 -2
- package/dist/external-runtimes/codex/runtime.d.ts +3 -0
- package/dist/external-runtimes/codex/runtime.js +9 -0
- package/dist/external-runtimes/index.d.ts +2 -0
- package/dist/external-runtimes/index.js +1 -0
- package/dist/external-runtimes/session-factory.d.ts +2 -0
- package/dist/external-runtimes/session-factory.js +3 -0
- package/dist/git/worktree/crud.d.ts +2 -0
- package/dist/git/worktree/crud.js +1 -1
- 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/package.json +2 -2
|
@@ -61,12 +61,15 @@ export declare class ClaudeCodeRuntime {
|
|
|
61
61
|
private readonly log;
|
|
62
62
|
private active?;
|
|
63
63
|
private claudeSessionId?;
|
|
64
|
+
private reportedModel?;
|
|
64
65
|
private closed;
|
|
65
66
|
private firstTurn;
|
|
66
67
|
private turnCount;
|
|
67
68
|
constructor(options: ClaudeRuntimeOptions, hooks?: ClaudeRuntimeHooks);
|
|
68
69
|
/** Claude session id, once the first turn has reported it. Resume key only. */
|
|
69
70
|
get runtimeSessionId(): string | undefined;
|
|
71
|
+
/** CLI-reported model, falling back to the requested model until reported. */
|
|
72
|
+
get resolvedModel(): string | undefined;
|
|
70
73
|
/**
|
|
71
74
|
* Run one turn. Resolves when the process exits, which is also when the turn is
|
|
72
75
|
* terminal — `claude -p` is one process per turn.
|
|
@@ -44,6 +44,7 @@ export class ClaudeCodeRuntime {
|
|
|
44
44
|
log;
|
|
45
45
|
active;
|
|
46
46
|
claudeSessionId;
|
|
47
|
+
reportedModel;
|
|
47
48
|
closed = false;
|
|
48
49
|
firstTurn = true;
|
|
49
50
|
turnCount = 0;
|
|
@@ -67,6 +68,10 @@ export class ClaudeCodeRuntime {
|
|
|
67
68
|
get runtimeSessionId() {
|
|
68
69
|
return this.claudeSessionId;
|
|
69
70
|
}
|
|
71
|
+
/** CLI-reported model, falling back to the requested model until reported. */
|
|
72
|
+
get resolvedModel() {
|
|
73
|
+
return this.reportedModel ?? this.options.model;
|
|
74
|
+
}
|
|
70
75
|
/**
|
|
71
76
|
* Run one turn. Resolves when the process exits, which is also when the turn is
|
|
72
77
|
* terminal — `claude -p` is one process per turn.
|
|
@@ -216,6 +221,26 @@ export class ClaudeCodeRuntime {
|
|
|
216
221
|
this.log("claude.unparsable_line", { bytes: line.length });
|
|
217
222
|
return;
|
|
218
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
|
+
}
|
|
219
244
|
for (const event of translator.translate(parsed))
|
|
220
245
|
emit(event);
|
|
221
246
|
if (!this.claudeSessionId && translator.runtimeSessionId) {
|
|
@@ -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
|
+
}>;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { killProcessGroup } from "@cjhyy/code-shell-core/extension";
|
|
6
|
+
const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
const MAX_STDERR_BYTES = 64 * 1024;
|
|
8
|
+
const MAX_INPUT_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
function usageFromResult(value) {
|
|
10
|
+
if (!value || typeof value !== "object")
|
|
11
|
+
return undefined;
|
|
12
|
+
const raw = value;
|
|
13
|
+
const input = raw.input_tokens;
|
|
14
|
+
const output = raw.output_tokens;
|
|
15
|
+
const read = raw.cache_read_input_tokens ?? 0;
|
|
16
|
+
const write = raw.cache_creation_input_tokens ?? 0;
|
|
17
|
+
if (![input, output, read, write].every((count) => Number.isSafeInteger(count) && Number(count) >= 0))
|
|
18
|
+
return undefined;
|
|
19
|
+
const promptTokens = Number(input) + Number(read) + Number(write);
|
|
20
|
+
const totalTokens = promptTokens + Number(output);
|
|
21
|
+
if (!Number.isSafeInteger(totalTokens))
|
|
22
|
+
return undefined;
|
|
23
|
+
return {
|
|
24
|
+
promptTokens,
|
|
25
|
+
completionTokens: Number(output),
|
|
26
|
+
totalTokens,
|
|
27
|
+
...(raw.cache_read_input_tokens !== undefined ? { cacheReadTokens: Number(read) } : {}),
|
|
28
|
+
...(raw.cache_creation_input_tokens !== undefined
|
|
29
|
+
? { cacheCreationTokens: Number(write) }
|
|
30
|
+
: {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Fresh CLI inference, with no conversation, Host tools or user customizations. */
|
|
34
|
+
export async function runClaudeTextInference(options, dependencies = {}) {
|
|
35
|
+
options.signal?.throwIfAborted();
|
|
36
|
+
if (!options.systemPrompt?.trim() || !options.userMessage?.trim())
|
|
37
|
+
throw new Error("Claude text inference requires a system prompt and user message");
|
|
38
|
+
if (Buffer.byteLength(options.systemPrompt) + Buffer.byteLength(options.userMessage) >
|
|
39
|
+
MAX_INPUT_BYTES)
|
|
40
|
+
throw new Error("Claude text inference input exceeds its byte limit");
|
|
41
|
+
const maxTokens = options.maxTokens ?? 3072;
|
|
42
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
43
|
+
if (!Number.isSafeInteger(maxTokens) || maxTokens < 1 || maxTokens > 128_000)
|
|
44
|
+
throw new Error("Invalid Claude text inference output budget");
|
|
45
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3_600_000)
|
|
46
|
+
throw new Error("Invalid Claude text inference timeout");
|
|
47
|
+
const privateDir = await mkdtemp(join(tmpdir(), "codeshell-claude-text-"));
|
|
48
|
+
try {
|
|
49
|
+
await chmod(privateDir, 0o700);
|
|
50
|
+
const systemPromptFile = join(privateDir, "system-prompt.txt");
|
|
51
|
+
await writeFile(systemPromptFile, options.systemPrompt, { mode: 0o600 });
|
|
52
|
+
await chmod(systemPromptFile, 0o600);
|
|
53
|
+
options.signal?.throwIfAborted();
|
|
54
|
+
const args = [
|
|
55
|
+
"-p",
|
|
56
|
+
"--output-format",
|
|
57
|
+
"stream-json",
|
|
58
|
+
"--verbose",
|
|
59
|
+
"--include-partial-messages",
|
|
60
|
+
"--safe-mode",
|
|
61
|
+
"--tools",
|
|
62
|
+
"",
|
|
63
|
+
"--disallowedTools",
|
|
64
|
+
"*",
|
|
65
|
+
"--strict-mcp-config",
|
|
66
|
+
"--disable-slash-commands",
|
|
67
|
+
"--no-session-persistence",
|
|
68
|
+
"--permission-mode",
|
|
69
|
+
"dontAsk",
|
|
70
|
+
"--permission-prompts",
|
|
71
|
+
"none",
|
|
72
|
+
"--max-turns",
|
|
73
|
+
"1",
|
|
74
|
+
"--system-prompt-file",
|
|
75
|
+
systemPromptFile,
|
|
76
|
+
...(options.model ? ["--model", options.model] : []),
|
|
77
|
+
];
|
|
78
|
+
// Safe mode keeps the CLI's auth/provider selection, unlike --bare. Managed
|
|
79
|
+
// policy (including managed hooks) remains authoritative inside the CLI.
|
|
80
|
+
const env = {
|
|
81
|
+
...(options.env ?? process.env),
|
|
82
|
+
CLAUDE_CODE_SAFE_MODE: "1",
|
|
83
|
+
CLAUDE_CODE_MAX_OUTPUT_TOKENS: String(maxTokens),
|
|
84
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
85
|
+
CLAUDE_CODE_DISABLE_TERMINAL_TITLE: "1",
|
|
86
|
+
CLAUDE_CODE_DISABLE_FILE_MENTIONS: "1",
|
|
87
|
+
};
|
|
88
|
+
for (const name of [
|
|
89
|
+
"CLAUDECODE",
|
|
90
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
91
|
+
"CLAUDE_CODE_SIMPLE",
|
|
92
|
+
"CODESHELL_CODEX_MCP_TOKEN",
|
|
93
|
+
"CODE_SHELL_HOST_MCP_SERVERS",
|
|
94
|
+
"CODESHELL_MODEL_TRACE_FD",
|
|
95
|
+
])
|
|
96
|
+
delete env[name];
|
|
97
|
+
return await new Promise((resolve, reject) => {
|
|
98
|
+
const launch = dependencies.spawn ?? spawn;
|
|
99
|
+
const child = launch(options.command ?? "claude", args, {
|
|
100
|
+
cwd: options.cwd ?? privateDir,
|
|
101
|
+
env,
|
|
102
|
+
shell: false,
|
|
103
|
+
detached: process.platform !== "win32",
|
|
104
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
105
|
+
});
|
|
106
|
+
let outputBytes = 0;
|
|
107
|
+
let stderrBytes = 0;
|
|
108
|
+
let pending = "";
|
|
109
|
+
let terminal;
|
|
110
|
+
let stopReason;
|
|
111
|
+
let failure;
|
|
112
|
+
let settling = false;
|
|
113
|
+
let stopping;
|
|
114
|
+
const stop = () => {
|
|
115
|
+
stopping ??= child.pid ? killProcessGroup(child.pid, { graceMs: 250 }) : Promise.resolve();
|
|
116
|
+
return stopping;
|
|
117
|
+
};
|
|
118
|
+
const finish = (code, signal) => {
|
|
119
|
+
if (settling)
|
|
120
|
+
return;
|
|
121
|
+
settling = true;
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
124
|
+
void stop().then(() => {
|
|
125
|
+
child.stdin.destroy();
|
|
126
|
+
child.stdout.destroy();
|
|
127
|
+
child.stderr.destroy();
|
|
128
|
+
if (failure)
|
|
129
|
+
return reject(failure);
|
|
130
|
+
if (code !== 0 || signal)
|
|
131
|
+
return reject(new Error(`Claude text inference exited without success (${signal ?? code})`));
|
|
132
|
+
try {
|
|
133
|
+
if (pending.trim())
|
|
134
|
+
accept(pending);
|
|
135
|
+
if (!terminal)
|
|
136
|
+
throw new Error("Claude text inference produced no terminal result");
|
|
137
|
+
resolve({
|
|
138
|
+
text: terminal.result,
|
|
139
|
+
usage: usageFromResult(terminal.usage),
|
|
140
|
+
stopReason,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
reject(error);
|
|
145
|
+
}
|
|
146
|
+
}, reject);
|
|
147
|
+
};
|
|
148
|
+
const fail = (error) => {
|
|
149
|
+
if (settling || failure)
|
|
150
|
+
return;
|
|
151
|
+
failure = error;
|
|
152
|
+
void stop().then(() => finish(), reject);
|
|
153
|
+
};
|
|
154
|
+
const acceptStopReason = (value) => {
|
|
155
|
+
if (typeof value !== "string")
|
|
156
|
+
return;
|
|
157
|
+
if (!["end_turn", "stop", "stop_sequence"].includes(value))
|
|
158
|
+
throw new Error(`Claude text inference stopped without complete text (${value})`);
|
|
159
|
+
stopReason = value;
|
|
160
|
+
};
|
|
161
|
+
const accept = (line) => {
|
|
162
|
+
if (!line.trim())
|
|
163
|
+
return;
|
|
164
|
+
let item;
|
|
165
|
+
try {
|
|
166
|
+
item = JSON.parse(line);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw new Error("Claude text inference emitted malformed JSON");
|
|
170
|
+
}
|
|
171
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
172
|
+
throw new Error("Claude text inference emitted a malformed event");
|
|
173
|
+
const content = item.message?.content;
|
|
174
|
+
const block = item.event?.content_block;
|
|
175
|
+
if (["tool_progress", "tool_use_summary"].includes(item.type) ||
|
|
176
|
+
(item.type === "control_request" && item.request?.subtype === "can_use_tool") ||
|
|
177
|
+
["tool_use", "server_tool_use"].includes(block?.type) ||
|
|
178
|
+
(Array.isArray(content) &&
|
|
179
|
+
content.some((part) => ["tool_use", "server_tool_use", "tool_result"].includes(part?.type))) ||
|
|
180
|
+
(item.type === "system" && item.subtype === "init" && item.tools?.length > 0))
|
|
181
|
+
throw new Error("Claude text inference attempted to use tools");
|
|
182
|
+
if (item.type === "assistant")
|
|
183
|
+
acceptStopReason(item.message?.stop_reason);
|
|
184
|
+
if (item.type === "stream_event" && item.event?.type === "message_delta")
|
|
185
|
+
acceptStopReason(item.event.delta?.stop_reason);
|
|
186
|
+
if (item.type !== "result")
|
|
187
|
+
return;
|
|
188
|
+
if (terminal)
|
|
189
|
+
throw new Error("Claude text inference emitted multiple terminal results");
|
|
190
|
+
if (item.is_error !== false ||
|
|
191
|
+
item.subtype !== "success" ||
|
|
192
|
+
typeof item.result !== "string" ||
|
|
193
|
+
!item.result.trim() ||
|
|
194
|
+
(Array.isArray(item.errors) && item.errors.length > 0))
|
|
195
|
+
throw new Error("Claude text inference returned an incomplete or failed result");
|
|
196
|
+
acceptStopReason(item.stop_reason);
|
|
197
|
+
terminal = item;
|
|
198
|
+
};
|
|
199
|
+
const onAbort = () => fail(Object.assign(new Error("Claude text inference aborted"), { name: "AbortError" }));
|
|
200
|
+
const timer = setTimeout(() => fail(new Error(`Claude text inference timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
201
|
+
child.stdout.setEncoding("utf8");
|
|
202
|
+
child.stdout.on("data", (chunk) => {
|
|
203
|
+
if (settling || failure)
|
|
204
|
+
return;
|
|
205
|
+
outputBytes += Buffer.byteLength(chunk);
|
|
206
|
+
if (outputBytes > MAX_OUTPUT_BYTES)
|
|
207
|
+
return fail(new Error("Claude text inference output exceeds its byte limit"));
|
|
208
|
+
pending += chunk;
|
|
209
|
+
try {
|
|
210
|
+
for (let newline; (newline = pending.indexOf("\n")) >= 0;) {
|
|
211
|
+
const line = pending.slice(0, newline);
|
|
212
|
+
pending = pending.slice(newline + 1);
|
|
213
|
+
accept(line);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
fail(error);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
child.stderr.on("data", (chunk) => {
|
|
221
|
+
stderrBytes += chunk.byteLength;
|
|
222
|
+
if (stderrBytes > MAX_STDERR_BYTES)
|
|
223
|
+
fail(new Error("Claude text inference diagnostics exceed their byte limit"));
|
|
224
|
+
});
|
|
225
|
+
child.stdin.on("error", (error) => fail(error));
|
|
226
|
+
child.stdout.on("error", (error) => fail(error));
|
|
227
|
+
child.stderr.on("error", (error) => fail(error));
|
|
228
|
+
child.once("error", (error) => fail(error));
|
|
229
|
+
child.once("close", finish);
|
|
230
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
231
|
+
if (options.signal?.aborted)
|
|
232
|
+
onAbort();
|
|
233
|
+
else
|
|
234
|
+
child.stdin.end(options.userMessage);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
await rm(privateDir, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -39,6 +39,7 @@ export declare class CodexAppServerClient {
|
|
|
39
39
|
private requestHandler?;
|
|
40
40
|
private closeHandler?;
|
|
41
41
|
private closed;
|
|
42
|
+
private closing?;
|
|
42
43
|
private closeReason?;
|
|
43
44
|
/** Buffer lines that arrive before a handler exists, then drain in order. */
|
|
44
45
|
private readonly preHandlerLines;
|
|
@@ -69,10 +70,12 @@ export declare class CodexAppServerClient {
|
|
|
69
70
|
request(method: string, params?: unknown, timeoutMs?: number): Promise<unknown>;
|
|
70
71
|
notify(method: string, params?: unknown): void;
|
|
71
72
|
private write;
|
|
73
|
+
private failInput;
|
|
72
74
|
private failAll;
|
|
73
75
|
/** Why the transport closed, once it has. */
|
|
74
76
|
get closedReason(): string | undefined;
|
|
75
77
|
get isClosed(): boolean;
|
|
76
78
|
/** Stop via stdin EOF, then SIGTERM and SIGKILL if the server does not exit. */
|
|
77
79
|
close(): Promise<void>;
|
|
80
|
+
private stop;
|
|
78
81
|
}
|
|
@@ -38,6 +38,7 @@ export class CodexAppServerClient {
|
|
|
38
38
|
requestHandler;
|
|
39
39
|
closeHandler;
|
|
40
40
|
closed = false;
|
|
41
|
+
closing;
|
|
41
42
|
closeReason;
|
|
42
43
|
/** Buffer lines that arrive before a handler exists, then drain in order. */
|
|
43
44
|
preHandlerLines = [];
|
|
@@ -62,6 +63,8 @@ export class CodexAppServerClient {
|
|
|
62
63
|
}
|
|
63
64
|
/** Spawn the app-server. Handlers must already be registered. */
|
|
64
65
|
start() {
|
|
66
|
+
if (this.closed)
|
|
67
|
+
throw new Error(this.closeReason ?? "app-server client is closed");
|
|
65
68
|
if (this.child)
|
|
66
69
|
return;
|
|
67
70
|
const command = this.options.command ?? "codex";
|
|
@@ -74,6 +77,12 @@ export class CodexAppServerClient {
|
|
|
74
77
|
stdio: ["pipe", "pipe", "pipe"],
|
|
75
78
|
shell: false,
|
|
76
79
|
});
|
|
80
|
+
this.child.stdin.on("error", (error) => {
|
|
81
|
+
// Failed spawns have no pid; ChildProcess.error owns their OS diagnostic.
|
|
82
|
+
if (this.child?.pid === undefined)
|
|
83
|
+
return;
|
|
84
|
+
this.failInput(error);
|
|
85
|
+
});
|
|
77
86
|
this.child.stdout.setEncoding("utf8");
|
|
78
87
|
this.lines = createInterface({ input: this.child.stdout, crlfDelay: Infinity });
|
|
79
88
|
this.lines.on("line", (line) => this.acceptLine(line));
|
|
@@ -118,6 +127,8 @@ export class CodexAppServerClient {
|
|
|
118
127
|
this.handleLine(line);
|
|
119
128
|
}
|
|
120
129
|
handleLine(line) {
|
|
130
|
+
if (this.closed)
|
|
131
|
+
return;
|
|
121
132
|
if (line.length > MAX_LINE_BYTES) {
|
|
122
133
|
// Oversized is a transport failure: parsing it would stall the event loop.
|
|
123
134
|
this.failAll("app-server sent an oversized line");
|
|
@@ -212,7 +223,17 @@ export class CodexAppServerClient {
|
|
|
212
223
|
}, timeoutMs);
|
|
213
224
|
}
|
|
214
225
|
this.pending.set(id, entry);
|
|
215
|
-
|
|
226
|
+
try {
|
|
227
|
+
this.write({ id, method, params: params ?? {} });
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
// Invalid caller data was never written. Release only this request;
|
|
231
|
+
// serialization failures say nothing about the transport's health.
|
|
232
|
+
this.pending.delete(id);
|
|
233
|
+
if (entry.timer)
|
|
234
|
+
clearTimeout(entry.timer);
|
|
235
|
+
reject(error);
|
|
236
|
+
}
|
|
216
237
|
});
|
|
217
238
|
}
|
|
218
239
|
notify(method, params) {
|
|
@@ -222,9 +243,27 @@ export class CodexAppServerClient {
|
|
|
222
243
|
}
|
|
223
244
|
write(payload) {
|
|
224
245
|
const child = this.child;
|
|
225
|
-
if (
|
|
246
|
+
if (this.closed)
|
|
226
247
|
return;
|
|
227
|
-
child.
|
|
248
|
+
if (child && child.pid === undefined)
|
|
249
|
+
return;
|
|
250
|
+
if (!child || child.stdin.destroyed || !child.stdin.writable) {
|
|
251
|
+
this.failInput();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const line = `${JSON.stringify(payload)}\n`;
|
|
255
|
+
try {
|
|
256
|
+
child.stdin.write(line);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
this.failInput(error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
failInput(error) {
|
|
263
|
+
this.failAll(`app-server input pipe closed${error?.code ? ` (${diagnosticText(error.code)})` : ""}`);
|
|
264
|
+
// Reject outstanding RPCs now and terminate even when the process keeps
|
|
265
|
+
// running after closing fd 0. close() owns one bounded cleanup sequence.
|
|
266
|
+
void this.close();
|
|
228
267
|
}
|
|
229
268
|
failAll(reason, details = {}) {
|
|
230
269
|
if (this.closed)
|
|
@@ -255,10 +294,18 @@ export class CodexAppServerClient {
|
|
|
255
294
|
return this.closed;
|
|
256
295
|
}
|
|
257
296
|
/** Stop via stdin EOF, then SIGTERM and SIGKILL if the server does not exit. */
|
|
258
|
-
|
|
297
|
+
close() {
|
|
298
|
+
if (!this.closing) {
|
|
299
|
+
// Publish cleanup ownership before notifying callers, whose onClose may
|
|
300
|
+
// itself call close(). One process must have only one escalation timer.
|
|
301
|
+
this.closing = Promise.resolve().then(() => this.stop());
|
|
302
|
+
this.failAll("app-server client closed");
|
|
303
|
+
this.lines?.close();
|
|
304
|
+
}
|
|
305
|
+
return this.closing;
|
|
306
|
+
}
|
|
307
|
+
async stop() {
|
|
259
308
|
const child = this.child;
|
|
260
|
-
this.failAll("app-server client closed");
|
|
261
|
-
this.lines?.close();
|
|
262
309
|
// Failed spawns have no pid and never emit `exit`. A process already killed
|
|
263
310
|
// by a signal likewise has no numeric exitCode and will not emit it again.
|
|
264
311
|
if (!child || !child.pid || child.exitCode !== null || child.signalCode !== null)
|
|
@@ -48,6 +48,21 @@ function toolArgsOf(item) {
|
|
|
48
48
|
}
|
|
49
49
|
return args;
|
|
50
50
|
}
|
|
51
|
+
function toolErrorOf(item, type) {
|
|
52
|
+
const detail = str(asRecord(item.error)?.message) ?? str(item.error);
|
|
53
|
+
if (detail)
|
|
54
|
+
return detail;
|
|
55
|
+
// A completed app-server item means the operation settled, not that its
|
|
56
|
+
// command succeeded. In particular, nonzero exits often have no error object.
|
|
57
|
+
if (type === "commandExecution" && Number.isInteger(item.exitCode) && item.exitCode !== 0) {
|
|
58
|
+
return `Command exited with code ${item.exitCode}.`;
|
|
59
|
+
}
|
|
60
|
+
if (item.status === "failed")
|
|
61
|
+
return `${type} failed.`;
|
|
62
|
+
if (item.status === "declined")
|
|
63
|
+
return `${type} was declined.`;
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
51
66
|
/**
|
|
52
67
|
* Map a Codex turn status onto a CodeShell `TerminalReason`.
|
|
53
68
|
*
|
|
@@ -350,8 +365,7 @@ export class CodexEventTranslator {
|
|
|
350
365
|
output = String(rawOutput);
|
|
351
366
|
}
|
|
352
367
|
}
|
|
353
|
-
const
|
|
354
|
-
const errorMessage = str(error?.message) ?? str(item.error);
|
|
368
|
+
const errorMessage = toolErrorOf(item, type);
|
|
355
369
|
const events = [];
|
|
356
370
|
// Correct the arguments first when they only materialized now, so the
|
|
357
371
|
// recorded call shows what actually ran before its result.
|
|
@@ -77,6 +77,7 @@ export declare class CodexRuntime {
|
|
|
77
77
|
private readonly log;
|
|
78
78
|
private translator?;
|
|
79
79
|
private threadId?;
|
|
80
|
+
private reportedModel?;
|
|
80
81
|
private started;
|
|
81
82
|
private resumed;
|
|
82
83
|
private firstTurn;
|
|
@@ -85,6 +86,8 @@ export declare class CodexRuntime {
|
|
|
85
86
|
/** Codex thread id, once the thread exists. Protocol routing only — never the
|
|
86
87
|
* authorization subject (§8.1). */
|
|
87
88
|
get runtimeSessionId(): string | undefined;
|
|
89
|
+
/** CLI-reported model, falling back to the requested model until reported. */
|
|
90
|
+
get resolvedModel(): string | undefined;
|
|
88
91
|
/**
|
|
89
92
|
* Spawn the app-server, handshake, and open a thread.
|
|
90
93
|
*
|
|
@@ -25,6 +25,7 @@ export class CodexRuntime {
|
|
|
25
25
|
log;
|
|
26
26
|
translator;
|
|
27
27
|
threadId;
|
|
28
|
+
reportedModel;
|
|
28
29
|
started = false;
|
|
29
30
|
resumed = false;
|
|
30
31
|
firstTurn = true;
|
|
@@ -52,6 +53,10 @@ export class CodexRuntime {
|
|
|
52
53
|
get runtimeSessionId() {
|
|
53
54
|
return this.threadId;
|
|
54
55
|
}
|
|
56
|
+
/** CLI-reported model, falling back to the requested model until reported. */
|
|
57
|
+
get resolvedModel() {
|
|
58
|
+
return this.reportedModel ?? this.options.model;
|
|
59
|
+
}
|
|
55
60
|
/**
|
|
56
61
|
* Spawn the app-server, handshake, and open a thread.
|
|
57
62
|
*
|
|
@@ -107,6 +112,10 @@ export class CodexRuntime {
|
|
|
107
112
|
throw new Error(`${this.resumed ? "thread/resume" : "thread/start"} returned no thread id`);
|
|
108
113
|
}
|
|
109
114
|
this.threadId = threadId;
|
|
115
|
+
const reportedModel = thread?.model;
|
|
116
|
+
if (typeof reportedModel === "string" && reportedModel.trim()) {
|
|
117
|
+
this.reportedModel = reportedModel.trim();
|
|
118
|
+
}
|
|
110
119
|
this.translator = new CodexEventTranslator({
|
|
111
120
|
threadId,
|
|
112
121
|
sessionId: this.options.businessSessionId,
|
|
@@ -35,6 +35,8 @@ export { writeClaudeMcpConfigFile } from "./claude-code/mcp-config.js";
|
|
|
35
35
|
export { ClaudeEventTranslator } from "./claude-code/event-translator.js";
|
|
36
36
|
export type { ClaudeEventTranslatorOptions } from "./claude-code/event-translator.js";
|
|
37
37
|
export { ClaudeCodeRuntime } from "./claude-code/runtime.js";
|
|
38
|
+
export { runClaudeTextInference } from "./claude-code/text-inference.js";
|
|
39
|
+
export type { ClaudeTextInferenceOptions } from "./claude-code/text-inference.js";
|
|
38
40
|
export { CLAUDE_PERMISSION_MODES, CLAUDE_PERMISSION_PROMPT_TOOL, claudeModeUsesPermissionPrompt, claudePermissionModeFor, withClaudePermissionPrompt, } from "./claude-code/permissions.js";
|
|
39
41
|
export type { ClaudePermissionMode, ClaudePermissionPromptDecider, ClaudePermissionPromptRequest, } from "./claude-code/permissions.js";
|
|
40
42
|
export type { ClaudeRuntimeOptions, ClaudeRuntimeHooks, ClaudeTurnHandle, } from "./claude-code/runtime.js";
|
|
@@ -26,5 +26,6 @@ export { buildClaudeMcpConfig, claudeAllowedToolNames, claudeBridgeArgs, CLAUDE_
|
|
|
26
26
|
export { writeClaudeMcpConfigFile } from "./claude-code/mcp-config.js";
|
|
27
27
|
export { ClaudeEventTranslator } from "./claude-code/event-translator.js";
|
|
28
28
|
export { ClaudeCodeRuntime } from "./claude-code/runtime.js";
|
|
29
|
+
export { runClaudeTextInference } from "./claude-code/text-inference.js";
|
|
29
30
|
export { CLAUDE_PERMISSION_MODES, CLAUDE_PERMISSION_PROMPT_TOOL, claudeModeUsesPermissionPrompt, claudePermissionModeFor, withClaudePermissionPrompt, } from "./claude-code/permissions.js";
|
|
30
31
|
export { startExternalRuntimeSession } from "./session-factory.js";
|
|
@@ -98,6 +98,8 @@ export interface ExternalRuntimeSession {
|
|
|
98
98
|
readonly businessSessionId: string;
|
|
99
99
|
/** Runtime-side id (Codex thread / Claude session). Resume + routing only. */
|
|
100
100
|
readonly runtimeSessionId: string | undefined;
|
|
101
|
+
/** Latest CLI-reported model; requested model is the fallback when unavailable. */
|
|
102
|
+
readonly resolvedModel?: string;
|
|
101
103
|
/** Tools actually exposed, after the allowlist and visibility guards. */
|
|
102
104
|
listTools(): readonly ToolDefinition[];
|
|
103
105
|
send(input: ExternalRuntimeTurnInput, onEvent?: (event: StreamEvent) => void): Promise<{
|
|
@@ -160,6 +160,9 @@ export async function startExternalRuntimeSession(options) {
|
|
|
160
160
|
get runtimeSessionId() {
|
|
161
161
|
return activeRuntime.runtimeSessionId;
|
|
162
162
|
},
|
|
163
|
+
get resolvedModel() {
|
|
164
|
+
return activeRuntime.resolvedModel;
|
|
165
|
+
},
|
|
163
166
|
listTools: () => activeHost.listTools(),
|
|
164
167
|
send: async (input, onEvent) => {
|
|
165
168
|
if (turnSink)
|
|
@@ -89,6 +89,8 @@ export interface RemoveWorktreeResult {
|
|
|
89
89
|
}
|
|
90
90
|
export interface RemoveWorktreeOptions {
|
|
91
91
|
prefix?: string;
|
|
92
|
+
/** False lets Git reject working-tree changes made after the caller's preflight. */
|
|
93
|
+
force?: boolean;
|
|
92
94
|
}
|
|
93
95
|
export interface WorktreeChangeState {
|
|
94
96
|
uncommitted: boolean;
|
|
@@ -356,7 +356,7 @@ export function removeWorktree(worktreePath, removeBranch = false, opts = {}) {
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
try {
|
|
359
|
-
execGitSync(mainRoot, ["worktree", "remove", worktreePath, "--force"], 30000);
|
|
359
|
+
execGitSync(mainRoot, ["worktree", "remove", worktreePath, ...(opts.force === false ? [] : ["--force"])], 30000);
|
|
360
360
|
}
|
|
361
361
|
catch (err) {
|
|
362
362
|
throw new Error(`failed to remove worktree ${worktreePath}: ${gitErrorMessage(err)}`, {
|
package/dist/lsp/client.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare class LSPClient extends EventEmitter {
|
|
|
28
28
|
private buffer;
|
|
29
29
|
private contentLength;
|
|
30
30
|
private initialized;
|
|
31
|
+
private failure;
|
|
31
32
|
constructor(command: string, args?: string[], cwd?: string | undefined);
|
|
32
33
|
start(): Promise<void>;
|
|
33
34
|
initialize(rootUri: string): Promise<unknown>;
|
|
@@ -36,6 +37,8 @@ export declare class LSPClient extends EventEmitter {
|
|
|
36
37
|
shutdown(): Promise<void>;
|
|
37
38
|
get isInitialized(): boolean;
|
|
38
39
|
get isAlive(): boolean;
|
|
40
|
+
private assertWritable;
|
|
41
|
+
private fail;
|
|
39
42
|
private handleData;
|
|
40
43
|
private rejectAll;
|
|
41
44
|
}
|
package/dist/lsp/client.js
CHANGED
|
@@ -18,6 +18,7 @@ export class LSPClient extends EventEmitter {
|
|
|
18
18
|
buffer = Buffer.alloc(0);
|
|
19
19
|
contentLength = -1;
|
|
20
20
|
initialized = false;
|
|
21
|
+
failure;
|
|
21
22
|
constructor(command, args = [], cwd) {
|
|
22
23
|
super();
|
|
23
24
|
this.command = command;
|
|
@@ -25,21 +26,56 @@ export class LSPClient extends EventEmitter {
|
|
|
25
26
|
this.cwd = cwd;
|
|
26
27
|
}
|
|
27
28
|
async start() {
|
|
28
|
-
|
|
29
|
+
if (this.isAlive)
|
|
30
|
+
return;
|
|
31
|
+
this.failure = undefined;
|
|
32
|
+
this.initialized = false;
|
|
33
|
+
this.buffer = Buffer.alloc(0);
|
|
34
|
+
this.contentLength = -1;
|
|
35
|
+
const child = spawn(this.command, this.args, {
|
|
29
36
|
cwd: this.cwd,
|
|
30
37
|
stdio: ["pipe", "pipe", "pipe"],
|
|
31
38
|
});
|
|
32
|
-
this.process
|
|
33
|
-
|
|
39
|
+
this.process = child;
|
|
40
|
+
const fail = (error) => {
|
|
41
|
+
if (this.process !== child)
|
|
42
|
+
return;
|
|
43
|
+
this.fail(error);
|
|
44
|
+
};
|
|
45
|
+
child.stdout?.on("data", (data) => {
|
|
46
|
+
if (this.process === child && !this.failure)
|
|
47
|
+
this.handleData(data);
|
|
48
|
+
});
|
|
49
|
+
child.stderr?.on("data", (_data) => {
|
|
34
50
|
// LSP servers often log to stderr — ignore
|
|
35
51
|
});
|
|
36
|
-
|
|
52
|
+
// Pipe errors are emitted on the streams, not on ChildProcess. Keep these
|
|
53
|
+
// listeners through teardown so late EPIPE events cannot escape the host.
|
|
54
|
+
child.stdin?.on("error", fail);
|
|
55
|
+
child.stdout?.on("error", fail);
|
|
56
|
+
child.stderr?.on("error", fail);
|
|
57
|
+
child.on("error", fail);
|
|
58
|
+
child.on("exit", (code, signal) => {
|
|
59
|
+
if (this.process !== child)
|
|
60
|
+
return;
|
|
61
|
+
this.fail(new Error(`LSP server exited with ${signal ? `signal ${signal}` : `code ${code}`}`));
|
|
37
62
|
this.emit("exit", code);
|
|
38
|
-
this.rejectAll(new Error(`LSP server exited with code ${code}`));
|
|
39
63
|
});
|
|
40
64
|
// Don't keep the event loop alive solely for the LSP child.
|
|
41
65
|
// Call after listeners are wired so event registration doesn't re-ref.
|
|
42
|
-
|
|
66
|
+
child.unref();
|
|
67
|
+
await new Promise((resolve, reject) => {
|
|
68
|
+
const onSpawn = () => {
|
|
69
|
+
child.off("error", onError);
|
|
70
|
+
resolve();
|
|
71
|
+
};
|
|
72
|
+
const onError = (error) => {
|
|
73
|
+
child.off("spawn", onSpawn);
|
|
74
|
+
reject(error);
|
|
75
|
+
};
|
|
76
|
+
child.once("spawn", onSpawn);
|
|
77
|
+
child.once("error", onError);
|
|
78
|
+
});
|
|
43
79
|
}
|
|
44
80
|
async initialize(rootUri) {
|
|
45
81
|
const result = await this.request("initialize", {
|
|
@@ -63,8 +99,7 @@ export class LSPClient extends EventEmitter {
|
|
|
63
99
|
return result;
|
|
64
100
|
}
|
|
65
101
|
async request(method, params) {
|
|
66
|
-
|
|
67
|
-
throw new Error("LSP server not started");
|
|
102
|
+
this.assertWritable();
|
|
68
103
|
const id = this.nextId++;
|
|
69
104
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
70
105
|
const content = JSON.stringify(message);
|
|
@@ -88,43 +123,79 @@ export class LSPClient extends EventEmitter {
|
|
|
88
123
|
reject(err);
|
|
89
124
|
},
|
|
90
125
|
});
|
|
91
|
-
|
|
126
|
+
try {
|
|
127
|
+
this.process.stdin.write(header + content);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
131
|
+
}
|
|
92
132
|
});
|
|
93
133
|
}
|
|
94
134
|
async notify(method, params) {
|
|
95
|
-
|
|
96
|
-
throw new Error("LSP server not started");
|
|
135
|
+
this.assertWritable();
|
|
97
136
|
const message = { jsonrpc: "2.0", method, params };
|
|
98
137
|
const content = JSON.stringify(message);
|
|
99
138
|
const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
|
|
100
|
-
|
|
139
|
+
try {
|
|
140
|
+
this.process.stdin.write(header + content);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
101
146
|
}
|
|
102
147
|
async shutdown() {
|
|
103
|
-
|
|
148
|
+
const child = this.process;
|
|
149
|
+
if (!child)
|
|
104
150
|
return;
|
|
105
151
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
152
|
+
if (this.isAlive && this.initialized) {
|
|
153
|
+
await this.request("shutdown", {});
|
|
154
|
+
await this.notify("exit", {});
|
|
155
|
+
}
|
|
108
156
|
}
|
|
109
157
|
catch {
|
|
110
158
|
// Force kill
|
|
111
159
|
}
|
|
112
|
-
this.process
|
|
113
|
-
|
|
114
|
-
this.
|
|
115
|
-
this.process.kill();
|
|
160
|
+
if (this.process !== child)
|
|
161
|
+
return;
|
|
162
|
+
this.fail(new Error("LSP server shut down"));
|
|
116
163
|
this.process = undefined;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
this.pending.clear();
|
|
164
|
+
child.stdin?.destroy();
|
|
165
|
+
child.stdout?.destroy();
|
|
166
|
+
child.stderr?.destroy();
|
|
122
167
|
}
|
|
123
168
|
get isInitialized() {
|
|
124
169
|
return this.initialized;
|
|
125
170
|
}
|
|
126
171
|
get isAlive() {
|
|
127
|
-
return this.process
|
|
172
|
+
return (!!this.process &&
|
|
173
|
+
!this.failure &&
|
|
174
|
+
!this.process.killed &&
|
|
175
|
+
this.process.exitCode === null &&
|
|
176
|
+
this.process.signalCode === null);
|
|
177
|
+
}
|
|
178
|
+
assertWritable() {
|
|
179
|
+
if (!this.isAlive || !this.process?.stdin || this.process.stdin.destroyed)
|
|
180
|
+
throw this.failure ?? new Error("LSP server is not running");
|
|
181
|
+
}
|
|
182
|
+
fail(error) {
|
|
183
|
+
if (this.failure)
|
|
184
|
+
return;
|
|
185
|
+
this.failure = error;
|
|
186
|
+
this.initialized = false;
|
|
187
|
+
this.rejectAll(error);
|
|
188
|
+
const child = this.process;
|
|
189
|
+
if (child?.pid && child.exitCode === null && child.signalCode === null) {
|
|
190
|
+
child.kill();
|
|
191
|
+
const timer = setTimeout(() => {
|
|
192
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
193
|
+
child.kill("SIGKILL");
|
|
194
|
+
}, 500);
|
|
195
|
+
timer.unref();
|
|
196
|
+
child.once("exit", () => clearTimeout(timer));
|
|
197
|
+
}
|
|
198
|
+
this.emit("failure", error);
|
|
128
199
|
}
|
|
129
200
|
handleData(data) {
|
|
130
201
|
// Append the new chunk as bytes. Buffer.concat avoids the UTF-8
|
package/dist/lsp/manager.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ type ServerState = "stopped" | "starting" | "ready" | "error";
|
|
|
7
7
|
export declare class LSPServerManager {
|
|
8
8
|
private servers;
|
|
9
9
|
private rootUri;
|
|
10
|
+
private stopping;
|
|
10
11
|
constructor(cwd: string);
|
|
11
12
|
/**
|
|
12
13
|
* Get or start a language server by name.
|
|
@@ -33,6 +34,7 @@ export declare class LSPServerManager {
|
|
|
33
34
|
* Shutdown all servers.
|
|
34
35
|
*/
|
|
35
36
|
shutdownAll(): Promise<void>;
|
|
37
|
+
private stopServers;
|
|
36
38
|
}
|
|
37
39
|
export declare function isCommandAvailable(command: string, env?: NodeJS.ProcessEnv): boolean;
|
|
38
40
|
export declare function initializeLSPManager(cwd: string): LSPServerManager;
|
package/dist/lsp/manager.js
CHANGED
|
@@ -11,6 +11,7 @@ import { rootUriToPath } from "./root-path.js";
|
|
|
11
11
|
export class LSPServerManager {
|
|
12
12
|
servers = new Map();
|
|
13
13
|
rootUri;
|
|
14
|
+
stopping;
|
|
14
15
|
constructor(cwd) {
|
|
15
16
|
this.rootUri = pathToFileURL(cwd).href;
|
|
16
17
|
// Register all built-in servers
|
|
@@ -22,19 +23,25 @@ export class LSPServerManager {
|
|
|
22
23
|
* Get or start a language server by name.
|
|
23
24
|
*/
|
|
24
25
|
async getClient(serverName) {
|
|
26
|
+
if (this.stopping)
|
|
27
|
+
return undefined;
|
|
25
28
|
const managed = this.servers.get(serverName);
|
|
26
29
|
if (!managed)
|
|
27
30
|
return undefined;
|
|
28
31
|
if (managed.state === "ready" && managed.client?.isAlive) {
|
|
29
32
|
return managed.client;
|
|
30
33
|
}
|
|
31
|
-
if (managed.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
if (managed.starting)
|
|
35
|
+
return managed.starting;
|
|
36
|
+
const starting = this.startServer(serverName);
|
|
37
|
+
managed.starting = starting;
|
|
38
|
+
try {
|
|
39
|
+
return await starting;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (managed.starting === starting)
|
|
43
|
+
managed.starting = undefined;
|
|
35
44
|
}
|
|
36
|
-
// Try to start
|
|
37
|
-
return this.startServer(serverName);
|
|
38
45
|
}
|
|
39
46
|
/**
|
|
40
47
|
* Start a specific language server.
|
|
@@ -50,17 +57,37 @@ export class LSPServerManager {
|
|
|
50
57
|
return undefined;
|
|
51
58
|
}
|
|
52
59
|
managed.state = "starting";
|
|
60
|
+
managed.error = undefined;
|
|
61
|
+
const client = new LSPClient(managed.config.command, managed.config.args, rootUriToPath(this.rootUri));
|
|
62
|
+
managed.client = client;
|
|
63
|
+
client.on("failure", (error) => {
|
|
64
|
+
if (managed.client !== client)
|
|
65
|
+
return;
|
|
66
|
+
managed.state = "error";
|
|
67
|
+
managed.error = error.message;
|
|
68
|
+
});
|
|
53
69
|
try {
|
|
54
|
-
const client = new LSPClient(managed.config.command, managed.config.args, rootUriToPath(this.rootUri));
|
|
55
70
|
await client.start();
|
|
71
|
+
if (managed.client !== client) {
|
|
72
|
+
await client.shutdown();
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
56
75
|
await client.initialize(this.rootUri);
|
|
57
|
-
|
|
76
|
+
// Shutdown can retire a startup while initialize is in flight.
|
|
77
|
+
if (managed.client !== client) {
|
|
78
|
+
await client.shutdown();
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
58
81
|
managed.state = "ready";
|
|
59
82
|
return client;
|
|
60
83
|
}
|
|
61
84
|
catch (err) {
|
|
62
|
-
managed.
|
|
63
|
-
|
|
85
|
+
if (managed.client === client) {
|
|
86
|
+
managed.state = "error";
|
|
87
|
+
managed.error = err.message;
|
|
88
|
+
managed.client = undefined;
|
|
89
|
+
}
|
|
90
|
+
await client.shutdown();
|
|
64
91
|
return undefined;
|
|
65
92
|
}
|
|
66
93
|
}
|
|
@@ -88,18 +115,32 @@ export class LSPServerManager {
|
|
|
88
115
|
/**
|
|
89
116
|
* Shutdown all servers.
|
|
90
117
|
*/
|
|
91
|
-
|
|
118
|
+
shutdownAll() {
|
|
119
|
+
// Fence new starts for the entire sweep, including while a previous
|
|
120
|
+
// server's shutdown handshake is pending. Registry eviction must not leave
|
|
121
|
+
// a replacement process in a manager that no longer has an owner.
|
|
122
|
+
if (!this.stopping) {
|
|
123
|
+
this.stopping = Promise.resolve()
|
|
124
|
+
.then(() => this.stopServers())
|
|
125
|
+
.finally(() => {
|
|
126
|
+
this.stopping = undefined;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return this.stopping;
|
|
130
|
+
}
|
|
131
|
+
async stopServers() {
|
|
92
132
|
for (const server of this.servers.values()) {
|
|
93
|
-
|
|
133
|
+
const client = server.client;
|
|
134
|
+
server.state = "stopped";
|
|
135
|
+
server.client = undefined;
|
|
136
|
+
if (client) {
|
|
94
137
|
try {
|
|
95
|
-
await
|
|
138
|
+
await client.shutdown();
|
|
96
139
|
}
|
|
97
140
|
catch {
|
|
98
141
|
// Force killed
|
|
99
142
|
}
|
|
100
143
|
}
|
|
101
|
-
server.state = "stopped";
|
|
102
|
-
server.client = undefined;
|
|
103
144
|
}
|
|
104
145
|
}
|
|
105
146
|
}
|
package/dist/prompt/coding.md
CHANGED
|
@@ -30,6 +30,7 @@ The default job in this preset is software engineering in the current working di
|
|
|
30
30
|
|
|
31
31
|
- When referencing specific functions or pieces of code include the pattern file_path:line_number to allow the user to easily navigate to the source code location.
|
|
32
32
|
- When referencing GitHub issues or pull requests, use the owner/repo#123 format so they render as clickable links.
|
|
33
|
+
- Before deleting a task branch, check both its merge target and configured upstream. `git branch -d` can refuse deletion when the branch is merged into local `HEAD` but its upstream still lags. Preserve that protection and report local versus remote integration accurately; do not automatically retry with `-D` or treat the refusal as a failed merge.
|
|
33
34
|
|
|
34
35
|
## Opening URLs and local previews
|
|
35
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cjhyy/code-shell-capability-coding",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.47",
|
|
4
4
|
"description": "Coding capability pack for the generic code-shell agent core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
42
|
+
"@cjhyy/code-shell-core": "0.9.47"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=20.10"
|