@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
|
@@ -7,6 +7,16 @@ import { textWithAttachmentReferences } from "../turn-input.js";
|
|
|
7
7
|
const CRITICAL_RPC_TIMEOUT_MS = 60_000;
|
|
8
8
|
/** Interrupt is a fail-safe; an unbounded wait silently defeats it. */
|
|
9
9
|
const INTERRUPT_TIMEOUT_MS = 10_000;
|
|
10
|
+
/** Read a turn id from a `turn/started` / `turn/completed` notification. */
|
|
11
|
+
function notificationTurnId(params) {
|
|
12
|
+
if (!params || typeof params !== "object")
|
|
13
|
+
return undefined;
|
|
14
|
+
const record = params;
|
|
15
|
+
const turn = record.turn && typeof record.turn === "object" ? record.turn : {};
|
|
16
|
+
if (typeof turn.id === "string" && turn.id)
|
|
17
|
+
return turn.id;
|
|
18
|
+
return typeof record.turnId === "string" && record.turnId ? record.turnId : undefined;
|
|
19
|
+
}
|
|
10
20
|
export class CodexRuntime {
|
|
11
21
|
options;
|
|
12
22
|
hooks;
|
|
@@ -15,6 +25,7 @@ export class CodexRuntime {
|
|
|
15
25
|
log;
|
|
16
26
|
translator;
|
|
17
27
|
threadId;
|
|
28
|
+
reportedModel;
|
|
18
29
|
started = false;
|
|
19
30
|
resumed = false;
|
|
20
31
|
firstTurn = true;
|
|
@@ -42,6 +53,10 @@ export class CodexRuntime {
|
|
|
42
53
|
get runtimeSessionId() {
|
|
43
54
|
return this.threadId;
|
|
44
55
|
}
|
|
56
|
+
/** CLI-reported model, falling back to the requested model until reported. */
|
|
57
|
+
get resolvedModel() {
|
|
58
|
+
return this.reportedModel ?? this.options.model;
|
|
59
|
+
}
|
|
45
60
|
/**
|
|
46
61
|
* Spawn the app-server, handshake, and open a thread.
|
|
47
62
|
*
|
|
@@ -55,6 +70,7 @@ export class CodexRuntime {
|
|
|
55
70
|
this.started = true;
|
|
56
71
|
this.client.onNotification((method, params) => this.onNotification(method, params));
|
|
57
72
|
this.client.onServerRequest((method, params) => this.onServerRequest(method, params));
|
|
73
|
+
this.client.onClose((reason) => this.onTransportClosed(reason));
|
|
58
74
|
this.client.start();
|
|
59
75
|
await this.client.request("initialize", {
|
|
60
76
|
clientInfo: { name: "codeshell", title: "CodeShell", version: "1" },
|
|
@@ -96,6 +112,10 @@ export class CodexRuntime {
|
|
|
96
112
|
throw new Error(`${this.resumed ? "thread/resume" : "thread/start"} returned no thread id`);
|
|
97
113
|
}
|
|
98
114
|
this.threadId = threadId;
|
|
115
|
+
const reportedModel = thread?.model;
|
|
116
|
+
if (typeof reportedModel === "string" && reportedModel.trim()) {
|
|
117
|
+
this.reportedModel = reportedModel.trim();
|
|
118
|
+
}
|
|
99
119
|
this.translator = new CodexEventTranslator({
|
|
100
120
|
threadId,
|
|
101
121
|
sessionId: this.options.businessSessionId,
|
|
@@ -125,9 +145,21 @@ export class CodexRuntime {
|
|
|
125
145
|
async send(input, onEvent) {
|
|
126
146
|
if (!this.threadId)
|
|
127
147
|
throw new Error("CodexRuntime.send() before start()");
|
|
148
|
+
// A dead app-server can never run another turn. Fail now, before any turn
|
|
149
|
+
// state exists, so the host rebuilds the runtime instead of waiting.
|
|
150
|
+
if (this.client.isClosed) {
|
|
151
|
+
throw new Error(`Codex app-server is not running (${this.client.closedReason ?? "closed"}); ` +
|
|
152
|
+
`the runtime must be restarted`);
|
|
153
|
+
}
|
|
128
154
|
let resolveDone;
|
|
129
155
|
const done = new Promise((resolve) => (resolveDone = resolve));
|
|
130
|
-
|
|
156
|
+
const turn = {
|
|
157
|
+
resolve: resolveDone,
|
|
158
|
+
onEvent,
|
|
159
|
+
starting: true,
|
|
160
|
+
interruptSent: false,
|
|
161
|
+
};
|
|
162
|
+
this.activeTurn = turn;
|
|
131
163
|
let text = textWithAttachmentReferences(input);
|
|
132
164
|
if (this.firstTurn && !this.resumed && this.options.initialContext) {
|
|
133
165
|
text = `${this.options.initialContext}\n\n<current_user_request>\n${text}\n</current_user_request>`;
|
|
@@ -142,18 +174,36 @@ export class CodexRuntime {
|
|
|
142
174
|
? { detail: attachment.detail }
|
|
143
175
|
: {}),
|
|
144
176
|
}));
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
let response;
|
|
178
|
+
try {
|
|
179
|
+
response = (await this.client.request("turn/start", {
|
|
180
|
+
threadId: this.threadId,
|
|
181
|
+
...(input.clientMessageId ? { clientUserMessageId: input.clientMessageId } : {}),
|
|
182
|
+
// `text_elements` is required by the protocol even when empty.
|
|
183
|
+
input: [{ type: "text", text, text_elements: [] }, ...imageInputs],
|
|
184
|
+
}, CRITICAL_RPC_TIMEOUT_MS));
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
// The rejection is the caller's terminal signal for this turn. Release the
|
|
188
|
+
// turn state so a Stop waiting on its id, or a later close, does not hang
|
|
189
|
+
// on a turn the caller never received.
|
|
190
|
+
turn.starting = false;
|
|
191
|
+
if (this.activeTurn === turn)
|
|
192
|
+
this.activeTurn = undefined;
|
|
193
|
+
turn.pendingInterrupt?.resolve();
|
|
194
|
+
turn.pendingInterrupt = undefined;
|
|
195
|
+
turn.resolve();
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
turn.starting = false;
|
|
199
|
+
// The response is authoritative for the id, but `turn/started` may already
|
|
200
|
+
// have supplied it (and already delivered a deferred Stop).
|
|
151
201
|
const turnId = response?.turn?.id;
|
|
152
|
-
if (
|
|
153
|
-
this.
|
|
202
|
+
if (turnId)
|
|
203
|
+
this.bindTurnId(turn, turnId);
|
|
154
204
|
return {
|
|
155
205
|
get turnId() {
|
|
156
|
-
return
|
|
206
|
+
return turn.id;
|
|
157
207
|
},
|
|
158
208
|
done,
|
|
159
209
|
};
|
|
@@ -167,8 +217,45 @@ export class CodexRuntime {
|
|
|
167
217
|
* that no further step starts (§13.3).
|
|
168
218
|
*/
|
|
169
219
|
async interrupt() {
|
|
170
|
-
const
|
|
171
|
-
if (!this.threadId || !
|
|
220
|
+
const turn = this.activeTurn;
|
|
221
|
+
if (!this.threadId || !turn)
|
|
222
|
+
return;
|
|
223
|
+
if (turn.id)
|
|
224
|
+
return this.requestInterrupt(turn.id);
|
|
225
|
+
// Stop between sending turn/start and learning the turn id: Codex may
|
|
226
|
+
// already be running the turn, so returning here would silently drop the
|
|
227
|
+
// Stop. Remember it, and send turn/interrupt the moment the id is known.
|
|
228
|
+
if (!turn.starting)
|
|
229
|
+
return;
|
|
230
|
+
if (!turn.pendingInterrupt) {
|
|
231
|
+
let resolve;
|
|
232
|
+
let reject;
|
|
233
|
+
const promise = new Promise((res, rej) => {
|
|
234
|
+
resolve = res;
|
|
235
|
+
reject = rej;
|
|
236
|
+
});
|
|
237
|
+
// Every caller awaits through the bounded race below; this only keeps a
|
|
238
|
+
// rejection that arrives after that race from becoming unhandled.
|
|
239
|
+
promise.catch(() => { });
|
|
240
|
+
turn.pendingInterrupt = { promise, resolve, reject };
|
|
241
|
+
}
|
|
242
|
+
// Bounded like the RPC itself. A timeout leaves the Stop recorded: it is
|
|
243
|
+
// still delivered if the id arrives later, but the caller learns now that
|
|
244
|
+
// the stop has not landed.
|
|
245
|
+
let timer;
|
|
246
|
+
const timeout = new Promise((_, reject) => {
|
|
247
|
+
timer = setTimeout(() => reject(new Error(`Codex did not report the turn id within ${INTERRUPT_TIMEOUT_MS}ms; ` +
|
|
248
|
+
`the interrupt has not been delivered yet`)), INTERRUPT_TIMEOUT_MS);
|
|
249
|
+
});
|
|
250
|
+
try {
|
|
251
|
+
await Promise.race([turn.pendingInterrupt.promise, timeout]);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async requestInterrupt(turnId) {
|
|
258
|
+
if (!this.threadId)
|
|
172
259
|
return;
|
|
173
260
|
try {
|
|
174
261
|
await this.client.request("turn/interrupt", { threadId: this.threadId, turnId }, INTERRUPT_TIMEOUT_MS);
|
|
@@ -185,23 +272,72 @@ export class CodexRuntime {
|
|
|
185
272
|
async close() {
|
|
186
273
|
// Settle any waiter first, so a close during a live turn cannot leave the
|
|
187
274
|
// caller awaiting forever (§13.4).
|
|
188
|
-
this.activeTurn
|
|
275
|
+
const turn = this.activeTurn;
|
|
189
276
|
this.activeTurn = undefined;
|
|
277
|
+
turn?.pendingInterrupt?.resolve();
|
|
278
|
+
turn?.resolve();
|
|
190
279
|
await this.client.close();
|
|
191
280
|
}
|
|
281
|
+
/** Learn the active turn's id; deliver a Stop that was waiting for it, once. */
|
|
282
|
+
bindTurnId(turn, turnId) {
|
|
283
|
+
turn.id = turnId;
|
|
284
|
+
const waiter = turn.pendingInterrupt;
|
|
285
|
+
if (!waiter || turn.interruptSent)
|
|
286
|
+
return;
|
|
287
|
+
turn.interruptSent = true;
|
|
288
|
+
turn.pendingInterrupt = undefined;
|
|
289
|
+
this.requestInterrupt(turnId).then(waiter.resolve, waiter.reject);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The app-server is gone (exited, failed, broken stream). A turn Codex already
|
|
293
|
+
* accepted would otherwise wait forever for a `turn/completed` that can never
|
|
294
|
+
* arrive, so report it terminal now as a model error. A turn whose
|
|
295
|
+
* `turn/start` is still unanswered is NOT reported here: that RPC rejects, and
|
|
296
|
+
* the rejected `send()` is its caller's terminal signal.
|
|
297
|
+
*/
|
|
298
|
+
onTransportClosed(reason) {
|
|
299
|
+
const turn = this.activeTurn;
|
|
300
|
+
if (!turn)
|
|
301
|
+
return;
|
|
302
|
+
this.activeTurn = undefined;
|
|
303
|
+
turn.pendingInterrupt?.resolve();
|
|
304
|
+
turn.pendingInterrupt = undefined;
|
|
305
|
+
if (!turn.starting) {
|
|
306
|
+
this.log("runtime.turn_lost", {
|
|
307
|
+
businessSessionId: this.options.businessSessionId,
|
|
308
|
+
reason: reason.slice(0, 200),
|
|
309
|
+
});
|
|
310
|
+
const sink = turn.onEvent ?? this.hooks.onEvent;
|
|
311
|
+
this.emitEvent({ type: "error", error: `Codex app-server stopped before the turn finished: ${reason}` }, sink);
|
|
312
|
+
this.emitEvent({ type: "turn_complete", reason: "model_error" }, sink);
|
|
313
|
+
}
|
|
314
|
+
turn.resolve();
|
|
315
|
+
}
|
|
192
316
|
onNotification(method, params) {
|
|
193
317
|
const events = this.translator?.translate({ method, params }) ?? [];
|
|
194
318
|
for (const event of events) {
|
|
319
|
+
// A fresh turn announced while ours has no id yet IS ours: the translator
|
|
320
|
+
// already discarded orphans of finished turns. This is the earliest point
|
|
321
|
+
// the id is known, and a Stop pressed before the RPC answers depends on it.
|
|
322
|
+
if (event.type === "stream_request_start" && method === "turn/started") {
|
|
323
|
+
const turn = this.activeTurn;
|
|
324
|
+
const turnId = notificationTurnId(params);
|
|
325
|
+
if (turn && !turn.id && turnId)
|
|
326
|
+
this.bindTurnId(turn, turnId);
|
|
327
|
+
}
|
|
195
328
|
this.emitEvent(event);
|
|
196
329
|
if (event.type === "turn_complete") {
|
|
197
|
-
this.activeTurn
|
|
330
|
+
const turn = this.activeTurn;
|
|
198
331
|
this.activeTurn = undefined;
|
|
332
|
+
turn?.pendingInterrupt?.resolve();
|
|
333
|
+
turn?.resolve();
|
|
199
334
|
}
|
|
200
335
|
}
|
|
201
336
|
}
|
|
202
|
-
emitEvent(event
|
|
337
|
+
emitEvent(event, sink = this.activeTurn?.onEvent ??
|
|
338
|
+
this.hooks.onEvent) {
|
|
203
339
|
try {
|
|
204
|
-
|
|
340
|
+
sink?.(event);
|
|
205
341
|
}
|
|
206
342
|
catch (error) {
|
|
207
343
|
this.log("runtime.event_handler_failed", {
|
|
@@ -35,6 +35,10 @@ 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";
|
|
40
|
+
export { CLAUDE_PERMISSION_MODES, CLAUDE_PERMISSION_PROMPT_TOOL, claudeModeUsesPermissionPrompt, claudePermissionModeFor, withClaudePermissionPrompt, } from "./claude-code/permissions.js";
|
|
41
|
+
export type { ClaudePermissionMode, ClaudePermissionPromptDecider, ClaudePermissionPromptRequest, } from "./claude-code/permissions.js";
|
|
38
42
|
export type { ClaudeRuntimeOptions, ClaudeRuntimeHooks, ClaudeTurnHandle, } from "./claude-code/runtime.js";
|
|
39
43
|
export type { CodexEventTranslatorOptions } from "./codex/event-translator.js";
|
|
40
44
|
export { startExternalRuntimeSession } from "./session-factory.js";
|
|
@@ -26,4 +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";
|
|
30
|
+
export { CLAUDE_PERMISSION_MODES, CLAUDE_PERMISSION_PROMPT_TOOL, claudeModeUsesPermissionPrompt, claudePermissionModeFor, withClaudePermissionPrompt, } from "./claude-code/permissions.js";
|
|
29
31
|
export { startExternalRuntimeSession } from "./session-factory.js";
|
|
@@ -23,6 +23,7 @@ import type { PermissionRule, StreamEvent, ToolDefinition } from "@cjhyy/code-sh
|
|
|
23
23
|
import { createSessionToolHost, type ExternalToolExposurePolicy, type ToolVisibilityInputs } from "@cjhyy/code-shell-core/extension";
|
|
24
24
|
import { type CodexRuntimeHooks, type CodexRuntimeOptions } from "./codex/runtime.js";
|
|
25
25
|
import { type ClaudeRuntimeHooks } from "./claude-code/runtime.js";
|
|
26
|
+
import { type ClaudePermissionMode } from "./claude-code/permissions.js";
|
|
26
27
|
import type { ExternalRuntimeTurnInput } from "./turn-input.js";
|
|
27
28
|
export type ExternalRuntimeKind = "codex" | "claude-code";
|
|
28
29
|
/**
|
|
@@ -55,6 +56,11 @@ export interface ExternalRuntimeSessionOptions {
|
|
|
55
56
|
contextOverrides?: Parameters<typeof createSessionToolHost>[0]["contextOverrides"];
|
|
56
57
|
captureRunContext?: Parameters<typeof createSessionToolHost>[0]["captureRunContext"];
|
|
57
58
|
settingsScope?: Parameters<typeof createSessionToolHost>[0]["settingsScope"];
|
|
59
|
+
/**
|
|
60
|
+
* Override the runtime's declared image capability (e.g. a host that knows
|
|
61
|
+
* the selected model is text-only). Defaults per runtime kind.
|
|
62
|
+
*/
|
|
63
|
+
modelSupportsVision?: boolean;
|
|
58
64
|
model?: string;
|
|
59
65
|
/** Runtime thread/session id recovered from durable Desktop state. */
|
|
60
66
|
resumeRuntimeSessionId?: string;
|
|
@@ -68,6 +74,14 @@ export interface ExternalRuntimeSessionOptions {
|
|
|
68
74
|
approvalPolicy?: string;
|
|
69
75
|
/** Claude Code only: extra CLI args. */
|
|
70
76
|
claudeExtraArgs?: readonly string[];
|
|
77
|
+
/**
|
|
78
|
+
* Claude Code only: `--permission-mode` for its native tools. Defaults to
|
|
79
|
+
* `claudePermissionModeFor({ permissionMode, planMode })`. A host whose
|
|
80
|
+
* user-facing selection differs from the host-tool `permissionMode` it passes
|
|
81
|
+
* (Desktop maps full access to `acceptEdits` for host tools) passes it
|
|
82
|
+
* explicitly.
|
|
83
|
+
*/
|
|
84
|
+
claudePermissionMode?: ClaudePermissionMode;
|
|
71
85
|
/**
|
|
72
86
|
* Override how the runtime process is launched (Codex: app-server client
|
|
73
87
|
* options; Claude: the `claude` binary). Exists so the assembly can be tested
|
|
@@ -84,6 +98,8 @@ export interface ExternalRuntimeSession {
|
|
|
84
98
|
readonly businessSessionId: string;
|
|
85
99
|
/** Runtime-side id (Codex thread / Claude session). Resume + routing only. */
|
|
86
100
|
readonly runtimeSessionId: string | undefined;
|
|
101
|
+
/** Latest CLI-reported model; requested model is the fallback when unavailable. */
|
|
102
|
+
readonly resolvedModel?: string;
|
|
87
103
|
/** Tools actually exposed, after the allowlist and visibility guards. */
|
|
88
104
|
listTools(): readonly ToolDefinition[];
|
|
89
105
|
send(input: ExternalRuntimeTurnInput, onEvent?: (event: StreamEvent) => void): Promise<{
|
|
@@ -3,6 +3,20 @@ import { startLoopbackMcpBridge, } from "./shared/mcp-bridge.js";
|
|
|
3
3
|
import { SessionContextStore } from "./shared/session-context-store.js";
|
|
4
4
|
import { CodexRuntime } from "./codex/runtime.js";
|
|
5
5
|
import { ClaudeCodeRuntime } from "./claude-code/runtime.js";
|
|
6
|
+
import { CLAUDE_MCP_SERVER_NAME } from "./claude-code/mcp-config.js";
|
|
7
|
+
import { CLAUDE_PERMISSION_PROMPT_TOOL, claudeModeUsesPermissionPrompt, claudePermissionModeFor, withClaudePermissionPrompt, } from "./claude-code/permissions.js";
|
|
8
|
+
/**
|
|
9
|
+
* Whether each runtime's model receives MCP image content from a tool result.
|
|
10
|
+
*
|
|
11
|
+
* Both runtimes drive vision-capable model families and forward MCP `image`
|
|
12
|
+
* content to the model. Without a declaration the host has no `llmConfig` to
|
|
13
|
+
* infer this from (the runtime picks and calls its own model), so image tools
|
|
14
|
+
* such as browser_observe(vision) failed closed and never captured pixels.
|
|
15
|
+
*/
|
|
16
|
+
const RUNTIME_ACCEPTS_TOOL_IMAGES = Object.freeze({
|
|
17
|
+
codex: true,
|
|
18
|
+
"claude-code": true,
|
|
19
|
+
});
|
|
6
20
|
/**
|
|
7
21
|
* Assemble and start an external-runtime session.
|
|
8
22
|
*
|
|
@@ -60,10 +74,30 @@ export async function startExternalRuntimeSession(options) {
|
|
|
60
74
|
getStreamCallback: () => turnSink,
|
|
61
75
|
...(options.captureRunContext ? { captureRunContext: options.captureRunContext } : {}),
|
|
62
76
|
...(options.settingsScope ? { settingsScope: options.settingsScope } : {}),
|
|
77
|
+
modelSupportsVision: options.modelSupportsVision ?? RUNTIME_ACCEPTS_TOOL_IMAGES[options.kind],
|
|
78
|
+
// `hooks` is deliberately not passed: the host then loads the same
|
|
79
|
+
// settings/plugin tool hooks a Native Engine registers for this cwd,
|
|
80
|
+
// filtered by `projectTrusted` and `settingsScope`. The runtime's own
|
|
81
|
+
// native hooks (Codex/Claude config) are separate and unaffected.
|
|
63
82
|
});
|
|
83
|
+
// Claude's NATIVE tools: an explicit mode always, and — only where the mode
|
|
84
|
+
// asks and the host can answer — a prompt tool on this session's bridge.
|
|
85
|
+
const claudePermissionMode = options.claudePermissionMode ??
|
|
86
|
+
claudePermissionModeFor({
|
|
87
|
+
permissionMode: options.permissionMode,
|
|
88
|
+
planMode: options.planMode,
|
|
89
|
+
});
|
|
90
|
+
const onClaudePermissionPrompt = options.hooks?.onClaudePermissionPrompt;
|
|
91
|
+
const claudePromptTool = options.kind === "claude-code" &&
|
|
92
|
+
onClaudePermissionPrompt &&
|
|
93
|
+
claudeModeUsesPermissionPrompt(claudePermissionMode)
|
|
94
|
+
? CLAUDE_PERMISSION_PROMPT_TOOL
|
|
95
|
+
: undefined;
|
|
64
96
|
// 3. Register before the runtime starts, so a tool call on the very first
|
|
65
97
|
// turn is already routable (§13.1).
|
|
66
|
-
store.register(pinnedThreadId,
|
|
98
|
+
store.register(pinnedThreadId, claudePromptTool && onClaudePermissionPrompt
|
|
99
|
+
? withClaudePermissionPrompt(host, onClaudePermissionPrompt, CLAUDE_MCP_SERVER_NAME)
|
|
100
|
+
: host);
|
|
67
101
|
// 4. Runtime.
|
|
68
102
|
const exposedToolNames = host.listTools().map((definition) => definition.name);
|
|
69
103
|
if (options.kind === "codex") {
|
|
@@ -100,6 +134,11 @@ export async function startExternalRuntimeSession(options) {
|
|
|
100
134
|
? { resumeRuntimeSessionId: options.resumeRuntimeSessionId }
|
|
101
135
|
: {}),
|
|
102
136
|
...(options.initialContext ? { initialContext: options.initialContext } : {}),
|
|
137
|
+
...(options.developerInstructions
|
|
138
|
+
? { developerInstructions: options.developerInstructions }
|
|
139
|
+
: {}),
|
|
140
|
+
permissionMode: claudePermissionMode,
|
|
141
|
+
...(claudePromptTool ? { permissionPromptTool: claudePromptTool } : {}),
|
|
103
142
|
...(options.claudeExtraArgs ? { extraArgs: options.claudeExtraArgs } : {}),
|
|
104
143
|
...(options.claudeCommand ? { command: options.claudeCommand } : {}),
|
|
105
144
|
log,
|
|
@@ -121,6 +160,9 @@ export async function startExternalRuntimeSession(options) {
|
|
|
121
160
|
get runtimeSessionId() {
|
|
122
161
|
return activeRuntime.runtimeSessionId;
|
|
123
162
|
},
|
|
163
|
+
get resolvedModel() {
|
|
164
|
+
return activeRuntime.resolvedModel;
|
|
165
|
+
},
|
|
124
166
|
listTools: () => activeHost.listTools(),
|
|
125
167
|
send: async (input, onEvent) => {
|
|
126
168
|
if (turnSink)
|
|
@@ -12,12 +12,46 @@ export interface BridgeToolHost {
|
|
|
12
12
|
id: string;
|
|
13
13
|
name: string;
|
|
14
14
|
input: unknown;
|
|
15
|
-
}): Promise<{
|
|
15
|
+
}, signal?: AbortSignal): Promise<{
|
|
16
16
|
result?: string;
|
|
17
17
|
error?: string;
|
|
18
18
|
isError?: boolean;
|
|
19
|
+
/** Structured blocks (images) a tool returned alongside its text. */
|
|
20
|
+
contentBlocks?: readonly BridgeContentBlock[];
|
|
19
21
|
}>;
|
|
20
22
|
}
|
|
23
|
+
/** The subset of core's `ContentBlock` the bridge reads. Structural, like
|
|
24
|
+
* {@link BridgeToolHost}, so this module stays independent of core. */
|
|
25
|
+
export interface BridgeContentBlock {
|
|
26
|
+
type: string;
|
|
27
|
+
text?: string;
|
|
28
|
+
source?: {
|
|
29
|
+
type?: string;
|
|
30
|
+
media_type?: string;
|
|
31
|
+
data?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
type McpContent = {
|
|
35
|
+
type: "text";
|
|
36
|
+
text: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: "image";
|
|
39
|
+
data: string;
|
|
40
|
+
mimeType: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* MCP `tools/call` content for a host result.
|
|
44
|
+
*
|
|
45
|
+
* The text always comes first and is always present: it is the tool's own
|
|
46
|
+
* summary of what it loaded, and a client that drops images still gets it.
|
|
47
|
+
* Image blocks then follow as MCP image content. Without them a browser
|
|
48
|
+
* screenshot reached the external model only as "[screenshot loaded]".
|
|
49
|
+
*
|
|
50
|
+
* Only well-formed base64 `image/*` blocks are forwarded; anything else a tool
|
|
51
|
+
* puts into `contentBlocks` is dropped rather than passed through in a shape
|
|
52
|
+
* the client never agreed to. An error result stays text-only.
|
|
53
|
+
*/
|
|
54
|
+
export declare function mcpContentForResult(outcome: Awaited<ReturnType<BridgeToolHost["execute"]>>): McpContent[];
|
|
21
55
|
export interface McpBridgeOptions {
|
|
22
56
|
store: SessionContextStore<BridgeToolHost>;
|
|
23
57
|
/** Logical MCP server name Codex is configured with. */
|
|
@@ -64,3 +98,4 @@ export declare function startLoopbackMcpBridge(options: McpBridgeOptions): Promi
|
|
|
64
98
|
* by environment variable, never in argv (§12.2).
|
|
65
99
|
*/
|
|
66
100
|
export declare function codexBridgeConfigArgs(handle: McpBridgeHandle, serverName?: string): string[];
|
|
101
|
+
export {};
|
|
@@ -20,6 +20,41 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { createServer } from "node:http";
|
|
22
22
|
import { randomBytes } from "node:crypto";
|
|
23
|
+
/**
|
|
24
|
+
* MCP `tools/call` content for a host result.
|
|
25
|
+
*
|
|
26
|
+
* The text always comes first and is always present: it is the tool's own
|
|
27
|
+
* summary of what it loaded, and a client that drops images still gets it.
|
|
28
|
+
* Image blocks then follow as MCP image content. Without them a browser
|
|
29
|
+
* screenshot reached the external model only as "[screenshot loaded]".
|
|
30
|
+
*
|
|
31
|
+
* Only well-formed base64 `image/*` blocks are forwarded; anything else a tool
|
|
32
|
+
* puts into `contentBlocks` is dropped rather than passed through in a shape
|
|
33
|
+
* the client never agreed to. An error result stays text-only.
|
|
34
|
+
*/
|
|
35
|
+
export function mcpContentForResult(outcome) {
|
|
36
|
+
const content = [
|
|
37
|
+
{ type: "text", text: String(outcome.result ?? outcome.error ?? "") },
|
|
38
|
+
];
|
|
39
|
+
if (outcome.isError || outcome.error !== undefined)
|
|
40
|
+
return content;
|
|
41
|
+
for (const block of outcome.contentBlocks ?? []) {
|
|
42
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
43
|
+
content.push({ type: "text", text: block.text });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const source = block.source;
|
|
47
|
+
if (block.type === "image" &&
|
|
48
|
+
source?.type === "base64" &&
|
|
49
|
+
typeof source.data === "string" &&
|
|
50
|
+
source.data.length > 0 &&
|
|
51
|
+
typeof source.media_type === "string" &&
|
|
52
|
+
source.media_type.startsWith("image/")) {
|
|
53
|
+
content.push({ type: "image", data: source.data, mimeType: source.media_type });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return content;
|
|
57
|
+
}
|
|
23
58
|
const MCP_PATH = "/mcp";
|
|
24
59
|
const DEFAULT_MAX_BODY = 1024 * 1024;
|
|
25
60
|
export const CODEX_MCP_TOKEN_ENV_VAR = "CODESHELL_CODEX_MCP_TOKEN";
|
|
@@ -82,6 +117,30 @@ export async function startLoopbackMcpBridge(options) {
|
|
|
82
117
|
* to steer routing by supplying one either.
|
|
83
118
|
*/
|
|
84
119
|
const threadIdFor = (params) => pinnedThreadId ?? threadIdFromMeta(params);
|
|
120
|
+
/**
|
|
121
|
+
* In-flight `tools/call` requests, so a client-side cancellation reaches the
|
|
122
|
+
* tool. Without this a Codex tool timeout abandoned the call while a
|
|
123
|
+
* foreground DriveAgent kept its child CLI running as an orphan.
|
|
124
|
+
*
|
|
125
|
+
* JSON-RPC ids are only unique per client, and one bridge can serve several
|
|
126
|
+
* Codex threads, so entries are keyed by thread as well as request id.
|
|
127
|
+
*/
|
|
128
|
+
const inFlight = new Set();
|
|
129
|
+
const cancelInFlight = (params) => {
|
|
130
|
+
const raw = params?.requestId;
|
|
131
|
+
if (raw === undefined || raw === null)
|
|
132
|
+
return;
|
|
133
|
+
const requestId = JSON.stringify(raw);
|
|
134
|
+
const threadId = threadIdFor(params);
|
|
135
|
+
// An unattributed cancellation may only act when exactly one thread has
|
|
136
|
+
// that request id in flight; guessing between two would cancel the wrong
|
|
137
|
+
// session's tool.
|
|
138
|
+
const matches = [...inFlight].filter((entry) => entry.requestId === requestId && (threadId === undefined || entry.threadId === threadId));
|
|
139
|
+
if (matches.length !== 1)
|
|
140
|
+
return;
|
|
141
|
+
const reason = params.reason;
|
|
142
|
+
matches[0].controller.abort(typeof reason === "string" && reason ? reason : "MCP request cancelled");
|
|
143
|
+
};
|
|
85
144
|
const server = createServer((req, res) => {
|
|
86
145
|
void handle(req, res).catch((error) => {
|
|
87
146
|
log("bridge.handler_failed", { error: error instanceof Error ? error.name : "unknown" });
|
|
@@ -199,6 +258,8 @@ export async function startLoopbackMcpBridge(options) {
|
|
|
199
258
|
return;
|
|
200
259
|
}
|
|
201
260
|
if (typeof message.method === "string" && message.method.startsWith("notifications/")) {
|
|
261
|
+
if (message.method === "notifications/cancelled")
|
|
262
|
+
cancelInFlight(message.params);
|
|
202
263
|
res.writeHead(202).end();
|
|
203
264
|
return;
|
|
204
265
|
}
|
|
@@ -255,15 +316,37 @@ export async function startLoopbackMcpBridge(options) {
|
|
|
255
316
|
}
|
|
256
317
|
const name = typeof params?.name === "string" ? params.name : "";
|
|
257
318
|
const started = Date.now();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
319
|
+
const pending = {
|
|
320
|
+
threadId,
|
|
321
|
+
requestId: JSON.stringify(message.id ?? null),
|
|
322
|
+
controller: new AbortController(),
|
|
323
|
+
};
|
|
324
|
+
inFlight.add(pending);
|
|
325
|
+
// A client that gives up drops the connection; treat that as cancellation.
|
|
326
|
+
// Node emits this unfinished `close` on disconnect (Bun's node:http does
|
|
327
|
+
// not, so the explicit notification above is the tested path). `req`'s
|
|
328
|
+
// own `close` fires once the body is read and cannot be used here.
|
|
329
|
+
const onClose = () => {
|
|
330
|
+
if (!res.writableFinished)
|
|
331
|
+
pending.controller.abort("MCP client disconnected");
|
|
332
|
+
};
|
|
333
|
+
res.once("close", onClose);
|
|
334
|
+
let outcome;
|
|
335
|
+
try {
|
|
336
|
+
// The host is the authorization boundary; the bridge only routes.
|
|
337
|
+
outcome = await resolved.host.execute({
|
|
338
|
+
// One turn can contain many calls, including parallel ones. A turn id
|
|
339
|
+
// aliases their transcript/UI entries and overwrites navigate with a
|
|
340
|
+
// later observe error. Mint a host identity for each actual invocation.
|
|
341
|
+
id: `mcp-${randomBytes(12).toString("hex")}`,
|
|
342
|
+
name,
|
|
343
|
+
input: params?.arguments ?? {},
|
|
344
|
+
}, pending.controller.signal);
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
inFlight.delete(pending);
|
|
348
|
+
res.off("close", onClose);
|
|
349
|
+
}
|
|
267
350
|
log("bridge.call", {
|
|
268
351
|
businessSessionId: resolved.host.businessSessionId,
|
|
269
352
|
threadIdPrefix: threadId?.slice(0, 8),
|
|
@@ -271,10 +354,7 @@ export async function startLoopbackMcpBridge(options) {
|
|
|
271
354
|
resultStatus: outcome.isError ? "error" : "ok",
|
|
272
355
|
durationMs: Date.now() - started,
|
|
273
356
|
});
|
|
274
|
-
reply({
|
|
275
|
-
content: [{ type: "text", text: String(outcome.result ?? outcome.error ?? "") }],
|
|
276
|
-
isError: Boolean(outcome.isError),
|
|
277
|
-
});
|
|
357
|
+
reply({ content: mcpContentForResult(outcome), isError: Boolean(outcome.isError) });
|
|
278
358
|
return;
|
|
279
359
|
}
|
|
280
360
|
reply({});
|
package/dist/git/parse-log.d.ts
CHANGED
|
@@ -5,9 +5,13 @@ export interface GitLogEntry {
|
|
|
5
5
|
author: string;
|
|
6
6
|
date: string;
|
|
7
7
|
}
|
|
8
|
+
/** ASCII unit separator: unlike `|`, it does not occur in real commit subjects or author names. */
|
|
9
|
+
export declare const GIT_LOG_FIELD_SEPARATOR = "\u001F";
|
|
10
|
+
/** `git log --format` producing the fields parsed by {@link parseGitLog}. */
|
|
11
|
+
export declare const GIT_LOG_FORMAT = "%H%x1f%s%x1f%an%x1f%ci";
|
|
8
12
|
/**
|
|
9
|
-
* Parse `git log --format
|
|
10
|
-
* malformed/short lines (missing
|
|
13
|
+
* Parse `git log --format=GIT_LOG_FORMAT` output into entries. Tolerates
|
|
14
|
+
* malformed/short lines (missing separators) by defaulting absent fields
|
|
11
15
|
* to "" rather than destructuring `undefined` and crashing on `.slice`
|
|
12
16
|
* (review-2026-05-30). Blank lines are skipped.
|
|
13
17
|
*/
|
package/dist/git/parse-log.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
/** ASCII unit separator: unlike `|`, it does not occur in real commit subjects or author names. */
|
|
2
|
+
export const GIT_LOG_FIELD_SEPARATOR = "\x1f";
|
|
3
|
+
/** `git log --format` producing the fields parsed by {@link parseGitLog}. */
|
|
4
|
+
export const GIT_LOG_FORMAT = "%H%x1f%s%x1f%an%x1f%ci";
|
|
1
5
|
/**
|
|
2
|
-
* Parse `git log --format
|
|
3
|
-
* malformed/short lines (missing
|
|
6
|
+
* Parse `git log --format=GIT_LOG_FORMAT` output into entries. Tolerates
|
|
7
|
+
* malformed/short lines (missing separators) by defaulting absent fields
|
|
4
8
|
* to "" rather than destructuring `undefined` and crashing on `.slice`
|
|
5
9
|
* (review-2026-05-30). Blank lines are skipped.
|
|
6
10
|
*/
|
|
@@ -9,7 +13,7 @@ export function parseGitLog(raw) {
|
|
|
9
13
|
.split("\n")
|
|
10
14
|
.filter((line) => line.trim() !== "")
|
|
11
15
|
.map((line) => {
|
|
12
|
-
const parts = line.split(
|
|
16
|
+
const parts = line.split(GIT_LOG_FIELD_SEPARATOR);
|
|
13
17
|
const hash = parts[0] ?? "";
|
|
14
18
|
return {
|
|
15
19
|
hash: hash.slice(0, 8),
|
package/dist/git/utils.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { type GitLogEntry } from "./parse-log.js";
|
|
|
11
11
|
export interface GitStatusEntry {
|
|
12
12
|
status: string;
|
|
13
13
|
path: string;
|
|
14
|
+
/** Source path of a rename/copy entry. */
|
|
15
|
+
origPath?: string;
|
|
14
16
|
}
|
|
15
17
|
export type { GitLogEntry };
|
|
16
18
|
export declare function isGitRepo(cwd: string): boolean;
|
|
@@ -36,6 +38,8 @@ export declare function getGitDiffStat(cwd: string, opts?: {
|
|
|
36
38
|
staged?: boolean;
|
|
37
39
|
file?: string;
|
|
38
40
|
}): string;
|
|
41
|
+
/** Untracked, non-ignored files under `cwd`, relative to `cwd` (what `git add .` would add). */
|
|
42
|
+
export declare function getUntrackedFiles(cwd: string): string[];
|
|
39
43
|
export declare function getGitLog(cwd: string, n?: number): GitLogEntry[];
|
|
40
44
|
export declare function getRemoteUrl(cwd: string): string | undefined;
|
|
41
45
|
export declare function gitAdd(cwd: string, files?: string[]): void;
|