@workerdeck/core 0.9.0 → 0.11.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 +66 -1
- package/build/index.mjs +368 -3
- package/build/index.mjs.map +1 -1
- package/package.json +3 -3
package/build/index.d.mts
CHANGED
|
@@ -189,6 +189,10 @@ interface Runner {
|
|
|
189
189
|
reconnectMcpServer?(name: string): Promise<void>;
|
|
190
190
|
/** Enable or disable one MCP server by name. Throws if it fails. */
|
|
191
191
|
setMcpServerEnabled?(name: string, enabled: boolean): Promise<void>;
|
|
192
|
+
/** Set (or clear, with undefined) the host's display title — `meta.title`, which
|
|
193
|
+
* `info().title` prefers over the derived one. A host-facing edit only: nothing
|
|
194
|
+
* is sent to the engine. */
|
|
195
|
+
setTitle(title: string | undefined): void;
|
|
192
196
|
/** Resolve a pending permission request. Returns false if the id is unknown (e.g. timed out). */
|
|
193
197
|
resolvePermission(requestId: string, decision: PermissionDecision): boolean;
|
|
194
198
|
interrupt(): Promise<void>;
|
|
@@ -253,6 +257,9 @@ declare class SessionRunner implements Runner {
|
|
|
253
257
|
get apiKeySource(): string | undefined;
|
|
254
258
|
get pendingApprovals(): PermissionRequest[];
|
|
255
259
|
info(): SessionInfo;
|
|
260
|
+
/** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
|
|
261
|
+
* it (undefined) restores the derived title. The engine is never told. */
|
|
262
|
+
setTitle(title: string | undefined): void;
|
|
256
263
|
/** Begin the session. Idempotent; returns the run promise (resolves when the query ends). */
|
|
257
264
|
start(): Promise<void>;
|
|
258
265
|
/** Queue a user message for the session (starts the next turn when idle).
|
|
@@ -444,6 +451,9 @@ declare class AiSdkRunner implements Runner {
|
|
|
444
451
|
* deferred executor). Idempotent by executionId.
|
|
445
452
|
*/
|
|
446
453
|
settleExecution(executionId: string, result: ToolExecutionResult): boolean;
|
|
454
|
+
/** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
|
|
455
|
+
* it (undefined) restores the derived title. The engine is never told. */
|
|
456
|
+
setTitle(title: string | undefined): void;
|
|
447
457
|
}
|
|
448
458
|
//#endregion
|
|
449
459
|
//#region src/claude-auth.d.ts
|
|
@@ -1118,6 +1128,32 @@ type AppServerWebSearchItem = {
|
|
|
1118
1128
|
type: 'webSearch';
|
|
1119
1129
|
query: string;
|
|
1120
1130
|
};
|
|
1131
|
+
/**
|
|
1132
|
+
* A picture the model made with codex's built-in `image_gen` tool.
|
|
1133
|
+
*
|
|
1134
|
+
* `savedPath` is an absolute path on the **host** — by default under
|
|
1135
|
+
* `$CODEX_HOME/generated_images/`, or inside the workspace when the model was
|
|
1136
|
+
* told the asset belongs to the project. It is the only reference we get: the
|
|
1137
|
+
* app-server never sends the bytes, and neither do we (the event log carries
|
|
1138
|
+
* references, never base64 — see the protocol's note on attachments).
|
|
1139
|
+
*
|
|
1140
|
+
* `result` is an undocumented free-form string. Treated as untrusted length:
|
|
1141
|
+
* short values are shown, anything long enough to be an encoded image is not.
|
|
1142
|
+
*/
|
|
1143
|
+
type AppServerImageGenerationItem = {
|
|
1144
|
+
id: string;
|
|
1145
|
+
type: 'imageGeneration';
|
|
1146
|
+
status: string;
|
|
1147
|
+
revisedPrompt?: string | null;
|
|
1148
|
+
result: string;
|
|
1149
|
+
savedPath?: string;
|
|
1150
|
+
};
|
|
1151
|
+
/** The model *looked at* an image on disk (`path`, host-absolute). */
|
|
1152
|
+
type AppServerImageViewItem = {
|
|
1153
|
+
id: string;
|
|
1154
|
+
type: 'imageView';
|
|
1155
|
+
path: string;
|
|
1156
|
+
};
|
|
1121
1157
|
/** The user's own message, echoed back as an item — dropped (the runner
|
|
1122
1158
|
* already emitted its `user_message`). */
|
|
1123
1159
|
type AppServerUserMessageItem = {
|
|
@@ -1125,7 +1161,7 @@ type AppServerUserMessageItem = {
|
|
|
1125
1161
|
type: 'userMessage';
|
|
1126
1162
|
content?: unknown;
|
|
1127
1163
|
};
|
|
1128
|
-
type AppServerItem = AppServerAgentMessageItem | AppServerReasoningItem | AppServerCommandExecutionItem | AppServerFileChangeItem | AppServerMcpToolCallItem | AppServerWebSearchItem | AppServerUserMessageItem;
|
|
1164
|
+
type AppServerItem = AppServerAgentMessageItem | AppServerReasoningItem | AppServerCommandExecutionItem | AppServerFileChangeItem | AppServerMcpToolCallItem | AppServerWebSearchItem | AppServerImageGenerationItem | AppServerImageViewItem | AppServerUserMessageItem;
|
|
1129
1165
|
/** The `Turn` object of `turn/started` / `turn/completed`. */
|
|
1130
1166
|
type AppServerTurn = {
|
|
1131
1167
|
id: string; /** 'inProgress' | 'completed' | 'failed' | 'interrupted' — open. */
|
|
@@ -1319,6 +1355,9 @@ declare class CodexRunner implements Runner {
|
|
|
1319
1355
|
get lastSeq(): number;
|
|
1320
1356
|
get pendingApprovals(): PermissionRequest[];
|
|
1321
1357
|
info(): SessionInfo;
|
|
1358
|
+
/** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
|
|
1359
|
+
* it (undefined) restores the derived title. The engine is never told. */
|
|
1360
|
+
setTitle(title: string | undefined): void;
|
|
1322
1361
|
start(): Promise<void>;
|
|
1323
1362
|
sendMessage(text: string, attachments?: readonly AttachmentInput[]): void;
|
|
1324
1363
|
/** Resolve a pending approval. Returns false if the id is unknown (e.g.
|
|
@@ -1330,6 +1369,32 @@ declare class CodexRunner implements Runner {
|
|
|
1330
1369
|
fail(message: string): void;
|
|
1331
1370
|
close(reason?: 'client' | 'server' | 'error'): void;
|
|
1332
1371
|
subscribe(listener: SessionEventListener, afterSeq?: number): () => void;
|
|
1372
|
+
/**
|
|
1373
|
+
* The session's MCP servers, live from the binary.
|
|
1374
|
+
*
|
|
1375
|
+
* Two sources merged, because codex splits them: `mcpServerStatus/list` says
|
|
1376
|
+
* what is configured and what each server exposes (including every tool's
|
|
1377
|
+
* full JSON Schema, which the Agent SDK does not give us), and the
|
|
1378
|
+
* `mcpServer/startupStatus/updated` notifications say which of them are
|
|
1379
|
+
* actually up.
|
|
1380
|
+
*
|
|
1381
|
+
* Answers **before the session has connected**, over a throwaway child, for
|
|
1382
|
+
* the same reason the skill list does: a codex session spawns nothing until
|
|
1383
|
+
* it has work, and a panel that said "no MCP servers configured" until the
|
|
1384
|
+
* first turn would be stating something false about the operator's config.
|
|
1385
|
+
* The request blocks until the servers are enumerated (measured: complete on
|
|
1386
|
+
* the very first call), so there is no half-populated answer to race.
|
|
1387
|
+
*
|
|
1388
|
+
* Resolves undefined only when there is genuinely nothing to say — the
|
|
1389
|
+
* session is closed, or the child could not be spoken to. The route turns
|
|
1390
|
+
* that into a 501.
|
|
1391
|
+
*
|
|
1392
|
+
* **Listing only.** There is no per-server reconnect or toggle on this
|
|
1393
|
+
* transport — hence no `reconnectMcpServer`/`setMcpServerEnabled` here, and
|
|
1394
|
+
* `ENGINE_CAPABILITIES.codex.mcpServerActions: false` so clients render the
|
|
1395
|
+
* panel read-only instead of offering buttons that cannot work.
|
|
1396
|
+
*/
|
|
1397
|
+
mcpServers(): Promise<McpServerStatusInfo[] | undefined>;
|
|
1333
1398
|
}
|
|
1334
1399
|
//#endregion
|
|
1335
1400
|
//#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
|
-
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION } from "@workerdeck/protocol";
|
|
4
|
+
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, transcriptActivity } 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";
|
|
@@ -413,6 +413,7 @@ var SessionRunner = class {
|
|
|
413
413
|
#events = [];
|
|
414
414
|
#listeners = /* @__PURE__ */ new Set();
|
|
415
415
|
#seq = 0;
|
|
416
|
+
#activityCount = 0;
|
|
416
417
|
#status = "starting";
|
|
417
418
|
#statusDetail;
|
|
418
419
|
#sdkSessionId;
|
|
@@ -469,6 +470,7 @@ var SessionRunner = class {
|
|
|
469
470
|
apiKeySource: this.#apiKeySource,
|
|
470
471
|
createdAt: this.createdAt,
|
|
471
472
|
lastSeq: this.#seq,
|
|
473
|
+
activityCount: this.#activityCount,
|
|
472
474
|
pendingPermissionCount: this.#pending.size,
|
|
473
475
|
meta: this.#config.meta,
|
|
474
476
|
title: this.#title(),
|
|
@@ -484,6 +486,17 @@ var SessionRunner = class {
|
|
|
484
486
|
if (!prompt) return void 0;
|
|
485
487
|
return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
|
|
486
488
|
}
|
|
489
|
+
/** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
|
|
490
|
+
* it (undefined) restores the derived title. The engine is never told. */
|
|
491
|
+
setTitle(title) {
|
|
492
|
+
const meta = { ...this.#config.meta };
|
|
493
|
+
if (title) meta.title = title;
|
|
494
|
+
else delete meta.title;
|
|
495
|
+
this.#config = {
|
|
496
|
+
...this.#config,
|
|
497
|
+
meta
|
|
498
|
+
};
|
|
499
|
+
}
|
|
487
500
|
/** Begin the session. Idempotent; returns the run promise (resolves when the query ends). */
|
|
488
501
|
start() {
|
|
489
502
|
if (this.#started) return this.#runPromise;
|
|
@@ -946,6 +959,7 @@ var SessionRunner = class {
|
|
|
946
959
|
ts: Date.now()
|
|
947
960
|
};
|
|
948
961
|
this.#lastActivityAt = event.ts;
|
|
962
|
+
this.#activityCount += transcriptActivity(body);
|
|
949
963
|
this.#events.push(event);
|
|
950
964
|
for (const listener of this.#listeners) try {
|
|
951
965
|
listener(event);
|
|
@@ -994,6 +1008,7 @@ var AiSdkRunner = class {
|
|
|
994
1008
|
#events = [];
|
|
995
1009
|
#listeners = /* @__PURE__ */ new Set();
|
|
996
1010
|
#seq = 0;
|
|
1011
|
+
#activityCount = 0;
|
|
997
1012
|
#status = "starting";
|
|
998
1013
|
#permissionMode;
|
|
999
1014
|
#messages = [];
|
|
@@ -1042,6 +1057,7 @@ var AiSdkRunner = class {
|
|
|
1042
1057
|
if (!state || !Array.isArray(state.messages)) throw new Error("session snapshot is missing its provider-engine state");
|
|
1043
1058
|
this.#seq = snapshot.seq;
|
|
1044
1059
|
this.#events = [...snapshot.events];
|
|
1060
|
+
this.#activityCount = this.#events.reduce((total, event) => total + transcriptActivity(event), 0);
|
|
1045
1061
|
this.#messages = [...state.messages];
|
|
1046
1062
|
for (const call of state.pendingToolCalls) this.#pendingToolCalls.set(call.toolCallId, call);
|
|
1047
1063
|
this.#dispatched = new Set(state.dispatched);
|
|
@@ -1091,6 +1107,7 @@ var AiSdkRunner = class {
|
|
|
1091
1107
|
permissionMode: this.#permissionMode,
|
|
1092
1108
|
createdAt: this.createdAt,
|
|
1093
1109
|
lastSeq: this.#seq,
|
|
1110
|
+
activityCount: this.#activityCount,
|
|
1094
1111
|
pendingPermissionCount: 0,
|
|
1095
1112
|
meta: this.#config.meta,
|
|
1096
1113
|
title: this.#title(),
|
|
@@ -1677,6 +1694,17 @@ var AiSdkRunner = class {
|
|
|
1677
1694
|
if (!prompt) return void 0;
|
|
1678
1695
|
return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
|
|
1679
1696
|
}
|
|
1697
|
+
/** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
|
|
1698
|
+
* it (undefined) restores the derived title. The engine is never told. */
|
|
1699
|
+
setTitle(title) {
|
|
1700
|
+
const meta = { ...this.#config.meta };
|
|
1701
|
+
if (title) meta.title = title;
|
|
1702
|
+
else delete meta.title;
|
|
1703
|
+
this.#config = {
|
|
1704
|
+
...this.#config,
|
|
1705
|
+
meta
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1680
1708
|
#setStatus(status, detail) {
|
|
1681
1709
|
if (this.#status === status) return;
|
|
1682
1710
|
if (this.#status === "closed" || this.#status === "failed") return;
|
|
@@ -1694,6 +1722,7 @@ var AiSdkRunner = class {
|
|
|
1694
1722
|
ts: Date.now()
|
|
1695
1723
|
};
|
|
1696
1724
|
this.#lastActivityAt = event.ts;
|
|
1725
|
+
this.#activityCount += transcriptActivity(body);
|
|
1697
1726
|
this.#events.push(event);
|
|
1698
1727
|
for (const listener of this.#listeners) try {
|
|
1699
1728
|
listener(event);
|
|
@@ -2987,6 +3016,118 @@ const APPROVAL_POLICY_BY_MODE = {
|
|
|
2987
3016
|
* default, so unattended codex sessions land the same way Claude ones do. */
|
|
2988
3017
|
const DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
|
|
2989
3018
|
/**
|
|
3019
|
+
* Tool name for codex's built-in `image_gen`. A stable string because it is a
|
|
3020
|
+
* rendering contract: both clients key an icon (and, where they can reach the
|
|
3021
|
+
* host filesystem, an inline preview) off it.
|
|
3022
|
+
*/
|
|
3023
|
+
const CODEX_IMAGE_TOOL = "CodexImageGeneration";
|
|
3024
|
+
/** Longest `result` worth putting in a tool card. The field is free-form and
|
|
3025
|
+
* undocumented; anything past this is assumed to be an encoded image rather
|
|
3026
|
+
* than a sentence, and encoded images do not go in the event log. */
|
|
3027
|
+
const MAX_IMAGE_RESULT_CHARS = 512;
|
|
3028
|
+
const shortResult = (result) => result.length > 0 && result.length <= MAX_IMAGE_RESULT_CHARS && !result.startsWith("data:");
|
|
3029
|
+
/**
|
|
3030
|
+
* `file_produced.fileId` — derived from the path, not minted fresh.
|
|
3031
|
+
*
|
|
3032
|
+
* Two properties fall out of that and both are load-bearing: codex reports the
|
|
3033
|
+
* same `savedPath` on the progress item and again on the completed one, so a
|
|
3034
|
+
* derived id makes the second emission a no-op instead of a duplicate row; and
|
|
3035
|
+
* a session rebuilt from a snapshot re-derives the same ids, so a client's
|
|
3036
|
+
* cached URL still resolves after a park/restore.
|
|
3037
|
+
*/
|
|
3038
|
+
function producedFileId(path) {
|
|
3039
|
+
return createHash("sha256").update(path).digest("hex").slice(0, 32);
|
|
3040
|
+
}
|
|
3041
|
+
/** Media type from the extension, for the handful a client renders inline.
|
|
3042
|
+
* Undefined for everything else — the route sniffs, and guessing here is how a
|
|
3043
|
+
* text file ends up labelled `image/png`. */
|
|
3044
|
+
function producedMediaType(path) {
|
|
3045
|
+
return PRODUCED_MEDIA_TYPES[path.slice(path.lastIndexOf(".") + 1).toLowerCase()];
|
|
3046
|
+
}
|
|
3047
|
+
const PRODUCED_MEDIA_TYPES = {
|
|
3048
|
+
png: "image/png",
|
|
3049
|
+
jpg: "image/jpeg",
|
|
3050
|
+
jpeg: "image/jpeg",
|
|
3051
|
+
gif: "image/gif",
|
|
3052
|
+
webp: "image/webp",
|
|
3053
|
+
svg: "image/svg+xml",
|
|
3054
|
+
pdf: "application/pdf"
|
|
3055
|
+
};
|
|
3056
|
+
/**
|
|
3057
|
+
* Codex's `SkillMetadata` as the protocol states it. `interface.shortDescription`
|
|
3058
|
+
* beats the legacy top-level one (codex's own comment says to prefer it), and
|
|
3059
|
+
* `enabled` defaults to true — an entry codex listed without the field is one it
|
|
3060
|
+
* considers live, and defaulting to false would hide working skills.
|
|
3061
|
+
*/
|
|
3062
|
+
function skillInfo(skill) {
|
|
3063
|
+
return {
|
|
3064
|
+
name: skill.name,
|
|
3065
|
+
...skill.description ? { description: skill.description } : {},
|
|
3066
|
+
...skill.interface?.shortDescription ?? skill.shortDescription ? { shortDescription: skill.interface?.shortDescription ?? skill.shortDescription } : {},
|
|
3067
|
+
...skill.interface?.displayName ? { displayName: skill.interface.displayName } : {},
|
|
3068
|
+
...skill.interface?.defaultPrompt ? { defaultPrompt: skill.interface.defaultPrompt } : {},
|
|
3069
|
+
...skill.scope ? { scope: skill.scope } : {},
|
|
3070
|
+
enabled: skill.enabled !== false
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Codex's MCP status → the protocol's, which is Claude Code's vocabulary
|
|
3075
|
+
* ('connected' | 'failed' | 'needs-auth' | 'pending' | 'disabled').
|
|
3076
|
+
*
|
|
3077
|
+
* Two inputs, and the auth one wins where it applies: a server that started
|
|
3078
|
+
* fine but has no credential is *needs-auth*, not connected, because that is
|
|
3079
|
+
* the thing the operator has to act on. `notLoggedIn` is the only auth value
|
|
3080
|
+
* that means "unusable" — `unsupported` is the normal answer for a stdio server
|
|
3081
|
+
* that has no auth concept at all.
|
|
3082
|
+
*
|
|
3083
|
+
* A server with no startup notification yet is 'pending', not 'connected':
|
|
3084
|
+
* `mcpServerStatus/list` alone only proves it is *configured*.
|
|
3085
|
+
*/
|
|
3086
|
+
function mcpStatusOf(authStatus, update, hasTools) {
|
|
3087
|
+
if (update?.status === "failed") return update.failureReason === "reauthenticationRequired" ? "needs-auth" : "failed";
|
|
3088
|
+
if (update?.status === "cancelled") return "failed";
|
|
3089
|
+
if (authStatus === "notLoggedIn") return "needs-auth";
|
|
3090
|
+
if (update?.status === "ready") return "connected";
|
|
3091
|
+
if (hasTools) return "connected";
|
|
3092
|
+
return "pending";
|
|
3093
|
+
}
|
|
3094
|
+
/** One `mcpServerStatus/list` entry as the protocol states it. */
|
|
3095
|
+
function mcpServerInfo(server, update) {
|
|
3096
|
+
const tools = Object.entries(server.tools ?? {}).flatMap(([key, tool]) => {
|
|
3097
|
+
if (!tool) return [];
|
|
3098
|
+
const annotations = tool.annotations;
|
|
3099
|
+
return [{
|
|
3100
|
+
name: tool.name ?? key,
|
|
3101
|
+
...tool.description ? { description: tool.description } : {},
|
|
3102
|
+
...tool.inputSchema !== void 0 ? { inputSchema: tool.inputSchema } : {},
|
|
3103
|
+
...annotations ? { annotations: {
|
|
3104
|
+
...annotations.readOnlyHint != null ? { readOnly: annotations.readOnlyHint } : {},
|
|
3105
|
+
...annotations.destructiveHint != null ? { destructive: annotations.destructiveHint } : {},
|
|
3106
|
+
...annotations.openWorldHint != null ? { openWorld: annotations.openWorldHint } : {}
|
|
3107
|
+
} } : {}
|
|
3108
|
+
}];
|
|
3109
|
+
});
|
|
3110
|
+
return {
|
|
3111
|
+
name: server.name,
|
|
3112
|
+
status: mcpStatusOf(server.authStatus ?? void 0, update, tools.length > 0),
|
|
3113
|
+
...update?.error ? { error: update.error } : {},
|
|
3114
|
+
...server.serverInfo?.name ? { serverInfo: {
|
|
3115
|
+
name: server.serverInfo.name,
|
|
3116
|
+
version: server.serverInfo.version ?? ""
|
|
3117
|
+
} } : {},
|
|
3118
|
+
...tools.length > 0 ? { tools } : {}
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
/** What the card shows while the picture is being made, and after. `savedPath`
|
|
3122
|
+
* only exists once it lands — a client keys its preview off it, so it is a
|
|
3123
|
+
* field rather than a sentence in the result text. */
|
|
3124
|
+
function imageGenerationInput(item) {
|
|
3125
|
+
return {
|
|
3126
|
+
...item.revisedPrompt ? { prompt: item.revisedPrompt } : {},
|
|
3127
|
+
...item.savedPath ? { savedPath: item.savedPath } : {}
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
2990
3131
|
* The experimental per-request decision list, normalized to names: a string
|
|
2991
3132
|
* entry is its own name, a structured entry (`{acceptWithExecpolicyAmendment:
|
|
2992
3133
|
* …}`) is named by its key. Undefined = the request stated no list and the
|
|
@@ -3207,6 +3348,7 @@ var CodexRunner = class {
|
|
|
3207
3348
|
#events = [];
|
|
3208
3349
|
#listeners = /* @__PURE__ */ new Set();
|
|
3209
3350
|
#seq = 0;
|
|
3351
|
+
#activityCount = 0;
|
|
3210
3352
|
#status = "starting";
|
|
3211
3353
|
#sdkSessionId;
|
|
3212
3354
|
#model;
|
|
@@ -3246,6 +3388,21 @@ var CodexRunner = class {
|
|
|
3246
3388
|
/** Set around history replay: {@link #emit} stamps `replay: true` onto the
|
|
3247
3389
|
* message events the live item mapping produces. */
|
|
3248
3390
|
#replayingHistory = false;
|
|
3391
|
+
/** Last `skills` payload emitted, serialized — the comparison that keeps a
|
|
3392
|
+
* `skills/changed` storm (the watcher fires per touched file) from filling
|
|
3393
|
+
* the event log with identical lists. */
|
|
3394
|
+
#skillsFingerprint;
|
|
3395
|
+
/** In-flight `skills/list`, so a burst of `skills/changed` makes one call.
|
|
3396
|
+
* The pending promise is reused rather than queued: the request has no
|
|
3397
|
+
* arguments, so a second one would ask the same question. */
|
|
3398
|
+
#skillsRefresh;
|
|
3399
|
+
/** Host paths already announced via `file_produced`, so the same picture
|
|
3400
|
+
* reported on both the progress and the completed item registers once. */
|
|
3401
|
+
#producedPaths = /* @__PURE__ */ new Set();
|
|
3402
|
+
/** Per-server liveness, accumulated from `mcpServer/startupStatus/updated`.
|
|
3403
|
+
* `mcpServerStatus/list` does not carry a status field at all, so without
|
|
3404
|
+
* this every server would read as "configured" and never as up or down. */
|
|
3405
|
+
#mcpStatus = /* @__PURE__ */ new Map();
|
|
3249
3406
|
constructor(config, id = randomUUID()) {
|
|
3250
3407
|
const mode = config.permissionMode ?? "default";
|
|
3251
3408
|
if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
|
|
@@ -3294,6 +3451,7 @@ var CodexRunner = class {
|
|
|
3294
3451
|
canBypassPermissions: true,
|
|
3295
3452
|
createdAt: this.createdAt,
|
|
3296
3453
|
lastSeq: this.#seq,
|
|
3454
|
+
activityCount: this.#activityCount,
|
|
3297
3455
|
pendingPermissionCount: this.#approvals.size,
|
|
3298
3456
|
meta: this.#config.meta,
|
|
3299
3457
|
title: this.#title(),
|
|
@@ -3309,6 +3467,17 @@ var CodexRunner = class {
|
|
|
3309
3467
|
if (!prompt) return void 0;
|
|
3310
3468
|
return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
|
|
3311
3469
|
}
|
|
3470
|
+
/** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
|
|
3471
|
+
* it (undefined) restores the derived title. The engine is never told. */
|
|
3472
|
+
setTitle(title) {
|
|
3473
|
+
const meta = { ...this.#config.meta };
|
|
3474
|
+
if (title) meta.title = title;
|
|
3475
|
+
else delete meta.title;
|
|
3476
|
+
this.#config = {
|
|
3477
|
+
...this.#config,
|
|
3478
|
+
meta
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3312
3481
|
start() {
|
|
3313
3482
|
if (this.#started) return this.#turnChain;
|
|
3314
3483
|
this.#started = true;
|
|
@@ -3317,8 +3486,62 @@ var CodexRunner = class {
|
|
|
3317
3486
|
this.#turnChain = this.#turnChain.then(() => this.#backfillHistory());
|
|
3318
3487
|
} else this.#setStatus("idle");
|
|
3319
3488
|
if (this.#config.prompt) this.sendMessage(this.#config.prompt);
|
|
3489
|
+
if (!this.#config.prompt && !this.#config.resume) this.#probeSkills();
|
|
3320
3490
|
return this.#turnChain;
|
|
3321
3491
|
}
|
|
3492
|
+
/**
|
|
3493
|
+
* List skills over a **throwaway** connection, for a session with nothing else
|
|
3494
|
+
* to do yet.
|
|
3495
|
+
*
|
|
3496
|
+
* `skills/list` needs a live child but not a thread, so this spawns one, asks,
|
|
3497
|
+
* and closes it — rather than bringing up the session's own child early and
|
|
3498
|
+
* leaving a codex process parked behind every session someone created and
|
|
3499
|
+
* never typed into. The session's real connection re-lists when it arrives;
|
|
3500
|
+
* the fingerprint compare in {@link #refreshSkills} makes that a no-op.
|
|
3501
|
+
*
|
|
3502
|
+
* Entirely best-effort and never awaited: a missing binary, a failed spawn or
|
|
3503
|
+
* a rejected handshake here must not turn a session that has not started into
|
|
3504
|
+
* a session that failed.
|
|
3505
|
+
*/
|
|
3506
|
+
async #probeSkills() {
|
|
3507
|
+
let connection;
|
|
3508
|
+
try {
|
|
3509
|
+
connection = await this.#openScratchConnection();
|
|
3510
|
+
if (this.#closed) return;
|
|
3511
|
+
await this.#refreshSkills(connection);
|
|
3512
|
+
} catch {} finally {
|
|
3513
|
+
connection?.close();
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
/**
|
|
3517
|
+
* A handshaken child that is **not** the session's — for the questions a
|
|
3518
|
+
* client can ask before the session has anything to run (its skills, its MCP
|
|
3519
|
+
* servers). The caller owns it and must close it.
|
|
3520
|
+
*
|
|
3521
|
+
* No onNotification/onRequest/onClose wiring on purpose: this child answers
|
|
3522
|
+
* one question and goes away, so its notifications are noise and its death is
|
|
3523
|
+
* not the session's problem. The alternative — bringing the session's real
|
|
3524
|
+
* child up early — would park a codex process behind every session someone
|
|
3525
|
+
* created and never typed into.
|
|
3526
|
+
*/
|
|
3527
|
+
async #openScratchConnection() {
|
|
3528
|
+
const connection = this.#config.connectFn({ env: this.#childEnv() });
|
|
3529
|
+
try {
|
|
3530
|
+
await connection.request("initialize", {
|
|
3531
|
+
clientInfo: {
|
|
3532
|
+
name: "workerdeck",
|
|
3533
|
+
title: "WorkerDeck",
|
|
3534
|
+
version: `protocol-${PROTOCOL_VERSION}`
|
|
3535
|
+
},
|
|
3536
|
+
capabilities: { experimentalApi: true }
|
|
3537
|
+
});
|
|
3538
|
+
connection.notify("initialized");
|
|
3539
|
+
return connection;
|
|
3540
|
+
} catch (error) {
|
|
3541
|
+
connection.close();
|
|
3542
|
+
throw error;
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3322
3545
|
sendMessage(text, attachments) {
|
|
3323
3546
|
if (this.#closed) throw new Error("session is closed");
|
|
3324
3547
|
const input = this.#buildInput(text, attachments ?? []);
|
|
@@ -3534,9 +3757,117 @@ var CodexRunner = class {
|
|
|
3534
3757
|
};
|
|
3535
3758
|
this.#threadLoaded = true;
|
|
3536
3759
|
}
|
|
3760
|
+
this.#refreshSkills(connection);
|
|
3537
3761
|
return connection;
|
|
3538
3762
|
}
|
|
3539
3763
|
/**
|
|
3764
|
+
* Re-read `skills/list` and publish it, if it changed.
|
|
3765
|
+
*
|
|
3766
|
+
* **`cwds` is passed explicitly, and must be.** The schema documents the empty
|
|
3767
|
+
* case as "the current session working directory", which reads like the
|
|
3768
|
+
* thread's — it is not. Measured against 0.146.0: with no `cwds`, and *after*
|
|
3769
|
+
* a `thread/start` carrying this session's cwd, the response comes back keyed
|
|
3770
|
+
* to the app-server child's own process directory (for WorkerDeck, wherever
|
|
3771
|
+
* the gateway was launched) and reports no repo-scoped skills at all. So a
|
|
3772
|
+
* project's own `.codex/skills/**` were invisible until this argument existed.
|
|
3773
|
+
*
|
|
3774
|
+
* Best-effort throughout. A binary too old to know the method, a broken
|
|
3775
|
+
* manifest, a child that died mid-call — none of that is worth failing a
|
|
3776
|
+
* session over, and the panel simply stays absent.
|
|
3777
|
+
*/
|
|
3778
|
+
async #refreshSkills(connection) {
|
|
3779
|
+
if (this.#skillsRefresh) return this.#skillsRefresh;
|
|
3780
|
+
const run = (async () => {
|
|
3781
|
+
try {
|
|
3782
|
+
const result = await connection.request("skills/list", { cwds: [this.#config.cwd] });
|
|
3783
|
+
if (this.#closed) return;
|
|
3784
|
+
const entries = Array.isArray(result?.data) ? result.data : [];
|
|
3785
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3786
|
+
const skills = [];
|
|
3787
|
+
for (const entry of entries) for (const skill of entry?.skills ?? []) {
|
|
3788
|
+
if (typeof skill?.name !== "string" || seen.has(skill.name)) continue;
|
|
3789
|
+
seen.add(skill.name);
|
|
3790
|
+
skills.push(skillInfo(skill));
|
|
3791
|
+
}
|
|
3792
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
3793
|
+
const fingerprint = JSON.stringify(skills);
|
|
3794
|
+
if (fingerprint === this.#skillsFingerprint) return;
|
|
3795
|
+
this.#skillsFingerprint = fingerprint;
|
|
3796
|
+
this.#emit({
|
|
3797
|
+
type: "skills",
|
|
3798
|
+
skills
|
|
3799
|
+
});
|
|
3800
|
+
} catch {} finally {
|
|
3801
|
+
this.#skillsRefresh = void 0;
|
|
3802
|
+
}
|
|
3803
|
+
})();
|
|
3804
|
+
this.#skillsRefresh = run;
|
|
3805
|
+
return run;
|
|
3806
|
+
}
|
|
3807
|
+
/**
|
|
3808
|
+
* The session's MCP servers, live from the binary.
|
|
3809
|
+
*
|
|
3810
|
+
* Two sources merged, because codex splits them: `mcpServerStatus/list` says
|
|
3811
|
+
* what is configured and what each server exposes (including every tool's
|
|
3812
|
+
* full JSON Schema, which the Agent SDK does not give us), and the
|
|
3813
|
+
* `mcpServer/startupStatus/updated` notifications say which of them are
|
|
3814
|
+
* actually up.
|
|
3815
|
+
*
|
|
3816
|
+
* Answers **before the session has connected**, over a throwaway child, for
|
|
3817
|
+
* the same reason the skill list does: a codex session spawns nothing until
|
|
3818
|
+
* it has work, and a panel that said "no MCP servers configured" until the
|
|
3819
|
+
* first turn would be stating something false about the operator's config.
|
|
3820
|
+
* The request blocks until the servers are enumerated (measured: complete on
|
|
3821
|
+
* the very first call), so there is no half-populated answer to race.
|
|
3822
|
+
*
|
|
3823
|
+
* Resolves undefined only when there is genuinely nothing to say — the
|
|
3824
|
+
* session is closed, or the child could not be spoken to. The route turns
|
|
3825
|
+
* that into a 501.
|
|
3826
|
+
*
|
|
3827
|
+
* **Listing only.** There is no per-server reconnect or toggle on this
|
|
3828
|
+
* transport — hence no `reconnectMcpServer`/`setMcpServerEnabled` here, and
|
|
3829
|
+
* `ENGINE_CAPABILITIES.codex.mcpServerActions: false` so clients render the
|
|
3830
|
+
* panel read-only instead of offering buttons that cannot work.
|
|
3831
|
+
*/
|
|
3832
|
+
async mcpServers() {
|
|
3833
|
+
if (this.#closed) return void 0;
|
|
3834
|
+
const live = this.#connection;
|
|
3835
|
+
let scratch;
|
|
3836
|
+
try {
|
|
3837
|
+
return ((await (live ?? (scratch = await this.#openScratchConnection())).request("mcpServerStatus/list", {}))?.data ?? []).map((server) => mcpServerInfo(server, this.#mcpStatus.get(server.name)));
|
|
3838
|
+
} catch {
|
|
3839
|
+
return;
|
|
3840
|
+
} finally {
|
|
3841
|
+
scratch?.close();
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
/**
|
|
3845
|
+
* Announce a file the ENGINE wrote on the host, so a client can fetch it
|
|
3846
|
+
* without the operator having declared its directory as a host-file root.
|
|
3847
|
+
*
|
|
3848
|
+
* Deliberately narrow: only paths codex reports as *written by its own tool*
|
|
3849
|
+
* belong here. A path the model merely read (`imageView`) is an agent-chosen
|
|
3850
|
+
* claim, and those keep going through `/fs/*` and its root allowlist — see
|
|
3851
|
+
* the note on `file_produced` in the protocol.
|
|
3852
|
+
*/
|
|
3853
|
+
#emitFileProduced(path, toolUseId) {
|
|
3854
|
+
if (this.#producedPaths.has(path)) return;
|
|
3855
|
+
this.#producedPaths.add(path);
|
|
3856
|
+
let bytes;
|
|
3857
|
+
try {
|
|
3858
|
+
const stat = statSync(path);
|
|
3859
|
+
if (stat.isFile()) bytes = stat.size;
|
|
3860
|
+
} catch {}
|
|
3861
|
+
this.#emit({
|
|
3862
|
+
type: "file_produced",
|
|
3863
|
+
fileId: producedFileId(path),
|
|
3864
|
+
path,
|
|
3865
|
+
...producedMediaType(path) ? { mediaType: producedMediaType(path) } : {},
|
|
3866
|
+
...bytes !== void 0 ? { bytes } : {},
|
|
3867
|
+
toolUseId
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
/**
|
|
3540
3871
|
* On resume, replay the thread's prior turns as `replay: true` events,
|
|
3541
3872
|
* seq'd before any live turn — the SessionRunner backfill contract, fed
|
|
3542
3873
|
* from `thread/resume`'s own `thread.turns`. When the resume response says
|
|
@@ -3751,6 +4082,21 @@ var CodexRunner = class {
|
|
|
3751
4082
|
active.contextWindow = update.tokenUsage?.modelContextWindow ?? void 0;
|
|
3752
4083
|
return;
|
|
3753
4084
|
}
|
|
4085
|
+
case "mcpServer/startupStatus/updated": {
|
|
4086
|
+
const update = params;
|
|
4087
|
+
if (typeof update?.name !== "string") return;
|
|
4088
|
+
this.#mcpStatus.set(update.name, {
|
|
4089
|
+
status: typeof update.status === "string" ? update.status : "starting",
|
|
4090
|
+
...update.error ? { error: update.error } : {},
|
|
4091
|
+
...update.failureReason ? { failureReason: update.failureReason } : {}
|
|
4092
|
+
});
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
case "skills/changed": {
|
|
4096
|
+
const connection = this.#connection;
|
|
4097
|
+
if (connection) this.#refreshSkills(connection);
|
|
4098
|
+
return;
|
|
4099
|
+
}
|
|
3754
4100
|
case "account/rateLimits/updated":
|
|
3755
4101
|
this.#emitRateLimits(params?.rateLimits);
|
|
3756
4102
|
return;
|
|
@@ -3926,6 +4272,12 @@ var CodexRunner = class {
|
|
|
3926
4272
|
if (item.type === "mcpToolCall" && !active.toolUseEmitted.has(id)) {
|
|
3927
4273
|
active.toolUseEmitted.add(id);
|
|
3928
4274
|
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
|
|
4275
|
+
return;
|
|
4276
|
+
}
|
|
4277
|
+
if (item.type === "imageGeneration" && !active.toolUseEmitted.has(id)) {
|
|
4278
|
+
active.toolUseEmitted.add(id);
|
|
4279
|
+
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
|
|
4280
|
+
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
3929
4281
|
}
|
|
3930
4282
|
}
|
|
3931
4283
|
#handleItemCompleted(item, active) {
|
|
@@ -3983,6 +4335,18 @@ var CodexRunner = class {
|
|
|
3983
4335
|
this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
|
|
3984
4336
|
this.#emitToolResult(id, "", false);
|
|
3985
4337
|
return;
|
|
4338
|
+
case "imageGeneration": {
|
|
4339
|
+
active.toolUseEmitted.add(id);
|
|
4340
|
+
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
|
|
4341
|
+
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
4342
|
+
const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
|
|
4343
|
+
this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
|
|
4344
|
+
return;
|
|
4345
|
+
}
|
|
4346
|
+
case "imageView":
|
|
4347
|
+
this.#emitToolUse(id, "CodexImageView", { path: item.path });
|
|
4348
|
+
this.#emitToolResult(id, item.path, false);
|
|
4349
|
+
return;
|
|
3986
4350
|
default: {
|
|
3987
4351
|
const unknown = item;
|
|
3988
4352
|
this.#emit({
|
|
@@ -4174,6 +4538,7 @@ var CodexRunner = class {
|
|
|
4174
4538
|
ts: Date.now()
|
|
4175
4539
|
};
|
|
4176
4540
|
this.#lastActivityAt = event.ts;
|
|
4541
|
+
this.#activityCount += transcriptActivity(body);
|
|
4177
4542
|
this.#events.push(event);
|
|
4178
4543
|
for (const listener of this.#listeners) try {
|
|
4179
4544
|
listener(event);
|