@suwujs/king-ai 0.2.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/README.md +96 -0
- package/dist/src/agent-config-validation.d.ts +9 -0
- package/dist/src/agent-config-validation.js +30 -0
- package/dist/src/api.d.ts +4 -0
- package/dist/src/api.js +48 -0
- package/dist/src/attachments.d.ts +45 -0
- package/dist/src/attachments.js +322 -0
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +1697 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +20 -0
- package/dist/src/cron.d.ts +11 -0
- package/dist/src/cron.js +65 -0
- package/dist/src/daemon.d.ts +36 -0
- package/dist/src/daemon.js +373 -0
- package/dist/src/engine.d.ts +32 -0
- package/dist/src/engine.js +1014 -0
- package/dist/src/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat.js +28 -0
- package/dist/src/host-api.d.ts +40 -0
- package/dist/src/host-api.js +59 -0
- package/dist/src/host-control.d.ts +48 -0
- package/dist/src/host-control.js +1279 -0
- package/dist/src/host-export.d.ts +50 -0
- package/dist/src/host-export.js +187 -0
- package/dist/src/host-feedback.d.ts +78 -0
- package/dist/src/host-feedback.js +178 -0
- package/dist/src/host-home.d.ts +13 -0
- package/dist/src/host-home.js +54 -0
- package/dist/src/host-ledger.d.ts +261 -0
- package/dist/src/host-ledger.js +554 -0
- package/dist/src/host-loop-events.d.ts +69 -0
- package/dist/src/host-loop-events.js +288 -0
- package/dist/src/host-permission.d.ts +36 -0
- package/dist/src/host-permission.js +180 -0
- package/dist/src/host-policy.d.ts +15 -0
- package/dist/src/host-policy.js +36 -0
- package/dist/src/host-run-executor.d.ts +13 -0
- package/dist/src/host-run-executor.js +221 -0
- package/dist/src/host-run-heartbeat.d.ts +40 -0
- package/dist/src/host-run-heartbeat.js +103 -0
- package/dist/src/host-run-layout.d.ts +17 -0
- package/dist/src/host-run-layout.js +387 -0
- package/dist/src/host-run-meta.d.ts +41 -0
- package/dist/src/host-run-meta.js +115 -0
- package/dist/src/host-run-spec.d.ts +149 -0
- package/dist/src/host-run-spec.js +465 -0
- package/dist/src/host-runs.d.ts +77 -0
- package/dist/src/host-runs.js +195 -0
- package/dist/src/host-sdk.d.ts +412 -0
- package/dist/src/host-sdk.js +628 -0
- package/dist/src/host-server.d.ts +26 -0
- package/dist/src/host-server.js +921 -0
- package/dist/src/host-timeline.d.ts +24 -0
- package/dist/src/host-timeline.js +161 -0
- package/dist/src/jsonl.d.ts +13 -0
- package/dist/src/jsonl.js +47 -0
- package/dist/src/lifecycle.d.ts +5 -0
- package/dist/src/lifecycle.js +18 -0
- package/dist/src/message-routing.d.ts +32 -0
- package/dist/src/message-routing.js +119 -0
- package/dist/src/paths.d.ts +19 -0
- package/dist/src/paths.js +26 -0
- package/dist/src/project-profile.d.ts +49 -0
- package/dist/src/project-profile.js +356 -0
- package/dist/src/remediation.d.ts +14 -0
- package/dist/src/remediation.js +114 -0
- package/dist/src/remote-devices.d.ts +41 -0
- package/dist/src/remote-devices.js +156 -0
- package/dist/src/remote-diagnostics.d.ts +39 -0
- package/dist/src/remote-diagnostics.js +199 -0
- package/dist/src/remote-ssh.d.ts +39 -0
- package/dist/src/remote-ssh.js +129 -0
- package/dist/src/run-stream.d.ts +57 -0
- package/dist/src/run-stream.js +119 -0
- package/dist/src/runner.d.ts +131 -0
- package/dist/src/runner.js +1161 -0
- package/dist/src/runtime-data.d.ts +68 -0
- package/dist/src/runtime-data.js +172 -0
- package/dist/src/service.d.ts +114 -0
- package/dist/src/service.js +631 -0
- package/dist/src/shared-skills.d.ts +26 -0
- package/dist/src/shared-skills.js +85 -0
- package/dist/src/shim.d.ts +1 -0
- package/dist/src/shim.js +64 -0
- package/dist/src/skill-check.d.ts +17 -0
- package/dist/src/skill-check.js +158 -0
- package/dist/src/sse.d.ts +9 -0
- package/dist/src/sse.js +36 -0
- package/dist/src/team-routing.d.ts +55 -0
- package/dist/src/team-routing.js +131 -0
- package/dist/src/team-workflow.d.ts +78 -0
- package/dist/src/team-workflow.js +253 -0
- package/dist/src/text.d.ts +7 -0
- package/dist/src/text.js +27 -0
- package/dist/src/types.d.ts +98 -0
- package/dist/src/types.js +1 -0
- package/dist/src/usage.d.ts +116 -0
- package/dist/src/usage.js +350 -0
- package/dist/src/workspace.d.ts +9 -0
- package/dist/src/workspace.js +56 -0
- package/dist/src/worktree.d.ts +47 -0
- package/dist/src/worktree.js +201 -0
- package/package.json +63 -0
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, delimiter as PATH_DELIMITER } from "node:path";
|
|
5
|
+
import { cleanLine, stripLoneSurrogates } from "./text.js";
|
|
6
|
+
const IS_WIN = process.platform === "win32";
|
|
7
|
+
const DOCTOR_PROMPT = "Connectivity check. Reply with exactly: OK";
|
|
8
|
+
const MAX_FAILURE_CHARS = 4000;
|
|
9
|
+
const TURN_TIMEOUT_MS = Number(process.env.KING_AI_TURN_TIMEOUT_MS) || 0;
|
|
10
|
+
const SESSION_TIMEOUT_MS = Number(process.env.KING_AI_SESSION_TIMEOUT_MS) || 0;
|
|
11
|
+
export function splitExtraArgs(raw) {
|
|
12
|
+
if (!raw)
|
|
13
|
+
return [];
|
|
14
|
+
const args = [];
|
|
15
|
+
let current = "";
|
|
16
|
+
let quote = null;
|
|
17
|
+
let escaping = false;
|
|
18
|
+
for (const ch of raw) {
|
|
19
|
+
if (escaping) {
|
|
20
|
+
current += ch;
|
|
21
|
+
escaping = false;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (ch === "\\") {
|
|
25
|
+
escaping = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (quote) {
|
|
29
|
+
if (ch === quote)
|
|
30
|
+
quote = null;
|
|
31
|
+
else
|
|
32
|
+
current += ch;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (ch === "'" || ch === '"') {
|
|
36
|
+
quote = ch;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (/\s/.test(ch)) {
|
|
40
|
+
if (current) {
|
|
41
|
+
args.push(current);
|
|
42
|
+
current = "";
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
current += ch;
|
|
47
|
+
}
|
|
48
|
+
if (escaping)
|
|
49
|
+
current += "\\";
|
|
50
|
+
if (current)
|
|
51
|
+
args.push(current);
|
|
52
|
+
return args;
|
|
53
|
+
}
|
|
54
|
+
function envExtraArgs(...names) {
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
const args = splitExtraArgs(process.env[name]);
|
|
57
|
+
if (args.length)
|
|
58
|
+
return args;
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
function pushTail(lines, line, max = 80) {
|
|
63
|
+
lines.push(line);
|
|
64
|
+
if (lines.length > max)
|
|
65
|
+
lines.splice(0, lines.length - max);
|
|
66
|
+
}
|
|
67
|
+
function textOfContent(content) {
|
|
68
|
+
if (typeof content === "string")
|
|
69
|
+
return content;
|
|
70
|
+
if (!Array.isArray(content))
|
|
71
|
+
return "";
|
|
72
|
+
return content
|
|
73
|
+
.map((part) => {
|
|
74
|
+
if (typeof part === "string")
|
|
75
|
+
return part;
|
|
76
|
+
if (part &&
|
|
77
|
+
typeof part === "object" &&
|
|
78
|
+
"type" in part &&
|
|
79
|
+
part.type === "text" &&
|
|
80
|
+
"text" in part &&
|
|
81
|
+
typeof part.text === "string") {
|
|
82
|
+
return part.text;
|
|
83
|
+
}
|
|
84
|
+
return "";
|
|
85
|
+
})
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.join(" ");
|
|
88
|
+
}
|
|
89
|
+
export function formatEngineLogLine(engine, line) {
|
|
90
|
+
const cleaned = cleanLine(line);
|
|
91
|
+
if (!cleaned)
|
|
92
|
+
return null;
|
|
93
|
+
if (!cleaned.startsWith("{"))
|
|
94
|
+
return cleaned;
|
|
95
|
+
try {
|
|
96
|
+
const obj = JSON.parse(cleaned);
|
|
97
|
+
if (engine === "claude") {
|
|
98
|
+
if (obj.type === "system" && obj.subtype === "init")
|
|
99
|
+
return "[claude] session initialized";
|
|
100
|
+
if (obj.type === "assistant") {
|
|
101
|
+
const message = obj.message;
|
|
102
|
+
const text = textOfContent(message?.content).replace(/\s+/g, " ").trim();
|
|
103
|
+
return text ? `[claude] ${text.slice(0, 500)}` : null;
|
|
104
|
+
}
|
|
105
|
+
if (obj.type === "user") {
|
|
106
|
+
const message = obj.message;
|
|
107
|
+
const text = textOfContent(message?.content).replace(/\s+/g, " ").trim();
|
|
108
|
+
return text ? `[tool] ${text.slice(0, 500)}` : null;
|
|
109
|
+
}
|
|
110
|
+
if (obj.type === "result") {
|
|
111
|
+
if (obj.is_error === true)
|
|
112
|
+
return `[claude] failed: ${String(obj.result ?? "error").slice(0, 500)}`;
|
|
113
|
+
return "[claude] turn completed";
|
|
114
|
+
}
|
|
115
|
+
if (obj.subtype === "status" && obj.status === "compacting")
|
|
116
|
+
return "[claude] native context compaction started";
|
|
117
|
+
if (obj.subtype === "compact_boundary")
|
|
118
|
+
return "[claude] native context compaction finished";
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (engine === "codex") {
|
|
122
|
+
if (process.env.KING_AI_CODEX_VERBOSE === "1")
|
|
123
|
+
return cleaned;
|
|
124
|
+
const method = typeof obj.method === "string" ? obj.method : "";
|
|
125
|
+
const params = obj.params;
|
|
126
|
+
const item = params?.item;
|
|
127
|
+
if (method === "item/started" && item?.type === "commandExecution" && typeof item.command === "string") {
|
|
128
|
+
return `[codex] $ ${item.command.replace(/\s+/g, " ").slice(0, 500)}`;
|
|
129
|
+
}
|
|
130
|
+
if (method === "item/completed" && item?.type === "agentMessage" && typeof item.text === "string") {
|
|
131
|
+
return `[codex] ${item.text.replace(/\s+/g, " ").slice(0, 500)}`;
|
|
132
|
+
}
|
|
133
|
+
if (method === "turn/completed")
|
|
134
|
+
return "[codex] turn completed";
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return cleaned;
|
|
140
|
+
}
|
|
141
|
+
return cleaned;
|
|
142
|
+
}
|
|
143
|
+
function resolveSpawn(bin) {
|
|
144
|
+
if (!IS_WIN)
|
|
145
|
+
return { command: bin, shell: false, wantsStdinPrompt: false };
|
|
146
|
+
const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
147
|
+
for (const dir of (process.env.PATH ?? "").split(PATH_DELIMITER)) {
|
|
148
|
+
for (const ext of ["", ...exts]) {
|
|
149
|
+
const candidate = join(dir, bin + ext);
|
|
150
|
+
if (existsSync(candidate)) {
|
|
151
|
+
const batch = /\.(cmd|bat)$/i.test(candidate);
|
|
152
|
+
return { command: candidate, shell: batch, wantsStdinPrompt: batch };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { command: bin, shell: true, wantsStdinPrompt: true };
|
|
157
|
+
}
|
|
158
|
+
export async function binOnPath(bin) {
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
const probe = spawn(IS_WIN ? "where" : "which", [bin], { stdio: "ignore" });
|
|
161
|
+
probe.on("error", () => resolve(false));
|
|
162
|
+
probe.on("close", (code) => resolve(code === 0));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async function exists(path) {
|
|
166
|
+
try {
|
|
167
|
+
await access(path);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function ensureCommonHome(home) {
|
|
175
|
+
await mkdir(join(home, "memory"), { recursive: true });
|
|
176
|
+
await mkdir(join(home, "notes"), { recursive: true });
|
|
177
|
+
await mkdir(join(home, "workspace"), { recursive: true });
|
|
178
|
+
const index = join(home, "memory", "MEMORY.md");
|
|
179
|
+
if (!(await exists(index))) {
|
|
180
|
+
await writeFile(index, "# Memory index\n\nOne line per durable fact, pointing at the file that holds it:\n`- [Title](file.md) - one-line hook`\n\nWrite the fact itself in its own `memory/<topic>.md` file; keep this index short.\n", "utf8");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export function personaHeader(persona) {
|
|
184
|
+
return `# ${persona.name}${persona.role ? ` - ${persona.role}` : ""}
|
|
185
|
+
|
|
186
|
+
You are ${persona.name}, a teammate running from this local agent home.
|
|
187
|
+
|
|
188
|
+
This directory is your private home and working directory. It persists across wakes and is yours alone.
|
|
189
|
+
|
|
190
|
+
Layout:
|
|
191
|
+
- CLAUDE.md this file; keep it short.
|
|
192
|
+
- memory/ durable notes indexed by memory/MEMORY.md.
|
|
193
|
+
- notes/ scratch notes and reply drafts.
|
|
194
|
+
- .claude/skills/ your skills.
|
|
195
|
+
- workspace/ project files, clones, downloads, builds, and temporary work. Use workspace/ for project work instead of cluttering the home root.
|
|
196
|
+
|
|
197
|
+
Privacy boundary:
|
|
198
|
+
- Stay inside this home directory unless the operator explicitly asks otherwise in this runtime.
|
|
199
|
+
- Do not read, list, search, quote, summarize, or send files outside this home directory.
|
|
200
|
+
- If a task seems to need a file outside this home, ask in the runtime first.
|
|
201
|
+
|
|
202
|
+
Use the \`king-ai\` command on PATH to interact with the remote runtime.
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
function failurePreview(exitCode, signalName, stderr, stdout) {
|
|
206
|
+
const detail = [...stderr, ...stdout].join("\n").trim();
|
|
207
|
+
const prefix = signalName ? `process terminated by ${signalName}` : `process exited with code ${exitCode}`;
|
|
208
|
+
return detail ? `${prefix}\n${detail}`.slice(0, MAX_FAILURE_CHARS) : prefix;
|
|
209
|
+
}
|
|
210
|
+
export function claudeStreamUserMessage(text) {
|
|
211
|
+
return JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: stripLoneSurrogates(text) }] } }) + "\n";
|
|
212
|
+
}
|
|
213
|
+
function codexUserInput(text, imagePaths = []) {
|
|
214
|
+
return [
|
|
215
|
+
{ type: "text", text: stripLoneSurrogates(text), text_elements: [] },
|
|
216
|
+
...imagePaths.filter((path) => path.trim()).map((path) => ({ type: "localImage", path }))
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
function codexImageArgs(imagePaths) {
|
|
220
|
+
return (imagePaths ?? []).filter((path) => path.trim()).flatMap((path) => ["--image", path]);
|
|
221
|
+
}
|
|
222
|
+
function unknownRecord(value) {
|
|
223
|
+
return value && typeof value === "object" ? value : null;
|
|
224
|
+
}
|
|
225
|
+
function nestedRecord(value, key) {
|
|
226
|
+
return unknownRecord(unknownRecord(value)?.[key]);
|
|
227
|
+
}
|
|
228
|
+
function errorMessage(value, fallback) {
|
|
229
|
+
const rec = unknownRecord(value);
|
|
230
|
+
const nested = unknownRecord(rec?.error);
|
|
231
|
+
const message = rec?.message ?? nested?.message;
|
|
232
|
+
return typeof message === "string" && message.trim() ? message.slice(0, MAX_FAILURE_CHARS) : fallback;
|
|
233
|
+
}
|
|
234
|
+
export function reduceCodexAppEvent(state, msg) {
|
|
235
|
+
const logs = [];
|
|
236
|
+
let activeTurnId = state.activeTurnId;
|
|
237
|
+
let steerGate = state.steerGate;
|
|
238
|
+
const method = typeof msg.method === "string" ? msg.method : "";
|
|
239
|
+
const result = unknownRecord(msg.result);
|
|
240
|
+
const params = unknownRecord(msg.params);
|
|
241
|
+
const threadId = nestedRecord(result, "thread")?.id ?? (method === "thread/started" ? nestedRecord(params, "thread")?.id : undefined);
|
|
242
|
+
const turnId = nestedRecord(result, "turn")?.id ?? result?.turnId ?? (method === "turn/started" ? nestedRecord(params, "turn")?.id : undefined);
|
|
243
|
+
if (typeof turnId === "string") {
|
|
244
|
+
activeTurnId = turnId;
|
|
245
|
+
steerGate = false;
|
|
246
|
+
}
|
|
247
|
+
if (method === "thread/tokenUsage/updated") {
|
|
248
|
+
return { logs, activeTurnId, steerGate, usage: nestedRecord(params, "tokenUsage")?.total };
|
|
249
|
+
}
|
|
250
|
+
if (method === "account/rateLimits/updated") {
|
|
251
|
+
const pct = unknownRecord(nestedRecord(params, "rateLimits")?.primary)?.usedPercent;
|
|
252
|
+
if (typeof pct === "number" && pct >= 90)
|
|
253
|
+
logs.push(`[codex] account rate limit at ${Math.round(pct)}% - turns will start failing when it reaches 100%`);
|
|
254
|
+
return { logs, activeTurnId, steerGate };
|
|
255
|
+
}
|
|
256
|
+
if (method === "item/started" || method === "item/completed") {
|
|
257
|
+
const item = nestedRecord(params, "item");
|
|
258
|
+
const type = item?.type;
|
|
259
|
+
if (type === "contextCompaction") {
|
|
260
|
+
logs.push(`[codex] native context compaction ${method === "item/started" ? "started" : "finished"}`);
|
|
261
|
+
}
|
|
262
|
+
else if (item && type === "commandExecution" && method === "item/started" && typeof item.command === "string") {
|
|
263
|
+
logs.push(`[codex] $ ${item.command.replace(/\s+/g, " ").slice(0, 200)}`);
|
|
264
|
+
}
|
|
265
|
+
else if (item && type === "agentMessage" && method === "item/completed" && typeof item.text === "string" && item.text.trim()) {
|
|
266
|
+
logs.push(`[codex] ${item.text.replace(/\s+/g, " ").slice(0, 200)}`);
|
|
267
|
+
}
|
|
268
|
+
if (method === "item/completed")
|
|
269
|
+
steerGate = true;
|
|
270
|
+
return { logs, activeTurnId, steerGate };
|
|
271
|
+
}
|
|
272
|
+
if (method === "item/agentMessage/delta" || method === "item/reasoning/textDelta" || method === "item/reasoning/summaryTextDelta") {
|
|
273
|
+
return { logs, activeTurnId, steerGate: false };
|
|
274
|
+
}
|
|
275
|
+
if (method === "turn/completed") {
|
|
276
|
+
const turn = nestedRecord(params, "turn") ?? nestedRecord(result, "turn");
|
|
277
|
+
const failed = turn?.status === "failed" ? String(nestedRecord(turn, "error")?.message ?? "codex turn failed") : undefined;
|
|
278
|
+
return { logs, activeTurnId: null, steerGate: false, turnCompletedError: failed };
|
|
279
|
+
}
|
|
280
|
+
return { logs, activeTurnId, steerGate, threadId: typeof threadId === "string" ? threadId : undefined };
|
|
281
|
+
}
|
|
282
|
+
function spawnCapture(bin, args, opts) {
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
const child = spawn(bin, args, {
|
|
285
|
+
cwd: opts.cwd,
|
|
286
|
+
env: opts.env,
|
|
287
|
+
stdio: [opts.stdinText == null ? "ignore" : "pipe", "pipe", "pipe"],
|
|
288
|
+
shell: opts.shell ?? false
|
|
289
|
+
});
|
|
290
|
+
if (opts.stdinText != null) {
|
|
291
|
+
child.stdin?.write(opts.stdinText);
|
|
292
|
+
child.stdin?.end();
|
|
293
|
+
}
|
|
294
|
+
const onAbort = () => child.kill("SIGTERM");
|
|
295
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
296
|
+
let stdout = "";
|
|
297
|
+
const stderr = [];
|
|
298
|
+
child.stdout?.on("data", (buf) => {
|
|
299
|
+
const text = buf.toString("utf8");
|
|
300
|
+
stdout += text;
|
|
301
|
+
const line = cleanLine(text);
|
|
302
|
+
if (line)
|
|
303
|
+
opts.onLog?.(line);
|
|
304
|
+
});
|
|
305
|
+
child.stderr?.on("data", (buf) => {
|
|
306
|
+
for (const raw of buf.toString("utf8").split("\n")) {
|
|
307
|
+
const line = cleanLine(raw);
|
|
308
|
+
if (line)
|
|
309
|
+
stderr.push(line);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
child.on("error", (err) => {
|
|
313
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
314
|
+
resolve({ text: "", error: err.message });
|
|
315
|
+
});
|
|
316
|
+
child.on("close", (code, signalName) => {
|
|
317
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
318
|
+
const exitCode = code ?? (signalName ? 128 : 1);
|
|
319
|
+
resolve({
|
|
320
|
+
text: stdout.trim(),
|
|
321
|
+
error: exitCode === 0 ? undefined : failurePreview(exitCode, signalName, stderr, [])
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function spawnEngine(bin, args, opts) {
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
const child = spawn(bin, args, {
|
|
329
|
+
cwd: opts.home,
|
|
330
|
+
env: opts.env,
|
|
331
|
+
stdio: [opts.stdinText == null ? "ignore" : "pipe", "pipe", "pipe"],
|
|
332
|
+
shell: opts.shell ?? false
|
|
333
|
+
});
|
|
334
|
+
if (opts.stdinText != null) {
|
|
335
|
+
child.stdin?.write(opts.stdinText);
|
|
336
|
+
child.stdin?.end();
|
|
337
|
+
}
|
|
338
|
+
const stderr = [];
|
|
339
|
+
const stdout = [];
|
|
340
|
+
let sessionId = null;
|
|
341
|
+
let usage;
|
|
342
|
+
let model = null;
|
|
343
|
+
let timer = null;
|
|
344
|
+
const onAbort = () => child.kill("SIGTERM");
|
|
345
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
346
|
+
if (TURN_TIMEOUT_MS > 0)
|
|
347
|
+
timer = setTimeout(onAbort, TURN_TIMEOUT_MS);
|
|
348
|
+
child.stdout?.on("data", (buf) => {
|
|
349
|
+
for (const raw of buf.toString("utf8").split("\n")) {
|
|
350
|
+
const line = cleanLine(raw);
|
|
351
|
+
if (!line)
|
|
352
|
+
continue;
|
|
353
|
+
stdout.push(line);
|
|
354
|
+
opts.onLog(line);
|
|
355
|
+
if (!line.startsWith("{"))
|
|
356
|
+
continue;
|
|
357
|
+
try {
|
|
358
|
+
const obj = JSON.parse(line);
|
|
359
|
+
if (typeof obj.session_id === "string")
|
|
360
|
+
sessionId = obj.session_id;
|
|
361
|
+
if (obj.type === "result" && typeof obj.usage === "object")
|
|
362
|
+
usage = obj.usage;
|
|
363
|
+
const message = obj.message;
|
|
364
|
+
if (typeof obj.model === "string")
|
|
365
|
+
model = obj.model;
|
|
366
|
+
if (typeof message?.model === "string")
|
|
367
|
+
model = message.model;
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// Ignore non-event JSON.
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
child.stderr?.on("data", (buf) => {
|
|
375
|
+
for (const raw of buf.toString("utf8").split("\n")) {
|
|
376
|
+
const line = cleanLine(raw);
|
|
377
|
+
if (!line)
|
|
378
|
+
continue;
|
|
379
|
+
stderr.push(line);
|
|
380
|
+
opts.onLog(line);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
child.on("error", reject);
|
|
384
|
+
child.on("close", (code, signalName) => {
|
|
385
|
+
if (timer)
|
|
386
|
+
clearTimeout(timer);
|
|
387
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
388
|
+
const exitCode = code ?? (signalName ? 128 : 1);
|
|
389
|
+
resolve({
|
|
390
|
+
exitCode,
|
|
391
|
+
error: exitCode === 0 ? undefined : failurePreview(exitCode, signalName, stderr, stdout),
|
|
392
|
+
sessionId,
|
|
393
|
+
usage,
|
|
394
|
+
model
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
class ClaudeSession {
|
|
400
|
+
carriesStandingPrompt;
|
|
401
|
+
child;
|
|
402
|
+
outBuf = "";
|
|
403
|
+
pending = null;
|
|
404
|
+
exited = false;
|
|
405
|
+
exitCode = 0;
|
|
406
|
+
sid = null;
|
|
407
|
+
currentModel = null;
|
|
408
|
+
stderrTail = [];
|
|
409
|
+
stdoutTail = [];
|
|
410
|
+
steerQueue = [];
|
|
411
|
+
constructor(bin, args, opts, carriesStandingPrompt) {
|
|
412
|
+
this.carriesStandingPrompt = carriesStandingPrompt;
|
|
413
|
+
this.child = spawn(bin, args, {
|
|
414
|
+
cwd: opts.home,
|
|
415
|
+
env: opts.env,
|
|
416
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
417
|
+
shell: opts.shell ?? false
|
|
418
|
+
});
|
|
419
|
+
this.child.stdout?.on("data", (buf) => this.onStdout(buf, opts.onLog));
|
|
420
|
+
this.child.stderr?.on("data", (buf) => this.onStderr(buf, opts.onLog));
|
|
421
|
+
this.child.on("error", (err) => this.die(1, err.message));
|
|
422
|
+
this.child.on("close", (code, signalName) => this.die(code ?? (signalName ? 128 : 1), signalName ? `terminated by ${signalName}` : `exited with code ${code ?? 1}`));
|
|
423
|
+
if (opts.stdinText)
|
|
424
|
+
this.child.stdin?.write(opts.stdinText);
|
|
425
|
+
}
|
|
426
|
+
get alive() {
|
|
427
|
+
return !this.exited && this.child.stdin?.writable === true;
|
|
428
|
+
}
|
|
429
|
+
get sessionId() {
|
|
430
|
+
return this.sid;
|
|
431
|
+
}
|
|
432
|
+
send(prompt) {
|
|
433
|
+
if (this.pending)
|
|
434
|
+
return Promise.resolve({ exitCode: 1, error: "engine session is already running a turn", sessionId: this.sid });
|
|
435
|
+
if (!this.alive) {
|
|
436
|
+
const exitCode = this.exitCode || 1;
|
|
437
|
+
const detail = failurePreview(exitCode, null, this.stderrTail, this.stdoutTail);
|
|
438
|
+
return Promise.resolve({ exitCode, error: detail || "engine session is not alive", sessionId: this.sid });
|
|
439
|
+
}
|
|
440
|
+
return new Promise((resolve) => {
|
|
441
|
+
const pending = { resolve, stdout: [], stderr: [], timer: null };
|
|
442
|
+
if (SESSION_TIMEOUT_MS > 0) {
|
|
443
|
+
pending.timer = setTimeout(() => {
|
|
444
|
+
this.settle({
|
|
445
|
+
exitCode: 124,
|
|
446
|
+
error: `engine turn exceeded KING_AI_TURN_TIMEOUT_MS (${Math.round(SESSION_TIMEOUT_MS / 1000)}s) - aborted; session will respawn`,
|
|
447
|
+
sessionId: this.sid
|
|
448
|
+
});
|
|
449
|
+
this.stop();
|
|
450
|
+
}, SESSION_TIMEOUT_MS);
|
|
451
|
+
pending.timer.unref?.();
|
|
452
|
+
}
|
|
453
|
+
this.pending = pending;
|
|
454
|
+
this.child.stdin?.write(claudeStreamUserMessage(prompt));
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
steer(text) {
|
|
458
|
+
if (!this.pending || !this.alive || !text.trim())
|
|
459
|
+
return;
|
|
460
|
+
this.steerQueue.push(text);
|
|
461
|
+
}
|
|
462
|
+
stop() {
|
|
463
|
+
this.exited = true;
|
|
464
|
+
this.child.stdin?.end();
|
|
465
|
+
this.child.kill("SIGTERM");
|
|
466
|
+
}
|
|
467
|
+
onStdout(buf, onLog) {
|
|
468
|
+
this.outBuf += buf.toString("utf8");
|
|
469
|
+
let nl;
|
|
470
|
+
while ((nl = this.outBuf.indexOf("\n")) >= 0) {
|
|
471
|
+
const line = cleanLine(this.outBuf.slice(0, nl));
|
|
472
|
+
this.outBuf = this.outBuf.slice(nl + 1);
|
|
473
|
+
if (!line)
|
|
474
|
+
continue;
|
|
475
|
+
pushTail(this.stdoutTail, line);
|
|
476
|
+
if (this.pending)
|
|
477
|
+
pushTail(this.pending.stdout, line);
|
|
478
|
+
const display = formatEngineLogLine("claude", line);
|
|
479
|
+
if (display)
|
|
480
|
+
onLog(display);
|
|
481
|
+
if (!line.startsWith("{"))
|
|
482
|
+
continue;
|
|
483
|
+
try {
|
|
484
|
+
const obj = JSON.parse(line);
|
|
485
|
+
if (typeof obj.session_id === "string")
|
|
486
|
+
this.sid = obj.session_id;
|
|
487
|
+
if (typeof obj.model === "string")
|
|
488
|
+
this.currentModel = obj.model;
|
|
489
|
+
const message = obj.message;
|
|
490
|
+
if (typeof message?.model === "string")
|
|
491
|
+
this.currentModel = message.model;
|
|
492
|
+
if (obj.type === "result") {
|
|
493
|
+
this.steerQueue = [];
|
|
494
|
+
const isError = obj.is_error === true;
|
|
495
|
+
const detail = failurePreview(1, null, this.pending?.stderr ?? [], this.pending?.stdout ?? []);
|
|
496
|
+
const resultText = String(obj.result ?? "").trim();
|
|
497
|
+
this.settle({
|
|
498
|
+
exitCode: isError ? 1 : 0,
|
|
499
|
+
error: isError ? (resultText && resultText !== "error" ? resultText : detail || "engine turn error").slice(0, MAX_FAILURE_CHARS) : undefined,
|
|
500
|
+
sessionId: this.sid,
|
|
501
|
+
usage: obj.usage && typeof obj.usage === "object" ? obj.usage : undefined,
|
|
502
|
+
model: this.currentModel
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
else if (obj.type === "user") {
|
|
506
|
+
this.flushSteer();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// Ignore malformed event JSON.
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
onStderr(buf, onLog) {
|
|
515
|
+
for (const raw of buf.toString("utf8").split("\n")) {
|
|
516
|
+
const line = cleanLine(raw);
|
|
517
|
+
if (!line)
|
|
518
|
+
continue;
|
|
519
|
+
pushTail(this.stderrTail, line);
|
|
520
|
+
if (this.pending)
|
|
521
|
+
pushTail(this.pending.stderr, line);
|
|
522
|
+
onLog(line);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
flushSteer() {
|
|
526
|
+
if (!this.pending || !this.alive)
|
|
527
|
+
return;
|
|
528
|
+
while (this.steerQueue.length > 0) {
|
|
529
|
+
const text = this.steerQueue.shift();
|
|
530
|
+
if (!text)
|
|
531
|
+
continue;
|
|
532
|
+
try {
|
|
533
|
+
this.child.stdin?.write(claudeStreamUserMessage(text));
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
settle(result) {
|
|
541
|
+
const pending = this.pending;
|
|
542
|
+
this.pending = null;
|
|
543
|
+
if (!pending)
|
|
544
|
+
return;
|
|
545
|
+
if (pending.timer)
|
|
546
|
+
clearTimeout(pending.timer);
|
|
547
|
+
pending.resolve(result);
|
|
548
|
+
}
|
|
549
|
+
die(exitCode, error) {
|
|
550
|
+
this.exited = true;
|
|
551
|
+
this.exitCode = exitCode;
|
|
552
|
+
const pending = this.pending;
|
|
553
|
+
if (!pending)
|
|
554
|
+
return;
|
|
555
|
+
const detail = failurePreview(exitCode, null, pending.stderr, pending.stdout);
|
|
556
|
+
this.settle({ exitCode, error: detail || error, sessionId: this.sid });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function ensureGitRepoForCodex(home) {
|
|
560
|
+
if (existsSync(join(home, ".git")))
|
|
561
|
+
return;
|
|
562
|
+
const gitEnv = { cwd: home, stdio: "ignore" };
|
|
563
|
+
const identity = ["-c", "user.name=king-ai", "-c", "user.email=king-ai@local", "-c", "commit.gpgsign=false"];
|
|
564
|
+
execFileSync("git", ["init"], gitEnv);
|
|
565
|
+
execFileSync("git", [...identity, "commit", "--allow-empty", "-m", "king-ai init"], gitEnv);
|
|
566
|
+
}
|
|
567
|
+
class CodexSession {
|
|
568
|
+
opts;
|
|
569
|
+
child;
|
|
570
|
+
outBuf = "";
|
|
571
|
+
reqId = 0;
|
|
572
|
+
initializeId = null;
|
|
573
|
+
threadReqId = null;
|
|
574
|
+
turnStart = { input: 0, cached: 0, output: 0 };
|
|
575
|
+
usage = { input: 0, cached: 0, output: 0 };
|
|
576
|
+
pending = null;
|
|
577
|
+
queuedTurn = null;
|
|
578
|
+
ready = false;
|
|
579
|
+
exited = false;
|
|
580
|
+
exitCode = 0;
|
|
581
|
+
threadId = null;
|
|
582
|
+
threadWasResume = false;
|
|
583
|
+
activeTurnId = null;
|
|
584
|
+
steerGate = false;
|
|
585
|
+
stderrTail = [];
|
|
586
|
+
stdoutTail = [];
|
|
587
|
+
carriesStandingPrompt;
|
|
588
|
+
constructor(bin, args, opts) {
|
|
589
|
+
this.opts = opts;
|
|
590
|
+
this.threadId = opts.resumeSessionId ?? null;
|
|
591
|
+
this.carriesStandingPrompt = !!opts.standingPrompt;
|
|
592
|
+
this.child = spawn(bin, args, { cwd: opts.home, env: opts.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
593
|
+
this.child.stdout?.on("data", (buf) => this.onStdout(buf));
|
|
594
|
+
this.child.stderr?.on("data", (buf) => {
|
|
595
|
+
for (const raw of buf.toString("utf8").split("\n")) {
|
|
596
|
+
const line = cleanLine(raw);
|
|
597
|
+
if (!line)
|
|
598
|
+
continue;
|
|
599
|
+
pushTail(this.stderrTail, line);
|
|
600
|
+
opts.onLog(line);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
this.child.on("error", (err) => this.die(1, err.message));
|
|
604
|
+
this.child.on("close", (code, sig) => this.die(code ?? (sig ? 128 : 1), sig ? `terminated by ${sig}` : `exited with code ${code ?? 1}`));
|
|
605
|
+
queueMicrotask(() => {
|
|
606
|
+
this.initializeId = this.req("initialize", { clientInfo: { name: "king-ai", version: "0.1.0" }, capabilities: { experimentalApi: true } });
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
get alive() {
|
|
610
|
+
return !this.exited && this.child.stdin?.writable === true;
|
|
611
|
+
}
|
|
612
|
+
get sessionId() {
|
|
613
|
+
return this.threadId;
|
|
614
|
+
}
|
|
615
|
+
send(prompt, options) {
|
|
616
|
+
if (this.pending)
|
|
617
|
+
return Promise.resolve({ exitCode: 1, error: "engine session is already running a turn", sessionId: this.threadId });
|
|
618
|
+
if (!this.alive) {
|
|
619
|
+
const exitCode = this.exitCode || 1;
|
|
620
|
+
return Promise.resolve({ exitCode, error: failurePreview(exitCode, null, this.stderrTail, this.stdoutTail), sessionId: this.threadId });
|
|
621
|
+
}
|
|
622
|
+
return new Promise((resolve) => {
|
|
623
|
+
const pending = { resolve, timer: null };
|
|
624
|
+
if (SESSION_TIMEOUT_MS > 0) {
|
|
625
|
+
pending.timer = setTimeout(() => {
|
|
626
|
+
this.stop();
|
|
627
|
+
this.settle("engine session timed out", 124);
|
|
628
|
+
}, SESSION_TIMEOUT_MS);
|
|
629
|
+
pending.timer.unref?.();
|
|
630
|
+
}
|
|
631
|
+
this.pending = pending;
|
|
632
|
+
this.turnStart = { ...this.usage };
|
|
633
|
+
if (this.ready && this.threadId)
|
|
634
|
+
this.startTurn(prompt, options);
|
|
635
|
+
else
|
|
636
|
+
this.queuedTurn = { prompt, options };
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
steer(text) {
|
|
640
|
+
if (!this.threadId || !this.activeTurnId || this.steerGate || !this.alive || !text.trim())
|
|
641
|
+
return;
|
|
642
|
+
this.req("turn/steer", {
|
|
643
|
+
threadId: this.threadId,
|
|
644
|
+
expectedTurnId: this.activeTurnId,
|
|
645
|
+
input: codexUserInput(text)
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
stop() {
|
|
649
|
+
this.exited = true;
|
|
650
|
+
this.child.stdin?.end();
|
|
651
|
+
this.child.kill("SIGTERM");
|
|
652
|
+
}
|
|
653
|
+
nextId() {
|
|
654
|
+
this.reqId += 1;
|
|
655
|
+
return this.reqId;
|
|
656
|
+
}
|
|
657
|
+
req(method, params) {
|
|
658
|
+
const id = this.nextId();
|
|
659
|
+
this.child.stdin?.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
660
|
+
return id;
|
|
661
|
+
}
|
|
662
|
+
notify(method, params) {
|
|
663
|
+
this.child.stdin?.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
|
|
664
|
+
}
|
|
665
|
+
threadParams() {
|
|
666
|
+
return {
|
|
667
|
+
cwd: this.opts.home,
|
|
668
|
+
approvalPolicy: "never",
|
|
669
|
+
sandbox: "danger-full-access",
|
|
670
|
+
experimentalRawEvents: true,
|
|
671
|
+
...(this.opts.standingPrompt ? { developerInstructions: this.opts.standingPrompt } : {}),
|
|
672
|
+
...(this.opts.model ? { model: this.opts.model } : {})
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
startThread() {
|
|
676
|
+
const params = this.threadParams();
|
|
677
|
+
this.threadWasResume = !!this.threadId;
|
|
678
|
+
this.threadReqId = this.threadId
|
|
679
|
+
? this.req("thread/resume", { threadId: this.threadId, ...params })
|
|
680
|
+
: this.req("thread/start", params);
|
|
681
|
+
}
|
|
682
|
+
startTurn(prompt, options) {
|
|
683
|
+
if (!this.threadId)
|
|
684
|
+
return;
|
|
685
|
+
this.req("turn/start", { threadId: this.threadId, input: codexUserInput(prompt, options?.imagePaths) });
|
|
686
|
+
}
|
|
687
|
+
onStdout(buf) {
|
|
688
|
+
this.outBuf += buf.toString("utf8");
|
|
689
|
+
let nl;
|
|
690
|
+
while ((nl = this.outBuf.indexOf("\n")) >= 0) {
|
|
691
|
+
const raw = this.outBuf.slice(0, nl);
|
|
692
|
+
this.outBuf = this.outBuf.slice(nl + 1);
|
|
693
|
+
const line = cleanLine(raw);
|
|
694
|
+
if (!line)
|
|
695
|
+
continue;
|
|
696
|
+
pushTail(this.stdoutTail, line);
|
|
697
|
+
let msg;
|
|
698
|
+
try {
|
|
699
|
+
msg = JSON.parse(line);
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
this.opts.onLog(line);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (process.env.KING_AI_CODEX_VERBOSE === "1")
|
|
706
|
+
this.opts.onLog(line);
|
|
707
|
+
this.handle(msg);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
handle(msg) {
|
|
711
|
+
if (msg.id === this.initializeId) {
|
|
712
|
+
this.initializeId = null;
|
|
713
|
+
if (msg.error)
|
|
714
|
+
return this.settle(errorMessage(msg.error, "codex initialize failed"));
|
|
715
|
+
this.notify("initialized", {});
|
|
716
|
+
this.startThread();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (msg.id === this.threadReqId && msg.error) {
|
|
720
|
+
if (this.threadWasResume) {
|
|
721
|
+
this.opts.onLog(`[codex] thread/resume failed (${errorMessage(msg.error, "unknown error")}) - starting a fresh thread`);
|
|
722
|
+
this.threadId = null;
|
|
723
|
+
this.threadWasResume = false;
|
|
724
|
+
this.startThread();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
this.threadReqId = null;
|
|
728
|
+
this.settle(errorMessage(msg.error, "codex thread start failed"));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const method = typeof msg.method === "string" ? msg.method : "";
|
|
732
|
+
if (method === "error") {
|
|
733
|
+
this.settle(errorMessage(msg.params, "codex app-server error"));
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (msg.error) {
|
|
737
|
+
this.settle(errorMessage(msg.error, "codex app-server error"));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const reduced = reduceCodexAppEvent({ activeTurnId: this.activeTurnId, steerGate: this.steerGate }, msg);
|
|
741
|
+
for (const line of reduced.logs)
|
|
742
|
+
this.opts.onLog(line);
|
|
743
|
+
this.activeTurnId = reduced.activeTurnId;
|
|
744
|
+
this.steerGate = reduced.steerGate;
|
|
745
|
+
if (reduced.usage)
|
|
746
|
+
this.updateUsage(reduced.usage);
|
|
747
|
+
if (reduced.threadId) {
|
|
748
|
+
this.threadId = reduced.threadId;
|
|
749
|
+
this.threadReqId = null;
|
|
750
|
+
this.threadWasResume = false;
|
|
751
|
+
this.ready = true;
|
|
752
|
+
if (this.queuedTurn && this.pending) {
|
|
753
|
+
const queued = this.queuedTurn;
|
|
754
|
+
this.queuedTurn = null;
|
|
755
|
+
this.startTurn(queued.prompt, queued.options);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (method === "turn/completed") {
|
|
759
|
+
this.settle(reduced.turnCompletedError);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
updateUsage(total) {
|
|
763
|
+
if (!total || typeof total !== "object")
|
|
764
|
+
return;
|
|
765
|
+
const rec = total;
|
|
766
|
+
const num = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
|
|
767
|
+
this.usage = {
|
|
768
|
+
input: Math.max(this.usage.input, num(rec.inputTokens)),
|
|
769
|
+
cached: Math.max(this.usage.cached, num(rec.cachedInputTokens)),
|
|
770
|
+
output: Math.max(this.usage.output, num(rec.outputTokens) + num(rec.reasoningOutputTokens))
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
turnUsage() {
|
|
774
|
+
const inputTotal = Math.max(0, this.usage.input - this.turnStart.input);
|
|
775
|
+
const cached = Math.max(0, this.usage.cached - this.turnStart.cached);
|
|
776
|
+
return {
|
|
777
|
+
input_tokens: Math.max(0, inputTotal - cached),
|
|
778
|
+
cache_read_input_tokens: cached,
|
|
779
|
+
output_tokens: Math.max(0, this.usage.output - this.turnStart.output)
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
settle(error, exitCode = error ? 1 : 0) {
|
|
783
|
+
const pending = this.pending;
|
|
784
|
+
this.pending = null;
|
|
785
|
+
if (!pending)
|
|
786
|
+
return;
|
|
787
|
+
if (pending.timer)
|
|
788
|
+
clearTimeout(pending.timer);
|
|
789
|
+
pending.resolve({
|
|
790
|
+
exitCode,
|
|
791
|
+
error,
|
|
792
|
+
sessionId: this.threadId,
|
|
793
|
+
usage: this.turnUsage(),
|
|
794
|
+
model: this.opts.model ?? null
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
die(exitCode, error) {
|
|
798
|
+
this.exited = true;
|
|
799
|
+
this.exitCode = exitCode;
|
|
800
|
+
const detail = failurePreview(exitCode, null, this.stderrTail, this.stdoutTail);
|
|
801
|
+
this.settle(detail || error, exitCode);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
class ClaudeAdapter {
|
|
805
|
+
id = "claude";
|
|
806
|
+
bin = "claude";
|
|
807
|
+
async seedHome(home, persona) {
|
|
808
|
+
await ensureCommonHome(home);
|
|
809
|
+
await mkdir(join(home, ".claude", "skills"), { recursive: true });
|
|
810
|
+
const claudeMd = join(home, "CLAUDE.md");
|
|
811
|
+
if (!(await exists(claudeMd)))
|
|
812
|
+
await writeFile(claudeMd, personaHeader(persona), "utf8");
|
|
813
|
+
}
|
|
814
|
+
async classify(args) {
|
|
815
|
+
const extra = envExtraArgs("KING_AI_TRIAGE_ARGS");
|
|
816
|
+
const model = ["--model", args.model || "haiku"];
|
|
817
|
+
const { command, shell, wantsStdinPrompt } = resolveSpawn(this.bin);
|
|
818
|
+
const usingJson = extra.length === 0;
|
|
819
|
+
const base = extra.length ? [...extra, "-p"] : ["-p", ...model, "--output-format", "json", "--dangerously-skip-permissions", "--strict-mcp-config"];
|
|
820
|
+
const argv = wantsStdinPrompt ? base : extra.length ? [...base, args.prompt] : ["-p", args.prompt, ...base.slice(1)];
|
|
821
|
+
const res = await spawnCapture(command, argv, {
|
|
822
|
+
cwd: args.cwd,
|
|
823
|
+
env: { ...args.env, MAX_THINKING_TOKENS: "0" },
|
|
824
|
+
signal: args.signal,
|
|
825
|
+
shell,
|
|
826
|
+
stdinText: wantsStdinPrompt ? args.prompt : undefined,
|
|
827
|
+
onLog: args.onLog
|
|
828
|
+
});
|
|
829
|
+
if (res.error || !usingJson)
|
|
830
|
+
return res;
|
|
831
|
+
try {
|
|
832
|
+
const parsed = JSON.parse(res.text);
|
|
833
|
+
return { text: parsed.result ?? res.text, usage: parsed.usage };
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
return res;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
probe(args) {
|
|
840
|
+
const model = args.tier === "small" ? ["--model", "haiku"] : [];
|
|
841
|
+
const { command, shell, wantsStdinPrompt } = resolveSpawn(this.bin);
|
|
842
|
+
const base = ["-p", ...model, "--output-format", "text", "--dangerously-skip-permissions", "--strict-mcp-config"];
|
|
843
|
+
const argv = wantsStdinPrompt ? base : ["-p", DOCTOR_PROMPT, ...base.slice(1)];
|
|
844
|
+
return spawnCapture(command, argv, {
|
|
845
|
+
cwd: args.cwd,
|
|
846
|
+
env: { ...args.env, MAX_THINKING_TOKENS: "0" },
|
|
847
|
+
signal: args.signal,
|
|
848
|
+
shell,
|
|
849
|
+
stdinText: wantsStdinPrompt ? DOCTOR_PROMPT : undefined
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
run(args) {
|
|
853
|
+
const extra = envExtraArgs("KING_AI_CLAUDE_ARGS");
|
|
854
|
+
const model = args.model ? ["--model", args.model] : [];
|
|
855
|
+
const resume = args.resumeSessionId ? ["--resume", args.resumeSessionId] : [];
|
|
856
|
+
const { command, shell, wantsStdinPrompt } = resolveSpawn(this.bin);
|
|
857
|
+
const base = extra.length ? [...extra, ...resume, "-p"] : ["-p", ...resume, ...model, "--output-format", "stream-json", "--verbose", "--dangerously-skip-permissions"];
|
|
858
|
+
const argv = wantsStdinPrompt ? base : extra.length ? [...base, args.prompt] : ["-p", args.prompt, ...base.slice(1)];
|
|
859
|
+
const env = { ...args.env, MAX_THINKING_TOKENS: args.env.MAX_THINKING_TOKENS ?? "0" };
|
|
860
|
+
if (args.fastModel)
|
|
861
|
+
env.ANTHROPIC_SMALL_FAST_MODEL = args.fastModel;
|
|
862
|
+
return spawnEngine(command, argv, {
|
|
863
|
+
...args,
|
|
864
|
+
env,
|
|
865
|
+
shell,
|
|
866
|
+
stdinText: wantsStdinPrompt ? args.prompt : undefined
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
startSession(args) {
|
|
870
|
+
if (IS_WIN || envExtraArgs("KING_AI_CLAUDE_ARGS").length)
|
|
871
|
+
return null;
|
|
872
|
+
const model = args.model ? ["--model", args.model] : [];
|
|
873
|
+
const resume = args.resumeSessionId ? ["--resume", args.resumeSessionId] : [];
|
|
874
|
+
const standingFile = join(args.home, ".king-ai-standing-prompt.md");
|
|
875
|
+
let systemPrompt = [];
|
|
876
|
+
let carriesStandingPrompt = false;
|
|
877
|
+
if (args.standingPrompt) {
|
|
878
|
+
try {
|
|
879
|
+
writeFileSync(standingFile, args.standingPrompt, { mode: 0o600 });
|
|
880
|
+
systemPrompt = ["--append-system-prompt-file", standingFile];
|
|
881
|
+
carriesStandingPrompt = true;
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
systemPrompt = [];
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const argv = [
|
|
888
|
+
"-p",
|
|
889
|
+
"--input-format",
|
|
890
|
+
"stream-json",
|
|
891
|
+
"--output-format",
|
|
892
|
+
"stream-json",
|
|
893
|
+
"--verbose",
|
|
894
|
+
...resume,
|
|
895
|
+
...systemPrompt,
|
|
896
|
+
...model,
|
|
897
|
+
"--dangerously-skip-permissions"
|
|
898
|
+
];
|
|
899
|
+
const env = { ...args.env, MAX_THINKING_TOKENS: args.env.MAX_THINKING_TOKENS ?? "0" };
|
|
900
|
+
if (args.fastModel)
|
|
901
|
+
env.ANTHROPIC_SMALL_FAST_MODEL = args.fastModel;
|
|
902
|
+
return new ClaudeSession(this.bin, argv, { ...args, env }, carriesStandingPrompt);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
class CodexAdapter {
|
|
906
|
+
id = "codex";
|
|
907
|
+
bin = "codex";
|
|
908
|
+
async seedHome(home, persona) {
|
|
909
|
+
await ensureCommonHome(home);
|
|
910
|
+
const agentsMd = join(home, "AGENTS.md");
|
|
911
|
+
if (!(await exists(agentsMd)))
|
|
912
|
+
await writeFile(agentsMd, personaHeader(persona), "utf8");
|
|
913
|
+
}
|
|
914
|
+
classify(args) {
|
|
915
|
+
const extra = envExtraArgs("KING_AI_TRIAGE_ARGS");
|
|
916
|
+
const model = ["--model", args.model || "gpt-5.4-mini"];
|
|
917
|
+
const { command, shell } = resolveSpawn(this.bin);
|
|
918
|
+
const argv = extra.length ? ["exec", ...extra, args.prompt] : ["exec", ...model, "--skip-git-repo-check", args.prompt];
|
|
919
|
+
return spawnCapture(command, argv, {
|
|
920
|
+
cwd: args.cwd,
|
|
921
|
+
env: args.env,
|
|
922
|
+
signal: args.signal,
|
|
923
|
+
shell,
|
|
924
|
+
onLog: args.onLog
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
probe(args) {
|
|
928
|
+
const model = args.tier === "small" ? ["--model", "gpt-5.4-mini"] : [];
|
|
929
|
+
const { command, shell } = resolveSpawn(this.bin);
|
|
930
|
+
return spawnCapture(command, ["exec", ...model, "--skip-git-repo-check", DOCTOR_PROMPT], {
|
|
931
|
+
cwd: args.cwd,
|
|
932
|
+
env: args.env,
|
|
933
|
+
signal: args.signal,
|
|
934
|
+
shell
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
run(args) {
|
|
938
|
+
const extra = envExtraArgs("KING_AI_CODEX_ARGS");
|
|
939
|
+
const model = args.model ? ["--model", args.model] : [];
|
|
940
|
+
const { command, shell } = resolveSpawn(this.bin);
|
|
941
|
+
const base = extra.length ? extra : ["--dangerously-bypass-approvals-and-sandbox", "--skip-git-repo-check"];
|
|
942
|
+
return spawnEngine(command, ["exec", ...model, ...codexImageArgs(args.imagePaths), ...base, args.prompt], {
|
|
943
|
+
...args,
|
|
944
|
+
shell
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
startSession(args) {
|
|
948
|
+
if (IS_WIN ||
|
|
949
|
+
envExtraArgs("KING_AI_CODEX_ARGS").length ||
|
|
950
|
+
process.env.KING_AI_CODEX_NO_APP_SERVER === "1")
|
|
951
|
+
return null;
|
|
952
|
+
try {
|
|
953
|
+
ensureGitRepoForCodex(args.home);
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
args.onLog(`[codex] could not initialize git repo for app-server: ${err instanceof Error ? err.message : String(err)}`);
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
return new CodexSession(this.bin, ["app-server", "--listen", "stdio://"], args);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
export const ADAPTERS = {
|
|
963
|
+
claude: new ClaudeAdapter(),
|
|
964
|
+
codex: new CodexAdapter()
|
|
965
|
+
};
|
|
966
|
+
export function getAdapter(id) {
|
|
967
|
+
return ADAPTERS[id];
|
|
968
|
+
}
|
|
969
|
+
export async function detectEngines() {
|
|
970
|
+
const entries = await Promise.all(Object.keys(ADAPTERS).map(async (id) => ((await binOnPath(ADAPTERS[id].bin)) ? id : null)));
|
|
971
|
+
return entries.filter((id) => id != null);
|
|
972
|
+
}
|
|
973
|
+
function parseResponseMode(value) {
|
|
974
|
+
return value === "me" || value === "each" || value === "one-of-us" ? value : undefined;
|
|
975
|
+
}
|
|
976
|
+
function extractJsonObject(text) {
|
|
977
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
978
|
+
const body = fenced ? fenced[1] : text;
|
|
979
|
+
const start = body.indexOf("{");
|
|
980
|
+
const end = body.lastIndexOf("}");
|
|
981
|
+
return start >= 0 && end > start ? body.slice(start, end + 1) : body.trim();
|
|
982
|
+
}
|
|
983
|
+
function salvageTriage(text) {
|
|
984
|
+
const actionable = text.match(/"actionable"\s*:\s*(true|false)/i);
|
|
985
|
+
if (!actionable)
|
|
986
|
+
return null;
|
|
987
|
+
const reason = text.match(/"reason"\s*:\s*"((?:[^"\\]|\\.)*)"/i);
|
|
988
|
+
const promptNote = text.match(/"prompt_?note"\s*:\s*"((?:[^"\\]|\\.)*)"/i);
|
|
989
|
+
const responseMode = text.match(/"response_?mode"\s*:\s*"(me|each|one-of-us)"/i);
|
|
990
|
+
return {
|
|
991
|
+
actionable: actionable[1].toLowerCase() === "true",
|
|
992
|
+
reason: reason ? reason[1].replace(/\\"/g, '"').slice(0, 500) : "recovered from partial triage output",
|
|
993
|
+
promptNote: promptNote ? promptNote[1].replace(/\\"/g, '"').slice(0, 1200) : undefined,
|
|
994
|
+
responseMode: parseResponseMode(responseMode?.[1])
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
export function parseTriage(text) {
|
|
998
|
+
const cleaned = stripLoneSurrogates(text).trim();
|
|
999
|
+
const raw = extractJsonObject(cleaned);
|
|
1000
|
+
try {
|
|
1001
|
+
const obj = JSON.parse(raw);
|
|
1002
|
+
if (typeof obj.actionable !== "boolean")
|
|
1003
|
+
return salvageTriage(cleaned);
|
|
1004
|
+
return {
|
|
1005
|
+
actionable: obj.actionable,
|
|
1006
|
+
reason: typeof obj.reason === "string" ? obj.reason.slice(0, 500) : undefined,
|
|
1007
|
+
promptNote: typeof obj.promptNote === "string" ? obj.promptNote.slice(0, 1200) : typeof obj.prompt_note === "string" ? obj.prompt_note.slice(0, 1200) : undefined,
|
|
1008
|
+
responseMode: parseResponseMode(obj.responseMode ?? obj.response_mode)
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
1012
|
+
return salvageTriage(cleaned);
|
|
1013
|
+
}
|
|
1014
|
+
}
|