autopilot-ai 1.2.2 → 1.4.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/dist/sdk/index.js CHANGED
@@ -325,9 +325,9 @@ var require_node_gyp_build = __commonJS({
325
325
  var debug = getFirst(path.join(dir, "build/Debug"), matchBuild);
326
326
  if (debug) return debug;
327
327
  }
328
- var prebuild = resolve4(dir);
328
+ var prebuild = resolve6(dir);
329
329
  if (prebuild) return prebuild;
330
- var nearby = resolve4(path.dirname(process.execPath));
330
+ var nearby = resolve6(path.dirname(process.execPath));
331
331
  if (nearby) return nearby;
332
332
  var target = [
333
333
  "platform=" + platform3,
@@ -343,7 +343,7 @@ var require_node_gyp_build = __commonJS({
343
343
  // eslint-disable-line
344
344
  ].filter(Boolean).join(" ");
345
345
  throw new Error("No native build was found for " + target + "\n loaded from: " + dir + "\n");
346
- function resolve4(dir2) {
346
+ function resolve6(dir2) {
347
347
  var tuples = readdirSync2(path.join(dir2, "prebuilds")).map(parseTuple);
348
348
  var tuple = tuples.filter(matchTuple(platform3, arch)).sort(compareTuples)[0];
349
349
  if (!tuple) return;
@@ -1196,12 +1196,12 @@ function normalizeHook(event, raw, source) {
1196
1196
  if (!raw || typeof raw !== "object") return null;
1197
1197
  const r = raw;
1198
1198
  if (typeof r.command !== "string" || !r.command.trim()) return null;
1199
- const enabled = typeof r.enabled === "boolean" ? r.enabled : true;
1199
+ const enabled2 = typeof r.enabled === "boolean" ? r.enabled : true;
1200
1200
  const matcher = typeof r.matcher === "string" ? r.matcher : void 0;
1201
1201
  const timeoutMs = typeof r.timeoutMs === "number" && r.timeoutMs > 0 ? r.timeoutMs : void 0;
1202
1202
  const description = typeof r.description === "string" ? r.description : void 0;
1203
1203
  const id = typeof r.id === "string" && r.id.length > 0 ? r.id : deriveHookId(event, r.command);
1204
- return { id, matcher, command: r.command, timeoutMs, enabled, description, source };
1204
+ return { id, matcher, command: r.command, timeoutMs, enabled: enabled2, description, source };
1205
1205
  }
1206
1206
  function mergeHookMaps(a, b) {
1207
1207
  const out = {};
@@ -1237,7 +1237,7 @@ var init_settings = __esm({
1237
1237
  });
1238
1238
 
1239
1239
  // src/hooks/runner.ts
1240
- import { spawn as spawn3 } from "child_process";
1240
+ import { spawn as spawn4 } from "child_process";
1241
1241
  function isVetoEvent(event) {
1242
1242
  return event === "PreToolUse" || event === "UserPromptSubmit";
1243
1243
  }
@@ -1354,8 +1354,8 @@ var init_runner = __esm({
1354
1354
  init_logger();
1355
1355
  DEFAULT_TIMEOUT_MS = 3e4;
1356
1356
  STREAM_CAP_BYTES = 4 * 1024;
1357
- defaultSpawn = (command, payloadJson, env, cwd, timeoutMs, signal) => new Promise((resolve4) => {
1358
- const child = spawn3(command, {
1357
+ defaultSpawn = (command, payloadJson, env, cwd, timeoutMs, signal) => new Promise((resolve6) => {
1358
+ const child = spawn4(command, {
1359
1359
  shell: true,
1360
1360
  cwd,
1361
1361
  env,
@@ -1369,7 +1369,7 @@ var init_runner = __esm({
1369
1369
  settled = true;
1370
1370
  clearTimeout(timer2);
1371
1371
  signal?.removeEventListener("abort", onAbort);
1372
- resolve4({ exitCode, stdout, stderr, timedOut });
1372
+ resolve6({ exitCode, stdout, stderr, timedOut });
1373
1373
  };
1374
1374
  const onAbort = () => {
1375
1375
  child.kill("SIGTERM");
@@ -1442,7 +1442,7 @@ var init_manager = __esm({
1442
1442
  });
1443
1443
 
1444
1444
  // src/sdk/session.ts
1445
- import { resolve as resolve3 } from "path";
1445
+ import { resolve as resolve5 } from "path";
1446
1446
  import { homedir as homedir11 } from "os";
1447
1447
  import { join as join18 } from "path";
1448
1448
  import { existsSync as existsSync2 } from "fs";
@@ -1490,10 +1490,10 @@ async function* readSSE(stream, signal, idleTimeoutMs, postFirstByteIdleTimeoutM
1490
1490
  gotFirstByte = true;
1491
1491
  buffer += decoder.decode(value, { stream: true });
1492
1492
  buffer = buffer.replace(/\r\n/g, "\n");
1493
- let sep2;
1494
- while ((sep2 = buffer.indexOf("\n\n")) !== -1) {
1495
- const event = buffer.slice(0, sep2);
1496
- buffer = buffer.slice(sep2 + 2);
1493
+ let sep3;
1494
+ while ((sep3 = buffer.indexOf("\n\n")) !== -1) {
1495
+ const event = buffer.slice(0, sep3);
1496
+ buffer = buffer.slice(sep3 + 2);
1497
1497
  const data = extractData(event);
1498
1498
  if (data !== null) yield data;
1499
1499
  }
@@ -2296,11 +2296,11 @@ function extractApiError(parsed, rawText) {
2296
2296
  return null;
2297
2297
  }
2298
2298
  function sleep(ms, signal) {
2299
- return new Promise((resolve4, reject) => {
2299
+ return new Promise((resolve6, reject) => {
2300
2300
  if (signal?.aborted) return reject(new DOMException("aborted", "AbortError"));
2301
2301
  const t = setTimeout(() => {
2302
2302
  signal?.removeEventListener("abort", onAbort);
2303
- resolve4();
2303
+ resolve6();
2304
2304
  }, ms);
2305
2305
  const onAbort = () => {
2306
2306
  clearTimeout(t);
@@ -3148,7 +3148,7 @@ function truncateForEmbedding(text) {
3148
3148
  return text.slice(0, MAX_EMBED_CHARS);
3149
3149
  }
3150
3150
  async function sleep2(ms) {
3151
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3151
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
3152
3152
  }
3153
3153
  async function fetchWithRetry(url, init, retries = 3) {
3154
3154
  let lastError;
@@ -3347,6 +3347,56 @@ init_mode();
3347
3347
  import { platform, release, homedir as homedir4 } from "os";
3348
3348
  import { basename, join as join7 } from "path";
3349
3349
  import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
3350
+
3351
+ // src/agent/user-shell.ts
3352
+ var enabled = false;
3353
+ function userShellCommandsEnabled() {
3354
+ return enabled;
3355
+ }
3356
+ var USER_SHELL_PROMPT_SECTION = `## Commands the user runs with \`!\`
3357
+
3358
+ The user can run a shell command directly in this session by typing \`!\` followed by the command in the prompt (for example \`! gh auth login\`). It runs in their real terminal with full keyboard input, and the output is added to this conversation for you to read.
3359
+
3360
+ Your \`bash\` tool has no terminal: stdin is closed and there is no TTY, so anything that waits for typed input fails or hangs. Instead of running the command yourself, suggest \`! <exact command>\` when:
3361
+ - it needs a login or interactive auth (\`gh auth login\`, \`npm login\`, \`docker login\`, \`gcloud auth login\`, \`aws sso login\`, \`az login\`, \`wrangler login\`, \`vercel login\`, \u2026)
3362
+ - it prompts for a password, passphrase, one-time code, or confirmation (\`sudo \u2026\`, \`ssh\` with a passphrase or unknown host key, \`git push\` over HTTPS asking for credentials)
3363
+ - it is an interactive program (REPLs, setup wizards without non-interactive flags, \`git rebase -i\`, editors, TUIs)
3364
+ - the user would otherwise have to paste a secret (token, password) into the chat
3365
+ - one of your bash calls failed with a "not logged in", "authentication required", "not a tty", or "terminal required" error, or timed out waiting for input
3366
+ - the user declined permission for a command but still wants it run
3367
+
3368
+ Prefer non-interactive flags when they exist (\`--yes\`, \`-m\`, \`--with-token\` reading an env var, \`CI=1\`) and run the command yourself. Don't suggest \`!\` for commands your bash tool can run. When you do suggest it, give the exact line to type (e.g. "run \`! gh auth login\` in the prompt, then tell me when you're done") and stop; the output will appear in the conversation.`;
3369
+ var AUTH_OR_TTY_OUTPUT = [
3370
+ /not logged in(?:to)?\b/i,
3371
+ /you are not logged in/i,
3372
+ /\b(?:authentication|login|sign[- ]?in) (?:is )?required\b/i,
3373
+ /\bplease (?:run|use) [`'"]?[\w.-]+ (?:auth )?log ?in\b/i,
3374
+ /\bto (?:authenticate|get started with [\w ]+CLI), (?:please )?run\b/i,
3375
+ /\bgh auth login\b/i,
3376
+ /\bnot a tty\b/i,
3377
+ /\binappropriate ioctl for device\b/i,
3378
+ /\b(?:stdin|input) is not a (?:tty|terminal)\b/i,
3379
+ /\b(?:a|no) (?:terminal|tty) (?:is )?(?:required|present)\b/i,
3380
+ /\bmust be run (?:from|in) an? (?:terminal|interactive)/i,
3381
+ /\brequires an interactive (?:terminal|shell|session)\b/i,
3382
+ /\bcannot prompt\b|\bunable to prompt\b|\bprompts? (?:are )?disabled\b/i,
3383
+ /\bcould not read (?:username|password) for\b/i,
3384
+ /\bterminal prompts disabled\b/i,
3385
+ /\bhost key verification failed\b/i
3386
+ ];
3387
+ var INTERACTIVE_COMMAND = /^(?:sudo\b|ssh\b|passwd\b|su\b|gh auth login\b|npm (?:login|adduser)\b|yarn (?:npm )?login\b|pnpm login\b|docker login\b|gcloud auth\b|aws (?:sso )?(?:login|configure)\b|az login\b|firebase login\b|vercel login\b|wrangler login\b|netlify login\b|heroku login\b|fly(?:ctl)? auth login\b|op signin\b|gpg\b)/i;
3388
+ var TRAILING_PROMPT = /(?:password|passphrase|username|token|code|otp)[^\n]{0,40}:\s*$|\[(?:y\/n|Y\/n|y\/N)\]\s*$|\(y(?:es)?\/n(?:o)?\)\s*$|\?\s*$/i;
3389
+ function interactiveCommandHint(o) {
3390
+ if (!o.timedOut && o.exitCode === 0) return null;
3391
+ const cmd = o.command.trim().replace(/^cd\s+[^\s&;]+\s*(?:&&|;)\s*/, "");
3392
+ const tail = o.output.slice(-4e3);
3393
+ const looksInteractive = AUTH_OR_TTY_OUTPUT.some((re) => re.test(tail)) || INTERACTIVE_COMMAND.test(cmd) || o.timedOut && TRAILING_PROMPT.test(tail.trimEnd());
3394
+ if (!looksInteractive) return null;
3395
+ const suggestion = cmd.length <= 200 && !cmd.includes("\n") ? `\`! ${cmd}\`` : "`! <command>`";
3396
+ return `[autopilot] This command appears to need a login, a password, or an interactive terminal, which your bash tool cannot provide. Don't retry it as-is. If a non-interactive alternative exists, use it; otherwise ask the user to run ${suggestion} in the prompt and wait for its output to appear in the conversation.`;
3397
+ }
3398
+
3399
+ // src/agent/system-prompt.ts
3350
3400
  var CONTEXT_FILENAMES = ["KIMI.md", "KIMIFLARE.md", "AGENT.md"];
3351
3401
  var MAX_CONTEXT_BYTES = 20 * 1024;
3352
3402
  function loadContextFile(cwd) {
@@ -3409,6 +3459,9 @@ If the user asks what model you are, answer with exactly: \`${opts.model}\`. Thi
3409
3459
  const lspBlock = hasLsp ? "\n\nLSP tools are available for semantic code intelligence. Prefer `lsp_definition` over `grep` when looking for the source of a symbol. Prefer `lsp_references` over `grep` when finding usages. Use `lsp_hover` to confirm types before refactoring." : "";
3410
3460
  const tools = `Tools available:
3411
3461
  ${toolsBlock}`;
3462
+ const userShellBlock = userShellCommandsEnabled() ? `
3463
+
3464
+ ${USER_SHELL_PROMPT_SECTION}` : "";
3412
3465
  const ctx = loadContextFile(opts.cwd);
3413
3466
  const contextBlock = ctx ? `
3414
3467
 
@@ -3445,7 +3498,12 @@ ${opts.lspContext}` : "";
3445
3498
  The following MCP server information was pre-computed by the coordinator.
3446
3499
 
3447
3500
  ${opts.mcpContext}` : "";
3448
- return identity + "\n\n" + env + "\n\n" + tools + lspBlock + contextBlock + modeBlock + skillsBlock + memoryBlock + lspContextBlock + mcpContextBlock;
3501
+ const delegationBlock = opts.delegationDirective ? `
3502
+
3503
+ ## Subagent policy for this turn
3504
+
3505
+ ${opts.delegationDirective}` : "";
3506
+ return identity + "\n\n" + env + "\n\n" + tools + lspBlock + userShellBlock + contextBlock + modeBlock + skillsBlock + memoryBlock + lspContextBlock + mcpContextBlock + delegationBlock;
3449
3507
  }
3450
3508
  function buildSystemPrompt(opts) {
3451
3509
  return buildStaticPrefix(opts) + "\n\n" + buildSessionPrefix(opts);
@@ -3589,7 +3647,8 @@ async function runAgentTurn(opts) {
3589
3647
  model: opts.model,
3590
3648
  mode: opts.mode,
3591
3649
  skillContext: skillResult.skillContext,
3592
- preferPullRequests: opts.preferPullRequests
3650
+ preferPullRequests: opts.preferPullRequests,
3651
+ delegationDirective: opts.delegationDirective
3593
3652
  })
3594
3653
  };
3595
3654
  } else {
@@ -3601,7 +3660,8 @@ async function runAgentTurn(opts) {
3601
3660
  model: opts.model,
3602
3661
  mode: opts.mode,
3603
3662
  skillContext: skillResult.skillContext,
3604
- preferPullRequests: opts.preferPullRequests
3663
+ preferPullRequests: opts.preferPullRequests,
3664
+ delegationDirective: opts.delegationDirective
3605
3665
  })
3606
3666
  };
3607
3667
  }
@@ -3911,9 +3971,11 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
3911
3971
  return;
3912
3972
  }
3913
3973
  let blockedCount = 0;
3974
+ const availableToolNames = new Set(opts.tools.map((tool) => tool.name));
3975
+ if (codeMode) availableToolNames.add("execute_code");
3914
3976
  const allReadOnly = toolCalls.length > 1 && toolCalls.every((tc) => {
3915
3977
  const tool = opts.executor.list().find((t) => t.name === tc.function.name);
3916
- return tool?.isReadOnly === true;
3978
+ return availableToolNames.has(tc.function.name) && tool?.isReadOnly === true;
3917
3979
  });
3918
3980
  if (allReadOnly) {
3919
3981
  opts.callbacks.onInfo?.(`${toolCalls.length} read-only tools running in parallel`);
@@ -4153,6 +4215,26 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
4153
4215
  for (const [i, tc] of toolCalls.entries()) {
4154
4216
  if (opts.signal.aborted) throw new DOMException("aborted", "AbortError");
4155
4217
  const loopSignature = `${tc.function.name}:${stableStringify(tc.function.arguments)}`;
4218
+ if (!availableToolNames.has(tc.function.name)) {
4219
+ const unavailableResult = {
4220
+ tool_call_id: tc.id,
4221
+ name: tc.function.name,
4222
+ content: `Tool ${tc.function.name} is not available under this turn's policy. Continue without it or ask the user to enable it.`,
4223
+ ok: false
4224
+ };
4225
+ toolResults.push(unavailableResult);
4226
+ opts.messages.push({
4227
+ role: "tool",
4228
+ tool_call_id: tc.id,
4229
+ content: sanitizeString(unavailableResult.content),
4230
+ name: tc.function.name
4231
+ });
4232
+ opts.callbacks.onToolResult?.(unavailableResult);
4233
+ recentToolCalls.push(loopSignature);
4234
+ if (recentToolCalls.length > LOOP_WINDOW) recentToolCalls.shift();
4235
+ blockedCount++;
4236
+ continue;
4237
+ }
4156
4238
  const loopCount = recentToolCalls.filter((s) => s === loopSignature).length;
4157
4239
  if (loopCount >= LOOP_THRESHOLD) {
4158
4240
  const warning = `Loop detected: you have called ${tc.function.name} with the same arguments multiple times in a row. Consider a different approach.`;
@@ -4905,15 +4987,15 @@ function makeErrorOutput(message) {
4905
4987
  return { content: message, rawBytes: bytes, reducedBytes: bytes };
4906
4988
  }
4907
4989
  async function execOnce(command, cwd) {
4908
- return new Promise((resolve4) => {
4990
+ return new Promise((resolve6) => {
4909
4991
  const child = spawn("bash", ["-lc", command], { cwd });
4910
4992
  let out = "";
4911
4993
  child.stdout.on("data", (d) => {
4912
4994
  out += d.toString("utf8");
4913
4995
  });
4914
4996
  child.on("close", (code) => {
4915
- if (code !== 0) return resolve4(void 0);
4916
- resolve4(out.trim() || void 0);
4997
+ if (code !== 0) return resolve6(void 0);
4998
+ resolve6(out.trim() || void 0);
4917
4999
  });
4918
5000
  });
4919
5001
  }
@@ -5051,7 +5133,7 @@ async function runBash(args, ctx) {
5051
5133
  if (pushGuard) return pushGuard;
5052
5134
  const { shell, args: shellArgs, isPosix } = getShellCommand(ctx.shell);
5053
5135
  const command = isPosix ? injectCoauthor(args.command, ctx.coauthor) : args.command;
5054
- return new Promise((resolve4, reject) => {
5136
+ return new Promise((resolve6, reject) => {
5055
5137
  logger.debug("bash:spawn", { command: args.command.slice(0, 200), cwd: ctx.cwd, shell });
5056
5138
  const child = spawn(shell, [...shellArgs, command], {
5057
5139
  cwd: ctx.cwd,
@@ -5060,6 +5142,7 @@ async function runBash(args, ctx) {
5060
5142
  GIT_EDITOR: "true"
5061
5143
  }
5062
5144
  });
5145
+ child.stdin?.end();
5063
5146
  let stdout = "";
5064
5147
  let stderr = "";
5065
5148
  let killedByTimeout = false;
@@ -5103,8 +5186,18 @@ ${stdout.trimEnd()}`);
5103
5186
  if (stderr) parts.push(`--- stderr ---
5104
5187
  ${stderr.trimEnd()}`);
5105
5188
  if (!stdout && !stderr) parts.push("(no output)");
5189
+ if (userShellCommandsEnabled() && !killedByAbort) {
5190
+ const hint = interactiveCommandHint({
5191
+ command: args.command,
5192
+ exitCode: code,
5193
+ timedOut: killedByTimeout,
5194
+ output: `${stdout}
5195
+ ${stderr}`
5196
+ });
5197
+ if (hint) parts.push(hint);
5198
+ }
5106
5199
  const raw = parts.join("\n");
5107
- resolve4({
5200
+ resolve6({
5108
5201
  content: raw,
5109
5202
  rawBytes: Buffer.byteLength(raw, "utf8"),
5110
5203
  reducedBytes: Buffer.byteLength(raw, "utf8")
@@ -6449,7 +6542,7 @@ var browserFetchTool = {
6449
6542
  };
6450
6543
  async function autoScroll(page) {
6451
6544
  await page.evaluate(async () => {
6452
- await new Promise((resolve4) => {
6545
+ await new Promise((resolve6) => {
6453
6546
  let totalHeight = 0;
6454
6547
  const distance = 300;
6455
6548
  const timer2 = setInterval(() => {
@@ -6458,12 +6551,12 @@ async function autoScroll(page) {
6458
6551
  totalHeight += distance;
6459
6552
  if (totalHeight >= scrollHeight) {
6460
6553
  clearInterval(timer2);
6461
- resolve4();
6554
+ resolve6();
6462
6555
  }
6463
6556
  }, 100);
6464
6557
  setTimeout(() => {
6465
6558
  clearInterval(timer2);
6466
- resolve4();
6559
+ resolve6();
6467
6560
  }, 1e4);
6468
6561
  });
6469
6562
  });
@@ -6688,9 +6781,13 @@ function readReasoningEffortEnv() {
6688
6781
  const raw = process.env.KIMI_REASONING_EFFORT?.toLowerCase();
6689
6782
  return EFFORTS.includes(raw ?? "") ? raw : void 0;
6690
6783
  }
6784
+ function readSubagentPolicyEnv() {
6785
+ const raw = process.env.KIMIFLARE_SUBAGENT_POLICY?.toLowerCase();
6786
+ return raw === "off" || raw === "suggest" || raw === "auto" ? raw : void 0;
6787
+ }
6691
6788
  function readCoauthorEnv() {
6692
- const enabled = process.env.KIMIFLARE_COAUTHOR;
6693
- if (enabled === "0" || enabled === "false") return void 0;
6789
+ const enabled2 = process.env.KIMIFLARE_COAUTHOR;
6790
+ if (enabled2 === "0" || enabled2 === "false") return void 0;
6694
6791
  const name = process.env.KIMIFLARE_COAUTHOR_NAME || "kimiflare";
6695
6792
  const email = process.env.KIMIFLARE_COAUTHOR_EMAIL || "kimiflare@proton.me";
6696
6793
  return { enabled: true, name, email };
@@ -6724,6 +6821,7 @@ async function loadConfig() {
6724
6821
  if (!openrouterApiKey && !baseUrl) return null;
6725
6822
  const envModel = process.env.KIMI_MODEL || void 0;
6726
6823
  const envEffort = readReasoningEffortEnv();
6824
+ const envSubagentPolicy = readSubagentPolicyEnv();
6727
6825
  const envCoauthor = readCoauthorEnv();
6728
6826
  const envCacheStable = process.env.KIMIFLARE_CACHE_STABLE_PROMPTS;
6729
6827
  const cacheStablePrompts = envCacheStable === "0" || envCacheStable === "false" ? false : true;
@@ -6744,6 +6842,7 @@ async function loadConfig() {
6744
6842
  model: m(envModel ?? persisted.model) ?? DEFAULT_MODEL,
6745
6843
  openrouterProvider: persisted.openrouterProvider,
6746
6844
  reasoningEffort: envEffort ?? persisted.reasoningEffort,
6845
+ subagentPolicy: envSubagentPolicy ?? (persisted.subagentPolicy === "off" || persisted.subagentPolicy === "suggest" || persisted.subagentPolicy === "auto" ? persisted.subagentPolicy : "suggest"),
6747
6846
  modesEnabled: persisted.modesEnabled,
6748
6847
  coauthor: envCoauthor?.enabled ?? persisted.coauthor ?? true,
6749
6848
  coauthorName: envCoauthor?.name ?? persisted.coauthorName,
@@ -6777,6 +6876,7 @@ async function loadConfig() {
6777
6876
  githubTokenExpiry: persisted.githubTokenExpiry,
6778
6877
  githubRepo: persisted.githubRepo,
6779
6878
  workerEndpoint: process.env.KIMIFLARE_WORKER_ENDPOINT ?? persisted.workerEndpoint,
6879
+ workerBackend: process.env.KIMIFLARE_WORKER_BACKEND === "hotcell" ? "hotcell" : process.env.KIMIFLARE_WORKER_BACKEND === "remote" ? "remote" : persisted.workerBackend === "hotcell" ? "hotcell" : "remote",
6780
6880
  workerBudgetUsd: readNumberEnv("KIMIFLARE_WORKER_BUDGET_USD") ?? persisted.workerBudgetUsd,
6781
6881
  workerBudgetMaxUsd: readNumberEnv("KIMIFLARE_WORKER_BUDGET_MAX_USD") ?? persisted.workerBudgetMaxUsd,
6782
6882
  workerMaxParallel: readNumberEnv("KIMIFLARE_WORKER_MAX_PARALLEL") ?? persisted.workerMaxParallel,
@@ -6882,6 +6982,330 @@ async function saveConfig(cfg) {
6882
6982
  return p;
6883
6983
  }
6884
6984
 
6985
+ // src/tools/hotcell-worker.ts
6986
+ import { spawn as spawn2 } from "child_process";
6987
+ import { promisify as promisify2 } from "util";
6988
+ import { execFile as execFile2 } from "child_process";
6989
+ import { resolve as resolve3 } from "path";
6990
+ var execFileAsync = promisify2(execFile2);
6991
+ var MAX_OUTPUT_BYTES = 1e6;
6992
+ var MAX_PROMPT_CHARS = 4e4;
6993
+ var DEFAULT_CELL_TIMEOUT_MS = 3e5;
6994
+ var MAX_PARALLEL_WORKERS = 3;
6995
+ var runningWorkers = 0;
6996
+ var workerQueue = [];
6997
+ async function runHotcellWorker(options) {
6998
+ const workerId = `hotcell-${crypto.randomUUID().slice(0, 8)}`;
6999
+ const cwd = resolve3(options.cwd ?? process.cwd());
7000
+ const model = options.model.trim();
7001
+ if (!model) throw new Error("Hotcell workers require the coordinator's explicit model ID.");
7002
+ validateModelId(model);
7003
+ if (options.task.length + (options.context?.length ?? 0) > MAX_PROMPT_CHARS) {
7004
+ throw new Error(`Hotcell worker prompt exceeds the ${MAX_PROMPT_CHARS.toLocaleString()} character limit.`);
7005
+ }
7006
+ if (!Number.isFinite(options.budgetUsd) || options.budgetUsd <= 0) {
7007
+ throw new Error("Hotcell worker spend cap must be a positive number.");
7008
+ }
7009
+ if (options.signal?.aborted) return terminalResult(workerId, options.task, "cancelled", "Cancelled before worker setup.");
7010
+ await acquireSlot(options.maxParallel ?? MAX_PARALLEL_WORKERS, options.signal);
7011
+ let cellId;
7012
+ let result;
7013
+ const command = options.hotcellCommand ?? process.env.HOTCELL_BIN ?? "hotcell";
7014
+ const execute = options.processRunner ?? runProcess;
7015
+ const timeoutMs = options.timeoutMs ?? DEFAULT_CELL_TIMEOUT_MS;
7016
+ try {
7017
+ const repo = await getCleanRepository(cwd);
7018
+ const cellName = "autopilot-" + workerId;
7019
+ const created = await execute(command, [
7020
+ "create",
7021
+ "-n",
7022
+ "1",
7023
+ "--name",
7024
+ cellName,
7025
+ "--repo",
7026
+ repo.url,
7027
+ "--ref",
7028
+ repo.ref,
7029
+ "--egress",
7030
+ "--egress-spend-cap",
7031
+ String(options.budgetUsd),
7032
+ "--memory",
7033
+ "1024",
7034
+ "--cpus",
7035
+ "2"
7036
+ ], { cwd, signal: options.signal, timeoutMs: 3e5 });
7037
+ cellId = parseCellId(created.stdout);
7038
+ if (!cellId) {
7039
+ try {
7040
+ const cells = await execute(command, ["ls"], { cwd, timeoutMs: 1e4 });
7041
+ cellId = parseNamedCellId(cells.stdout, cellName);
7042
+ } catch {
7043
+ }
7044
+ }
7045
+ if (created.timedOut || created.aborted) {
7046
+ result = terminalResult(
7047
+ workerId,
7048
+ options.task,
7049
+ options.signal?.aborted ? "cancelled" : "timed_out",
7050
+ options.signal?.aborted ? "Cancelled during Hotcell setup." : "Hotcell setup timed out before the worker started.",
7051
+ model
7052
+ );
7053
+ return result;
7054
+ }
7055
+ if (created.code !== 0) throw new Error(formatSetupFailure(created));
7056
+ if (!cellId) throw new Error("Hotcell created a sandbox but did not return a recognizable cell ID.");
7057
+ if (/no providers configured|providers:\s*\(none\)/i.test(created.stdout)) {
7058
+ result = terminalResult(workerId, options.task, "failed", "Hotcell has no OpenRouter gateway route. Run `hotcell keys add openrouter` on the daemon host, then retry.", model);
7059
+ return result;
7060
+ }
7061
+ const prompt = [
7062
+ "You are an isolated, read-only research worker. You cannot edit files, run shell commands, push, publish, or call write-capable integrations.",
7063
+ "Use the exact requested model and return concise findings, recommendations, and relevant file paths. Do not claim to have changed files.",
7064
+ `Mission:
7065
+ ${options.task}`,
7066
+ options.context ? `Coordinator context:
7067
+ ${options.context}` : ""
7068
+ ].filter(Boolean).join("\n\n");
7069
+ const encodedPrompt = Buffer.from(prompt, "utf8").toString("base64");
7070
+ const shellCommand = [
7071
+ 'GIT_DIR="$(find /workspace -mindepth 2 -maxdepth 5 -name .git -print -quit)"',
7072
+ 'test -n "$GIT_DIR"',
7073
+ 'REPO_ROOT="${GIT_DIR%/.git}"',
7074
+ `git -C "$REPO_ROOT" fetch --quiet origin ${shellQuote(repo.commit)}`,
7075
+ `git -C "$REPO_ROOT" checkout --quiet --detach ${shellQuote(repo.commit)}`,
7076
+ 'cd "$REPO_ROOT"',
7077
+ "npm ci --no-audit --no-fund --loglevel=error 1>&2",
7078
+ 'export OPENROUTER_BASE_URL="${OPENROUTER_BASE_URL%/}/v1"',
7079
+ `PROMPT="$(printf '%s' '${encodedPrompt}' | base64 -d)"`,
7080
+ `node --import tsx "$REPO_ROOT/src/index.tsx" --format json --max-input-tokens 14000 --model ${shellQuote(model)} --worker-profile research -p "$PROMPT"`
7081
+ ].join(" && ");
7082
+ const execution = await execute(command, ["exec", cellId, shellCommand, "--cwd", "/workspace"], {
7083
+ cwd,
7084
+ signal: options.signal,
7085
+ timeoutMs
7086
+ });
7087
+ const stats = await execute(command, ["stats", cellId], { cwd, timeoutMs: 3e4 }).catch(() => void 0);
7088
+ const metrics = stats?.code === 0 ? parseHotcellStats(stats.stdout) : {};
7089
+ const json = parsePrintOutput(execution.stdout);
7090
+ const summary = typeof json?.text === "string" ? json.text.trim() : execution.stdout.trim();
7091
+ const tokensUsed = typeof json?.usage?.totalTokens === "number" ? json.usage.totalTokens : 0;
7092
+ const budgetExhausted = execution.code === 42;
7093
+ const diagnostic = `${execution.stdout}
7094
+ ${execution.stderr}`.toLowerCase();
7095
+ const executionDiagnostic = sanitizeHotcellDiagnostic(execution.stderr);
7096
+ const spendExhausted = /(?:http\s*)?402|spend cap|spending limit/.test(diagnostic);
7097
+ const status = execution.timedOut ? "timed_out" : execution.aborted ? options.signal?.aborted ? "cancelled" : "failed" : spendExhausted ? "spend_exhausted" : budgetExhausted ? "budget_exhausted" : execution.code === 0 ? "completed" : "failed";
7098
+ const findings = summary ? [{ topic: "Research findings", summary: summary.slice(0, 4e4), confidence: "medium", sources: [], relevance: "high" }] : [];
7099
+ result = {
7100
+ workerId,
7101
+ status,
7102
+ task: options.task,
7103
+ findings,
7104
+ recommendations: [],
7105
+ filesRead: [],
7106
+ webSources: [],
7107
+ costUsd: metrics.costUsd ?? 0,
7108
+ tokensUsed: metrics.tokensUsed ?? tokensUsed,
7109
+ reasoning: "",
7110
+ model,
7111
+ exitCode: execution.code,
7112
+ ...budgetExhausted || spendExhausted ? { budgetExceeded: true, partialResult: findings.length > 0 } : {},
7113
+ ...execution.code !== 0 && !budgetExhausted ? {
7114
+ error: status === "timed_out" ? "Worker timed out." : status === "cancelled" ? "Worker cancelled." : spendExhausted ? "Hotcell per-cell spend cap was reached." : execution.outputLimitExceeded ? "Worker output exceeded the 1 MB result limit." : /unknown model|model.{0,30}(?:not found|invalid|unavailable|unsupported)/i.test(diagnostic) ? "The requested model is unavailable in the Hotcell OpenRouter gateway." : /no openrouter api key|openrouter.{0,30}(?:key|gateway).{0,30}(?:missing|not configured)/i.test(diagnostic) ? "Hotcell OpenRouter gateway credentials are not configured. Run `hotcell keys add openrouter` on the daemon host." : `Worker process exited with code ${execution.code}${executionDiagnostic ? `: ${executionDiagnostic}` : "."}`
7115
+ } : {}
7116
+ };
7117
+ } catch (error) {
7118
+ const message = error instanceof Error ? error.message : "Unknown Hotcell worker error.";
7119
+ result = terminalResult(workerId, options.task, options.signal?.aborted ? "cancelled" : "failed", message, model);
7120
+ } finally {
7121
+ if (cellId) {
7122
+ try {
7123
+ const removed = await execute(command, ["rm", cellId], { cwd, timeoutMs: 3e4 });
7124
+ if (removed.code !== 0 && result) {
7125
+ result = {
7126
+ ...result,
7127
+ status: result.status === "completed" ? "failed" : result.status,
7128
+ error: [result.error, "Hotcell cleanup failed; remove the cell manually to revoke its gateway token."].filter(Boolean).join(" ")
7129
+ };
7130
+ }
7131
+ } catch {
7132
+ if (result) {
7133
+ result = {
7134
+ ...result,
7135
+ status: result.status === "completed" ? "failed" : result.status,
7136
+ error: [result.error, "Hotcell cleanup failed; remove the cell manually to revoke its gateway token."].filter(Boolean).join(" ")
7137
+ };
7138
+ }
7139
+ }
7140
+ }
7141
+ releaseSlot();
7142
+ }
7143
+ return result;
7144
+ }
7145
+ async function getCleanRepository(cwd) {
7146
+ const root = (await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd })).stdout.trim();
7147
+ const status = (await execFileAsync("git", ["status", "--porcelain"], { cwd: root })).stdout;
7148
+ if (status.trim()) throw new Error("Hotcell workers require a clean Git checkout; commit or stash local changes first.");
7149
+ const commit = (await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: root })).stdout.trim();
7150
+ const ref = (await execFileAsync("git", ["branch", "--show-current"], { cwd: root })).stdout.trim();
7151
+ if (!ref) throw new Error("Hotcell workers require a named branch so the daemon can clone before pinning the exact commit.");
7152
+ const url = (await execFileAsync("git", ["config", "--get", "remote.origin.url"], { cwd: root })).stdout.trim();
7153
+ if (!/^(https:\/\/|git@|ssh:\/\/)/i.test(url) || /:\/\/[^/]*@/.test(url)) {
7154
+ throw new Error("Hotcell workers require a credential-free HTTPS or SSH origin URL that the Hotcell daemon can clone.");
7155
+ }
7156
+ return { url, commit, ref };
7157
+ }
7158
+ function parseCellId(output) {
7159
+ const uuid = output.match(/\b[a-f0-9]{8}(?:-[a-f0-9]{4,}){0,4}\b/gi)?.at(-1);
7160
+ if (uuid) return uuid;
7161
+ const labeled = output.match(/(?:sandbox|cell)(?:\s+id)?\s*[:= ]\s*([a-z0-9][a-z0-9_-]{7,79})/i)?.[1];
7162
+ if (labeled) return labeled;
7163
+ return output.split(/\r?\n/).map((line) => line.trim()).reverse().find((line) => /^[a-z0-9][a-z0-9_-]{7,79}$/i.test(line));
7164
+ }
7165
+ function parseNamedCellId(output, name) {
7166
+ for (const line of output.split(/\r?\n/)) {
7167
+ const columns = line.trim().split(/\s+/);
7168
+ if (columns[1] === name && /^[a-z0-9][a-z0-9_-]{7,79}$/i.test(columns[0] ?? "")) return columns[0];
7169
+ }
7170
+ return void 0;
7171
+ }
7172
+ function formatSetupFailure(result) {
7173
+ const detail = sanitizeHotcellDiagnostic(`${result.stderr}
7174
+ ${result.stdout}`);
7175
+ return `Hotcell setup failed (exit ${result.code})${detail ? `: ${detail}` : ". Check that the daemon, repository access, and gateway route are configured."}`;
7176
+ }
7177
+ function sanitizeHotcellDiagnostic(text) {
7178
+ return text.replace(/\b(?:sk-or-v1-|sk-ant-|gh[pousr]_|github_pat_)[A-Za-z0-9_-]{8,}\b/gi, "[REDACTED]").replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]").replace(/\b[A-Z0-9_]*(?:API[_-]?KEY|ACCESS[_-]?TOKEN|SECRET|PASSWORD)\s*[:=]\s*[^\s,;]+/gi, "[REDACTED]").trim().slice(-1500);
7179
+ }
7180
+ function shellQuote(value) {
7181
+ return `'${value.replace(/'/g, `'\\''`)}'`;
7182
+ }
7183
+ function parseHotcellStats(output) {
7184
+ const llm = output.match(/LLM:\s*([\d,]+) calls,\s*([\d,]+) in \+ ([\d,]+) out tokens,\s*\$([\d.]+)/i);
7185
+ const totalCost = output.match(/Cost:\s*\$?([\d.]+)/i);
7186
+ const tokensUsed = llm ? Number(llm[2].replaceAll(",", "")) + Number(llm[3].replaceAll(",", "")) : void 0;
7187
+ const costUsd = totalCost ? Number(totalCost[1]) : llm ? Number(llm[4]) : void 0;
7188
+ return {
7189
+ ...Number.isFinite(tokensUsed) ? { tokensUsed } : {},
7190
+ ...Number.isFinite(costUsd) ? { costUsd } : {}
7191
+ };
7192
+ }
7193
+ function parsePrintOutput(stdout) {
7194
+ const candidate = stdout.trim();
7195
+ try {
7196
+ const parsed = JSON.parse(candidate);
7197
+ if (!parsed || typeof parsed !== "object") return void 0;
7198
+ return parsed;
7199
+ } catch {
7200
+ return void 0;
7201
+ }
7202
+ }
7203
+ async function runProcess(executable, args, options) {
7204
+ if (options.signal?.aborted) return { code: 130, stdout: "", stderr: "", aborted: true };
7205
+ return await new Promise((resolvePromise, reject) => {
7206
+ const child = spawn2(executable, args, { cwd: options.cwd, stdio: ["ignore", "pipe", "pipe"] });
7207
+ const stdout = [];
7208
+ const stderr = [];
7209
+ let bytes = 0;
7210
+ let aborted2 = false;
7211
+ let timedOut = false;
7212
+ let outputLimitExceeded = false;
7213
+ let settled = false;
7214
+ const terminate = () => {
7215
+ child.kill("SIGTERM");
7216
+ const forceTimer = setTimeout(() => child.kill("SIGKILL"), 2e3);
7217
+ forceTimer.unref();
7218
+ };
7219
+ const abort = () => {
7220
+ aborted2 = true;
7221
+ terminate();
7222
+ };
7223
+ const timer2 = setTimeout(() => {
7224
+ timedOut = true;
7225
+ terminate();
7226
+ }, options.timeoutMs);
7227
+ timer2.unref();
7228
+ options.signal?.addEventListener("abort", abort, { once: true });
7229
+ const collect = (target, chunk) => {
7230
+ bytes += chunk.length;
7231
+ if (bytes > MAX_OUTPUT_BYTES) {
7232
+ outputLimitExceeded = true;
7233
+ terminate();
7234
+ return;
7235
+ }
7236
+ target.push(chunk);
7237
+ };
7238
+ child.stdout.on("data", (chunk) => collect(stdout, chunk));
7239
+ child.stderr.on("data", (chunk) => collect(stderr, chunk));
7240
+ child.once("error", (error) => {
7241
+ if (settled) return;
7242
+ settled = true;
7243
+ clearTimeout(timer2);
7244
+ options.signal?.removeEventListener("abort", abort);
7245
+ reject(error);
7246
+ });
7247
+ child.once("close", (code) => {
7248
+ if (settled) return;
7249
+ settled = true;
7250
+ clearTimeout(timer2);
7251
+ options.signal?.removeEventListener("abort", abort);
7252
+ resolvePromise({
7253
+ code: code ?? (aborted2 ? 130 : 1),
7254
+ stdout: Buffer.concat(stdout).toString("utf8"),
7255
+ stderr: Buffer.concat(stderr).toString("utf8"),
7256
+ aborted: aborted2,
7257
+ timedOut,
7258
+ outputLimitExceeded
7259
+ });
7260
+ });
7261
+ });
7262
+ }
7263
+ function terminalResult(workerId, task, status, error, model) {
7264
+ return {
7265
+ workerId,
7266
+ status,
7267
+ task,
7268
+ findings: [],
7269
+ recommendations: [],
7270
+ filesRead: [],
7271
+ webSources: [],
7272
+ costUsd: 0,
7273
+ tokensUsed: 0,
7274
+ reasoning: "",
7275
+ ...model ? { model } : {},
7276
+ error
7277
+ };
7278
+ }
7279
+ async function acquireSlot(limit, signal) {
7280
+ const cap = Math.max(1, Math.min(MAX_PARALLEL_WORKERS, Math.floor(limit) || 1));
7281
+ if (runningWorkers < cap) {
7282
+ runningWorkers++;
7283
+ return;
7284
+ }
7285
+ await new Promise((resolvePromise, reject) => {
7286
+ const wake = () => {
7287
+ if (signal?.aborted) {
7288
+ const index = workerQueue.indexOf(wake);
7289
+ if (index >= 0) workerQueue.splice(index, 1);
7290
+ reject(new Error("Hotcell worker cancelled while waiting for a slot."));
7291
+ return;
7292
+ }
7293
+ if (runningWorkers < cap) {
7294
+ const index = workerQueue.indexOf(wake);
7295
+ if (index >= 0) workerQueue.splice(index, 1);
7296
+ runningWorkers++;
7297
+ resolvePromise();
7298
+ }
7299
+ };
7300
+ workerQueue.push(wake);
7301
+ signal?.addEventListener("abort", wake, { once: true });
7302
+ });
7303
+ }
7304
+ function releaseSlot() {
7305
+ runningWorkers = Math.max(0, runningWorkers - 1);
7306
+ for (const wake of workerQueue.slice()) wake();
7307
+ }
7308
+
6885
7309
  // src/tools/spawn-worker.ts
6886
7310
  var DEFAULT_WORKER_TIMEOUT_MS = 3e5;
6887
7311
  async function callWorkerEndpoint(endpoint, apiKey, payload, signal, timeoutMs = DEFAULT_WORKER_TIMEOUT_MS) {
@@ -6917,10 +7341,9 @@ async function callWorkerEndpoint(endpoint, apiKey, payload, signal, timeoutMs =
6917
7341
  var spawnWorkerTool = {
6918
7342
  name: "spawn_worker",
6919
7343
  description: [
6920
- "Spawn a standalone remote worker agent to perform research or execute a plan.",
6921
- "Workers run independently with their own full context window and tool access.",
6922
- "Mode 'plan': read-only research worker that returns structured findings.",
6923
- "Mode 'execute': write-enabled worker that creates a branch, implements changes, and opens a PR.",
7344
+ "Spawn a standalone worker using the configured remote endpoint or an opt-in local Hotcell sandbox.",
7345
+ "Mode 'plan': read-only research. Local Hotcell workers use a clean committed revision and cannot edit files.",
7346
+ "Mode 'execute': remote write + PR only; local Hotcell execute mode is intentionally unavailable.",
6924
7347
  "Use for heavy tasks that benefit from parallel research (e.g. 'research OAuth2, testing, and migration')."
6925
7348
  ].join(" "),
6926
7349
  parameters: {
@@ -6953,11 +7376,11 @@ var spawnWorkerTool = {
6953
7376
  tools: {
6954
7377
  type: "string",
6955
7378
  enum: ["all", "read-only"],
6956
- description: "Tool set available to the worker. Default 'all' for plan mode, ignored for execute."
7379
+ description: "Requested remote tool set. Local Hotcell research always uses a narrow read-only profile and rejects `all`."
6957
7380
  },
6958
7381
  model: {
6959
7382
  type: "string",
6960
- description: `Model to use for the worker. Defaults to the session's model (${DEFAULT_MODEL} if unset).`
7383
+ description: "Model to use for the worker. Defaults to the active session model."
6961
7384
  },
6962
7385
  branchName: {
6963
7386
  type: "string",
@@ -6985,25 +7408,71 @@ var spawnWorkerTool = {
6985
7408
  body: args.task.slice(0, 200)
6986
7409
  }),
6987
7410
  async run(args, ctx) {
6988
- const endpoint = process.env.KIMIFLARE_WORKER_ENDPOINT;
7411
+ const cfg = await loadConfig().catch(() => null);
7412
+ const timeoutMs = cfg?.workerTimeoutMs ?? readNumberEnv2("KIMIFLARE_WORKER_TIMEOUT_MS") ?? DEFAULT_WORKER_TIMEOUT_MS;
7413
+ const budgetCeiling = resolveWorkerBudgetUsd(cfg);
7414
+ if (cfg?.workerBackend === "hotcell") {
7415
+ if (cfg.baseUrl) {
7416
+ return textOutput("Hotcell workers currently require an OpenRouter-backed session; custom model endpoints cannot be routed through the Hotcell gateway.");
7417
+ }
7418
+ if (args.mode !== "plan") {
7419
+ return textOutput("Hotcell workers currently support read-only plan mode only. Execute mode is disabled until reviewed patch artifacts are supported.");
7420
+ }
7421
+ if (args.tools === "all") {
7422
+ return textOutput("Hotcell research workers are restricted to read-only tools; the requested broader toolset is not available.");
7423
+ }
7424
+ const model = args.model ?? ctx.model;
7425
+ if (!model) {
7426
+ return textOutput("Hotcell worker requires the coordinator's active model ID. No model was provided; refusing to fall back to a default.");
7427
+ }
7428
+ const requestedBudget = args.budget?.maxCostUsd ?? budgetCeiling;
7429
+ const budgetUsd = Math.min(requestedBudget, budgetCeiling);
7430
+ if (!Number.isFinite(budgetUsd) || budgetUsd <= 0) {
7431
+ return textOutput("Hotcell worker spend cap must be a positive number.");
7432
+ }
7433
+ try {
7434
+ const result = await runHotcellWorker({
7435
+ task: args.task,
7436
+ context: args.context,
7437
+ model,
7438
+ budgetUsd,
7439
+ timeoutMs,
7440
+ maxParallel: cfg?.workerMaxParallel,
7441
+ cwd: ctx.cwd,
7442
+ signal: ctx.signal
7443
+ });
7444
+ if (result.status !== "completed" && result.status !== "budget_exhausted") {
7445
+ return textOutput(`Hotcell worker ${result.status}: ${result.error ?? "unknown error"}`);
7446
+ }
7447
+ const lines = [
7448
+ `Hotcell worker ${result.status}${result.status === "budget_exhausted" ? " (partial result)" : ""}.`,
7449
+ `Model: ${result.model ?? model} \xB7 Tokens: ${result.tokensUsed.toLocaleString()} \xB7 Cost: ${result.costUsd > 0 ? `${result.costUsd.toFixed(4)} total Hotcell cost` : "unavailable from Hotcell stats"}.`,
7450
+ ...result.findings.map((finding) => `
7451
+ ## ${finding.topic}
7452
+ ${finding.summary}`)
7453
+ ];
7454
+ if (result.status === "budget_exhausted") {
7455
+ lines.push("\nWorker input-token budget was exhausted; review partial findings before relying on them.");
7456
+ }
7457
+ return textOutput(lines.join("\n"));
7458
+ } catch (error) {
7459
+ return textOutput(`Failed to spawn Hotcell worker: ${error instanceof Error ? error.message : String(error)}`);
7460
+ }
7461
+ }
7462
+ const endpoint = process.env.KIMIFLARE_WORKER_ENDPOINT ?? cfg?.workerEndpoint ?? cfg?.remoteWorkerUrl;
6989
7463
  if (!endpoint) {
6990
- const msg = "Worker endpoint not configured. Set KIMIFLARE_WORKER_ENDPOINT or workerEndpoint in config.";
6991
- const bytes = Buffer.byteLength(msg, "utf8");
6992
- return { content: msg, rawBytes: bytes, reducedBytes: bytes };
7464
+ return textOutput("Worker endpoint not configured. Set KIMIFLARE_WORKER_ENDPOINT or workerEndpoint in config, or set workerBackend to hotcell.");
6993
7465
  }
6994
7466
  const apiKey = process.env.KIMIFLARE_WORKER_API_KEY;
6995
- const timeoutMs = readNumberEnv2("KIMIFLARE_WORKER_TIMEOUT_MS") ?? DEFAULT_WORKER_TIMEOUT_MS;
6996
- const cfg = await loadConfig().catch(() => null);
6997
- const budgetUsd = resolveWorkerBudgetUsd(cfg);
6998
7467
  const defaultModel = cfg?.model ?? DEFAULT_MODEL;
6999
7468
  const payload = {
7000
7469
  mode: args.mode,
7001
7470
  task: args.task,
7002
7471
  context: args.context ?? "",
7003
- budget: { maxCostUsd: budgetUsd },
7472
+ budget: { maxCostUsd: budgetCeiling },
7004
7473
  outputFormat: args.outputFormat ?? "structured",
7005
7474
  tools: args.tools ?? (args.mode === "plan" ? "read-only" : "all"),
7006
- model: args.model ?? defaultModel,
7475
+ model: args.model ?? ctx.model ?? defaultModel,
7007
7476
  ...args.mode === "execute" ? {
7008
7477
  branchName: args.branchName,
7009
7478
  baseBranch: args.baseBranch ?? "main",
@@ -7057,6 +7526,10 @@ var spawnWorkerTool = {
7057
7526
  }
7058
7527
  }
7059
7528
  };
7529
+ function textOutput(content) {
7530
+ const bytes = Buffer.byteLength(content, "utf8");
7531
+ return { content, rawBytes: bytes, reducedBytes: bytes };
7532
+ }
7060
7533
  function readNumberEnv2(name) {
7061
7534
  const raw = process.env[name];
7062
7535
  if (!raw) return void 0;
@@ -7670,6 +8143,8 @@ function makeExpandArtifactTool(store) {
7670
8143
  }
7671
8144
 
7672
8145
  // src/tools/executor.ts
8146
+ import { realpath as realpath2 } from "fs/promises";
8147
+ import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
7673
8148
  var ALL_TOOLS = [
7674
8149
  { ...readTool, isReadOnly: true },
7675
8150
  writeTool,
@@ -8553,7 +9028,7 @@ Context: This memory was explicitly provided by the user during a conversation.`
8553
9028
  };
8554
9029
 
8555
9030
  // src/lsp/connection.ts
8556
- import { spawn as spawn2 } from "child_process";
9031
+ import { spawn as spawn3 } from "child_process";
8557
9032
  import { EventEmitter } from "events";
8558
9033
  var LspConnection = class extends EventEmitter {
8559
9034
  child = null;
@@ -8570,7 +9045,7 @@ var LspConnection = class extends EventEmitter {
8570
9045
  if (this.child) {
8571
9046
  throw new Error("LSP connection already started");
8572
9047
  }
8573
- return new Promise((resolve4, reject) => {
9048
+ return new Promise((resolve6, reject) => {
8574
9049
  const abortController = new AbortController();
8575
9050
  const spawnTimer = setTimeout(() => {
8576
9051
  abortController.abort();
@@ -8578,7 +9053,7 @@ var LspConnection = class extends EventEmitter {
8578
9053
  reject(new Error(`LSP server spawn timed out after ${spawnTimeoutMs}ms`));
8579
9054
  }, spawnTimeoutMs);
8580
9055
  try {
8581
- const child = spawn2(command[0], command.slice(1), {
9056
+ const child = spawn3(command[0], command.slice(1), {
8582
9057
  env: { ...process.env, ...env },
8583
9058
  stdio: ["pipe", "pipe", "pipe"]
8584
9059
  });
@@ -8609,7 +9084,7 @@ var LspConnection = class extends EventEmitter {
8609
9084
  if (child.pid) {
8610
9085
  clearTimeout(spawnTimer);
8611
9086
  this.child = child;
8612
- resolve4();
9087
+ resolve6();
8613
9088
  }
8614
9089
  });
8615
9090
  } catch (err) {
@@ -8624,12 +9099,12 @@ var LspConnection = class extends EventEmitter {
8624
9099
  }
8625
9100
  const id = this.nextId++;
8626
9101
  const msg = { jsonrpc: "2.0", id, method, params };
8627
- return new Promise((resolve4, reject) => {
9102
+ return new Promise((resolve6, reject) => {
8628
9103
  const timer2 = setTimeout(() => {
8629
9104
  this.pending.delete(id);
8630
9105
  reject(toolTimeoutError(`LSP request '${method}'`, this.requestTimeoutMs));
8631
9106
  }, this.requestTimeoutMs);
8632
- const pending = { resolve: resolve4, reject, signal, timer: timer2 };
9107
+ const pending = { resolve: resolve6, reject, signal, timer: timer2 };
8633
9108
  this.pending.set(id, pending);
8634
9109
  if (signal) {
8635
9110
  const onAbort = () => {
@@ -9170,10 +9645,10 @@ var LspManager = class {
9170
9645
  };
9171
9646
 
9172
9647
  // src/lsp/adapter.ts
9173
- import { relative as relative3 } from "path";
9648
+ import { relative as relative4 } from "path";
9174
9649
  function formatLocation(loc, cwd) {
9175
9650
  const path = fromUri(loc.uri);
9176
- const rel = relative3(cwd, path) || path;
9651
+ const rel = relative4(cwd, path) || path;
9177
9652
  return `${rel}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`;
9178
9653
  }
9179
9654
  function formatLocations(locs, cwd) {
@@ -9210,7 +9685,7 @@ function formatWorkspaceSymbols(symbols, cwd) {
9210
9685
  if (!symbols || symbols.length === 0) return "No symbols found.";
9211
9686
  return symbols.map((s) => {
9212
9687
  const path = fromUri(s.location.uri);
9213
- const rel = relative3(cwd, path) || path;
9688
+ const rel = relative4(cwd, path) || path;
9214
9689
  const container = s.containerName ? ` in ${s.containerName}` : "";
9215
9690
  return `${s.name} (${symbolKindName(s.kind)})${container} \u2014 ${rel}:${s.location.range.start.line + 1}:${s.location.range.start.character + 1}`;
9216
9691
  }).join("\n");
@@ -9230,7 +9705,7 @@ function formatWorkspaceEdit(edit, cwd) {
9230
9705
  if (edit.changes) {
9231
9706
  for (const [uri, edits] of Object.entries(edit.changes)) {
9232
9707
  const path = fromUri(uri);
9233
- const rel = relative3(cwd, path) || path;
9708
+ const rel = relative4(cwd, path) || path;
9234
9709
  lines.push(`File: ${rel}`);
9235
9710
  for (const e of edits) {
9236
9711
  lines.push(` ${e.range.start.line + 1}:${e.range.start.character + 1}-${e.range.end.line + 1}:${e.range.end.character + 1}: ${e.newText}`);
@@ -9249,7 +9724,7 @@ function formatCodeActions(actions) {
9249
9724
  }
9250
9725
 
9251
9726
  // src/tools/lsp.ts
9252
- import { relative as relative4 } from "path";
9727
+ import { relative as relative5 } from "path";
9253
9728
  function makeOutput2(content) {
9254
9729
  const bytes = Buffer.byteLength(content, "utf8");
9255
9730
  return { content, rawBytes: bytes, reducedBytes: bytes };
@@ -9257,7 +9732,7 @@ function makeOutput2(content) {
9257
9732
  function resolveLspPath(args, ctx) {
9258
9733
  const raw = typeof args.path === "string" ? args.path : "";
9259
9734
  const resolved = resolvePath(ctx.cwd, raw);
9260
- const rel = relative4(ctx.cwd, resolved);
9735
+ const rel = relative5(ctx.cwd, resolved);
9261
9736
  if (isPathOutside(rel)) {
9262
9737
  throw new Error(`Path outside workspace: ${raw}`);
9263
9738
  }
@@ -9990,7 +10465,7 @@ function createDefaultPermissionHandler(options) {
9990
10465
  async function createAgentSession(opts) {
9991
10466
  const config2 = await resolveSdkConfig(opts);
9992
10467
  if (!resolveCustomEndpoint(config2)) await ensureOpenRouterCatalog();
9993
- const cwd = resolve3(opts.cwd ?? process.cwd());
10468
+ const cwd = resolve5(opts.cwd ?? process.cwd());
9994
10469
  const tools = opts.tools ?? ALL_TOOLS;
9995
10470
  let hooks;
9996
10471
  if (opts.enableHooks) {
@@ -10375,12 +10850,12 @@ var InternalSession = class {
10375
10850
  toolName: req.tool.name,
10376
10851
  args: req.args
10377
10852
  });
10378
- const decision = await new Promise((resolve4) => {
10379
- this.permissionResolvers.set(requestId, resolve4);
10853
+ const decision = await new Promise((resolve6) => {
10854
+ this.permissionResolvers.set(requestId, resolve6);
10380
10855
  setTimeout(() => {
10381
10856
  if (this.permissionResolvers.has(requestId)) {
10382
10857
  this.permissionResolvers.delete(requestId);
10383
- resolve4("deny");
10858
+ resolve6("deny");
10384
10859
  }
10385
10860
  }, 3e5);
10386
10861
  });