svamp-cli 0.2.342 → 0.2.346
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/bin/skills/artifact/SKILL.md +1 -1
- package/bin/skills/crew/SKILL.md +21 -3
- package/dist/DefaultTransport-vfp78fug.mjs +47 -0
- package/dist/GeminiTransport-3OkhtFRO.mjs +130 -0
- package/dist/KimiTransport-D8n009Tr.mjs +141 -0
- package/dist/acpAgentConfig-CObOPlA8.mjs +48 -0
- package/dist/acpBackend-BZ1MJhQV.mjs +922 -0
- package/dist/{adminCommands-CRTBDZan.mjs → adminCommands-BvsYnRDA.mjs} +2 -18
- package/dist/{agentCommands-BuO988tJ.mjs → agentCommands-B2Nl7Fof.mjs} +6 -23
- package/dist/api-BHjyC0Ph.mjs +157 -0
- package/dist/{auth--WS0wezs.mjs → auth-CRz8TsEl.mjs} +1 -22
- package/dist/bootStatus-DqtNgJch.mjs +143 -0
- package/dist/claudeAuth-iAerdaMr.mjs +247 -0
- package/dist/cli.mjs +2939 -5
- package/dist/codexAppServerBackend-VGsYtbqg.mjs +786 -0
- package/dist/codexProvider-BtxYc5Rj.mjs +68 -0
- package/dist/{commands-DXM17M6n.mjs → commands-BB1Kcyt-.mjs} +13 -19
- package/dist/{commands-DU4B-4Gh.mjs → commands-BOpoVBOM.mjs} +55 -25
- package/dist/{commands-C0HQiu-S.mjs → commands-C2aODeVc.mjs} +58 -33
- package/dist/{commands-Cwxy-C5N.mjs → commands-CiRCwjkJ.mjs} +10 -32
- package/dist/{commands-DDuGBSsz.mjs → commands-D1pZq3d4.mjs} +103 -41
- package/dist/{commands-BKb1g8Gq.mjs → commands-D69mnkYu.mjs} +20 -30
- package/dist/{commands-BsKgZ5sQ.mjs → commands-DW5RnHZw.mjs} +2 -19
- package/dist/{commands-DXLOKJOj.mjs → commands-H4PsTGn8.mjs} +58 -26
- package/dist/cron-hHGdb5kU.mjs +132 -0
- package/dist/detectIsolation-CzoDCEnh.mjs +319 -0
- package/dist/envFlag-COOuJ3AH.mjs +11 -0
- package/dist/fileLock-BMPwCrB6.mjs +80 -0
- package/dist/{fleet-BHzlAvnu.mjs → fleet-DM_Q7w69.mjs} +85 -24
- package/dist/flushExit-C-i6SGjj.mjs +37 -0
- package/dist/friendlyName-1UZcyDhu.mjs +586 -0
- package/dist/{headlessCli-C2gvOpOt.mjs → headlessCli-C8HzQvis.mjs} +3 -21
- package/dist/hookSettings-DYMTUgVA.mjs +10399 -0
- package/dist/{httpServer-Be1Hcddr.mjs → httpServer-BzEDbOGn.mjs} +14 -5
- package/dist/hyphaClient-ybivOjZo.mjs +49 -0
- package/dist/index.mjs +40 -8
- package/dist/instance-CeHz5C8v.mjs +203 -0
- package/dist/kimiProvider-DbxODebR.mjs +69 -0
- package/dist/kimiSetup-NGsoYB5h.mjs +48 -0
- package/dist/loopVerify-B47sSjxe.mjs +444 -0
- package/dist/machineTools-DygM1jXW.mjs +580 -0
- package/dist/model-CUHojegn.mjs +66 -0
- package/dist/{notifyCommands-0aZUwKQK.mjs → notifyCommands-D-IKIxiq.mjs} +3 -18
- package/dist/package-DtuINqf5.mjs +64 -0
- package/dist/pinnedCodex-XW4qvOAe.mjs +11 -0
- package/dist/pinnedNono-5ZHb6328.mjs +15 -0
- package/dist/{rpc-rAQQ1q8t.mjs → rpc-BB6RF4-F.mjs} +38 -6
- package/dist/{rpc-B2Lc2BAB.mjs → rpc-Cf0grcru.mjs} +28 -20
- package/dist/{run-C7QVfyw4.mjs → run-ChdsgaSD.mjs} +17 -9
- package/dist/run-DkjwF-zA.mjs +9695 -0
- package/dist/sandboxDetect-Cdzsj5TC.mjs +19 -0
- package/dist/{scheduler-CzF4lOe_.mjs → scheduler-BY30ZrB9.mjs} +38 -6
- package/dist/securityContext-Cm5UTCuD.mjs +218 -0
- package/dist/serveAuth-DbVLRCRR.mjs +418 -0
- package/dist/{serveCommands-EIEqdaVu.mjs → serveCommands-CUHUiYnZ.mjs} +13 -34
- package/dist/sharingNotificationSync-qGIQ-3yf.mjs +359 -0
- package/dist/{sideband-BoutiRJi.mjs → sideband-68HhGT0i.mjs} +1 -21
- package/dist/store-Bn0ByYev.mjs +347 -0
- package/dist/store-fi0f-ff1.mjs +184 -0
- package/dist/types-57J3Re0n.mjs +17 -0
- package/dist/wantsHelp-6bMOeNUz.mjs +5 -0
- package/dist/wrapCommand-CdNawSaa.mjs +117 -0
- package/package.json +3 -3
- package/dist/cli-B24CmIli.mjs +0 -2780
- package/dist/package-CTbGYaJG.mjs +0 -64
- package/dist/run-CF4kAVPW.mjs +0 -26508
- package/dist/sandboxDetect-DNTcbgWD.mjs +0 -12
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { execFile, execSync, spawn } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { c as codexSupportsSteer } from './pinnedCodex-XW4qvOAe.mjs';
|
|
6
|
+
import { w as wrapWithIsolation } from './wrapCommand-CdNawSaa.mjs';
|
|
7
|
+
import { resolveCodexProvider, buildCodexProviderArgs } from './codexProvider-BtxYc5Rj.mjs';
|
|
8
|
+
|
|
9
|
+
function mapCodexEventToText(event) {
|
|
10
|
+
if (!event || typeof event !== "object") return "";
|
|
11
|
+
if (typeof event.message === "string") return event.message;
|
|
12
|
+
if (typeof event.text === "string") return event.text;
|
|
13
|
+
const content = event.content;
|
|
14
|
+
if (Array.isArray(content)) {
|
|
15
|
+
return content.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").join("");
|
|
16
|
+
}
|
|
17
|
+
const delta = event.delta;
|
|
18
|
+
if (delta && typeof delta === "object" && typeof delta.text === "string") return delta.text;
|
|
19
|
+
if (typeof delta === "string") return delta;
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
function summarizeCodexChanges(changes) {
|
|
23
|
+
if (!Array.isArray(changes) || changes.length === 0) return { filePath: null, patch: "", kind: null };
|
|
24
|
+
const parts = changes.map((c) => {
|
|
25
|
+
const path = typeof c?.path === "string" ? c.path : "";
|
|
26
|
+
const kind2 = c?.kind?.type ?? (typeof c?.kind === "string" ? c.kind : null);
|
|
27
|
+
const diff = typeof c?.diff === "string" ? c.diff : "";
|
|
28
|
+
return { path, kind: kind2, diff };
|
|
29
|
+
});
|
|
30
|
+
const filePath = parts.length === 1 ? parts[0].path || null : `${parts.length} files`;
|
|
31
|
+
const patch = parts.map((x) => parts.length > 1 && x.path ? `--- ${x.path}
|
|
32
|
+
${x.diff}` : x.diff).join("\n");
|
|
33
|
+
const kind = parts.length === 1 ? parts[0].kind : null;
|
|
34
|
+
return { filePath, patch, kind };
|
|
35
|
+
}
|
|
36
|
+
function normalizeNotification(method, params) {
|
|
37
|
+
if (method === "item/agentMessage/delta") {
|
|
38
|
+
return typeof params?.delta === "string" ? { type: "agent_message_delta", delta: params.delta } : null;
|
|
39
|
+
}
|
|
40
|
+
if (method === "item/reasoning/delta") {
|
|
41
|
+
return typeof params?.delta === "string" ? { type: "agent_reasoning_delta", delta: params.delta } : null;
|
|
42
|
+
}
|
|
43
|
+
if (method === "item/started" || method === "item/completed" || method === "item/updated") {
|
|
44
|
+
const item = params?.item;
|
|
45
|
+
if (!item || typeof item !== "object") return null;
|
|
46
|
+
const done = method === "item/completed";
|
|
47
|
+
switch (item.type) {
|
|
48
|
+
case "agentMessage":
|
|
49
|
+
return done && typeof item.text === "string" && item.text ? { type: "agent_message", message: item.text } : null;
|
|
50
|
+
case "reasoning": {
|
|
51
|
+
if (!done) return null;
|
|
52
|
+
const text = Array.isArray(item.content) ? item.content.join("") : Array.isArray(item.summary) ? item.summary.join("") : typeof item.text === "string" ? item.text : "";
|
|
53
|
+
return text ? { type: "agent_reasoning", text } : null;
|
|
54
|
+
}
|
|
55
|
+
case "commandExecution":
|
|
56
|
+
return done ? { type: "exec_command_end", call_id: item.id, exit_code: item.exitCode, stdout: item.aggregatedOutput ?? "", stderr: "" } : { type: "exec_command_begin", call_id: item.id, command: item.command, cwd: item.cwd };
|
|
57
|
+
case "fileChange":
|
|
58
|
+
return done ? { type: "patch_apply_end", call_id: item.id, applied: item.status === "completed" || item.status === "applied", changes: item.changes } : { type: "patch_apply_begin", call_id: item.id, changes: item.changes };
|
|
59
|
+
case "mcpToolCall":
|
|
60
|
+
return done ? { type: "mcp_tool_call_end", call_id: item.id, tool: item.tool, result: item.result, error: item.error } : { type: "mcp_tool_call_begin", call_id: item.id, tool: item.tool, server: item.server, arguments: item.arguments };
|
|
61
|
+
case "userMessage":
|
|
62
|
+
default:
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (method === "thread/tokenUsage/updated") {
|
|
67
|
+
return { type: "token_usage", usage: params?.usage ?? params };
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function codexCumulativeTokens(payload) {
|
|
72
|
+
if (!payload || typeof payload !== "object") return 0;
|
|
73
|
+
const tu = payload.tokenUsage ?? payload.token_usage ?? payload;
|
|
74
|
+
const bucket = tu?.total ?? tu?.cumulative ?? tu;
|
|
75
|
+
if (!bucket || typeof bucket !== "object") return 0;
|
|
76
|
+
const n = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
77
|
+
const input = n(bucket.inputTokens ?? bucket.input_tokens);
|
|
78
|
+
const cached = n(bucket.cachedInputTokens ?? bucket.cached_input_tokens ?? bucket.cacheReadInputTokens);
|
|
79
|
+
const output = n(bucket.outputTokens ?? bucket.output_tokens) + n(bucket.reasoningOutputTokens ?? bucket.reasoning_output_tokens);
|
|
80
|
+
const nonCachedInput = Math.max(0, input - cached);
|
|
81
|
+
return nonCachedInput + output + Math.round(cached * 0.1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseCodexVersion(v) {
|
|
85
|
+
const m = v.match(/codex-cli\s+(\d+)\.(\d+)\.(\d+)/);
|
|
86
|
+
if (!m) return null;
|
|
87
|
+
const [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
88
|
+
if (![major, minor, patch].every(Number.isFinite)) return null;
|
|
89
|
+
return { major, minor, patch };
|
|
90
|
+
}
|
|
91
|
+
function codexAppServerAvailable() {
|
|
92
|
+
try {
|
|
93
|
+
const v = parseCodexVersion(execSync("codex --version", { encoding: "utf8" }).trim());
|
|
94
|
+
if (!v) return false;
|
|
95
|
+
return v.major > 0 || v.minor >= 100;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const _execFileAsync = promisify(execFile);
|
|
101
|
+
let _steerSupport = null;
|
|
102
|
+
let _steerProbedAt = 0;
|
|
103
|
+
let _steerProbe = null;
|
|
104
|
+
const _STEER_TTL_MS = 3e4;
|
|
105
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
|
|
106
|
+
function _steerFresh() {
|
|
107
|
+
return _steerSupport !== null && Date.now() - _steerProbedAt < _STEER_TTL_MS;
|
|
108
|
+
}
|
|
109
|
+
async function codexSupportsSteerInstalledAsync() {
|
|
110
|
+
if (_steerFresh()) return _steerSupport;
|
|
111
|
+
if (_steerProbe) return _steerProbe;
|
|
112
|
+
_steerProbe = (async () => {
|
|
113
|
+
try {
|
|
114
|
+
const { stdout } = await _execFileAsync("codex", ["--version"], { encoding: "utf8" });
|
|
115
|
+
_steerSupport = codexSupportsSteer(parseCodexVersion(stdout.trim()));
|
|
116
|
+
} catch {
|
|
117
|
+
_steerSupport = false;
|
|
118
|
+
}
|
|
119
|
+
_steerProbedAt = Date.now();
|
|
120
|
+
_steerProbe = null;
|
|
121
|
+
return _steerSupport;
|
|
122
|
+
})();
|
|
123
|
+
return _steerProbe;
|
|
124
|
+
}
|
|
125
|
+
class CodexAppServerClient {
|
|
126
|
+
constructor(opts) {
|
|
127
|
+
this.opts = opts;
|
|
128
|
+
this.log = opts.log ?? (() => {
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
process = null;
|
|
132
|
+
readline = null;
|
|
133
|
+
nextId = 1;
|
|
134
|
+
pending = /* @__PURE__ */ new Map();
|
|
135
|
+
connected = false;
|
|
136
|
+
_threadId = null;
|
|
137
|
+
_turnId = null;
|
|
138
|
+
// A turn is "in flight" from turn/start until task_complete / turn_aborted (or turn/completed).
|
|
139
|
+
pendingTurn = null;
|
|
140
|
+
// #0455: bounded — only the most-recent turns can race the turn/completed fallback
|
|
141
|
+
// against codex/event. An unbounded Set leaked one string per turn for the life of a
|
|
142
|
+
// long/loop session. Keep a small ring of recent ids (insertion-ordered) and evict the
|
|
143
|
+
// oldest past the cap; cleared in dispose().
|
|
144
|
+
completedTurnIds = /* @__PURE__ */ new Set();
|
|
145
|
+
static MAX_COMPLETED_TURN_IDS = 64;
|
|
146
|
+
log;
|
|
147
|
+
get threadId() {
|
|
148
|
+
return this._threadId;
|
|
149
|
+
}
|
|
150
|
+
get isConnected() {
|
|
151
|
+
return this.connected;
|
|
152
|
+
}
|
|
153
|
+
/** The app-server child process (for the daemon's liveness watchdog). */
|
|
154
|
+
get childProcess() {
|
|
155
|
+
return this.process;
|
|
156
|
+
}
|
|
157
|
+
// ── lifecycle ────────────────────────────────────────────────────────────
|
|
158
|
+
async start() {
|
|
159
|
+
if (this.connected) return;
|
|
160
|
+
let command = "codex";
|
|
161
|
+
let args = ["app-server", "--listen", "stdio://", ...this.opts.extraArgs ?? []];
|
|
162
|
+
let isoEnv;
|
|
163
|
+
if (this.opts.isolationConfig) {
|
|
164
|
+
const wrapped = wrapWithIsolation(command, args, this.opts.isolationConfig);
|
|
165
|
+
command = wrapped.command;
|
|
166
|
+
args = wrapped.args;
|
|
167
|
+
if (wrapped.env) isoEnv = wrapped.env;
|
|
168
|
+
}
|
|
169
|
+
const child = spawn(command, args, {
|
|
170
|
+
cwd: this.opts.cwd,
|
|
171
|
+
// RUST_LOG mutes rollout noise on stderr; keep the rest of env for auth (~/.codex or key).
|
|
172
|
+
// isoEnv goes LAST, matching acpBackend.ts:290 exactly
|
|
173
|
+
// (`{ ...process.env, ...this.options.env, ...isoEnv }`): the sandbox's HOME redirect
|
|
174
|
+
// must win over both the inherited environment and any caller-supplied override, or
|
|
175
|
+
// the confinement is advisory. Keep the two spreads in the same order — the point of
|
|
176
|
+
// this fix is that the two backends stop disagreeing about a security boundary.
|
|
177
|
+
env: { RUST_LOG: "error", ...process.env, ...this.opts.env ?? {}, ...isoEnv ?? {} },
|
|
178
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
179
|
+
});
|
|
180
|
+
this.process = child;
|
|
181
|
+
child.stderr?.on("data", (d) => this.log("[codex-app-server:stderr]", d.toString().trim()));
|
|
182
|
+
child.on("exit", (code) => {
|
|
183
|
+
const wasConnected = this.connected;
|
|
184
|
+
if (this.process === child) this.process = null;
|
|
185
|
+
this.connected = false;
|
|
186
|
+
this.failAllPending(new Error(`codex app-server exited (code ${code})`));
|
|
187
|
+
this.resolvePendingTurn(true);
|
|
188
|
+
if (wasConnected) this.opts.onExit?.(code);
|
|
189
|
+
});
|
|
190
|
+
child.on("error", (err) => {
|
|
191
|
+
this.log("[codex-app-server] spawn error", err);
|
|
192
|
+
});
|
|
193
|
+
this.readline = createInterface({ input: child.stdout });
|
|
194
|
+
this.readline.on("line", (line) => this.onLine(line));
|
|
195
|
+
const initParams = {
|
|
196
|
+
clientInfo: { name: "svamp-codex", title: "Svamp Codex Client", version: "2.0.0" },
|
|
197
|
+
capabilities: { experimentalApi: true }
|
|
198
|
+
};
|
|
199
|
+
await this.request("initialize", initParams);
|
|
200
|
+
this.notify("initialized");
|
|
201
|
+
this.connected = true;
|
|
202
|
+
this.log("[codex-app-server] connected + initialized");
|
|
203
|
+
}
|
|
204
|
+
async startThread(o) {
|
|
205
|
+
const params = {
|
|
206
|
+
model: o.model ?? null,
|
|
207
|
+
modelProvider: null,
|
|
208
|
+
profile: null,
|
|
209
|
+
cwd: o.cwd ?? this.opts.cwd,
|
|
210
|
+
approvalPolicy: o.approvalPolicy ?? null,
|
|
211
|
+
sandbox: o.sandbox ?? null,
|
|
212
|
+
config: null,
|
|
213
|
+
baseInstructions: null,
|
|
214
|
+
developerInstructions: null,
|
|
215
|
+
compactPrompt: null,
|
|
216
|
+
includeApplyPatchTool: null,
|
|
217
|
+
experimentalRawEvents: false,
|
|
218
|
+
persistExtendedHistory: true
|
|
219
|
+
};
|
|
220
|
+
const r = await this.request("thread/start", params);
|
|
221
|
+
this._threadId = r.thread.id;
|
|
222
|
+
this._turnId = null;
|
|
223
|
+
this.log("[codex-app-server] thread started", this._threadId);
|
|
224
|
+
return { threadId: r.thread.id, model: r.model };
|
|
225
|
+
}
|
|
226
|
+
async resumeThread(o) {
|
|
227
|
+
const params = {
|
|
228
|
+
threadId: o.threadId,
|
|
229
|
+
model: o.model ?? null,
|
|
230
|
+
modelProvider: null,
|
|
231
|
+
cwd: o.cwd ?? this.opts.cwd,
|
|
232
|
+
approvalPolicy: o.approvalPolicy ?? null,
|
|
233
|
+
sandbox: o.sandbox ?? null,
|
|
234
|
+
config: null,
|
|
235
|
+
baseInstructions: null,
|
|
236
|
+
developerInstructions: null,
|
|
237
|
+
persistExtendedHistory: true
|
|
238
|
+
};
|
|
239
|
+
const r = await this.request("thread/resume", params);
|
|
240
|
+
this._threadId = r.thread.id;
|
|
241
|
+
this._turnId = null;
|
|
242
|
+
this.log("[codex-app-server] thread resumed", this._threadId);
|
|
243
|
+
return { threadId: r.thread.id, model: r.model };
|
|
244
|
+
}
|
|
245
|
+
/** Build the turn/start params (exported shape kept minimal; only set fields are sent). */
|
|
246
|
+
buildTurnParams(prompt, o) {
|
|
247
|
+
const params = { threadId: this._threadId, input: [{ type: "text", text: prompt }] };
|
|
248
|
+
if (o?.approvalPolicy) params.approvalPolicy = o.approvalPolicy;
|
|
249
|
+
if (o?.model) params.model = o.model;
|
|
250
|
+
if (o?.effort) params.effort = o.effort;
|
|
251
|
+
if (o?.sandbox) {
|
|
252
|
+
params.sandboxPolicy = { type: o.sandbox === "workspace-write" ? "workspaceWrite" : o.sandbox === "danger-full-access" ? "dangerFullAccess" : "readOnly" };
|
|
253
|
+
}
|
|
254
|
+
return params;
|
|
255
|
+
}
|
|
256
|
+
/** Start a turn; resolves immediately (completion is tracked via events / waitForTurn). */
|
|
257
|
+
async startTurn(prompt, o) {
|
|
258
|
+
if (!this._threadId) throw new Error("No active codex thread \u2014 call startThread first");
|
|
259
|
+
const result = await this.request("turn/start", this.buildTurnParams(prompt, o));
|
|
260
|
+
const id = result?.turn?.id;
|
|
261
|
+
if (typeof id === "string" && id) this._turnId = id;
|
|
262
|
+
}
|
|
263
|
+
/** Send a turn and await completion (resolves {aborted} on task_complete/turn_aborted/timeout). */
|
|
264
|
+
async sendTurnAndWait(prompt, o) {
|
|
265
|
+
const timeoutMs = o?.timeoutMs ?? 14 * 24 * 60 * 60 * 1e3;
|
|
266
|
+
let timer = null;
|
|
267
|
+
const completion = new Promise((resolve) => {
|
|
268
|
+
this.pendingTurn = { resolve };
|
|
269
|
+
timer = setTimeout(() => {
|
|
270
|
+
this.log("[codex-app-server] turn timeout");
|
|
271
|
+
this.resolvePendingTurn(true);
|
|
272
|
+
}, timeoutMs);
|
|
273
|
+
timer?.unref?.();
|
|
274
|
+
});
|
|
275
|
+
try {
|
|
276
|
+
await this.startTurn(prompt, o);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
if (timer) clearTimeout(timer);
|
|
279
|
+
this.pendingTurn = null;
|
|
280
|
+
throw e;
|
|
281
|
+
}
|
|
282
|
+
const aborted = await completion;
|
|
283
|
+
if (timer) clearTimeout(timer);
|
|
284
|
+
return { aborted };
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Inject additional input into the CURRENTLY RUNNING turn (Claude-Code-style mid-turn steering).
|
|
288
|
+
*
|
|
289
|
+
* Uses codex app-server's NATIVE `turn/steer` — the correct primitive for "append this message to
|
|
290
|
+
* the in-flight turn without interrupting it." The steered input is applied at the next model step
|
|
291
|
+
* (it does NOT abort a running tool call), the assistant finishes the current work and drains the
|
|
292
|
+
* steered message as user input before its next LLM call, and NO separate `turn/started` /
|
|
293
|
+
* `turn/completed` fires — the original single in-flight completion still resolves `sendTurnAndWait`.
|
|
294
|
+
* So we send it WITHOUT arming a new `pendingTurn` slot and WITHOUT overwriting `_turnId`.
|
|
295
|
+
*
|
|
296
|
+
* (The earlier #86 implementation fired a second `turn/start` mid-turn; on codex 0.144 that
|
|
297
|
+
* ABORTS/REPLACES the running turn instead of folding — the "cuts off the previous task" bug.)
|
|
298
|
+
*
|
|
299
|
+
* Returns false when there is no active turn to steer, or when the server REJECTS the steer
|
|
300
|
+
* (no active turn / expectedTurnId mismatch / non-steerable turn kind such as review or manual
|
|
301
|
+
* compact). The caller then falls back to client-side queueing (post-turn drain via sendPrompt),
|
|
302
|
+
* so a rejected steer degrades gracefully rather than losing the message.
|
|
303
|
+
*/
|
|
304
|
+
async injectInput(prompt, _o) {
|
|
305
|
+
if (!this._threadId || !this.pendingTurn || !this._turnId) return false;
|
|
306
|
+
if (!await codexSupportsSteerInstalledAsync()) return false;
|
|
307
|
+
const params = {
|
|
308
|
+
threadId: this._threadId,
|
|
309
|
+
expectedTurnId: this._turnId,
|
|
310
|
+
input: [{ type: "text", text: prompt }]
|
|
311
|
+
};
|
|
312
|
+
try {
|
|
313
|
+
await this.request("turn/steer", params);
|
|
314
|
+
return true;
|
|
315
|
+
} catch (e) {
|
|
316
|
+
this.log("[codex-app-server] turn/steer rejected \u2014 falling back to post-turn queue", e);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** Interrupt the in-flight turn (real server-side abort — the old MCP path couldn't do this). */
|
|
321
|
+
async interrupt() {
|
|
322
|
+
if (!this._threadId || !this._turnId) return;
|
|
323
|
+
const params = { threadId: this._threadId, turnId: this._turnId };
|
|
324
|
+
try {
|
|
325
|
+
await this.request("turn/interrupt", params, 1e4);
|
|
326
|
+
} catch (e) {
|
|
327
|
+
this.log("[codex-app-server] interrupt err (may be benign)", e);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async dispose() {
|
|
331
|
+
this.readline?.close();
|
|
332
|
+
this.readline = null;
|
|
333
|
+
const proc = this.process;
|
|
334
|
+
const pid = proc?.pid;
|
|
335
|
+
try {
|
|
336
|
+
proc?.stdin?.end();
|
|
337
|
+
proc?.kill("SIGTERM");
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
if (pid) {
|
|
341
|
+
const t = setTimeout(() => {
|
|
342
|
+
try {
|
|
343
|
+
process.kill(pid, 0);
|
|
344
|
+
process.kill(pid, "SIGKILL");
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
}, 2e3);
|
|
348
|
+
t.unref?.();
|
|
349
|
+
}
|
|
350
|
+
this.process = null;
|
|
351
|
+
this.connected = false;
|
|
352
|
+
this._threadId = null;
|
|
353
|
+
this._turnId = null;
|
|
354
|
+
this.completedTurnIds.clear();
|
|
355
|
+
this.failAllPending(new Error("codex app-server disposed"));
|
|
356
|
+
this.resolvePendingTurn(true);
|
|
357
|
+
}
|
|
358
|
+
// ── JSON-RPC plumbing ──────────────────────────────────────────────────────
|
|
359
|
+
request(method, params, timeoutMs) {
|
|
360
|
+
const id = this.nextId++;
|
|
361
|
+
const effTimeout = timeoutMs ?? this.opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
362
|
+
return new Promise((resolve, reject) => {
|
|
363
|
+
this.pending.set(id, { resolve, reject, method });
|
|
364
|
+
const t = setTimeout(() => {
|
|
365
|
+
if (this.pending.has(id)) {
|
|
366
|
+
this.pending.delete(id);
|
|
367
|
+
reject(new Error(`codex ${method} timed out after ${effTimeout}ms`));
|
|
368
|
+
}
|
|
369
|
+
}, effTimeout);
|
|
370
|
+
t.unref?.();
|
|
371
|
+
this.write({ jsonrpc: "2.0", id, method, params });
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
notify(method, params) {
|
|
375
|
+
this.write({ jsonrpc: "2.0", method, params });
|
|
376
|
+
}
|
|
377
|
+
respond(id, result) {
|
|
378
|
+
this.write({ jsonrpc: "2.0", id, result });
|
|
379
|
+
}
|
|
380
|
+
write(obj) {
|
|
381
|
+
try {
|
|
382
|
+
this.process?.stdin?.write(JSON.stringify(obj) + "\n");
|
|
383
|
+
} catch (e) {
|
|
384
|
+
this.log("[codex-app-server] write error", e);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
failAllPending(err) {
|
|
388
|
+
for (const [id, p] of this.pending) {
|
|
389
|
+
p.reject(err);
|
|
390
|
+
this.pending.delete(id);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
onLine(line) {
|
|
394
|
+
const trimmed = line.trim();
|
|
395
|
+
if (!trimmed) return;
|
|
396
|
+
let msg;
|
|
397
|
+
try {
|
|
398
|
+
msg = JSON.parse(trimmed);
|
|
399
|
+
} catch {
|
|
400
|
+
this.log("[codex-app-server] non-JSON line", trimmed.slice(0, 200));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (msg.id !== void 0 && msg.method === void 0) {
|
|
404
|
+
const p = this.pending.get(msg.id);
|
|
405
|
+
if (!p) return;
|
|
406
|
+
this.pending.delete(msg.id);
|
|
407
|
+
const r = msg;
|
|
408
|
+
if (r.error) p.reject(new Error(`codex ${p.method}: ${r.error.message}`));
|
|
409
|
+
else p.resolve(r.result);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (msg.id !== void 0 && typeof msg.method === "string") {
|
|
413
|
+
void this.handleServerRequest(msg.id, msg.method, msg.params);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (typeof msg.method === "string") {
|
|
417
|
+
this.handleNotification(msg.method, msg.params);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// ── server→client approval requests ────────────────────────────────────────
|
|
422
|
+
async handleServerRequest(id, method, params) {
|
|
423
|
+
const handler = this.opts.onApproval;
|
|
424
|
+
if (method === "item/commandExecution/requestApproval" || method === "execCommandApproval") {
|
|
425
|
+
const callId = String(params?.itemId ?? params?.callId ?? id);
|
|
426
|
+
const decision = handler ? await handler({ type: "exec", callId, command: Array.isArray(params?.command) ? params.command : params?.command != null ? [params.command] : [], cwd: params?.cwd, reason: params?.reason }).catch(() => "denied") : "denied";
|
|
427
|
+
this.respond(id, { decision: mapDecisionToWire(decision) });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (method === "item/fileChange/requestApproval" || method === "applyPatchApproval") {
|
|
431
|
+
const callId = String(params?.itemId ?? params?.callId ?? id);
|
|
432
|
+
const decision = handler ? await handler({ type: "patch", callId, fileChanges: params?.fileChanges, reason: params?.reason }).catch(() => "denied") : "denied";
|
|
433
|
+
this.respond(id, { decision: mapDecisionToWire(decision) });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (method === "mcpServer/elicitation/request") {
|
|
437
|
+
const callId = `${params?.serverName ?? "mcp"}:${id}`;
|
|
438
|
+
const decision = handler ? await handler({ type: "mcp", callId, serverName: params?.serverName, toolName: params?.serverName, message: params?.message, input: params?._meta?.tool_params ?? {} }).catch(() => "denied") : "denied";
|
|
439
|
+
const wire = mapDecisionToWire(decision);
|
|
440
|
+
this.respond(id, { action: wire === "acceptForSession" ? "accept" : wire });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
this.respond(id, {});
|
|
444
|
+
}
|
|
445
|
+
// ── notifications (events + lifecycle) ──────────────────────────────────────
|
|
446
|
+
handleNotification(method, params) {
|
|
447
|
+
if (method === "codex/event" || method.startsWith("codex/event/")) {
|
|
448
|
+
const emsg = params?.msg;
|
|
449
|
+
if (!emsg) return;
|
|
450
|
+
if (emsg.type === "task_started" && emsg.turn_id) this._turnId = String(emsg.turn_id);
|
|
451
|
+
this.opts.onEvent?.(emsg);
|
|
452
|
+
if (emsg.type === "task_complete" || emsg.type === "turn_aborted") {
|
|
453
|
+
const tid = emsg.turn_id ?? emsg.turnId ?? null;
|
|
454
|
+
if (tid) this.rememberCompletedTurn(String(tid));
|
|
455
|
+
this.resolvePendingTurn(emsg.type === "turn_aborted");
|
|
456
|
+
this._turnId = null;
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (method === "turn/started") {
|
|
461
|
+
const tid = params?.turn?.id ?? params?.turnId;
|
|
462
|
+
if (tid) this._turnId = String(tid);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (method === "turn/completed") {
|
|
466
|
+
const tid = params?.turn?.id ?? params?.turnId ?? null;
|
|
467
|
+
if (tid && this.completedTurnIds.has(String(tid))) return;
|
|
468
|
+
const status = params?.turn?.status ?? params?.status;
|
|
469
|
+
const aborted = typeof status === "string" && /abort|cancel|error/i.test(status);
|
|
470
|
+
this.resolvePendingTurn(aborted);
|
|
471
|
+
this._turnId = null;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const normalized = normalizeNotification(method, params);
|
|
475
|
+
if (normalized) this.opts.onEvent?.(normalized);
|
|
476
|
+
}
|
|
477
|
+
resolvePendingTurn(aborted) {
|
|
478
|
+
const p = this.pendingTurn;
|
|
479
|
+
this.pendingTurn = null;
|
|
480
|
+
p?.resolve(aborted);
|
|
481
|
+
}
|
|
482
|
+
/** #0455: record a completed turn id, evicting the oldest past the bounded cap. */
|
|
483
|
+
rememberCompletedTurn(tid) {
|
|
484
|
+
this.completedTurnIds.add(tid);
|
|
485
|
+
while (this.completedTurnIds.size > CodexAppServerClient.MAX_COMPLETED_TURN_IDS) {
|
|
486
|
+
const oldest = this.completedTurnIds.values().next().value;
|
|
487
|
+
if (oldest === void 0) break;
|
|
488
|
+
this.completedTurnIds.delete(oldest);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function mapDecisionToWire(d) {
|
|
493
|
+
switch (d) {
|
|
494
|
+
case "approved":
|
|
495
|
+
return "accept";
|
|
496
|
+
case "approved_for_session":
|
|
497
|
+
return "acceptForSession";
|
|
498
|
+
case "denied":
|
|
499
|
+
return "decline";
|
|
500
|
+
case "abort":
|
|
501
|
+
return "cancel";
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
class CodexAppServerBackend {
|
|
506
|
+
constructor(opts) {
|
|
507
|
+
this.opts = opts;
|
|
508
|
+
this.log = opts.log ?? (() => {
|
|
509
|
+
});
|
|
510
|
+
const provider = opts.provider ?? resolveCodexProvider();
|
|
511
|
+
const { configArgs, env: providerEnv, model: providerModel } = buildCodexProviderArgs(provider);
|
|
512
|
+
this.model = opts.model ?? providerModel;
|
|
513
|
+
this.approvalPolicy = opts.approvalPolicy;
|
|
514
|
+
this.sandbox = opts.sandbox;
|
|
515
|
+
this.client = new CodexAppServerClient({
|
|
516
|
+
cwd: opts.cwd,
|
|
517
|
+
env: { ...opts.env ?? {}, ...providerEnv },
|
|
518
|
+
extraArgs: configArgs,
|
|
519
|
+
log: this.log,
|
|
520
|
+
isolationConfig: opts.isolationConfig,
|
|
521
|
+
onEvent: (m) => this.handleCodexEvent(m),
|
|
522
|
+
onApproval: (r) => this.handleApproval(r),
|
|
523
|
+
onExit: (code) => this.emit({ type: "status", status: "error", detail: `codex app-server exited (code ${code})` })
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
listeners = [];
|
|
527
|
+
client;
|
|
528
|
+
sessionId = randomUUID();
|
|
529
|
+
log;
|
|
530
|
+
model;
|
|
531
|
+
// #0438: codex's own approval policy + sandbox, derived from the svamp permission mode. Mutable
|
|
532
|
+
// so a mid-session switchMode takes effect on the next turn. yolo → 'never' + 'danger-full-access'
|
|
533
|
+
// so codex NEVER prompts for writes (and never times out waiting on a prompt).
|
|
534
|
+
approvalPolicy;
|
|
535
|
+
sandbox;
|
|
536
|
+
started = false;
|
|
537
|
+
// Approval promises keyed by callId, resolved by respondToPermission().
|
|
538
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
539
|
+
// Whether we streamed any delta for the in-flight assistant message (to avoid a duplicate
|
|
540
|
+
// fullText push on the final agent_message when the model DID stream).
|
|
541
|
+
streamedThisMessage = false;
|
|
542
|
+
// #0412 P3: cost-weighted CUMULATIVE token total across this thread (from codex usage events),
|
|
543
|
+
// read by the daemon's loop ledger to enforce token/rate cost caps on Codex loops. Monotonic.
|
|
544
|
+
cumulativeTokens = 0;
|
|
545
|
+
// Tracks the in-flight turn so waitForResponseComplete() can await it.
|
|
546
|
+
turnDone = null;
|
|
547
|
+
/** True/false if codex supports app-server; null if codex isn't installed. */
|
|
548
|
+
static available() {
|
|
549
|
+
return codexAppServerAvailable();
|
|
550
|
+
}
|
|
551
|
+
// ── AgentBackend ───────────────────────────────────────────────────────────
|
|
552
|
+
async startSession(initialPrompt) {
|
|
553
|
+
this.emit({ type: "status", status: "starting" });
|
|
554
|
+
const avail = codexAppServerAvailable();
|
|
555
|
+
if (avail === null) throw new Error("Codex CLI not found. Install it (`npm i -g @openai/codex`) and authenticate (`svamp daemon codex-auth`).");
|
|
556
|
+
if (avail === false) throw new Error("Installed Codex is too old for app-server \u2014 upgrade to >= 0.100 (`npm i -g @openai/codex@latest`).");
|
|
557
|
+
await this.client.start();
|
|
558
|
+
if (this.opts.resumeThreadId) {
|
|
559
|
+
try {
|
|
560
|
+
await this.client.resumeThread({ threadId: this.opts.resumeThreadId, model: this.model, approvalPolicy: this.approvalPolicy, sandbox: this.sandbox });
|
|
561
|
+
} catch (err) {
|
|
562
|
+
this.log(`[Codex] resumeThread(${this.opts.resumeThreadId}) failed (${err?.message ?? err}) \u2014 starting a fresh thread instead (context lost, session preserved)`);
|
|
563
|
+
const r = await this.client.startThread({ model: this.model, approvalPolicy: this.approvalPolicy, sandbox: this.sandbox });
|
|
564
|
+
this.model = this.model ?? r.model;
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
const r = await this.client.startThread({ model: this.model, approvalPolicy: this.approvalPolicy, sandbox: this.sandbox });
|
|
568
|
+
this.model = this.model ?? r.model;
|
|
569
|
+
}
|
|
570
|
+
this.started = true;
|
|
571
|
+
this.emit({ type: "status", status: "idle" });
|
|
572
|
+
if (initialPrompt) await this.sendPrompt(this.sessionId, initialPrompt);
|
|
573
|
+
return { sessionId: this.sessionId };
|
|
574
|
+
}
|
|
575
|
+
async sendPrompt(_sessionId, prompt) {
|
|
576
|
+
if (!this.started) throw new Error("Codex session not started");
|
|
577
|
+
this.emit({ type: "status", status: "running" });
|
|
578
|
+
this.streamedThisMessage = false;
|
|
579
|
+
let resolveTurn;
|
|
580
|
+
this.turnDone = new Promise((res) => {
|
|
581
|
+
resolveTurn = res;
|
|
582
|
+
});
|
|
583
|
+
try {
|
|
584
|
+
await this.client.sendTurnAndWait(prompt, { model: this.model, approvalPolicy: this.approvalPolicy, sandbox: this.sandbox });
|
|
585
|
+
} catch (err) {
|
|
586
|
+
this.emit({ type: "status", status: "error", detail: err instanceof Error ? err.message : String(err) });
|
|
587
|
+
} finally {
|
|
588
|
+
this.emit({ type: "status", status: "idle" });
|
|
589
|
+
resolveTurn();
|
|
590
|
+
this.turnDone = null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async cancel(_sessionId) {
|
|
594
|
+
await this.client.interrupt();
|
|
595
|
+
this.emit({ type: "status", status: "cancelled" });
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Mid-turn message injection (Claude-Code parity): fold `prompt` into the RUNNING turn instead
|
|
599
|
+
* of deferring it to a separate post-turn turn. Codex app-server absorbs a mid-turn `turn/start`
|
|
600
|
+
* into the active turn (see CodexAppServerClient.injectInput). Returns true if it was injected
|
|
601
|
+
* into an in-flight turn; false when no turn is running (the daemon then falls back to a normal
|
|
602
|
+
* sendPrompt, which properly tracks completion). Guards on `turnDone` to minimize the race where
|
|
603
|
+
* the turn ends between the daemon's busy-check and this call.
|
|
604
|
+
*/
|
|
605
|
+
async injectInput(prompt) {
|
|
606
|
+
if (!this.started || !this.turnDone) return false;
|
|
607
|
+
return this.client.injectInput(prompt, { model: this.model, approvalPolicy: this.approvalPolicy, sandbox: this.sandbox });
|
|
608
|
+
}
|
|
609
|
+
onMessage(handler) {
|
|
610
|
+
this.listeners.push(handler);
|
|
611
|
+
}
|
|
612
|
+
offMessage(handler) {
|
|
613
|
+
this.listeners = this.listeners.filter((h) => h !== handler);
|
|
614
|
+
}
|
|
615
|
+
async respondToPermission(requestId, approved) {
|
|
616
|
+
const resolve = this.pendingApprovals.get(requestId);
|
|
617
|
+
if (resolve) {
|
|
618
|
+
this.pendingApprovals.delete(requestId);
|
|
619
|
+
resolve(approved ? "approved" : "denied");
|
|
620
|
+
}
|
|
621
|
+
this.emit({ type: "permission-response", id: requestId, approved });
|
|
622
|
+
}
|
|
623
|
+
async waitForResponseComplete(timeoutMs = 14 * 24 * 60 * 60 * 1e3) {
|
|
624
|
+
if (!this.turnDone) return;
|
|
625
|
+
await Promise.race([this.turnDone, new Promise((res) => {
|
|
626
|
+
const t = setTimeout(res, timeoutMs);
|
|
627
|
+
t.unref?.();
|
|
628
|
+
})]);
|
|
629
|
+
}
|
|
630
|
+
async dispose() {
|
|
631
|
+
for (const [, resolve] of this.pendingApprovals) resolve("denied");
|
|
632
|
+
this.pendingApprovals.clear();
|
|
633
|
+
await this.client.dispose();
|
|
634
|
+
}
|
|
635
|
+
/** The live Codex thread id (for the daemon to persist and resume after a restart). */
|
|
636
|
+
getThreadId() {
|
|
637
|
+
return this.client.threadId;
|
|
638
|
+
}
|
|
639
|
+
/** The underlying `codex app-server` child process, for the daemon's liveness watchdog. */
|
|
640
|
+
getProcess() {
|
|
641
|
+
return this.client.childProcess ?? void 0;
|
|
642
|
+
}
|
|
643
|
+
/** Update the model used for subsequent turns (per-turn override — no restart needed). */
|
|
644
|
+
setModel(model) {
|
|
645
|
+
this.model = model;
|
|
646
|
+
}
|
|
647
|
+
/** #0438: update codex's approval policy + sandbox (from a svamp permission-mode switch). Takes
|
|
648
|
+
* effect on the next turn (and next thread start). yolo → 'never' + 'danger-full-access'. */
|
|
649
|
+
setPermission(approvalPolicy, sandbox) {
|
|
650
|
+
this.approvalPolicy = approvalPolicy;
|
|
651
|
+
this.sandbox = sandbox;
|
|
652
|
+
}
|
|
653
|
+
/** The model label for assistant-message parity with Claude (frontend schema requires a non-empty `model`). */
|
|
654
|
+
getModel() {
|
|
655
|
+
return this.model || "codex";
|
|
656
|
+
}
|
|
657
|
+
/** #0412 P3: cost-weighted cumulative token total across the thread, so the loop ledger can
|
|
658
|
+
* enforce token/rate cost caps for Codex loops. 0 until the first usage event arrives. */
|
|
659
|
+
getCumulativeTokens() {
|
|
660
|
+
return this.cumulativeTokens;
|
|
661
|
+
}
|
|
662
|
+
// ── internal ─────────────────────────────────────────────────────────────
|
|
663
|
+
emit(msg) {
|
|
664
|
+
for (const h of this.listeners) {
|
|
665
|
+
try {
|
|
666
|
+
h(msg);
|
|
667
|
+
} catch (e) {
|
|
668
|
+
this.log("[codex-backend] listener error", e);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
handleApproval(req) {
|
|
673
|
+
const id = req.callId || randomUUID();
|
|
674
|
+
const reason = req.type === "exec" ? `Execute: ${(req.command ?? []).join(" ")}` : req.type === "patch" ? "Apply file changes" : req.toolName ? `Tool: ${req.toolName}` : "Codex requires approval";
|
|
675
|
+
const payload = req.type === "exec" ? { toolName: "CodexBash", input: { command: req.command, cwd: req.cwd } } : req.type === "patch" ? { toolName: "CodexPatch", input: { fileChanges: req.fileChanges } } : { toolName: req.toolName ?? "CodexTool", input: req.input };
|
|
676
|
+
this.emit({ type: "permission-request", id, reason, payload });
|
|
677
|
+
return new Promise((resolve) => {
|
|
678
|
+
this.pendingApprovals.set(id, resolve);
|
|
679
|
+
const t = setTimeout(() => {
|
|
680
|
+
if (this.pendingApprovals.delete(id)) resolve("denied");
|
|
681
|
+
}, 5 * 60 * 1e3);
|
|
682
|
+
t.unref?.();
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
handleCodexEvent(event) {
|
|
686
|
+
const t = event.type;
|
|
687
|
+
switch (t) {
|
|
688
|
+
case "task_started":
|
|
689
|
+
this.emit({ type: "status", status: "running" });
|
|
690
|
+
break;
|
|
691
|
+
case "task_complete":
|
|
692
|
+
case "turn_aborted":
|
|
693
|
+
break;
|
|
694
|
+
case "agent_message_delta": {
|
|
695
|
+
const delta = event.delta ?? (typeof event.text === "string" ? event.text : "");
|
|
696
|
+
if (delta) {
|
|
697
|
+
this.streamedThisMessage = true;
|
|
698
|
+
this.emit({ type: "model-output", textDelta: delta });
|
|
699
|
+
}
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case "agent_message": {
|
|
703
|
+
this.streamedThisMessage = false;
|
|
704
|
+
const text = mapCodexEventToText(event);
|
|
705
|
+
if (text) this.emit({ type: "model-output", fullText: text });
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case "agent_reasoning_delta": {
|
|
709
|
+
const text = event.delta?.text || event.text || (Array.isArray(event.content) ? event.content.map((c) => c?.text).join("") : "");
|
|
710
|
+
if (text) this.emit({ type: "event", name: "thinking", payload: { text, delta: true } });
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
case "agent_reasoning": {
|
|
714
|
+
const text = event.delta?.text || event.text || (Array.isArray(event.content) ? event.content.map((c) => c?.text).join("") : "");
|
|
715
|
+
if (text) this.emit({ type: "event", name: "thinking", payload: { text, delta: false } });
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
case "exec_command_begin": {
|
|
719
|
+
const callId = String(event.call_id ?? event.callId ?? randomUUID());
|
|
720
|
+
const command = Array.isArray(event.command) ? event.command.join(" ") : String(event.command ?? "");
|
|
721
|
+
this.emit({ type: "tool-call", toolName: "CodexBash", callId, args: { command, cwd: event.cwd } });
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
case "exec_command_end": {
|
|
725
|
+
const callId = String(event.call_id ?? event.callId ?? "");
|
|
726
|
+
this.emit({ type: "tool-result", toolName: "CodexBash", callId, result: { exitCode: event.exit_code ?? event.exitCode, stdout: event.stdout ?? "", stderr: event.stderr ?? "" } });
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
case "patch_apply_begin": {
|
|
730
|
+
const callId = String(event.call_id ?? event.callId ?? randomUUID());
|
|
731
|
+
const { filePath, patch, kind } = summarizeCodexChanges(event.changes);
|
|
732
|
+
this.emit({ type: "tool-call", toolName: "CodexPatch", callId, args: {
|
|
733
|
+
filePath: filePath ?? event.file_path ?? event.filePath ?? null,
|
|
734
|
+
patch: patch || (typeof event.patch === "string" ? event.patch : ""),
|
|
735
|
+
...kind ? { kind } : {}
|
|
736
|
+
} });
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
case "patch_apply_end": {
|
|
740
|
+
const callId = String(event.call_id ?? event.callId ?? "");
|
|
741
|
+
const { filePath } = summarizeCodexChanges(event.changes);
|
|
742
|
+
this.emit({ type: "tool-result", toolName: "CodexPatch", callId, result: {
|
|
743
|
+
filePath: filePath ?? event.file_path ?? event.filePath ?? null,
|
|
744
|
+
applied: event.applied,
|
|
745
|
+
error: event.error
|
|
746
|
+
} });
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
case "mcp_tool_call_begin": {
|
|
750
|
+
const callId = String(event.call_id ?? event.callId ?? randomUUID());
|
|
751
|
+
this.emit({ type: "tool-call", toolName: String(event.tool ?? "CodexMcpTool"), callId, args: { server: event.server, arguments: event.arguments } });
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case "mcp_tool_call_end": {
|
|
755
|
+
const callId = String(event.call_id ?? event.callId ?? "");
|
|
756
|
+
this.emit({ type: "tool-result", toolName: String(event.tool ?? "CodexMcpTool"), callId, result: { result: event.result, error: event.error } });
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
case "token_count":
|
|
760
|
+
case "token_usage": {
|
|
761
|
+
const payload = event.usage ?? event.info ?? event;
|
|
762
|
+
const cum = codexCumulativeTokens(payload);
|
|
763
|
+
if (cum > this.cumulativeTokens) this.cumulativeTokens = cum;
|
|
764
|
+
this.emit({ type: "event", name: "usage", payload });
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
case "error": {
|
|
768
|
+
const msg = event.message ?? event.error ?? "Codex error";
|
|
769
|
+
this.emit({ type: "status", status: "error", detail: String(msg) });
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
default:
|
|
773
|
+
if (t === "_rpc/thread/tokenUsage/updated") {
|
|
774
|
+
this.emit({ type: "event", name: "usage", payload: event.params });
|
|
775
|
+
}
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
var codexAppServerBackend = /*#__PURE__*/Object.freeze({
|
|
782
|
+
__proto__: null,
|
|
783
|
+
CodexAppServerBackend: CodexAppServerBackend
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
export { CodexAppServerBackend as C, codexAppServerBackend as a, codexAppServerAvailable as c };
|