@workerdeck/core 0.9.0 → 0.10.0
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/build/index.d.mts +53 -1
- package/build/index.mjs +324 -2
- package/build/index.mjs.map +1 -1
- package/package.json +3 -3
package/build/index.d.mts
CHANGED
|
@@ -1118,6 +1118,32 @@ type AppServerWebSearchItem = {
|
|
|
1118
1118
|
type: 'webSearch';
|
|
1119
1119
|
query: string;
|
|
1120
1120
|
};
|
|
1121
|
+
/**
|
|
1122
|
+
* A picture the model made with codex's built-in `image_gen` tool.
|
|
1123
|
+
*
|
|
1124
|
+
* `savedPath` is an absolute path on the **host** — by default under
|
|
1125
|
+
* `$CODEX_HOME/generated_images/`, or inside the workspace when the model was
|
|
1126
|
+
* told the asset belongs to the project. It is the only reference we get: the
|
|
1127
|
+
* app-server never sends the bytes, and neither do we (the event log carries
|
|
1128
|
+
* references, never base64 — see the protocol's note on attachments).
|
|
1129
|
+
*
|
|
1130
|
+
* `result` is an undocumented free-form string. Treated as untrusted length:
|
|
1131
|
+
* short values are shown, anything long enough to be an encoded image is not.
|
|
1132
|
+
*/
|
|
1133
|
+
type AppServerImageGenerationItem = {
|
|
1134
|
+
id: string;
|
|
1135
|
+
type: 'imageGeneration';
|
|
1136
|
+
status: string;
|
|
1137
|
+
revisedPrompt?: string | null;
|
|
1138
|
+
result: string;
|
|
1139
|
+
savedPath?: string;
|
|
1140
|
+
};
|
|
1141
|
+
/** The model *looked at* an image on disk (`path`, host-absolute). */
|
|
1142
|
+
type AppServerImageViewItem = {
|
|
1143
|
+
id: string;
|
|
1144
|
+
type: 'imageView';
|
|
1145
|
+
path: string;
|
|
1146
|
+
};
|
|
1121
1147
|
/** The user's own message, echoed back as an item — dropped (the runner
|
|
1122
1148
|
* already emitted its `user_message`). */
|
|
1123
1149
|
type AppServerUserMessageItem = {
|
|
@@ -1125,7 +1151,7 @@ type AppServerUserMessageItem = {
|
|
|
1125
1151
|
type: 'userMessage';
|
|
1126
1152
|
content?: unknown;
|
|
1127
1153
|
};
|
|
1128
|
-
type AppServerItem = AppServerAgentMessageItem | AppServerReasoningItem | AppServerCommandExecutionItem | AppServerFileChangeItem | AppServerMcpToolCallItem | AppServerWebSearchItem | AppServerUserMessageItem;
|
|
1154
|
+
type AppServerItem = AppServerAgentMessageItem | AppServerReasoningItem | AppServerCommandExecutionItem | AppServerFileChangeItem | AppServerMcpToolCallItem | AppServerWebSearchItem | AppServerImageGenerationItem | AppServerImageViewItem | AppServerUserMessageItem;
|
|
1129
1155
|
/** The `Turn` object of `turn/started` / `turn/completed`. */
|
|
1130
1156
|
type AppServerTurn = {
|
|
1131
1157
|
id: string; /** 'inProgress' | 'completed' | 'failed' | 'interrupted' — open. */
|
|
@@ -1330,6 +1356,32 @@ declare class CodexRunner implements Runner {
|
|
|
1330
1356
|
fail(message: string): void;
|
|
1331
1357
|
close(reason?: 'client' | 'server' | 'error'): void;
|
|
1332
1358
|
subscribe(listener: SessionEventListener, afterSeq?: number): () => void;
|
|
1359
|
+
/**
|
|
1360
|
+
* The session's MCP servers, live from the binary.
|
|
1361
|
+
*
|
|
1362
|
+
* Two sources merged, because codex splits them: `mcpServerStatus/list` says
|
|
1363
|
+
* what is configured and what each server exposes (including every tool's
|
|
1364
|
+
* full JSON Schema, which the Agent SDK does not give us), and the
|
|
1365
|
+
* `mcpServer/startupStatus/updated` notifications say which of them are
|
|
1366
|
+
* actually up.
|
|
1367
|
+
*
|
|
1368
|
+
* Answers **before the session has connected**, over a throwaway child, for
|
|
1369
|
+
* the same reason the skill list does: a codex session spawns nothing until
|
|
1370
|
+
* it has work, and a panel that said "no MCP servers configured" until the
|
|
1371
|
+
* first turn would be stating something false about the operator's config.
|
|
1372
|
+
* The request blocks until the servers are enumerated (measured: complete on
|
|
1373
|
+
* the very first call), so there is no half-populated answer to race.
|
|
1374
|
+
*
|
|
1375
|
+
* Resolves undefined only when there is genuinely nothing to say — the
|
|
1376
|
+
* session is closed, or the child could not be spoken to. The route turns
|
|
1377
|
+
* that into a 501.
|
|
1378
|
+
*
|
|
1379
|
+
* **Listing only.** There is no per-server reconnect or toggle on this
|
|
1380
|
+
* transport — hence no `reconnectMcpServer`/`setMcpServerEnabled` here, and
|
|
1381
|
+
* `ENGINE_CAPABILITIES.codex.mcpServerActions: false` so clients render the
|
|
1382
|
+
* panel read-only instead of offering buttons that cannot work.
|
|
1383
|
+
*/
|
|
1384
|
+
mcpServers(): Promise<McpServerStatusInfo[] | undefined>;
|
|
1333
1385
|
}
|
|
1334
1386
|
//#endregion
|
|
1335
1387
|
//#region src/engines/codex/process.d.ts
|
package/build/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
|
|
4
4
|
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION } from "@workerdeck/protocol";
|
|
5
5
|
import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
|
|
6
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
|
-
import { existsSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { createVfs, runScript } from "@workerdeck/sandbox";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { lookup } from "node:dns/promises";
|
|
@@ -2987,6 +2987,118 @@ const APPROVAL_POLICY_BY_MODE = {
|
|
|
2987
2987
|
* default, so unattended codex sessions land the same way Claude ones do. */
|
|
2988
2988
|
const DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
|
|
2989
2989
|
/**
|
|
2990
|
+
* Tool name for codex's built-in `image_gen`. A stable string because it is a
|
|
2991
|
+
* rendering contract: both clients key an icon (and, where they can reach the
|
|
2992
|
+
* host filesystem, an inline preview) off it.
|
|
2993
|
+
*/
|
|
2994
|
+
const CODEX_IMAGE_TOOL = "CodexImageGeneration";
|
|
2995
|
+
/** Longest `result` worth putting in a tool card. The field is free-form and
|
|
2996
|
+
* undocumented; anything past this is assumed to be an encoded image rather
|
|
2997
|
+
* than a sentence, and encoded images do not go in the event log. */
|
|
2998
|
+
const MAX_IMAGE_RESULT_CHARS = 512;
|
|
2999
|
+
const shortResult = (result) => result.length > 0 && result.length <= MAX_IMAGE_RESULT_CHARS && !result.startsWith("data:");
|
|
3000
|
+
/**
|
|
3001
|
+
* `file_produced.fileId` — derived from the path, not minted fresh.
|
|
3002
|
+
*
|
|
3003
|
+
* Two properties fall out of that and both are load-bearing: codex reports the
|
|
3004
|
+
* same `savedPath` on the progress item and again on the completed one, so a
|
|
3005
|
+
* derived id makes the second emission a no-op instead of a duplicate row; and
|
|
3006
|
+
* a session rebuilt from a snapshot re-derives the same ids, so a client's
|
|
3007
|
+
* cached URL still resolves after a park/restore.
|
|
3008
|
+
*/
|
|
3009
|
+
function producedFileId(path) {
|
|
3010
|
+
return createHash("sha256").update(path).digest("hex").slice(0, 32);
|
|
3011
|
+
}
|
|
3012
|
+
/** Media type from the extension, for the handful a client renders inline.
|
|
3013
|
+
* Undefined for everything else — the route sniffs, and guessing here is how a
|
|
3014
|
+
* text file ends up labelled `image/png`. */
|
|
3015
|
+
function producedMediaType(path) {
|
|
3016
|
+
return PRODUCED_MEDIA_TYPES[path.slice(path.lastIndexOf(".") + 1).toLowerCase()];
|
|
3017
|
+
}
|
|
3018
|
+
const PRODUCED_MEDIA_TYPES = {
|
|
3019
|
+
png: "image/png",
|
|
3020
|
+
jpg: "image/jpeg",
|
|
3021
|
+
jpeg: "image/jpeg",
|
|
3022
|
+
gif: "image/gif",
|
|
3023
|
+
webp: "image/webp",
|
|
3024
|
+
svg: "image/svg+xml",
|
|
3025
|
+
pdf: "application/pdf"
|
|
3026
|
+
};
|
|
3027
|
+
/**
|
|
3028
|
+
* Codex's `SkillMetadata` as the protocol states it. `interface.shortDescription`
|
|
3029
|
+
* beats the legacy top-level one (codex's own comment says to prefer it), and
|
|
3030
|
+
* `enabled` defaults to true — an entry codex listed without the field is one it
|
|
3031
|
+
* considers live, and defaulting to false would hide working skills.
|
|
3032
|
+
*/
|
|
3033
|
+
function skillInfo(skill) {
|
|
3034
|
+
return {
|
|
3035
|
+
name: skill.name,
|
|
3036
|
+
...skill.description ? { description: skill.description } : {},
|
|
3037
|
+
...skill.interface?.shortDescription ?? skill.shortDescription ? { shortDescription: skill.interface?.shortDescription ?? skill.shortDescription } : {},
|
|
3038
|
+
...skill.interface?.displayName ? { displayName: skill.interface.displayName } : {},
|
|
3039
|
+
...skill.interface?.defaultPrompt ? { defaultPrompt: skill.interface.defaultPrompt } : {},
|
|
3040
|
+
...skill.scope ? { scope: skill.scope } : {},
|
|
3041
|
+
enabled: skill.enabled !== false
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Codex's MCP status → the protocol's, which is Claude Code's vocabulary
|
|
3046
|
+
* ('connected' | 'failed' | 'needs-auth' | 'pending' | 'disabled').
|
|
3047
|
+
*
|
|
3048
|
+
* Two inputs, and the auth one wins where it applies: a server that started
|
|
3049
|
+
* fine but has no credential is *needs-auth*, not connected, because that is
|
|
3050
|
+
* the thing the operator has to act on. `notLoggedIn` is the only auth value
|
|
3051
|
+
* that means "unusable" — `unsupported` is the normal answer for a stdio server
|
|
3052
|
+
* that has no auth concept at all.
|
|
3053
|
+
*
|
|
3054
|
+
* A server with no startup notification yet is 'pending', not 'connected':
|
|
3055
|
+
* `mcpServerStatus/list` alone only proves it is *configured*.
|
|
3056
|
+
*/
|
|
3057
|
+
function mcpStatusOf(authStatus, update, hasTools) {
|
|
3058
|
+
if (update?.status === "failed") return update.failureReason === "reauthenticationRequired" ? "needs-auth" : "failed";
|
|
3059
|
+
if (update?.status === "cancelled") return "failed";
|
|
3060
|
+
if (authStatus === "notLoggedIn") return "needs-auth";
|
|
3061
|
+
if (update?.status === "ready") return "connected";
|
|
3062
|
+
if (hasTools) return "connected";
|
|
3063
|
+
return "pending";
|
|
3064
|
+
}
|
|
3065
|
+
/** One `mcpServerStatus/list` entry as the protocol states it. */
|
|
3066
|
+
function mcpServerInfo(server, update) {
|
|
3067
|
+
const tools = Object.entries(server.tools ?? {}).flatMap(([key, tool]) => {
|
|
3068
|
+
if (!tool) return [];
|
|
3069
|
+
const annotations = tool.annotations;
|
|
3070
|
+
return [{
|
|
3071
|
+
name: tool.name ?? key,
|
|
3072
|
+
...tool.description ? { description: tool.description } : {},
|
|
3073
|
+
...tool.inputSchema !== void 0 ? { inputSchema: tool.inputSchema } : {},
|
|
3074
|
+
...annotations ? { annotations: {
|
|
3075
|
+
...annotations.readOnlyHint != null ? { readOnly: annotations.readOnlyHint } : {},
|
|
3076
|
+
...annotations.destructiveHint != null ? { destructive: annotations.destructiveHint } : {},
|
|
3077
|
+
...annotations.openWorldHint != null ? { openWorld: annotations.openWorldHint } : {}
|
|
3078
|
+
} } : {}
|
|
3079
|
+
}];
|
|
3080
|
+
});
|
|
3081
|
+
return {
|
|
3082
|
+
name: server.name,
|
|
3083
|
+
status: mcpStatusOf(server.authStatus ?? void 0, update, tools.length > 0),
|
|
3084
|
+
...update?.error ? { error: update.error } : {},
|
|
3085
|
+
...server.serverInfo?.name ? { serverInfo: {
|
|
3086
|
+
name: server.serverInfo.name,
|
|
3087
|
+
version: server.serverInfo.version ?? ""
|
|
3088
|
+
} } : {},
|
|
3089
|
+
...tools.length > 0 ? { tools } : {}
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
/** What the card shows while the picture is being made, and after. `savedPath`
|
|
3093
|
+
* only exists once it lands — a client keys its preview off it, so it is a
|
|
3094
|
+
* field rather than a sentence in the result text. */
|
|
3095
|
+
function imageGenerationInput(item) {
|
|
3096
|
+
return {
|
|
3097
|
+
...item.revisedPrompt ? { prompt: item.revisedPrompt } : {},
|
|
3098
|
+
...item.savedPath ? { savedPath: item.savedPath } : {}
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
/**
|
|
2990
3102
|
* The experimental per-request decision list, normalized to names: a string
|
|
2991
3103
|
* entry is its own name, a structured entry (`{acceptWithExecpolicyAmendment:
|
|
2992
3104
|
* …}`) is named by its key. Undefined = the request stated no list and the
|
|
@@ -3246,6 +3358,21 @@ var CodexRunner = class {
|
|
|
3246
3358
|
/** Set around history replay: {@link #emit} stamps `replay: true` onto the
|
|
3247
3359
|
* message events the live item mapping produces. */
|
|
3248
3360
|
#replayingHistory = false;
|
|
3361
|
+
/** Last `skills` payload emitted, serialized — the comparison that keeps a
|
|
3362
|
+
* `skills/changed` storm (the watcher fires per touched file) from filling
|
|
3363
|
+
* the event log with identical lists. */
|
|
3364
|
+
#skillsFingerprint;
|
|
3365
|
+
/** In-flight `skills/list`, so a burst of `skills/changed` makes one call.
|
|
3366
|
+
* The pending promise is reused rather than queued: the request has no
|
|
3367
|
+
* arguments, so a second one would ask the same question. */
|
|
3368
|
+
#skillsRefresh;
|
|
3369
|
+
/** Host paths already announced via `file_produced`, so the same picture
|
|
3370
|
+
* reported on both the progress and the completed item registers once. */
|
|
3371
|
+
#producedPaths = /* @__PURE__ */ new Set();
|
|
3372
|
+
/** Per-server liveness, accumulated from `mcpServer/startupStatus/updated`.
|
|
3373
|
+
* `mcpServerStatus/list` does not carry a status field at all, so without
|
|
3374
|
+
* this every server would read as "configured" and never as up or down. */
|
|
3375
|
+
#mcpStatus = /* @__PURE__ */ new Map();
|
|
3249
3376
|
constructor(config, id = randomUUID()) {
|
|
3250
3377
|
const mode = config.permissionMode ?? "default";
|
|
3251
3378
|
if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
|
|
@@ -3317,8 +3444,62 @@ var CodexRunner = class {
|
|
|
3317
3444
|
this.#turnChain = this.#turnChain.then(() => this.#backfillHistory());
|
|
3318
3445
|
} else this.#setStatus("idle");
|
|
3319
3446
|
if (this.#config.prompt) this.sendMessage(this.#config.prompt);
|
|
3447
|
+
if (!this.#config.prompt && !this.#config.resume) this.#probeSkills();
|
|
3320
3448
|
return this.#turnChain;
|
|
3321
3449
|
}
|
|
3450
|
+
/**
|
|
3451
|
+
* List skills over a **throwaway** connection, for a session with nothing else
|
|
3452
|
+
* to do yet.
|
|
3453
|
+
*
|
|
3454
|
+
* `skills/list` needs a live child but not a thread, so this spawns one, asks,
|
|
3455
|
+
* and closes it — rather than bringing up the session's own child early and
|
|
3456
|
+
* leaving a codex process parked behind every session someone created and
|
|
3457
|
+
* never typed into. The session's real connection re-lists when it arrives;
|
|
3458
|
+
* the fingerprint compare in {@link #refreshSkills} makes that a no-op.
|
|
3459
|
+
*
|
|
3460
|
+
* Entirely best-effort and never awaited: a missing binary, a failed spawn or
|
|
3461
|
+
* a rejected handshake here must not turn a session that has not started into
|
|
3462
|
+
* a session that failed.
|
|
3463
|
+
*/
|
|
3464
|
+
async #probeSkills() {
|
|
3465
|
+
let connection;
|
|
3466
|
+
try {
|
|
3467
|
+
connection = await this.#openScratchConnection();
|
|
3468
|
+
if (this.#closed) return;
|
|
3469
|
+
await this.#refreshSkills(connection);
|
|
3470
|
+
} catch {} finally {
|
|
3471
|
+
connection?.close();
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
/**
|
|
3475
|
+
* A handshaken child that is **not** the session's — for the questions a
|
|
3476
|
+
* client can ask before the session has anything to run (its skills, its MCP
|
|
3477
|
+
* servers). The caller owns it and must close it.
|
|
3478
|
+
*
|
|
3479
|
+
* No onNotification/onRequest/onClose wiring on purpose: this child answers
|
|
3480
|
+
* one question and goes away, so its notifications are noise and its death is
|
|
3481
|
+
* not the session's problem. The alternative — bringing the session's real
|
|
3482
|
+
* child up early — would park a codex process behind every session someone
|
|
3483
|
+
* created and never typed into.
|
|
3484
|
+
*/
|
|
3485
|
+
async #openScratchConnection() {
|
|
3486
|
+
const connection = this.#config.connectFn({ env: this.#childEnv() });
|
|
3487
|
+
try {
|
|
3488
|
+
await connection.request("initialize", {
|
|
3489
|
+
clientInfo: {
|
|
3490
|
+
name: "workerdeck",
|
|
3491
|
+
title: "WorkerDeck",
|
|
3492
|
+
version: `protocol-${PROTOCOL_VERSION}`
|
|
3493
|
+
},
|
|
3494
|
+
capabilities: { experimentalApi: true }
|
|
3495
|
+
});
|
|
3496
|
+
connection.notify("initialized");
|
|
3497
|
+
return connection;
|
|
3498
|
+
} catch (error) {
|
|
3499
|
+
connection.close();
|
|
3500
|
+
throw error;
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3322
3503
|
sendMessage(text, attachments) {
|
|
3323
3504
|
if (this.#closed) throw new Error("session is closed");
|
|
3324
3505
|
const input = this.#buildInput(text, attachments ?? []);
|
|
@@ -3534,9 +3715,117 @@ var CodexRunner = class {
|
|
|
3534
3715
|
};
|
|
3535
3716
|
this.#threadLoaded = true;
|
|
3536
3717
|
}
|
|
3718
|
+
this.#refreshSkills(connection);
|
|
3537
3719
|
return connection;
|
|
3538
3720
|
}
|
|
3539
3721
|
/**
|
|
3722
|
+
* Re-read `skills/list` and publish it, if it changed.
|
|
3723
|
+
*
|
|
3724
|
+
* **`cwds` is passed explicitly, and must be.** The schema documents the empty
|
|
3725
|
+
* case as "the current session working directory", which reads like the
|
|
3726
|
+
* thread's — it is not. Measured against 0.146.0: with no `cwds`, and *after*
|
|
3727
|
+
* a `thread/start` carrying this session's cwd, the response comes back keyed
|
|
3728
|
+
* to the app-server child's own process directory (for WorkerDeck, wherever
|
|
3729
|
+
* the gateway was launched) and reports no repo-scoped skills at all. So a
|
|
3730
|
+
* project's own `.codex/skills/**` were invisible until this argument existed.
|
|
3731
|
+
*
|
|
3732
|
+
* Best-effort throughout. A binary too old to know the method, a broken
|
|
3733
|
+
* manifest, a child that died mid-call — none of that is worth failing a
|
|
3734
|
+
* session over, and the panel simply stays absent.
|
|
3735
|
+
*/
|
|
3736
|
+
async #refreshSkills(connection) {
|
|
3737
|
+
if (this.#skillsRefresh) return this.#skillsRefresh;
|
|
3738
|
+
const run = (async () => {
|
|
3739
|
+
try {
|
|
3740
|
+
const result = await connection.request("skills/list", { cwds: [this.#config.cwd] });
|
|
3741
|
+
if (this.#closed) return;
|
|
3742
|
+
const entries = Array.isArray(result?.data) ? result.data : [];
|
|
3743
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3744
|
+
const skills = [];
|
|
3745
|
+
for (const entry of entries) for (const skill of entry?.skills ?? []) {
|
|
3746
|
+
if (typeof skill?.name !== "string" || seen.has(skill.name)) continue;
|
|
3747
|
+
seen.add(skill.name);
|
|
3748
|
+
skills.push(skillInfo(skill));
|
|
3749
|
+
}
|
|
3750
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
3751
|
+
const fingerprint = JSON.stringify(skills);
|
|
3752
|
+
if (fingerprint === this.#skillsFingerprint) return;
|
|
3753
|
+
this.#skillsFingerprint = fingerprint;
|
|
3754
|
+
this.#emit({
|
|
3755
|
+
type: "skills",
|
|
3756
|
+
skills
|
|
3757
|
+
});
|
|
3758
|
+
} catch {} finally {
|
|
3759
|
+
this.#skillsRefresh = void 0;
|
|
3760
|
+
}
|
|
3761
|
+
})();
|
|
3762
|
+
this.#skillsRefresh = run;
|
|
3763
|
+
return run;
|
|
3764
|
+
}
|
|
3765
|
+
/**
|
|
3766
|
+
* The session's MCP servers, live from the binary.
|
|
3767
|
+
*
|
|
3768
|
+
* Two sources merged, because codex splits them: `mcpServerStatus/list` says
|
|
3769
|
+
* what is configured and what each server exposes (including every tool's
|
|
3770
|
+
* full JSON Schema, which the Agent SDK does not give us), and the
|
|
3771
|
+
* `mcpServer/startupStatus/updated` notifications say which of them are
|
|
3772
|
+
* actually up.
|
|
3773
|
+
*
|
|
3774
|
+
* Answers **before the session has connected**, over a throwaway child, for
|
|
3775
|
+
* the same reason the skill list does: a codex session spawns nothing until
|
|
3776
|
+
* it has work, and a panel that said "no MCP servers configured" until the
|
|
3777
|
+
* first turn would be stating something false about the operator's config.
|
|
3778
|
+
* The request blocks until the servers are enumerated (measured: complete on
|
|
3779
|
+
* the very first call), so there is no half-populated answer to race.
|
|
3780
|
+
*
|
|
3781
|
+
* Resolves undefined only when there is genuinely nothing to say — the
|
|
3782
|
+
* session is closed, or the child could not be spoken to. The route turns
|
|
3783
|
+
* that into a 501.
|
|
3784
|
+
*
|
|
3785
|
+
* **Listing only.** There is no per-server reconnect or toggle on this
|
|
3786
|
+
* transport — hence no `reconnectMcpServer`/`setMcpServerEnabled` here, and
|
|
3787
|
+
* `ENGINE_CAPABILITIES.codex.mcpServerActions: false` so clients render the
|
|
3788
|
+
* panel read-only instead of offering buttons that cannot work.
|
|
3789
|
+
*/
|
|
3790
|
+
async mcpServers() {
|
|
3791
|
+
if (this.#closed) return void 0;
|
|
3792
|
+
const live = this.#connection;
|
|
3793
|
+
let scratch;
|
|
3794
|
+
try {
|
|
3795
|
+
return ((await (live ?? (scratch = await this.#openScratchConnection())).request("mcpServerStatus/list", {}))?.data ?? []).map((server) => mcpServerInfo(server, this.#mcpStatus.get(server.name)));
|
|
3796
|
+
} catch {
|
|
3797
|
+
return;
|
|
3798
|
+
} finally {
|
|
3799
|
+
scratch?.close();
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
/**
|
|
3803
|
+
* Announce a file the ENGINE wrote on the host, so a client can fetch it
|
|
3804
|
+
* without the operator having declared its directory as a host-file root.
|
|
3805
|
+
*
|
|
3806
|
+
* Deliberately narrow: only paths codex reports as *written by its own tool*
|
|
3807
|
+
* belong here. A path the model merely read (`imageView`) is an agent-chosen
|
|
3808
|
+
* claim, and those keep going through `/fs/*` and its root allowlist — see
|
|
3809
|
+
* the note on `file_produced` in the protocol.
|
|
3810
|
+
*/
|
|
3811
|
+
#emitFileProduced(path, toolUseId) {
|
|
3812
|
+
if (this.#producedPaths.has(path)) return;
|
|
3813
|
+
this.#producedPaths.add(path);
|
|
3814
|
+
let bytes;
|
|
3815
|
+
try {
|
|
3816
|
+
const stat = statSync(path);
|
|
3817
|
+
if (stat.isFile()) bytes = stat.size;
|
|
3818
|
+
} catch {}
|
|
3819
|
+
this.#emit({
|
|
3820
|
+
type: "file_produced",
|
|
3821
|
+
fileId: producedFileId(path),
|
|
3822
|
+
path,
|
|
3823
|
+
...producedMediaType(path) ? { mediaType: producedMediaType(path) } : {},
|
|
3824
|
+
...bytes !== void 0 ? { bytes } : {},
|
|
3825
|
+
toolUseId
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
/**
|
|
3540
3829
|
* On resume, replay the thread's prior turns as `replay: true` events,
|
|
3541
3830
|
* seq'd before any live turn — the SessionRunner backfill contract, fed
|
|
3542
3831
|
* from `thread/resume`'s own `thread.turns`. When the resume response says
|
|
@@ -3751,6 +4040,21 @@ var CodexRunner = class {
|
|
|
3751
4040
|
active.contextWindow = update.tokenUsage?.modelContextWindow ?? void 0;
|
|
3752
4041
|
return;
|
|
3753
4042
|
}
|
|
4043
|
+
case "mcpServer/startupStatus/updated": {
|
|
4044
|
+
const update = params;
|
|
4045
|
+
if (typeof update?.name !== "string") return;
|
|
4046
|
+
this.#mcpStatus.set(update.name, {
|
|
4047
|
+
status: typeof update.status === "string" ? update.status : "starting",
|
|
4048
|
+
...update.error ? { error: update.error } : {},
|
|
4049
|
+
...update.failureReason ? { failureReason: update.failureReason } : {}
|
|
4050
|
+
});
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
case "skills/changed": {
|
|
4054
|
+
const connection = this.#connection;
|
|
4055
|
+
if (connection) this.#refreshSkills(connection);
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
3754
4058
|
case "account/rateLimits/updated":
|
|
3755
4059
|
this.#emitRateLimits(params?.rateLimits);
|
|
3756
4060
|
return;
|
|
@@ -3926,6 +4230,12 @@ var CodexRunner = class {
|
|
|
3926
4230
|
if (item.type === "mcpToolCall" && !active.toolUseEmitted.has(id)) {
|
|
3927
4231
|
active.toolUseEmitted.add(id);
|
|
3928
4232
|
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
|
|
4233
|
+
return;
|
|
4234
|
+
}
|
|
4235
|
+
if (item.type === "imageGeneration" && !active.toolUseEmitted.has(id)) {
|
|
4236
|
+
active.toolUseEmitted.add(id);
|
|
4237
|
+
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
|
|
4238
|
+
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
3929
4239
|
}
|
|
3930
4240
|
}
|
|
3931
4241
|
#handleItemCompleted(item, active) {
|
|
@@ -3983,6 +4293,18 @@ var CodexRunner = class {
|
|
|
3983
4293
|
this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
|
|
3984
4294
|
this.#emitToolResult(id, "", false);
|
|
3985
4295
|
return;
|
|
4296
|
+
case "imageGeneration": {
|
|
4297
|
+
active.toolUseEmitted.add(id);
|
|
4298
|
+
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
|
|
4299
|
+
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
4300
|
+
const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
|
|
4301
|
+
this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
|
|
4302
|
+
return;
|
|
4303
|
+
}
|
|
4304
|
+
case "imageView":
|
|
4305
|
+
this.#emitToolUse(id, "CodexImageView", { path: item.path });
|
|
4306
|
+
this.#emitToolResult(id, item.path, false);
|
|
4307
|
+
return;
|
|
3986
4308
|
default: {
|
|
3987
4309
|
const unknown = item;
|
|
3988
4310
|
this.#emit({
|