@aiden-ade/sandbox-agent 0.1.14 → 0.1.15

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/index.cjs CHANGED
@@ -4812,11 +4812,6 @@ var require_websocket_server = __commonJS({
4812
4812
 
4813
4813
  // ../agent-core/dist/index.js
4814
4814
  var import_child_process = require("child_process");
4815
- var import_crypto = require("crypto");
4816
- var import_fs = require("fs");
4817
- var import_os = require("os");
4818
- var import_path = require("path");
4819
- var import_readline = require("readline");
4820
4815
 
4821
4816
  // ../shared/dist/agent/agent-definitions.js
4822
4817
  var COORDINATOR_PROMPT = `## You are the Coordinator
@@ -4940,7 +4935,7 @@ var GENERAL_PROMPT = `## You are the General Agent
4940
4935
 
4941
4936
  You are a flexible general-purpose agent (an Aiden agent profile) for work that does not need a specialist profile. Wear whatever hat the moment demands \u2014 product thinker, planner, programmer, debugger, tester. Read the room and pick the hat.
4942
4937
 
4943
- When identifying yourself, say "I'm the General Agent (an Aiden agent profile)."
4938
+ Your name is Aiden, General Agent.
4944
4939
 
4945
4940
  Clarify the goal only when required, inspect the relevant context first, then complete the requested work directly. Keep changes focused, avoid unrelated refactors, and validate the result before reporting back.
4946
4941
 
@@ -5175,8 +5170,8 @@ var PLAN_MODE_PROMPT = [
5175
5170
  "Include dependencies, ordering, risks, key files/functions, and decision points where relevant.",
5176
5171
  "When the plan is ready, you MUST persist it by calling the create_plan MCP tool.",
5177
5172
  "Pass title, content, summary, taskId, and conversationId. Include teamId too when available.",
5178
- "If create_plan fails, retry it. Do not exit plan mode until the plan is persisted successfully.",
5179
- "After create_plan succeeds, call ExitPlanMode to hand the plan back for approval."
5173
+ "If create_plan fails, retry it. Do not finish plan mode until the plan is persisted successfully.",
5174
+ "After create_plan succeeds, present the plan title and artifact link to the user for approval."
5180
5175
  ].join("\n");
5181
5176
  var TEST_MODE_PROMPT = [
5182
5177
  "You are in test mode. Verify the implemented feature carefully.",
@@ -5206,6 +5201,17 @@ function getAidenAgentPrompt(agentId) {
5206
5201
  // ../shared/dist/agent/session-result.js
5207
5202
  var SESSION_RESUME_FAILED_MESSAGE = "Session resume failed. The previous session may have expired or is no longer available. Please start a new session.";
5208
5203
 
5204
+ // ../agent-core/dist/index.js
5205
+ var import_fs = require("fs");
5206
+ var import_os = require("os");
5207
+ var import_path = require("path");
5208
+ var import_crypto = require("crypto");
5209
+ var import_fs2 = require("fs");
5210
+ var import_fs3 = require("fs");
5211
+ var import_os2 = require("os");
5212
+ var import_path2 = require("path");
5213
+ var import_readline = require("readline");
5214
+
5209
5215
  // ../shared/dist/constants/agent.js
5210
5216
  var INTERACTIVE_TOOL_NAMES = /* @__PURE__ */ new Set([
5211
5217
  "askuserquestion",
@@ -5232,17 +5238,9 @@ var DEFAULT_TEAM_STATUSES = STATUS_OPTIONS.map((o) => ({
5232
5238
  }));
5233
5239
 
5234
5240
  // ../agent-core/dist/index.js
5235
- var import_fs2 = require("fs");
5236
- var import_os2 = require("os");
5237
- var import_path2 = require("path");
5238
- function formatCliSpawnError(command, error) {
5239
- if (error.code === "ENOENT") {
5240
- return new Error(
5241
- `CLI command not found: ${command}. Install it or configure this provider with the full executable path.`
5242
- );
5243
- }
5244
- return error;
5245
- }
5241
+ var import_fs4 = require("fs");
5242
+ var import_os3 = require("os");
5243
+ var import_path3 = require("path");
5246
5244
  function isLikelyProviderAuthError(stderr) {
5247
5245
  const lower = stderr.toLowerCase();
5248
5246
  return lower.includes("api key") || lower.includes("api_key") || lower.includes("invalid api key") || lower.includes("api key invalid") || lower.includes("unauthorized") || lower.includes("authentication failed") || lower.includes("authentication error") || lower.includes("provider settings") || lower.includes("invalid token") || lower.includes("token expired");
@@ -5287,23 +5285,6 @@ function isTransientError(errorMessage) {
5287
5285
  const lower = errorMessage.toLowerCase();
5288
5286
  return lower.includes("rate limit") || lower.includes("overloaded") || lower.includes("503") || lower.includes("429") || lower.includes("service unavailable") || lower.includes("etimedout") || lower.includes("econnreset") || lower.includes("econnrefused") || lower.includes("epipe");
5289
5287
  }
5290
- function buildPromptWithSystem(config, promptText) {
5291
- const parts2 = [
5292
- config.systemPrompt?.trim(),
5293
- config.systemPromptAppend?.trim(),
5294
- promptText.trim()
5295
- ].filter((value2) => Boolean(value2 && value2.length > 0));
5296
- return parts2.join("\n\n");
5297
- }
5298
- function buildPlanModePrefix(promptText) {
5299
- return [
5300
- "You are in plan-only mode.",
5301
- "Do not make code changes or execute write operations.",
5302
- "Focus on analysis, planning, and explaining the next steps.",
5303
- "",
5304
- promptText
5305
- ].join("\n");
5306
- }
5307
5288
  function planCliSpawn(command, args, env = process.env) {
5308
5289
  const isWindows = process.platform === "win32";
5309
5290
  const comSpec = env.ComSpec ?? process.env.ComSpec ?? "cmd.exe";
@@ -5339,48 +5320,334 @@ function spawnCli(command, args, context) {
5339
5320
  detached: process.platform !== "win32"
5340
5321
  });
5341
5322
  }
5342
- function createGenericCliBackend(options) {
5343
- return {
5344
- kind: options.kind,
5345
- supportTier: options.supportTier,
5346
- async run(context) {
5347
- const presenter = context.presenter;
5348
- const state = {
5349
- process: null,
5350
- iterations: 0,
5351
- summary: "",
5352
- allowContinuation: options.allowContinuation ?? false
5353
- };
5354
- const prompt = options.augmentPrompt?.(context) ?? (context.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(context.config, context.promptText)) : buildPromptWithSystem(context.config, context.promptText));
5355
- const args = options.buildArgs?.(context, prompt) ?? options.args.map((arg) => arg === "{{prompt}}" ? prompt : arg);
5356
- const child = spawnCli(options.command, args, context);
5357
- state.process = child;
5358
- context.onProcessSpawned?.(child);
5359
- const safeArgs = args.map(
5360
- (a, i) => i > 0 && args[i - 1] === "--system-prompt" ? `"<system-prompt ${a.length} chars>"` : a
5361
- );
5362
- console.info(`[${options.kind}] Spawning: ${options.command} ${safeArgs.join(" ")}`);
5363
- presenter.recordRawTranscript?.("system", `${options.command} ${args.join(" ")}`, {
5364
- backendKind: options.kind
5365
- });
5366
- child.stdin.on("error", (err) => {
5367
- if (err.code !== "EPIPE") {
5368
- console.error(`[${options.kind}] stdin error:`, err);
5323
+ function shouldUseReadOnlyRuntimePermissions(config) {
5324
+ if (isReadOnlyAgent(config.agentId)) return true;
5325
+ switch (config.mode) {
5326
+ case "plan":
5327
+ case "ask":
5328
+ case "review":
5329
+ return true;
5330
+ default:
5331
+ return false;
5332
+ }
5333
+ }
5334
+ function getClaudePermissionMode(config) {
5335
+ switch (config.mode) {
5336
+ case "plan":
5337
+ case "ask":
5338
+ case "review":
5339
+ return "plan";
5340
+ default:
5341
+ return "bypassPermissions";
5342
+ }
5343
+ }
5344
+ function getClaudeDisallowedTools(config) {
5345
+ return isReadOnlyAgent(config.agentId) ? ["Edit", "Write", "MultiEdit", "NotebookEdit"] : [];
5346
+ }
5347
+ function parseJsonObject(value2) {
5348
+ try {
5349
+ const parsed = JSON.parse(value2);
5350
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
5351
+ } catch {
5352
+ return null;
5353
+ }
5354
+ }
5355
+ function parseMaybeJson(value2) {
5356
+ if (typeof value2 !== "string") return value2;
5357
+ return parseJsonObject(value2) ?? value2;
5358
+ }
5359
+ function normalizeCodexMcpToolResult(output) {
5360
+ if (typeof output !== "string") return JSON.stringify(output ?? {}, null, 2);
5361
+ const outputMarker = "\nOutput:\n";
5362
+ const raw = output.includes(outputMarker) ? output.slice(output.indexOf(outputMarker) + outputMarker.length).trim() : output.trim();
5363
+ let parsed;
5364
+ try {
5365
+ parsed = JSON.parse(raw);
5366
+ } catch {
5367
+ return raw;
5368
+ }
5369
+ if (Array.isArray(parsed)) {
5370
+ const textBlocks = parsed.filter(
5371
+ (entry) => typeof entry === "object" && entry !== null && "text" in entry
5372
+ ).map((entry) => typeof entry.text === "string" ? entry.text : "").filter(Boolean);
5373
+ if (textBlocks.length > 0) return textBlocks.join("\n");
5374
+ }
5375
+ return raw;
5376
+ }
5377
+ function findCodexSessionLog(runtimeSessionId, codexHome) {
5378
+ if (!runtimeSessionId || !(0, import_fs.existsSync)(codexHome)) return null;
5379
+ const root = (0, import_path.join)(codexHome, "sessions");
5380
+ if (!(0, import_fs.existsSync)(root)) return null;
5381
+ const matches = [];
5382
+ const stack = [root];
5383
+ while (stack.length > 0) {
5384
+ const dir = stack.pop();
5385
+ let entries;
5386
+ try {
5387
+ entries = (0, import_fs.readdirSync)(dir, { withFileTypes: true });
5388
+ } catch {
5389
+ continue;
5390
+ }
5391
+ for (const entry of entries) {
5392
+ const path = (0, import_path.join)(dir, entry.name);
5393
+ if (entry.isDirectory()) {
5394
+ stack.push(path);
5395
+ } else if (entry.isFile() && entry.name.includes(runtimeSessionId) && entry.name.endsWith(".jsonl")) {
5396
+ try {
5397
+ matches.push({ path, mtimeMs: (0, import_fs.statSync)(path).mtimeMs });
5398
+ } catch {
5399
+ matches.push({ path, mtimeMs: 0 });
5369
5400
  }
5370
- });
5371
- if (options.stdinWriter) {
5372
- options.stdinWriter(child.stdin, context);
5373
- } else if (options.promptViaStdin) {
5374
- child.stdin.write(prompt);
5375
- child.stdin.end();
5376
- } else {
5377
- child.stdin.end();
5378
5401
  }
5379
- if (options.keepStdinOpen) {
5380
- state.stdin = child.stdin;
5402
+ }
5403
+ }
5404
+ matches.sort((a, b) => b.mtimeMs - a.mtimeMs);
5405
+ return matches[0]?.path ?? null;
5406
+ }
5407
+ async function replayCodexMcpToolEventsFromSessionLog(context, state) {
5408
+ const runtimeSessionId = state.runtimeSessionId;
5409
+ if (!runtimeSessionId) return;
5410
+ const codexHome = context.env.CODEX_HOME || process.env.CODEX_HOME || (0, import_path.join)((0, import_os.homedir)(), ".codex");
5411
+ const logPath = findCodexSessionLog(runtimeSessionId, codexHome);
5412
+ if (!logPath) return;
5413
+ const emittedToolIds = /* @__PURE__ */ new Set();
5414
+ const lines = (0, import_fs.readFileSync)(logPath, "utf8").split(/\r?\n/).filter(Boolean);
5415
+ for (const line of lines) {
5416
+ const entry = parseJsonObject(line);
5417
+ const payload = entry && typeof entry.payload === "object" && entry.payload !== null ? entry.payload : null;
5418
+ if (!payload) continue;
5419
+ if (payload.type === "function_call") {
5420
+ const callId = typeof payload.call_id === "string" ? payload.call_id : "";
5421
+ const name = typeof payload.name === "string" ? payload.name : "";
5422
+ const namespace = typeof payload.namespace === "string" ? payload.namespace : "";
5423
+ if (!callId || !name || !namespace.startsWith("mcp__")) continue;
5424
+ const toolName = `${namespace}${name}`;
5425
+ context.presenter.onToolUse(toolName, parseMaybeJson(payload.arguments), callId);
5426
+ emittedToolIds.add(callId);
5427
+ continue;
5428
+ }
5429
+ if (payload.type === "tool_search_call") {
5430
+ const callId = typeof payload.call_id === "string" ? payload.call_id : "";
5431
+ if (!callId) continue;
5432
+ context.presenter.onToolUse("tool_search", payload.arguments ?? {}, callId);
5433
+ emittedToolIds.add(callId);
5434
+ }
5435
+ }
5436
+ if (emittedToolIds.size === 0) return;
5437
+ for (const line of lines) {
5438
+ const entry = parseJsonObject(line);
5439
+ const payload = entry && typeof entry.payload === "object" && entry.payload !== null ? entry.payload : null;
5440
+ if (!payload || payload.type !== "function_call_output" && payload.type !== "tool_search_output") continue;
5441
+ const callId = typeof payload.call_id === "string" ? payload.call_id : "";
5442
+ if (!emittedToolIds.has(callId)) continue;
5443
+ try {
5444
+ const result = payload.type === "tool_search_output" ? JSON.stringify(payload.tools ?? [], null, 2) : normalizeCodexMcpToolResult(payload.output);
5445
+ context.presenter.onToolResult?.(callId, result);
5446
+ } catch {
5447
+ context.presenter.onToolResult?.(callId, String(payload.output ?? ""));
5448
+ }
5449
+ }
5450
+ }
5451
+ function kimiSessionId(cwd) {
5452
+ let resolved = cwd;
5453
+ try {
5454
+ resolved = (0, import_fs2.realpathSync)(cwd);
5455
+ } catch {
5456
+ }
5457
+ return (0, import_crypto.createHash)("md5").update(resolved).digest("hex");
5458
+ }
5459
+ function parseKimiStructuredLine(line, context, state) {
5460
+ const presenter = context.presenter;
5461
+ if (!state.runtimeSessionId) {
5462
+ state.runtimeSessionId = kimiSessionId(context.cwd);
5463
+ }
5464
+ let parsed;
5465
+ try {
5466
+ parsed = JSON.parse(line);
5467
+ } catch {
5468
+ const text = line.trim();
5469
+ if (text.length > 0) {
5470
+ if (text.toLowerCase().includes("llm not set")) state.error = text;
5471
+ void presenter.onLog(`[kimi_cli] ${line}`);
5472
+ }
5473
+ return;
5474
+ }
5475
+ const role = typeof parsed.role === "string" ? parsed.role : "";
5476
+ const contentBlocks = Array.isArray(parsed.content) ? parsed.content : [];
5477
+ const toolCalls = Array.isArray(parsed.tool_calls) ? parsed.tool_calls : [];
5478
+ if (role === "assistant") {
5479
+ const thinking = contentBlocks.filter((b) => b.type === "think" && typeof b.think === "string").map((b) => b.think).join("");
5480
+ if (thinking) {
5481
+ state.iterations = Math.max(state.iterations, 1);
5482
+ void presenter.onThinking(thinking);
5483
+ }
5484
+ if (toolCalls.length > 0) {
5485
+ state.iterations = Math.max(state.iterations, 1);
5486
+ for (const tc of toolCalls) {
5487
+ const fn = typeof tc.function === "object" && tc.function !== null ? tc.function : {};
5488
+ const toolId = typeof tc.id === "string" ? tc.id : `kimi-tool-${Date.now()}`;
5489
+ const toolName = typeof fn.name === "string" ? fn.name : "Tool";
5490
+ let args = {};
5491
+ if (typeof fn.arguments === "string") {
5492
+ try {
5493
+ args = JSON.parse(fn.arguments);
5494
+ } catch {
5495
+ }
5496
+ } else if (typeof fn.arguments === "object" && fn.arguments !== null) {
5497
+ args = fn.arguments;
5498
+ }
5499
+ void presenter.onToolUse(toolName, args, toolId);
5381
5500
  }
5382
- const stdoutRl = (0, import_readline.createInterface)({ input: child.stdout });
5383
- const stderrRl = (0, import_readline.createInterface)({ input: child.stderr });
5501
+ } else {
5502
+ const text = contentBlocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
5503
+ if (text) {
5504
+ state.iterations = Math.max(state.iterations, 1);
5505
+ state.summary += text;
5506
+ void presenter.onAssistantText(text);
5507
+ }
5508
+ }
5509
+ return;
5510
+ }
5511
+ if (role === "tool") {
5512
+ const toolCallId = typeof parsed.tool_call_id === "string" ? parsed.tool_call_id : `kimi-tool-${Date.now()}`;
5513
+ const allText = contentBlocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
5514
+ const userText = allText.filter((t) => !t.startsWith("<system>")).join("\n");
5515
+ void presenter.onToolResult?.(toolCallId, userText || allText.join("\n"));
5516
+ return;
5517
+ }
5518
+ }
5519
+ function buildPromptWithSystem(config, promptText) {
5520
+ const parts2 = [
5521
+ config.systemPrompt?.trim(),
5522
+ config.systemPromptAppend?.trim(),
5523
+ promptText.trim()
5524
+ ].filter((value2) => Boolean(value2 && value2.length > 0));
5525
+ return parts2.join("\n\n");
5526
+ }
5527
+ function buildPlanModePrefix(promptText) {
5528
+ return [
5529
+ "You are in plan-only mode.",
5530
+ "Do not make code changes or execute write operations.",
5531
+ "Focus on analysis, planning, and explaining the next steps.",
5532
+ "",
5533
+ promptText
5534
+ ].join("\n");
5535
+ }
5536
+ function imageFileExtension(mimeType) {
5537
+ const subtype = mimeType.split("/")[1]?.split(/[+;]/)[0]?.trim();
5538
+ if (!subtype) return "png";
5539
+ return subtype === "jpeg" ? "jpg" : subtype;
5540
+ }
5541
+ function createImageTempDirectory(cwd) {
5542
+ const candidates = [
5543
+ cwd ? (0, import_path2.join)(cwd, ".aiden-images-") : null,
5544
+ (0, import_path2.join)((0, import_os2.tmpdir)(), "aiden-images-")
5545
+ ].filter((value2) => Boolean(value2));
5546
+ for (const candidate of candidates) {
5547
+ try {
5548
+ return (0, import_fs3.mkdtempSync)(candidate);
5549
+ } catch {
5550
+ }
5551
+ }
5552
+ return (0, import_fs3.mkdtempSync)((0, import_path2.join)((0, import_os2.tmpdir)(), "aiden-images-"));
5553
+ }
5554
+ function writeImagesToTempFiles(images, cwd) {
5555
+ if (!images?.length) return { paths: [], files: [], cleanup: () => {
5556
+ } };
5557
+ const directory = createImageTempDirectory(cwd);
5558
+ const paths = [];
5559
+ const files = [];
5560
+ for (const img of images) {
5561
+ const ext = imageFileExtension(img.mimeType);
5562
+ const filePath = (0, import_path2.join)(directory, `aiden-img-${img.id}.${ext}`);
5563
+ (0, import_fs3.writeFileSync)(filePath, Buffer.from(img.data, "base64"));
5564
+ paths.push(filePath);
5565
+ files.push({
5566
+ filename: img.filename,
5567
+ mimeType: img.mimeType,
5568
+ width: img.width,
5569
+ height: img.height,
5570
+ path: filePath
5571
+ });
5572
+ }
5573
+ return {
5574
+ paths,
5575
+ files,
5576
+ cleanup: () => {
5577
+ try {
5578
+ (0, import_fs3.rmSync)(directory, { recursive: true, force: true });
5579
+ } catch {
5580
+ }
5581
+ }
5582
+ };
5583
+ }
5584
+ function appendImagePathReferences(prompt, images) {
5585
+ if (images.length === 0) return prompt;
5586
+ const imageLines = images.map((img) => {
5587
+ const dimensions = img.width > 0 && img.height > 0 ? `, ${img.width}x${img.height}` : "";
5588
+ return `- ${img.filename} (${img.mimeType}${dimensions}): @${img.path} (path: ${img.path})`;
5589
+ });
5590
+ return [
5591
+ prompt,
5592
+ "Attached images are available as local files. Use the @path references below when this CLI supports file references, or read the plain paths with filesystem tools:",
5593
+ ...imageLines
5594
+ ].join("\n\n");
5595
+ }
5596
+ function buildPromptWithImagePathReferences(context, images) {
5597
+ const base = buildPromptWithSystem(context.config, context.promptText);
5598
+ const prompt = context.config.mode === "plan" ? buildPlanModePrefix(base) : base;
5599
+ return appendImagePathReferences(prompt, images);
5600
+ }
5601
+ function formatCliSpawnError(command, error) {
5602
+ if (error.code === "ENOENT") {
5603
+ return new Error(
5604
+ `CLI command not found: ${command}. Install it or configure this provider with the full executable path.`
5605
+ );
5606
+ }
5607
+ return error;
5608
+ }
5609
+ function createGenericCliBackend(options) {
5610
+ return {
5611
+ kind: options.kind,
5612
+ supportTier: options.supportTier,
5613
+ async run(context) {
5614
+ const presenter = context.presenter;
5615
+ const state = {
5616
+ process: null,
5617
+ iterations: 0,
5618
+ summary: "",
5619
+ allowContinuation: options.allowContinuation ?? false
5620
+ };
5621
+ const prompt = options.augmentPrompt?.(context) ?? (context.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(context.config, context.promptText)) : buildPromptWithSystem(context.config, context.promptText));
5622
+ const args = options.buildArgs?.(context, prompt) ?? options.args.map((arg) => arg === "{{prompt}}" ? prompt : arg);
5623
+ const child = spawnCli(options.command, args, context);
5624
+ state.process = child;
5625
+ context.onProcessSpawned?.(child);
5626
+ const safeArgs = args.map(
5627
+ (a, i) => i > 0 && args[i - 1] === "--system-prompt" ? `"<system-prompt ${a.length} chars>"` : a
5628
+ );
5629
+ console.info(`[${options.kind}] Spawning: ${options.command} ${safeArgs.join(" ")}`);
5630
+ presenter.recordRawTranscript?.("system", `${options.command} ${args.join(" ")}`, {
5631
+ backendKind: options.kind
5632
+ });
5633
+ child.stdin.on("error", (err) => {
5634
+ if (err.code !== "EPIPE") {
5635
+ console.error(`[${options.kind}] stdin error:`, err);
5636
+ }
5637
+ });
5638
+ if (options.stdinWriter) {
5639
+ options.stdinWriter(child.stdin, context);
5640
+ } else if (options.promptViaStdin) {
5641
+ child.stdin.write(prompt);
5642
+ if (!options.keepStdinOpen) child.stdin.end();
5643
+ } else if (!options.keepStdinOpen) {
5644
+ child.stdin.end();
5645
+ }
5646
+ if (options.keepStdinOpen) {
5647
+ state.stdin = child.stdin;
5648
+ }
5649
+ const stdoutRl = (0, import_readline.createInterface)({ input: child.stdout });
5650
+ const stderrRl = (0, import_readline.createInterface)({ input: child.stderr });
5384
5651
  const stderrLines = [];
5385
5652
  stdoutRl.on("line", (line) => {
5386
5653
  presenter.recordRawTranscript?.("stdout", line);
@@ -5708,155 +5975,281 @@ function parseClaudeStructuredLine(line, context, state) {
5708
5975
  break;
5709
5976
  }
5710
5977
  }
5711
- function parseCodexStructuredLine(line, context, state) {
5712
- const presenter = context.presenter;
5713
- let parsed = null;
5714
- try {
5715
- parsed = JSON.parse(line);
5716
- } catch {
5717
- void presenter.onLog(`[codex] ${line}`);
5718
- return;
5719
- }
5720
- const type = typeof parsed.type === "string" ? parsed.type : "";
5721
- if (!type) return;
5722
- if (typeof parsed.thread_id === "string") {
5723
- state.runtimeSessionId = parsed.thread_id;
5724
- }
5725
- const extractCodexErrorMessage = (value2) => {
5726
- if (typeof value2 !== "string") return "";
5727
- const trimmed = value2.trim();
5728
- if (!trimmed) return "";
5729
- try {
5730
- const parsedValue = JSON.parse(trimmed);
5731
- if (typeof parsedValue.detail === "string" && parsedValue.detail.trim().length > 0) {
5732
- return parsedValue.detail.trim();
5733
- }
5734
- if (typeof parsedValue.message === "string" && parsedValue.message.trim().length > 0) {
5735
- return parsedValue.message.trim();
5978
+ function buildClaudeModelArg(baseModel, selectedContextWindow) {
5979
+ if (selectedContextWindow === "1m") return `${baseModel}[1m]`;
5980
+ return baseModel;
5981
+ }
5982
+ function createClaudeCliBackend(command = "claude", defaultArgs = []) {
5983
+ return {
5984
+ kind: "claude_cli",
5985
+ supportTier: "structured",
5986
+ async run(context) {
5987
+ const args = [
5988
+ "--verbose",
5989
+ "--output-format",
5990
+ "stream-json",
5991
+ "--input-format",
5992
+ "stream-json",
5993
+ "--permission-mode",
5994
+ getClaudePermissionMode(context.config)
5995
+ ];
5996
+ const disallowedTools = getClaudeDisallowedTools(context.config);
5997
+ if (disallowedTools.length > 0) {
5998
+ args.push("--disallowedTools", disallowedTools.join(","));
5736
5999
  }
5737
- } catch {
5738
- }
5739
- return trimmed;
5740
- };
5741
- switch (type) {
5742
- case "thread.started":
5743
- break;
5744
- case "turn.started":
5745
- state.iterations += 1;
5746
- break;
5747
- case "session_configured":
5748
- if (typeof parsed.session_id === "string") {
5749
- state.runtimeSessionId = parsed.session_id;
6000
+ const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
6001
+ if (!resumeId) {
6002
+ if (context.config.systemPrompt?.trim()) {
6003
+ args.push("--system-prompt", context.config.systemPrompt.trim());
6004
+ }
6005
+ if (context.config.systemPromptAppend?.trim()) {
6006
+ args.push("--append-system-prompt", context.config.systemPromptAppend.trim());
6007
+ }
5750
6008
  }
5751
- break;
5752
- case "task_started":
5753
- state.iterations += 1;
5754
- break;
5755
- case "item.started": {
5756
- const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
5757
- if (!item || item.type !== "command_execution" || typeof item.id !== "string") break;
5758
- const command = typeof item.command === "string" ? item.command : "";
5759
- void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
5760
- break;
5761
- }
5762
- case "item.completed": {
5763
- const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
5764
- if (!item || typeof item.type !== "string") break;
5765
- if (item.type === "reasoning" && typeof item.text === "string") {
5766
- void presenter.onThinking(item.text);
5767
- break;
6009
+ const baseModel = context.config.selectedModel?.trim();
6010
+ if (baseModel) {
6011
+ args.push("--model", buildClaudeModelArg(baseModel, context.config.selectedContextWindow));
5768
6012
  }
5769
- if (item.type === "agent_message" && typeof item.text === "string") {
5770
- state.summary += `${item.text}
5771
- `;
5772
- void presenter.onAssistantText(item.text);
5773
- break;
6013
+ const claudeEffort = context.config.selectedEffortLevel?.trim();
6014
+ if (claudeEffort) {
6015
+ args.push("--effort", claudeEffort);
5774
6016
  }
5775
- if (item.type === "command_execution" && typeof item.id === "string") {
5776
- const output = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
5777
- const itemExitCode = typeof item.exit_code === "number" ? item.exit_code : null;
5778
- const resultText = output.trim().length > 0 ? output : itemExitCode === null ? "" : `Exit code: ${itemExitCode}`;
5779
- void presenter.onToolResult?.(item.id, resultText);
5780
- break;
6017
+ if (resumeId) {
6018
+ args.push("--resume", resumeId);
6019
+ console.info("[claude_cli] Resuming session", { resumeId });
5781
6020
  }
5782
- break;
6021
+ args.push("--chrome");
6022
+ console.info("[claude_cli] Chrome flag added \u2014 final args:", args.join(" "));
6023
+ args.push(...defaultArgs);
6024
+ return createGenericCliBackend({
6025
+ kind: "claude_cli",
6026
+ supportTier: "structured",
6027
+ command,
6028
+ args,
6029
+ parseStructuredLine: parseClaudeStructuredLine,
6030
+ stdinWriter: (stdin, ctx) => {
6031
+ const contentBlocks = [];
6032
+ if (ctx.config.images?.length) {
6033
+ for (const img of ctx.config.images) {
6034
+ contentBlocks.push({
6035
+ type: "image",
6036
+ source: { type: "base64", media_type: img.mimeType, data: img.data }
6037
+ });
6038
+ }
6039
+ }
6040
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, ctx.promptText)) : buildPromptWithSystem(ctx.config, ctx.promptText);
6041
+ if (promptText.trim()) {
6042
+ contentBlocks.push({ type: "text", text: promptText });
6043
+ }
6044
+ console.info(`[claude_cli] stdinWriter: ${contentBlocks.length} content blocks (${ctx.config.images?.length ?? 0} images)`);
6045
+ const message = JSON.stringify({
6046
+ type: "user",
6047
+ message: { role: "user", content: contentBlocks },
6048
+ session_id: "default",
6049
+ parent_tool_use_id: null
6050
+ });
6051
+ stdin.write(message + "\n");
6052
+ },
6053
+ keepStdinOpen: true,
6054
+ allowContinuation: true,
6055
+ continuationIdleMs: 10 * 60 * 1e3
6056
+ // 10 minutes
6057
+ }).run(context);
5783
6058
  }
5784
- case "agent_message_delta":
5785
- case "agent_message_content_delta": {
5786
- const delta = typeof parsed.delta === "string" ? parsed.delta : "";
5787
- if (!delta) return;
5788
- state.summary += delta;
5789
- void presenter.onAssistantText(delta);
5790
- break;
5791
- }
5792
- case "agent_reasoning_delta":
5793
- case "reasoning_content_delta":
5794
- case "agent_reasoning_raw_content_delta": {
5795
- const delta = typeof parsed.delta === "string" ? parsed.delta : "";
5796
- if (!delta) return;
5797
- void presenter.onThinking(delta);
5798
- break;
6059
+ };
6060
+ }
6061
+ function createSupatestCliBackend(command = "supatest", defaultArgs = []) {
6062
+ return {
6063
+ kind: "supatest_cli",
6064
+ supportTier: "structured",
6065
+ async run(context) {
6066
+ const args = [
6067
+ "--cwd",
6068
+ context.cwd,
6069
+ "--output-format",
6070
+ "stream-json",
6071
+ "--input-format",
6072
+ "stream-json",
6073
+ "--permission-mode",
6074
+ getClaudePermissionMode(context.config)
6075
+ ];
6076
+ const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
6077
+ if (!resumeId) {
6078
+ if (context.config.systemPrompt?.trim()) {
6079
+ args.push("--system-prompt", context.config.systemPrompt.trim());
6080
+ }
6081
+ if (context.config.systemPromptAppend?.trim()) {
6082
+ args.push("--append-system-prompt", context.config.systemPromptAppend.trim());
6083
+ }
6084
+ }
6085
+ const baseModel = context.config.selectedModel?.trim();
6086
+ if (baseModel) {
6087
+ args.push("--model", buildClaudeModelArg(baseModel, context.config.selectedContextWindow));
6088
+ }
6089
+ if (resumeId) {
6090
+ args.push("--resume", resumeId);
6091
+ console.info("[supatest_cli] Resuming session", { resumeId });
6092
+ }
6093
+ args.push(...defaultArgs);
6094
+ return createGenericCliBackend({
6095
+ kind: "supatest_cli",
6096
+ supportTier: "structured",
6097
+ command,
6098
+ args,
6099
+ parseStructuredLine: parseClaudeStructuredLine,
6100
+ stdinWriter: (stdin, ctx) => {
6101
+ const contentBlocks = [];
6102
+ if (ctx.config.images?.length) {
6103
+ for (const img of ctx.config.images) {
6104
+ contentBlocks.push({
6105
+ type: "image",
6106
+ source: { type: "base64", media_type: img.mimeType, data: img.data }
6107
+ });
6108
+ }
6109
+ }
6110
+ const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, ctx.promptText)) : buildPromptWithSystem(ctx.config, ctx.promptText);
6111
+ if (promptText.trim()) {
6112
+ contentBlocks.push({ type: "text", text: promptText });
6113
+ }
6114
+ const message = JSON.stringify({
6115
+ type: "user",
6116
+ message: { role: "user", content: contentBlocks },
6117
+ session_id: "default",
6118
+ parent_tool_use_id: null
6119
+ });
6120
+ stdin.write(message + "\n");
6121
+ },
6122
+ keepStdinOpen: true
6123
+ }).run(context);
5799
6124
  }
5800
- case "exec_command_begin": {
5801
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
5802
- const command = Array.isArray(parsed.command) ? parsed.command.join(" ") : "";
5803
- const cwd = typeof parsed.cwd === "string" ? parsed.cwd : context.cwd;
5804
- void presenter.onToolUse("Bash", { command, cwd }, toolId);
5805
- break;
6125
+ };
6126
+ }
6127
+ function shouldEnableKimiCliThinking(model) {
6128
+ return model?.trim() === "kimi-k2.6-thinking";
6129
+ }
6130
+ function resolveKimiCliModelArg(model) {
6131
+ const trimmed = model?.trim();
6132
+ if (!trimmed) return void 0;
6133
+ if (trimmed === "kimi-latest" || trimmed === "kimi-k2.5" || trimmed === "kimi-k2.6" || shouldEnableKimiCliThinking(trimmed)) {
6134
+ return void 0;
6135
+ }
6136
+ if (trimmed.startsWith("claude-")) return void 0;
6137
+ return trimmed;
6138
+ }
6139
+ function createKimiCliBackend(command = "kimi", defaultArgs = []) {
6140
+ return {
6141
+ kind: "kimi_cli",
6142
+ supportTier: "structured",
6143
+ async run(context) {
6144
+ const { files: imageFiles, cleanup } = writeImagesToTempFiles(context.config.images, context.cwd);
6145
+ try {
6146
+ return await createGenericCliBackend({
6147
+ kind: "kimi_cli",
6148
+ supportTier: "structured",
6149
+ command,
6150
+ args: [],
6151
+ buildArgs: (ctx, prompt) => {
6152
+ const args = [
6153
+ "--print",
6154
+ "--output-format",
6155
+ "stream-json",
6156
+ "--work-dir",
6157
+ ctx.cwd
6158
+ ];
6159
+ const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
6160
+ if (resumeId) args.push("--session", resumeId);
6161
+ if (shouldEnableKimiCliThinking(ctx.config.selectedModel)) {
6162
+ args.push("--thinking");
6163
+ }
6164
+ const model = resolveKimiCliModelArg(ctx.config.selectedModel);
6165
+ if (model) args.push("--model", model);
6166
+ return [...args, ...defaultArgs, "--prompt", prompt];
6167
+ },
6168
+ augmentPrompt: (ctx) => buildPromptWithImagePathReferences(ctx, imageFiles),
6169
+ parseStructuredLine: parseKimiStructuredLine
6170
+ }).run(context);
6171
+ } finally {
6172
+ cleanup();
6173
+ }
5806
6174
  }
5807
- case "exec_command_end": {
5808
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
5809
- const output = typeof parsed.formatted_output === "string" ? parsed.formatted_output : typeof parsed.aggregated_output === "string" ? parsed.aggregated_output : "";
5810
- void presenter.onToolResult?.(toolId, output);
6175
+ };
6176
+ }
6177
+ function buildGeminiToolResultText(parsed) {
6178
+ const output = typeof parsed.output === "string" ? parsed.output : "";
6179
+ if (output.trim().length > 0) return output;
6180
+ const error = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
6181
+ const message = typeof error?.message === "string" ? error.message.trim() : "";
6182
+ const errorType = typeof error?.type === "string" ? error.type.trim() : "";
6183
+ if (message && errorType) return `${errorType}: ${message}`;
6184
+ if (message) return message;
6185
+ const status = typeof parsed.status === "string" ? parsed.status.trim() : "";
6186
+ return status || "Tool completed";
6187
+ }
6188
+ function parseGeminiStructuredLine(line, context, state) {
6189
+ const presenter = context.presenter;
6190
+ let parsed = null;
6191
+ try {
6192
+ parsed = JSON.parse(line);
6193
+ } catch {
6194
+ void presenter.onLog(`[gemini_cli] ${line}`);
6195
+ return;
6196
+ }
6197
+ const type = typeof parsed.type === "string" ? parsed.type : "";
6198
+ if (!type) return;
6199
+ if (typeof parsed.session_id === "string") state.runtimeSessionId = parsed.session_id;
6200
+ if (typeof parsed.model === "string") state.activeModel = parsed.model;
6201
+ switch (type) {
6202
+ case "init": {
6203
+ const model = state.activeModel || context.config.selectedModel || "gemini";
6204
+ void presenter.onLog(`[gemini_cli] Session initialized with ${model}`);
5811
6205
  break;
5812
6206
  }
5813
- case "mcp_tool_call_begin": {
5814
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
5815
- const invocation = typeof parsed.invocation === "object" && parsed.invocation !== null ? parsed.invocation : {};
5816
- const tool = typeof invocation.tool_name === "string" ? invocation.tool_name : typeof invocation.tool === "string" ? invocation.tool : "MCP Tool";
5817
- void presenter.onToolUse(tool, invocation, toolId);
6207
+ case "message": {
6208
+ if (typeof parsed.role !== "string" || parsed.role !== "assistant") break;
6209
+ const content = typeof parsed.content === "string" ? parsed.content : "";
6210
+ if (!content) break;
6211
+ state.iterations = Math.max(state.iterations, 1);
6212
+ state.summary += content;
6213
+ void presenter.onAssistantText(content);
5818
6214
  break;
5819
6215
  }
5820
- case "mcp_tool_call_end": {
5821
- const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
5822
- void presenter.onToolResult?.(toolId, JSON.stringify(parsed.result ?? {}, null, 2));
6216
+ case "tool_use": {
6217
+ const toolName = typeof parsed.tool_name === "string" ? parsed.tool_name : "Gemini Tool";
6218
+ const toolId = typeof parsed.tool_id === "string" ? parsed.tool_id : `gemini-tool-${Date.now()}`;
6219
+ state.iterations = Math.max(state.iterations, 1);
6220
+ void presenter.onToolUse(toolName, parsed.parameters ?? {}, toolId);
5823
6221
  break;
5824
6222
  }
5825
- case "token_count": {
5826
- const info = typeof parsed.info === "object" && parsed.info !== null ? parsed.info : {};
5827
- state.usage = {
5828
- model: typeof info.model === "string" ? info.model : "codex",
5829
- numTurns: Math.max(state.iterations, 1),
5830
- durationMs: 0,
5831
- inputTokens: typeof info.input_tokens === "number" ? info.input_tokens : 0,
5832
- outputTokens: typeof info.output_tokens === "number" ? info.output_tokens : 0,
5833
- cacheReadTokens: typeof info.cached_input_tokens === "number" ? info.cached_input_tokens : 0,
5834
- cacheCreationTokens: 0,
5835
- costUsd: typeof info.total_cost_usd === "number" ? info.total_cost_usd : 0
5836
- };
5837
- void presenter.onUsageUpdate?.({
5838
- model: state.usage.model,
5839
- inputTokens: state.usage.inputTokens,
5840
- outputTokens: state.usage.outputTokens,
5841
- cacheReadTokens: state.usage.cacheReadTokens,
5842
- cacheCreationTokens: state.usage.cacheCreationTokens
5843
- });
6223
+ case "tool_result": {
6224
+ const toolId = typeof parsed.tool_id === "string" ? parsed.tool_id : `gemini-tool-${Date.now()}`;
6225
+ state.iterations = Math.max(state.iterations, 1);
6226
+ void presenter.onToolResult?.(toolId, buildGeminiToolResultText(parsed));
5844
6227
  break;
5845
6228
  }
5846
- case "task_complete": {
5847
- const lastMessage = typeof parsed.last_agent_message === "string" ? parsed.last_agent_message : "";
5848
- if (lastMessage.length > 0) state.summary = lastMessage;
6229
+ case "error": {
6230
+ const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
6231
+ const message = typeof parsed.message === "string" && parsed.message.trim() || errObj && typeof errObj.message === "string" && errObj.message.trim() || typeof parsed.error === "string" && parsed.error.trim() || typeof parsed.error_message === "string" && parsed.error_message.trim() || "";
6232
+ const severity = typeof parsed.severity === "string" ? parsed.severity : "error";
6233
+ if (message) {
6234
+ if (severity === "error") {
6235
+ void presenter.onError(message);
6236
+ state.error = message;
6237
+ } else {
6238
+ void presenter.onLog(`[gemini_cli/${severity}] ${message}`);
6239
+ }
6240
+ }
5849
6241
  break;
5850
6242
  }
5851
- case "turn.completed": {
5852
- const usage = typeof parsed.usage === "object" && parsed.usage !== null ? parsed.usage : {};
6243
+ case "result": {
6244
+ const stats = typeof parsed.stats === "object" && parsed.stats !== null ? parsed.stats : {};
6245
+ state.iterations = Math.max(state.iterations, 1);
5853
6246
  state.usage = {
5854
- model: typeof context.config.selectedModel === "string" && context.config.selectedModel.trim() ? context.config.selectedModel : "codex",
6247
+ model: state.activeModel || (typeof context.config.selectedModel === "string" && context.config.selectedModel.trim() ? context.config.selectedModel : "gemini"),
5855
6248
  numTurns: Math.max(state.iterations, 1),
5856
- durationMs: 0,
5857
- inputTokens: typeof usage.input_tokens === "number" ? usage.input_tokens : 0,
5858
- outputTokens: typeof usage.output_tokens === "number" ? usage.output_tokens : 0,
5859
- cacheReadTokens: typeof usage.cached_input_tokens === "number" ? usage.cached_input_tokens : 0,
6249
+ durationMs: typeof stats.duration_ms === "number" ? stats.duration_ms : 0,
6250
+ inputTokens: typeof stats.input_tokens === "number" ? stats.input_tokens : typeof stats.input === "number" ? stats.input : 0,
6251
+ outputTokens: typeof stats.output_tokens === "number" ? stats.output_tokens : 0,
6252
+ cacheReadTokens: typeof stats.cached === "number" ? stats.cached : 0,
5860
6253
  cacheCreationTokens: 0,
5861
6254
  costUsd: 0
5862
6255
  };
@@ -5867,24 +6260,15 @@ function parseCodexStructuredLine(line, context, state) {
5867
6260
  cacheReadTokens: state.usage.cacheReadTokens,
5868
6261
  cacheCreationTokens: state.usage.cacheCreationTokens
5869
6262
  });
5870
- break;
5871
- }
5872
- case "error": {
5873
- const message = extractCodexErrorMessage(parsed.message) || extractCodexErrorMessage(parsed.error) || typeof parsed.detail === "string" && parsed.detail.trim() || "";
5874
- if (message) {
5875
- const normalizedMessage = normalizeCodexCliErrorMessage(message);
5876
- void presenter.onError(normalizedMessage);
5877
- state.error = normalizedMessage;
5878
- }
5879
- break;
5880
- }
5881
- case "turn.failed": {
5882
- const error = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : {};
5883
- const message = extractCodexErrorMessage(error.message) || extractCodexErrorMessage(error.detail) || typeof parsed.message === "string" && parsed.message.trim() || "";
5884
- if (message) {
5885
- const normalizedMessage = normalizeCodexCliErrorMessage(message);
5886
- void presenter.onError(normalizedMessage);
5887
- state.error = normalizedMessage;
6263
+ if (typeof parsed.status === "string" && parsed.status !== "success" && !state.error) {
6264
+ const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
6265
+ const errMsg = typeof parsed.error === "string" && parsed.error.trim() || errObj && typeof errObj.message === "string" && errObj.message.trim() || typeof parsed.error_message === "string" && parsed.error_message.trim() || typeof parsed.message === "string" && parsed.message.trim() || typeof parsed.result === "string" && parsed.result.trim() || parsed.status;
6266
+ state.error = errMsg;
6267
+ console.error("[gemini_cli] result status=", parsed.status, "error:", JSON.stringify({
6268
+ error: parsed.error,
6269
+ error_message: parsed.error_message,
6270
+ message: parsed.message
6271
+ }));
5888
6272
  }
5889
6273
  break;
5890
6274
  }
@@ -5892,109 +6276,36 @@ function parseCodexStructuredLine(line, context, state) {
5892
6276
  break;
5893
6277
  }
5894
6278
  }
5895
- function parseJsonObject(value2) {
5896
- try {
5897
- const parsed = JSON.parse(value2);
5898
- return typeof parsed === "object" && parsed !== null ? parsed : null;
5899
- } catch {
5900
- return null;
5901
- }
5902
- }
5903
- function parseMaybeJson(value2) {
5904
- if (typeof value2 !== "string") return value2;
5905
- return parseJsonObject(value2) ?? value2;
5906
- }
5907
- function normalizeCodexMcpToolResult(output) {
5908
- if (typeof output !== "string") return JSON.stringify(output ?? {}, null, 2);
5909
- const outputMarker = "\nOutput:\n";
5910
- const raw = output.includes(outputMarker) ? output.slice(output.indexOf(outputMarker) + outputMarker.length).trim() : output.trim();
5911
- let parsed;
5912
- try {
5913
- parsed = JSON.parse(raw);
5914
- } catch {
5915
- return raw;
5916
- }
5917
- if (Array.isArray(parsed)) {
5918
- const textBlocks = parsed.filter(
5919
- (entry) => typeof entry === "object" && entry !== null && "text" in entry
5920
- ).map((entry) => typeof entry.text === "string" ? entry.text : "").filter(Boolean);
5921
- if (textBlocks.length > 0) return textBlocks.join("\n");
5922
- }
5923
- return raw;
5924
- }
5925
- function findCodexSessionLog(runtimeSessionId, codexHome) {
5926
- if (!runtimeSessionId || !(0, import_fs.existsSync)(codexHome)) return null;
5927
- const root = (0, import_path.join)(codexHome, "sessions");
5928
- if (!(0, import_fs.existsSync)(root)) return null;
5929
- const matches = [];
5930
- const stack = [root];
5931
- while (stack.length > 0) {
5932
- const dir = stack.pop();
5933
- let entries;
5934
- try {
5935
- entries = (0, import_fs.readdirSync)(dir, { withFileTypes: true });
5936
- } catch {
5937
- continue;
5938
- }
5939
- for (const entry of entries) {
5940
- const path = (0, import_path.join)(dir, entry.name);
5941
- if (entry.isDirectory()) {
5942
- stack.push(path);
5943
- } else if (entry.isFile() && entry.name.includes(runtimeSessionId) && entry.name.endsWith(".jsonl")) {
5944
- try {
5945
- matches.push({ path, mtimeMs: (0, import_fs.statSync)(path).mtimeMs });
5946
- } catch {
5947
- matches.push({ path, mtimeMs: 0 });
5948
- }
6279
+ function createGeminiCliBackend(command = "gemini", defaultArgs = []) {
6280
+ return {
6281
+ kind: "gemini_cli",
6282
+ supportTier: "structured",
6283
+ async run(context) {
6284
+ const { files: imageFiles, cleanup } = writeImagesToTempFiles(context.config.images, context.cwd);
6285
+ try {
6286
+ return await createGenericCliBackend({
6287
+ kind: "gemini_cli",
6288
+ supportTier: "structured",
6289
+ command,
6290
+ args: [],
6291
+ buildArgs: (ctx) => {
6292
+ const args = ["-p", "", "--output-format", "stream-json"];
6293
+ const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
6294
+ const approvalMode = ctx.config.mode === "plan" || ctx.config.mode === "ask" || ctx.config.mode === "review" ? "plan" : "yolo";
6295
+ args.push("--approval-mode", approvalMode);
6296
+ if (resumeId) args.push("--resume", resumeId);
6297
+ if (ctx.config.selectedModel?.trim()) args.push("--model", ctx.config.selectedModel.trim());
6298
+ return [...args, ...defaultArgs];
6299
+ },
6300
+ promptViaStdin: true,
6301
+ augmentPrompt: (ctx) => buildPromptWithImagePathReferences(ctx, imageFiles),
6302
+ parseStructuredLine: parseGeminiStructuredLine
6303
+ }).run(context);
6304
+ } finally {
6305
+ cleanup();
5949
6306
  }
5950
6307
  }
5951
- }
5952
- matches.sort((a, b) => b.mtimeMs - a.mtimeMs);
5953
- return matches[0]?.path ?? null;
5954
- }
5955
- async function replayCodexMcpToolEventsFromSessionLog(context, state) {
5956
- const runtimeSessionId = state.runtimeSessionId;
5957
- if (!runtimeSessionId) return;
5958
- const codexHome = context.env.CODEX_HOME || process.env.CODEX_HOME || (0, import_path.join)((0, import_os.homedir)(), ".codex");
5959
- const logPath = findCodexSessionLog(runtimeSessionId, codexHome);
5960
- if (!logPath) return;
5961
- const emittedToolIds = /* @__PURE__ */ new Set();
5962
- const lines = (0, import_fs.readFileSync)(logPath, "utf8").split(/\r?\n/).filter(Boolean);
5963
- for (const line of lines) {
5964
- const entry = parseJsonObject(line);
5965
- const payload = entry && typeof entry.payload === "object" && entry.payload !== null ? entry.payload : null;
5966
- if (!payload) continue;
5967
- if (payload.type === "function_call") {
5968
- const callId = typeof payload.call_id === "string" ? payload.call_id : "";
5969
- const name = typeof payload.name === "string" ? payload.name : "";
5970
- const namespace = typeof payload.namespace === "string" ? payload.namespace : "";
5971
- if (!callId || !name || !namespace.startsWith("mcp__")) continue;
5972
- const toolName = `${namespace}${name}`;
5973
- context.presenter.onToolUse(toolName, parseMaybeJson(payload.arguments), callId);
5974
- emittedToolIds.add(callId);
5975
- continue;
5976
- }
5977
- if (payload.type === "tool_search_call") {
5978
- const callId = typeof payload.call_id === "string" ? payload.call_id : "";
5979
- if (!callId) continue;
5980
- context.presenter.onToolUse("tool_search", payload.arguments ?? {}, callId);
5981
- emittedToolIds.add(callId);
5982
- }
5983
- }
5984
- if (emittedToolIds.size === 0) return;
5985
- for (const line of lines) {
5986
- const entry = parseJsonObject(line);
5987
- const payload = entry && typeof entry.payload === "object" && entry.payload !== null ? entry.payload : null;
5988
- if (!payload || payload.type !== "function_call_output" && payload.type !== "tool_search_output") continue;
5989
- const callId = typeof payload.call_id === "string" ? payload.call_id : "";
5990
- if (!emittedToolIds.has(callId)) continue;
5991
- try {
5992
- const result = payload.type === "tool_search_output" ? JSON.stringify(payload.tools ?? [], null, 2) : normalizeCodexMcpToolResult(payload.output);
5993
- context.presenter.onToolResult?.(callId, result);
5994
- } catch {
5995
- context.presenter.onToolResult?.(callId, String(payload.output ?? ""));
5996
- }
5997
- }
6308
+ };
5998
6309
  }
5999
6310
  function extractCursorToolEntry(toolCall) {
6000
6311
  const [rawName, rawPayload] = Object.entries(toolCall)[0] ?? [];
@@ -6106,30 +6417,81 @@ function parseCursorStructuredLine(line, context, state) {
6106
6417
  state.error = resultText.trim() || "Cursor Agent run failed";
6107
6418
  void presenter.onError(state.error);
6108
6419
  }
6420
+ state.resultReceived = true;
6421
+ state.onResultReceived?.();
6422
+ if (state.stdin && !state.stdin.destroyed) {
6423
+ state.stdin.end();
6424
+ }
6109
6425
  break;
6110
6426
  }
6111
6427
  default:
6112
6428
  break;
6113
6429
  }
6114
6430
  }
6115
- function buildGeminiToolResultText(parsed) {
6116
- const output = typeof parsed.output === "string" ? parsed.output : "";
6117
- if (output.trim().length > 0) return output;
6118
- const error = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
6119
- const message = typeof error?.message === "string" ? error.message.trim() : "";
6120
- const errorType = typeof error?.type === "string" ? error.type.trim() : "";
6121
- if (message && errorType) return `${errorType}: ${message}`;
6122
- if (message) return message;
6123
- const status = typeof parsed.status === "string" ? parsed.status.trim() : "";
6124
- return status || "Tool completed";
6431
+ function isCursorAgentCliModelId(model) {
6432
+ return model === "auto" || /^composer-/.test(model) || /^gpt-\d/.test(model) || /^claude-\d/.test(model) || /^gemini-\d/.test(model) || /^grok-\d/.test(model) || /^kimi-/.test(model);
6125
6433
  }
6126
- function parseGeminiStructuredLine(line, context, state) {
6434
+ function shouldForceCursorAgent(config) {
6435
+ if (shouldUseReadOnlyRuntimePermissions(config)) return false;
6436
+ switch (config.mode) {
6437
+ case "plan":
6438
+ case "ask":
6439
+ case "review":
6440
+ return false;
6441
+ default:
6442
+ return true;
6443
+ }
6444
+ }
6445
+ function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = []) {
6446
+ return {
6447
+ kind: "cursor_agent_cli",
6448
+ supportTier: "structured",
6449
+ async run(context) {
6450
+ const { files: imageFiles, cleanup } = writeImagesToTempFiles(context.config.images, context.cwd);
6451
+ try {
6452
+ return await createGenericCliBackend({
6453
+ kind: "cursor_agent_cli",
6454
+ supportTier: "structured",
6455
+ command,
6456
+ args: [],
6457
+ buildArgs: (ctx, prompt) => {
6458
+ const args = [
6459
+ "--print",
6460
+ "--output-format",
6461
+ "stream-json",
6462
+ "--trust",
6463
+ "--workspace",
6464
+ ctx.cwd
6465
+ ];
6466
+ const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
6467
+ if (shouldForceCursorAgent(ctx.config)) args.push("--force");
6468
+ if (ctx.config.mode === "plan" || ctx.config.mode === "review") {
6469
+ args.push("--mode", "plan");
6470
+ } else if (ctx.config.mode === "ask") {
6471
+ args.push("--mode", "ask");
6472
+ }
6473
+ if (resumeId) args.push("--resume", resumeId);
6474
+ const model = ctx.config.selectedModel?.trim();
6475
+ if (model && isCursorAgentCliModelId(model)) args.push("--model", model);
6476
+ return [...args, ...defaultArgs, prompt];
6477
+ },
6478
+ keepStdinOpen: true,
6479
+ augmentPrompt: (ctx) => buildPromptWithImagePathReferences(ctx, imageFiles),
6480
+ parseStructuredLine: parseCursorStructuredLine
6481
+ }).run(context);
6482
+ } finally {
6483
+ cleanup();
6484
+ }
6485
+ }
6486
+ };
6487
+ }
6488
+ function parseDroidStructuredLine(line, context, state) {
6127
6489
  const presenter = context.presenter;
6128
6490
  let parsed = null;
6129
6491
  try {
6130
6492
  parsed = JSON.parse(line);
6131
6493
  } catch {
6132
- void presenter.onLog(`[gemini_cli] ${line}`);
6494
+ if (line.trim().length > 0) void presenter.onLog(`[droid_cli] ${line}`);
6133
6495
  return;
6134
6496
  }
6135
6497
  const type = typeof parsed.type === "string" ? parsed.type : "";
@@ -6137,581 +6499,441 @@ function parseGeminiStructuredLine(line, context, state) {
6137
6499
  if (typeof parsed.session_id === "string") state.runtimeSessionId = parsed.session_id;
6138
6500
  if (typeof parsed.model === "string") state.activeModel = parsed.model;
6139
6501
  switch (type) {
6140
- case "init": {
6141
- const model = state.activeModel || context.config.selectedModel || "gemini";
6142
- void presenter.onLog(`[gemini_cli] Session initialized with ${model}`);
6502
+ case "system": {
6503
+ const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
6504
+ if (subtype === "init") {
6505
+ if (typeof parsed.session_id === "string") state.runtimeSessionId = parsed.session_id;
6506
+ if (typeof parsed.model === "string") state.activeModel = parsed.model;
6507
+ }
6143
6508
  break;
6144
6509
  }
6145
6510
  case "message": {
6146
- if (typeof parsed.role !== "string" || parsed.role !== "assistant") break;
6147
- const content = typeof parsed.content === "string" ? parsed.content : "";
6148
- if (!content) break;
6511
+ const role = typeof parsed.role === "string" ? parsed.role : "";
6512
+ if (role !== "assistant") break;
6513
+ const text = typeof parsed.text === "string" ? parsed.text : "";
6514
+ if (!text) break;
6149
6515
  state.iterations = Math.max(state.iterations, 1);
6150
- state.summary += content;
6151
- void presenter.onAssistantText(content);
6516
+ state.summary += text;
6517
+ void presenter.onAssistantText(text);
6152
6518
  break;
6153
6519
  }
6154
- case "tool_use": {
6155
- const toolName = typeof parsed.tool_name === "string" ? parsed.tool_name : "Gemini Tool";
6156
- const toolId = typeof parsed.tool_id === "string" ? parsed.tool_id : `gemini-tool-${Date.now()}`;
6520
+ case "tool_call": {
6521
+ const toolId = typeof parsed.id === "string" ? parsed.id : `droid-tool-${Date.now()}`;
6522
+ const toolName = typeof parsed.toolName === "string" ? parsed.toolName : "Tool";
6523
+ const parameters = typeof parsed.parameters === "object" && parsed.parameters !== null ? parsed.parameters : {};
6157
6524
  state.iterations = Math.max(state.iterations, 1);
6158
- void presenter.onToolUse(toolName, parsed.parameters ?? {}, toolId);
6525
+ void presenter.onToolUse(toolName, parameters, toolId);
6159
6526
  break;
6160
6527
  }
6161
6528
  case "tool_result": {
6162
- const toolId = typeof parsed.tool_id === "string" ? parsed.tool_id : `gemini-tool-${Date.now()}`;
6163
- state.iterations = Math.max(state.iterations, 1);
6164
- void presenter.onToolResult?.(toolId, buildGeminiToolResultText(parsed));
6529
+ const toolId = typeof parsed.id === "string" ? parsed.id : `droid-tool-${Date.now()}`;
6530
+ const value2 = parsed.value;
6531
+ const content = typeof value2 === "string" ? value2 : value2 !== void 0 ? JSON.stringify(value2) : "";
6532
+ const isError = parsed.isError === true;
6533
+ void presenter.onToolResult?.(toolId, content, isError);
6165
6534
  break;
6166
6535
  }
6167
- case "error": {
6168
- const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
6169
- const message = typeof parsed.message === "string" && parsed.message.trim() || errObj && typeof errObj.message === "string" && errObj.message.trim() || typeof parsed.error === "string" && parsed.error.trim() || typeof parsed.error_message === "string" && parsed.error_message.trim() || "";
6170
- const severity = typeof parsed.severity === "string" ? parsed.severity : "error";
6171
- if (message) {
6172
- if (severity === "error") {
6173
- void presenter.onError(message);
6174
- state.error = message;
6175
- } else {
6176
- void presenter.onLog(`[gemini_cli/${severity}] ${message}`);
6177
- }
6536
+ case "completion": {
6537
+ const finalText = typeof parsed.finalText === "string" ? parsed.finalText : "";
6538
+ const numTurns = typeof parsed.numTurns === "number" ? parsed.numTurns : 1;
6539
+ const durationMs = typeof parsed.durationMs === "number" ? parsed.durationMs : 0;
6540
+ state.iterations = Math.max(state.iterations, numTurns, finalText ? 1 : 0);
6541
+ if (finalText.trim() && !state.summary.trim()) {
6542
+ state.summary = finalText;
6543
+ void presenter.onAssistantText(finalText);
6178
6544
  }
6179
- break;
6180
- }
6181
- case "result": {
6182
- const stats = typeof parsed.stats === "object" && parsed.stats !== null ? parsed.stats : {};
6183
- state.iterations = Math.max(state.iterations, 1);
6184
6545
  state.usage = {
6185
- model: state.activeModel || (typeof context.config.selectedModel === "string" && context.config.selectedModel.trim() ? context.config.selectedModel : "gemini"),
6186
- numTurns: Math.max(state.iterations, 1),
6187
- durationMs: typeof stats.duration_ms === "number" ? stats.duration_ms : 0,
6188
- inputTokens: typeof stats.input_tokens === "number" ? stats.input_tokens : typeof stats.input === "number" ? stats.input : 0,
6189
- outputTokens: typeof stats.output_tokens === "number" ? stats.output_tokens : 0,
6190
- cacheReadTokens: typeof stats.cached === "number" ? stats.cached : 0,
6546
+ model: state.activeModel || (typeof context.config.selectedModel === "string" && context.config.selectedModel.trim() ? context.config.selectedModel : "droid"),
6547
+ numTurns: Math.max(numTurns, 1),
6548
+ durationMs,
6549
+ inputTokens: 0,
6550
+ outputTokens: 0,
6551
+ cacheReadTokens: 0,
6191
6552
  cacheCreationTokens: 0,
6192
6553
  costUsd: 0
6193
6554
  };
6194
6555
  void presenter.onUsageUpdate?.({
6195
6556
  model: state.usage.model,
6196
- inputTokens: state.usage.inputTokens,
6197
- outputTokens: state.usage.outputTokens,
6198
- cacheReadTokens: state.usage.cacheReadTokens,
6199
- cacheCreationTokens: state.usage.cacheCreationTokens
6557
+ inputTokens: 0,
6558
+ outputTokens: 0,
6559
+ cacheReadTokens: 0,
6560
+ cacheCreationTokens: 0
6200
6561
  });
6201
- if (typeof parsed.status === "string" && parsed.status !== "success" && !state.error) {
6202
- const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
6203
- const errMsg = typeof parsed.error === "string" && parsed.error.trim() || errObj && typeof errObj.message === "string" && errObj.message.trim() || typeof parsed.error_message === "string" && parsed.error_message.trim() || typeof parsed.message === "string" && parsed.message.trim() || typeof parsed.result === "string" && parsed.result.trim() || parsed.status;
6204
- state.error = errMsg;
6205
- console.error("[gemini_cli] result status=", parsed.status, "error:", JSON.stringify({
6206
- error: parsed.error,
6207
- error_message: parsed.error_message,
6208
- message: parsed.message
6209
- }));
6210
- }
6211
6562
  break;
6212
6563
  }
6213
6564
  default:
6214
6565
  break;
6215
6566
  }
6216
6567
  }
6217
- function shouldUseReadOnlyRuntimePermissions(config) {
6218
- if (isReadOnlyAgent(config.agentId)) return true;
6219
- switch (config.mode) {
6220
- case "plan":
6221
- case "ask":
6222
- case "review":
6223
- return true;
6224
- default:
6225
- return false;
6226
- }
6227
- }
6228
- function getClaudePermissionMode(config) {
6229
- switch (config.mode) {
6230
- case "plan":
6231
- case "ask":
6232
- case "review":
6233
- return "plan";
6234
- default:
6235
- return "bypassPermissions";
6236
- }
6237
- }
6238
- function getClaudeDisallowedTools(config) {
6239
- return isReadOnlyAgent(config.agentId) ? ["Edit", "Write", "MultiEdit", "NotebookEdit"] : [];
6240
- }
6241
- function shouldForceCursorAgent(config) {
6242
- if (shouldUseReadOnlyRuntimePermissions(config)) return false;
6243
- switch (config.mode) {
6244
- case "plan":
6245
- case "ask":
6246
- case "review":
6247
- return false;
6248
- default:
6249
- return true;
6250
- }
6251
- }
6252
- function writeImagesToTempFiles(images) {
6253
- if (!images?.length) return { paths: [], cleanup: () => {
6254
- } };
6255
- const paths = [];
6256
- for (const img of images) {
6257
- const ext = img.mimeType.split("/")[1]?.replace("jpeg", "jpg") ?? "png";
6258
- const filePath = (0, import_path.join)((0, import_os.tmpdir)(), `aiden-img-${img.id}.${ext}`);
6259
- (0, import_fs.writeFileSync)(filePath, Buffer.from(img.data, "base64"));
6260
- paths.push(filePath);
6261
- }
6262
- return {
6263
- paths,
6264
- cleanup: () => {
6265
- for (const p of paths) {
6266
- try {
6267
- (0, import_fs.rmSync)(p, { force: true });
6268
- } catch {
6269
- }
6270
- }
6271
- }
6272
- };
6273
- }
6274
- function buildClaudeModelArg(baseModel, selectedContextWindow) {
6275
- if (selectedContextWindow === "1m") return `${baseModel}[1m]`;
6276
- return baseModel;
6568
+ function droidExecPermissionArgs(mode) {
6569
+ if (mode === "plan" || mode === "ask" || mode === "review") return [];
6570
+ return ["--auto", "high"];
6277
6571
  }
6278
- function createClaudeCliBackend(command = "claude", defaultArgs = []) {
6572
+ function createDroidCliBackend(command = "droid", defaultArgs = []) {
6279
6573
  return {
6280
- kind: "claude_cli",
6574
+ kind: "droid_cli",
6281
6575
  supportTier: "structured",
6282
6576
  async run(context) {
6283
- const args = [
6284
- "--verbose",
6285
- "--output-format",
6286
- "stream-json",
6287
- "--input-format",
6288
- "stream-json",
6289
- "--permission-mode",
6290
- getClaudePermissionMode(context.config)
6291
- ];
6292
- const disallowedTools = getClaudeDisallowedTools(context.config);
6293
- if (disallowedTools.length > 0) {
6294
- args.push("--disallowedTools", disallowedTools.join(","));
6295
- }
6296
- const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
6297
- if (!resumeId) {
6298
- if (context.config.systemPrompt?.trim()) {
6299
- args.push("--system-prompt", context.config.systemPrompt.trim());
6300
- }
6301
- if (context.config.systemPromptAppend?.trim()) {
6302
- args.push("--append-system-prompt", context.config.systemPromptAppend.trim());
6303
- }
6304
- }
6305
- const baseModel = context.config.selectedModel?.trim();
6306
- if (baseModel) {
6307
- args.push("--model", buildClaudeModelArg(baseModel, context.config.selectedContextWindow));
6308
- }
6309
- const claudeEffort = context.config.selectedEffortLevel?.trim();
6310
- if (claudeEffort) {
6311
- args.push("--effort", claudeEffort);
6312
- }
6313
- if (resumeId) {
6314
- args.push("--resume", resumeId);
6315
- console.info("[claude_cli] Resuming session", { resumeId });
6577
+ const { files: imageFiles, cleanup } = writeImagesToTempFiles(context.config.images, context.cwd);
6578
+ try {
6579
+ return await createGenericCliBackend({
6580
+ kind: "droid_cli",
6581
+ supportTier: "structured",
6582
+ command,
6583
+ args: [],
6584
+ buildArgs: (ctx, prompt) => {
6585
+ const args = [
6586
+ "exec",
6587
+ "--output-format",
6588
+ "stream-json",
6589
+ ...droidExecPermissionArgs(ctx.config.mode),
6590
+ ...defaultArgs
6591
+ ];
6592
+ const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
6593
+ if (resumeId) args.push("--session-id", resumeId);
6594
+ const model = ctx.config.selectedModel?.trim();
6595
+ if (model) args.push("-m", model);
6596
+ args.push(prompt);
6597
+ return args;
6598
+ },
6599
+ augmentPrompt: (ctx) => buildPromptWithImagePathReferences(ctx, imageFiles),
6600
+ parseStructuredLine: parseDroidStructuredLine
6601
+ }).run(context);
6602
+ } finally {
6603
+ cleanup();
6316
6604
  }
6317
- args.push("--chrome");
6318
- console.info("[claude_cli] Chrome flag added \u2014 final args:", args.join(" "));
6319
- args.push(...defaultArgs);
6320
- return createGenericCliBackend({
6321
- kind: "claude_cli",
6322
- supportTier: "structured",
6323
- command,
6324
- args,
6325
- parseStructuredLine: parseClaudeStructuredLine,
6326
- stdinWriter: (stdin, ctx) => {
6327
- const contentBlocks = [];
6328
- if (ctx.config.images?.length) {
6329
- for (const img of ctx.config.images) {
6330
- contentBlocks.push({
6331
- type: "image",
6332
- source: { type: "base64", media_type: img.mimeType, data: img.data }
6333
- });
6334
- }
6335
- }
6336
- const promptText = ctx.config.mode === "plan" ? buildPlanModePrefix(buildPromptWithSystem(ctx.config, ctx.promptText)) : buildPromptWithSystem(ctx.config, ctx.promptText);
6337
- if (promptText.trim()) {
6338
- contentBlocks.push({ type: "text", text: promptText });
6339
- }
6340
- console.info(`[claude_cli] stdinWriter: ${contentBlocks.length} content blocks (${ctx.config.images?.length ?? 0} images)`);
6341
- const message = JSON.stringify({
6342
- type: "user",
6343
- message: { role: "user", content: contentBlocks },
6344
- session_id: "default",
6345
- parent_tool_use_id: null
6346
- });
6347
- stdin.write(message + "\n");
6348
- },
6349
- keepStdinOpen: true,
6350
- allowContinuation: true,
6351
- continuationIdleMs: 10 * 60 * 1e3
6352
- // 10 minutes
6353
- }).run(context);
6354
6605
  }
6355
6606
  };
6356
6607
  }
6357
- function kimiSessionId(cwd) {
6358
- let resolved = cwd;
6359
- try {
6360
- resolved = (0, import_fs.realpathSync)(cwd);
6361
- } catch {
6362
- }
6363
- return (0, import_crypto.createHash)("md5").update(resolved).digest("hex");
6364
- }
6365
- function shouldEnableKimiCliThinking(model) {
6366
- return model?.trim() === "kimi-k2.6-thinking";
6367
- }
6368
- function resolveKimiCliModelArg(model) {
6369
- const trimmed = model?.trim();
6370
- if (!trimmed) return void 0;
6371
- if (trimmed === "kimi-latest" || trimmed === "kimi-k2.5" || trimmed === "kimi-k2.6" || shouldEnableKimiCliThinking(trimmed)) {
6372
- return void 0;
6373
- }
6374
- if (trimmed.startsWith("claude-")) return void 0;
6375
- return trimmed;
6376
- }
6377
- function parseKimiStructuredLine(line, context, state) {
6608
+ function parseOpencodeStructuredLine(line, context, state) {
6378
6609
  const presenter = context.presenter;
6379
- if (!state.runtimeSessionId) {
6380
- state.runtimeSessionId = kimiSessionId(context.cwd);
6381
- }
6382
- let parsed;
6610
+ let parsed = null;
6383
6611
  try {
6384
6612
  parsed = JSON.parse(line);
6385
6613
  } catch {
6386
- const text = line.trim();
6387
- if (text.length > 0) {
6388
- if (text.toLowerCase().includes("llm not set")) state.error = text;
6389
- void presenter.onLog(`[kimi_cli] ${line}`);
6390
- }
6614
+ if (line.trim().length > 0) void presenter.onLog(`[opencode] ${line}`);
6391
6615
  return;
6392
6616
  }
6393
- const role = typeof parsed.role === "string" ? parsed.role : "";
6394
- const contentBlocks = Array.isArray(parsed.content) ? parsed.content : [];
6395
- const toolCalls = Array.isArray(parsed.tool_calls) ? parsed.tool_calls : [];
6396
- if (role === "assistant") {
6397
- const thinking = contentBlocks.filter((b) => b.type === "think" && typeof b.think === "string").map((b) => b.think).join("");
6398
- if (thinking) {
6399
- state.iterations = Math.max(state.iterations, 1);
6400
- void presenter.onThinking(thinking);
6617
+ const type = typeof parsed.type === "string" ? parsed.type : "";
6618
+ if (!type) return;
6619
+ const sessionID = typeof parsed.sessionID === "string" ? parsed.sessionID : void 0;
6620
+ if (sessionID) state.runtimeSessionId = sessionID;
6621
+ const part = typeof parsed.part === "object" && parsed.part !== null ? parsed.part : null;
6622
+ switch (type) {
6623
+ case "step_start": {
6624
+ state.iterations += 1;
6625
+ void presenter.onLog(`[opencode] Step ${state.iterations} started`);
6626
+ break;
6401
6627
  }
6402
- if (toolCalls.length > 0) {
6403
- state.iterations = Math.max(state.iterations, 1);
6404
- for (const tc of toolCalls) {
6405
- const fn = typeof tc.function === "object" && tc.function !== null ? tc.function : {};
6406
- const toolId = typeof tc.id === "string" ? tc.id : `kimi-tool-${Date.now()}`;
6407
- const toolName = typeof fn.name === "string" ? fn.name : "Tool";
6408
- let args = {};
6409
- if (typeof fn.arguments === "string") {
6628
+ case "text": {
6629
+ const text = part && typeof part.text === "string" ? part.text : "";
6630
+ if (text) {
6631
+ state.summary += text;
6632
+ void presenter.onAssistantText(text);
6633
+ }
6634
+ break;
6635
+ }
6636
+ case "tool_use":
6637
+ case "tool.execute": {
6638
+ const toolName = part && typeof part.name === "string" ? part.name : "OpenCode Tool";
6639
+ const toolId = part && typeof part.id === "string" ? part.id : `opencode-tool-${Date.now()}`;
6640
+ void presenter.onToolUse(toolName, part ?? {}, toolId);
6641
+ break;
6642
+ }
6643
+ case "tool_result":
6644
+ case "tool.result": {
6645
+ const toolId = part && typeof part.id === "string" ? part.id : `opencode-tool-${Date.now()}`;
6646
+ const output = part && typeof part.output === "string" ? part.output : JSON.stringify(part);
6647
+ void presenter.onToolResult?.(toolId, output);
6648
+ break;
6649
+ }
6650
+ case "step_finish": {
6651
+ if (part && typeof part.tokens === "object" && part.tokens !== null) {
6652
+ const tokens = part.tokens;
6653
+ state.usage = {
6654
+ model: typeof context.config.selectedModel === "string" && context.config.selectedModel.trim() || "opencode",
6655
+ numTurns: Math.max(state.iterations, 1),
6656
+ durationMs: 0,
6657
+ inputTokens: tokens.input ?? 0,
6658
+ outputTokens: tokens.output ?? 0,
6659
+ cacheReadTokens: tokens.cache_read ?? 0,
6660
+ cacheCreationTokens: tokens.cache_creation ?? 0,
6661
+ costUsd: 0
6662
+ };
6663
+ void presenter.onUsageUpdate?.({
6664
+ model: state.usage.model,
6665
+ inputTokens: state.usage.inputTokens,
6666
+ outputTokens: state.usage.outputTokens,
6667
+ cacheReadTokens: state.usage.cacheReadTokens,
6668
+ cacheCreationTokens: state.usage.cacheCreationTokens
6669
+ });
6670
+ }
6671
+ break;
6672
+ }
6673
+ case "session.error":
6674
+ case "error": {
6675
+ const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
6676
+ if (!state.error) {
6677
+ console.error("[opencode] error event (raw):", JSON.stringify(parsed).slice(0, 500));
6678
+ }
6679
+ const rawMessage = typeof parsed.message === "string" && parsed.message.trim() || part && typeof part.message === "string" && part.message.trim() || typeof parsed.error === "string" && parsed.error.trim() || errObj && typeof errObj.message === "string" && errObj.message.trim() || part && typeof part.error === "string" && part.error.trim() || // Also try data.message — some opencode versions nest the message here
6680
+ (typeof parsed.data?.message === "string" ? parsed.data.message.trim() : "") || "";
6681
+ const message = rawMessage || "OpenCode encountered an error. Check your provider and model settings.";
6682
+ if (!state.error) {
6683
+ void presenter.onError(message);
6684
+ state.error = message;
6685
+ }
6686
+ const pid = state.process?.pid;
6687
+ if (pid) {
6688
+ try {
6689
+ process.kill(-pid, "SIGTERM");
6690
+ } catch {
6410
6691
  try {
6411
- args = JSON.parse(fn.arguments);
6692
+ state.process?.kill("SIGTERM");
6412
6693
  } catch {
6413
6694
  }
6414
- } else if (typeof fn.arguments === "object" && fn.arguments !== null) {
6415
- args = fn.arguments;
6416
6695
  }
6417
- void presenter.onToolUse(toolName, args, toolId);
6418
6696
  }
6419
- } else {
6420
- const text = contentBlocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
6421
- if (text) {
6422
- state.iterations = Math.max(state.iterations, 1);
6423
- state.summary += text;
6424
- void presenter.onAssistantText(text);
6697
+ break;
6698
+ }
6699
+ default:
6700
+ break;
6701
+ }
6702
+ }
6703
+ function createOpencodeCliBackend(command = "opencode", defaultArgs = []) {
6704
+ return {
6705
+ kind: "opencode_cli",
6706
+ supportTier: "structured",
6707
+ async run(context) {
6708
+ const { paths: imagePaths, cleanup } = writeImagesToTempFiles(context.config.images, context.cwd);
6709
+ try {
6710
+ return await createGenericCliBackend({
6711
+ kind: "opencode_cli",
6712
+ supportTier: "structured",
6713
+ command,
6714
+ args: [],
6715
+ buildArgs: (ctx, prompt) => {
6716
+ const args = ["run", "--format", "json"];
6717
+ const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
6718
+ if (resumeId) args.push("--session", resumeId);
6719
+ const model = ctx.config.selectedModel?.trim();
6720
+ if (model && model.includes("/")) args.push("--model", model);
6721
+ const fileArgs = imagePaths.flatMap((p) => ["--file", p]);
6722
+ if (fileArgs.length > 0) {
6723
+ return [...args, ...fileArgs, ...defaultArgs, "--", prompt];
6724
+ }
6725
+ return [...args, ...defaultArgs, prompt];
6726
+ },
6727
+ augmentPrompt: (ctx) => {
6728
+ const base = buildPromptWithSystem(ctx.config, ctx.promptText);
6729
+ return ctx.config.mode === "plan" ? buildPlanModePrefix(base) : base;
6730
+ },
6731
+ parseStructuredLine: parseOpencodeStructuredLine
6732
+ }).run(context);
6733
+ } finally {
6734
+ cleanup();
6425
6735
  }
6426
6736
  }
6427
- return;
6428
- }
6429
- if (role === "tool") {
6430
- const toolCallId = typeof parsed.tool_call_id === "string" ? parsed.tool_call_id : `kimi-tool-${Date.now()}`;
6431
- const allText = contentBlocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
6432
- const userText = allText.filter((t) => !t.startsWith("<system>")).join("\n");
6433
- void presenter.onToolResult?.(toolCallId, userText || allText.join("\n"));
6434
- return;
6435
- }
6737
+ };
6436
6738
  }
6437
- function createKimiCliBackend(command = "kimi", defaultArgs = []) {
6438
- return createGenericCliBackend({
6439
- kind: "kimi_cli",
6440
- supportTier: "structured",
6441
- command,
6442
- args: [],
6443
- buildArgs: (context, prompt) => {
6444
- const args = [
6445
- "--print",
6446
- "--output-format",
6447
- "stream-json",
6448
- "--work-dir",
6449
- context.cwd
6450
- ];
6451
- const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
6452
- if (resumeId) args.push("--session", resumeId);
6453
- if (shouldEnableKimiCliThinking(context.config.selectedModel)) {
6454
- args.push("--thinking");
6739
+ function createGenericCliPassthroughBackend(command, defaultArgs = []) {
6740
+ return {
6741
+ kind: "generic_cli",
6742
+ supportTier: "text",
6743
+ async run(context) {
6744
+ const { files: imageFiles, cleanup } = writeImagesToTempFiles(context.config.images, context.cwd);
6745
+ try {
6746
+ return await createGenericCliBackend({
6747
+ kind: "generic_cli",
6748
+ supportTier: "text",
6749
+ command,
6750
+ args: defaultArgs,
6751
+ promptViaStdin: true,
6752
+ augmentPrompt: (ctx) => buildPromptWithImagePathReferences(ctx, imageFiles)
6753
+ }).run(context);
6754
+ } finally {
6755
+ cleanup();
6455
6756
  }
6456
- const model = resolveKimiCliModelArg(context.config.selectedModel);
6457
- if (model) args.push("--model", model);
6458
- return [...args, ...defaultArgs, "--prompt", prompt];
6459
- },
6460
- augmentPrompt: (context) => {
6461
- const base = buildPromptWithSystem(context.config, context.promptText);
6462
- return context.config.mode === "plan" ? buildPlanModePrefix(base) : base;
6463
- },
6464
- parseStructuredLine: parseKimiStructuredLine
6465
- });
6466
- }
6467
- function createGeminiCliBackend(command = "gemini", defaultArgs = []) {
6468
- return createGenericCliBackend({
6469
- kind: "gemini_cli",
6470
- supportTier: "structured",
6471
- command,
6472
- args: [],
6473
- buildArgs: (context) => {
6474
- const args = ["-p", "", "--output-format", "stream-json"];
6475
- const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
6476
- const approvalMode = context.config.mode === "plan" || context.config.mode === "ask" || context.config.mode === "review" ? "plan" : "yolo";
6477
- args.push("--approval-mode", approvalMode);
6478
- if (resumeId) args.push("--resume", resumeId);
6479
- if (context.config.selectedModel?.trim()) args.push("--model", context.config.selectedModel.trim());
6480
- return [...args, ...defaultArgs];
6481
- },
6482
- promptViaStdin: true,
6483
- augmentPrompt: (context) => {
6484
- const basePrompt = buildPromptWithSystem(context.config, context.promptText);
6485
- return context.config.mode === "plan" ? buildPlanModePrefix(basePrompt) : basePrompt;
6486
- },
6487
- parseStructuredLine: parseGeminiStructuredLine
6488
- });
6489
- }
6490
- function isCursorAgentCliModelId(model) {
6491
- return model === "auto" || /^composer-/.test(model) || /^gpt-\d/.test(model) || /^claude-\d/.test(model) || /^gemini-\d/.test(model) || /^grok-\d/.test(model) || /^kimi-/.test(model);
6492
- }
6493
- function createCursorAgentCliBackend(command = "cursor-agent", defaultArgs = []) {
6494
- return createGenericCliBackend({
6495
- kind: "cursor_agent_cli",
6496
- supportTier: "structured",
6497
- command,
6498
- args: [],
6499
- buildArgs: (context, prompt) => {
6500
- const args = ["--print", "--output-format", "stream-json"];
6501
- const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
6502
- if (shouldForceCursorAgent(context.config)) args.push("--force");
6503
- if (resumeId) args.push("--resume", resumeId);
6504
- const model = context.config.selectedModel?.trim();
6505
- if (model && isCursorAgentCliModelId(model)) args.push("--model", model);
6506
- return [...args, ...defaultArgs, prompt];
6507
- },
6508
- augmentPrompt: (context) => {
6509
- const base = buildPromptWithSystem(context.config, context.promptText);
6510
- return context.config.mode === "plan" ? buildPlanModePrefix(base) : base;
6511
- },
6512
- parseStructuredLine: parseCursorStructuredLine
6513
- });
6514
- }
6515
- function droidExecPermissionArgs(mode) {
6516
- if (mode === "plan" || mode === "ask" || mode === "review") return [];
6517
- return ["--auto", "high"];
6757
+ }
6758
+ };
6518
6759
  }
6519
- function parseDroidStructuredLine(line, context, state) {
6760
+ function parseCodexStructuredLine(line, context, state) {
6520
6761
  const presenter = context.presenter;
6521
6762
  let parsed = null;
6522
6763
  try {
6523
6764
  parsed = JSON.parse(line);
6524
6765
  } catch {
6525
- if (line.trim().length > 0) void presenter.onLog(`[droid_cli] ${line}`);
6766
+ void presenter.onLog(`[codex] ${line}`);
6526
6767
  return;
6527
6768
  }
6528
6769
  const type = typeof parsed.type === "string" ? parsed.type : "";
6529
6770
  if (!type) return;
6530
- if (typeof parsed.session_id === "string") state.runtimeSessionId = parsed.session_id;
6531
- if (typeof parsed.model === "string") state.activeModel = parsed.model;
6771
+ if (typeof parsed.thread_id === "string") {
6772
+ state.runtimeSessionId = parsed.thread_id;
6773
+ }
6774
+ const extractCodexErrorMessage = (value2) => {
6775
+ if (typeof value2 !== "string") return "";
6776
+ const trimmed = value2.trim();
6777
+ if (!trimmed) return "";
6778
+ try {
6779
+ const parsedValue = JSON.parse(trimmed);
6780
+ if (typeof parsedValue.detail === "string" && parsedValue.detail.trim().length > 0) {
6781
+ return parsedValue.detail.trim();
6782
+ }
6783
+ if (typeof parsedValue.message === "string" && parsedValue.message.trim().length > 0) {
6784
+ return parsedValue.message.trim();
6785
+ }
6786
+ } catch {
6787
+ }
6788
+ return trimmed;
6789
+ };
6532
6790
  switch (type) {
6533
- case "system": {
6534
- const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
6535
- if (subtype === "init") {
6536
- if (typeof parsed.session_id === "string") state.runtimeSessionId = parsed.session_id;
6537
- if (typeof parsed.model === "string") state.activeModel = parsed.model;
6791
+ case "thread.started":
6792
+ break;
6793
+ case "turn.started":
6794
+ state.iterations += 1;
6795
+ break;
6796
+ case "session_configured":
6797
+ if (typeof parsed.session_id === "string") {
6798
+ state.runtimeSessionId = parsed.session_id;
6538
6799
  }
6539
6800
  break;
6801
+ case "task_started":
6802
+ state.iterations += 1;
6803
+ break;
6804
+ case "item.started": {
6805
+ const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
6806
+ if (!item || item.type !== "command_execution" || typeof item.id !== "string") break;
6807
+ const command = typeof item.command === "string" ? item.command : "";
6808
+ void presenter.onToolUse("Bash", { command, cwd: context.cwd }, item.id);
6809
+ break;
6540
6810
  }
6541
- case "message": {
6542
- const role = typeof parsed.role === "string" ? parsed.role : "";
6543
- if (role !== "assistant") break;
6544
- const text = typeof parsed.text === "string" ? parsed.text : "";
6545
- if (!text) break;
6546
- state.iterations = Math.max(state.iterations, 1);
6547
- state.summary += text;
6548
- void presenter.onAssistantText(text);
6811
+ case "item.completed": {
6812
+ const item = typeof parsed.item === "object" && parsed.item !== null ? parsed.item : null;
6813
+ if (!item || typeof item.type !== "string") break;
6814
+ if (item.type === "reasoning" && typeof item.text === "string") {
6815
+ void presenter.onThinking(item.text);
6816
+ break;
6817
+ }
6818
+ if (item.type === "agent_message" && typeof item.text === "string") {
6819
+ state.summary += `${item.text}
6820
+ `;
6821
+ void presenter.onAssistantText(item.text);
6822
+ break;
6823
+ }
6824
+ if (item.type === "command_execution" && typeof item.id === "string") {
6825
+ const output = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
6826
+ const itemExitCode = typeof item.exit_code === "number" ? item.exit_code : null;
6827
+ const resultText = output.trim().length > 0 ? output : itemExitCode === null ? "" : `Exit code: ${itemExitCode}`;
6828
+ void presenter.onToolResult?.(item.id, resultText);
6829
+ break;
6830
+ }
6549
6831
  break;
6550
6832
  }
6551
- case "tool_call": {
6552
- const toolId = typeof parsed.id === "string" ? parsed.id : `droid-tool-${Date.now()}`;
6553
- const toolName = typeof parsed.toolName === "string" ? parsed.toolName : "Tool";
6554
- const parameters = typeof parsed.parameters === "object" && parsed.parameters !== null ? parsed.parameters : {};
6555
- state.iterations = Math.max(state.iterations, 1);
6556
- void presenter.onToolUse(toolName, parameters, toolId);
6833
+ case "agent_message_delta":
6834
+ case "agent_message_content_delta": {
6835
+ const delta = typeof parsed.delta === "string" ? parsed.delta : "";
6836
+ if (!delta) return;
6837
+ state.summary += delta;
6838
+ void presenter.onAssistantText(delta);
6557
6839
  break;
6558
6840
  }
6559
- case "tool_result": {
6560
- const toolId = typeof parsed.id === "string" ? parsed.id : `droid-tool-${Date.now()}`;
6561
- const value2 = parsed.value;
6562
- const content = typeof value2 === "string" ? value2 : value2 !== void 0 ? JSON.stringify(value2) : "";
6563
- const isError = parsed.isError === true;
6564
- void presenter.onToolResult?.(toolId, content, isError);
6841
+ case "agent_reasoning_delta":
6842
+ case "reasoning_content_delta":
6843
+ case "agent_reasoning_raw_content_delta": {
6844
+ const delta = typeof parsed.delta === "string" ? parsed.delta : "";
6845
+ if (!delta) return;
6846
+ void presenter.onThinking(delta);
6565
6847
  break;
6566
6848
  }
6567
- case "completion": {
6568
- const finalText = typeof parsed.finalText === "string" ? parsed.finalText : "";
6569
- const numTurns = typeof parsed.numTurns === "number" ? parsed.numTurns : 1;
6570
- const durationMs = typeof parsed.durationMs === "number" ? parsed.durationMs : 0;
6571
- state.iterations = Math.max(state.iterations, numTurns, finalText ? 1 : 0);
6572
- if (finalText.trim() && !state.summary.trim()) {
6573
- state.summary = finalText;
6574
- void presenter.onAssistantText(finalText);
6575
- }
6576
- state.usage = {
6577
- model: state.activeModel || (typeof context.config.selectedModel === "string" && context.config.selectedModel.trim() ? context.config.selectedModel : "droid"),
6578
- numTurns: Math.max(numTurns, 1),
6579
- durationMs,
6580
- inputTokens: 0,
6581
- outputTokens: 0,
6582
- cacheReadTokens: 0,
6583
- cacheCreationTokens: 0,
6584
- costUsd: 0
6585
- };
6586
- void presenter.onUsageUpdate?.({
6587
- model: state.usage.model,
6588
- inputTokens: 0,
6589
- outputTokens: 0,
6590
- cacheReadTokens: 0,
6591
- cacheCreationTokens: 0
6592
- });
6849
+ case "exec_command_begin": {
6850
+ const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
6851
+ const command = Array.isArray(parsed.command) ? parsed.command.join(" ") : "";
6852
+ const cwd = typeof parsed.cwd === "string" ? parsed.cwd : context.cwd;
6853
+ void presenter.onToolUse("Bash", { command, cwd }, toolId);
6593
6854
  break;
6594
6855
  }
6595
- default:
6856
+ case "exec_command_end": {
6857
+ const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `exec-${Date.now()}`;
6858
+ const output = typeof parsed.formatted_output === "string" ? parsed.formatted_output : typeof parsed.aggregated_output === "string" ? parsed.aggregated_output : "";
6859
+ void presenter.onToolResult?.(toolId, output);
6596
6860
  break;
6597
- }
6598
- }
6599
- function createDroidCliBackend(command = "droid", defaultArgs = []) {
6600
- return createGenericCliBackend({
6601
- kind: "droid_cli",
6602
- supportTier: "structured",
6603
- command,
6604
- args: [],
6605
- buildArgs: (context, prompt) => {
6606
- const args = [
6607
- "exec",
6608
- "--output-format",
6609
- "stream-json",
6610
- ...droidExecPermissionArgs(context.config.mode),
6611
- ...defaultArgs
6612
- ];
6613
- const resumeId = context.config.runtimeSessionId?.trim() || context.config.providerSessionId?.trim();
6614
- if (resumeId) args.push("--session-id", resumeId);
6615
- const model = context.config.selectedModel?.trim();
6616
- if (model) args.push("-m", model);
6617
- args.push(prompt);
6618
- return args;
6619
- },
6620
- augmentPrompt: (context) => {
6621
- const base = buildPromptWithSystem(context.config, context.promptText);
6622
- return context.config.mode === "plan" ? buildPlanModePrefix(base) : base;
6623
- },
6624
- parseStructuredLine: parseDroidStructuredLine
6625
- });
6626
- }
6627
- function parseOpencodeStructuredLine(line, context, state) {
6628
- const presenter = context.presenter;
6629
- let parsed = null;
6630
- try {
6631
- parsed = JSON.parse(line);
6632
- } catch {
6633
- if (line.trim().length > 0) void presenter.onLog(`[opencode] ${line}`);
6634
- return;
6635
- }
6636
- const type = typeof parsed.type === "string" ? parsed.type : "";
6637
- if (!type) return;
6638
- const sessionID = typeof parsed.sessionID === "string" ? parsed.sessionID : void 0;
6639
- if (sessionID) state.runtimeSessionId = sessionID;
6640
- const part = typeof parsed.part === "object" && parsed.part !== null ? parsed.part : null;
6641
- switch (type) {
6642
- case "step_start": {
6643
- state.iterations += 1;
6644
- void presenter.onLog(`[opencode] Step ${state.iterations} started`);
6861
+ }
6862
+ case "mcp_tool_call_begin": {
6863
+ const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
6864
+ const invocation = typeof parsed.invocation === "object" && parsed.invocation !== null ? parsed.invocation : {};
6865
+ const tool = typeof invocation.tool_name === "string" ? invocation.tool_name : typeof invocation.tool === "string" ? invocation.tool : "MCP Tool";
6866
+ void presenter.onToolUse(tool, invocation, toolId);
6645
6867
  break;
6646
6868
  }
6647
- case "text": {
6648
- const text = part && typeof part.text === "string" ? part.text : "";
6649
- if (text) {
6650
- state.summary += text;
6651
- void presenter.onAssistantText(text);
6652
- }
6869
+ case "mcp_tool_call_end": {
6870
+ const toolId = typeof parsed.call_id === "string" ? parsed.call_id : `mcp-${Date.now()}`;
6871
+ void presenter.onToolResult?.(toolId, JSON.stringify(parsed.result ?? {}, null, 2));
6872
+ break;
6873
+ }
6874
+ case "token_count": {
6875
+ const info = typeof parsed.info === "object" && parsed.info !== null ? parsed.info : {};
6876
+ state.usage = {
6877
+ model: typeof info.model === "string" ? info.model : "codex",
6878
+ numTurns: Math.max(state.iterations, 1),
6879
+ durationMs: 0,
6880
+ inputTokens: typeof info.input_tokens === "number" ? info.input_tokens : 0,
6881
+ outputTokens: typeof info.output_tokens === "number" ? info.output_tokens : 0,
6882
+ cacheReadTokens: typeof info.cached_input_tokens === "number" ? info.cached_input_tokens : 0,
6883
+ cacheCreationTokens: 0,
6884
+ costUsd: typeof info.total_cost_usd === "number" ? info.total_cost_usd : 0
6885
+ };
6886
+ void presenter.onUsageUpdate?.({
6887
+ model: state.usage.model,
6888
+ inputTokens: state.usage.inputTokens,
6889
+ outputTokens: state.usage.outputTokens,
6890
+ cacheReadTokens: state.usage.cacheReadTokens,
6891
+ cacheCreationTokens: state.usage.cacheCreationTokens
6892
+ });
6653
6893
  break;
6654
6894
  }
6655
- case "tool_use":
6656
- case "tool.execute": {
6657
- const toolName = part && typeof part.name === "string" ? part.name : "OpenCode Tool";
6658
- const toolId = part && typeof part.id === "string" ? part.id : `opencode-tool-${Date.now()}`;
6659
- void presenter.onToolUse(toolName, part ?? {}, toolId);
6895
+ case "task_complete": {
6896
+ const lastMessage = typeof parsed.last_agent_message === "string" ? parsed.last_agent_message : "";
6897
+ if (lastMessage.length > 0) state.summary = lastMessage;
6660
6898
  break;
6661
6899
  }
6662
- case "tool_result":
6663
- case "tool.result": {
6664
- const toolId = part && typeof part.id === "string" ? part.id : `opencode-tool-${Date.now()}`;
6665
- const output = part && typeof part.output === "string" ? part.output : JSON.stringify(part);
6666
- void presenter.onToolResult?.(toolId, output);
6900
+ case "turn.completed": {
6901
+ const usage = typeof parsed.usage === "object" && parsed.usage !== null ? parsed.usage : {};
6902
+ state.usage = {
6903
+ model: typeof context.config.selectedModel === "string" && context.config.selectedModel.trim() ? context.config.selectedModel : "codex",
6904
+ numTurns: Math.max(state.iterations, 1),
6905
+ durationMs: 0,
6906
+ inputTokens: typeof usage.input_tokens === "number" ? usage.input_tokens : 0,
6907
+ outputTokens: typeof usage.output_tokens === "number" ? usage.output_tokens : 0,
6908
+ cacheReadTokens: typeof usage.cached_input_tokens === "number" ? usage.cached_input_tokens : 0,
6909
+ cacheCreationTokens: 0,
6910
+ costUsd: 0
6911
+ };
6912
+ void presenter.onUsageUpdate?.({
6913
+ model: state.usage.model,
6914
+ inputTokens: state.usage.inputTokens,
6915
+ outputTokens: state.usage.outputTokens,
6916
+ cacheReadTokens: state.usage.cacheReadTokens,
6917
+ cacheCreationTokens: state.usage.cacheCreationTokens
6918
+ });
6667
6919
  break;
6668
6920
  }
6669
- case "step_finish": {
6670
- if (part && typeof part.tokens === "object" && part.tokens !== null) {
6671
- const tokens = part.tokens;
6672
- state.usage = {
6673
- model: typeof context.config.selectedModel === "string" && context.config.selectedModel.trim() || "opencode",
6674
- numTurns: Math.max(state.iterations, 1),
6675
- durationMs: 0,
6676
- inputTokens: tokens.input ?? 0,
6677
- outputTokens: tokens.output ?? 0,
6678
- cacheReadTokens: tokens.cache_read ?? 0,
6679
- cacheCreationTokens: tokens.cache_creation ?? 0,
6680
- costUsd: 0
6681
- };
6682
- void presenter.onUsageUpdate?.({
6683
- model: state.usage.model,
6684
- inputTokens: state.usage.inputTokens,
6685
- outputTokens: state.usage.outputTokens,
6686
- cacheReadTokens: state.usage.cacheReadTokens,
6687
- cacheCreationTokens: state.usage.cacheCreationTokens
6688
- });
6921
+ case "error": {
6922
+ const message = extractCodexErrorMessage(parsed.message) || extractCodexErrorMessage(parsed.error) || typeof parsed.detail === "string" && parsed.detail.trim() || "";
6923
+ if (message) {
6924
+ const normalizedMessage = normalizeCodexCliErrorMessage(message);
6925
+ void presenter.onError(normalizedMessage);
6926
+ state.error = normalizedMessage;
6689
6927
  }
6690
6928
  break;
6691
6929
  }
6692
- case "session.error":
6693
- case "error": {
6694
- const errObj = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : null;
6695
- if (!state.error) {
6696
- console.error("[opencode] error event (raw):", JSON.stringify(parsed).slice(0, 500));
6697
- }
6698
- const rawMessage = typeof parsed.message === "string" && parsed.message.trim() || part && typeof part.message === "string" && part.message.trim() || typeof parsed.error === "string" && parsed.error.trim() || errObj && typeof errObj.message === "string" && errObj.message.trim() || part && typeof part.error === "string" && part.error.trim() || // Also try data.message — some opencode versions nest the message here
6699
- (typeof parsed.data?.message === "string" ? parsed.data.message.trim() : "") || "";
6700
- const message = rawMessage || "OpenCode encountered an error. Check your provider and model settings.";
6701
- if (!state.error) {
6702
- void presenter.onError(message);
6703
- state.error = message;
6704
- }
6705
- const pid = state.process?.pid;
6706
- if (pid) {
6707
- try {
6708
- process.kill(-pid, "SIGTERM");
6709
- } catch {
6710
- try {
6711
- state.process?.kill("SIGTERM");
6712
- } catch {
6713
- }
6714
- }
6930
+ case "turn.failed": {
6931
+ const error = typeof parsed.error === "object" && parsed.error !== null ? parsed.error : {};
6932
+ const message = extractCodexErrorMessage(error.message) || extractCodexErrorMessage(error.detail) || typeof parsed.message === "string" && parsed.message.trim() || "";
6933
+ if (message) {
6934
+ const normalizedMessage = normalizeCodexCliErrorMessage(message);
6935
+ void presenter.onError(normalizedMessage);
6936
+ state.error = normalizedMessage;
6715
6937
  }
6716
6938
  break;
6717
6939
  }
@@ -6719,51 +6941,6 @@ function parseOpencodeStructuredLine(line, context, state) {
6719
6941
  break;
6720
6942
  }
6721
6943
  }
6722
- function createOpencodeCliBackend(command = "opencode", defaultArgs = []) {
6723
- return {
6724
- kind: "opencode_cli",
6725
- supportTier: "structured",
6726
- async run(context) {
6727
- const { paths: imagePaths, cleanup } = writeImagesToTempFiles(context.config.images);
6728
- try {
6729
- return await createGenericCliBackend({
6730
- kind: "opencode_cli",
6731
- supportTier: "structured",
6732
- command,
6733
- args: [],
6734
- buildArgs: (ctx, prompt) => {
6735
- const args = ["run", "--format", "json"];
6736
- const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
6737
- if (resumeId) args.push("--session", resumeId);
6738
- const model = ctx.config.selectedModel?.trim();
6739
- if (model && model.includes("/")) args.push("--model", model);
6740
- const fileArgs = imagePaths.flatMap((p) => ["--file", p]);
6741
- if (fileArgs.length > 0) {
6742
- return [...args, ...fileArgs, ...defaultArgs, "--", prompt];
6743
- }
6744
- return [...args, ...defaultArgs, prompt];
6745
- },
6746
- augmentPrompt: (ctx) => {
6747
- const base = buildPromptWithSystem(ctx.config, ctx.promptText);
6748
- return ctx.config.mode === "plan" ? buildPlanModePrefix(base) : base;
6749
- },
6750
- parseStructuredLine: parseOpencodeStructuredLine
6751
- }).run(context);
6752
- } finally {
6753
- cleanup();
6754
- }
6755
- }
6756
- };
6757
- }
6758
- function createGenericCliPassthroughBackend(command, defaultArgs = []) {
6759
- return createGenericCliBackend({
6760
- kind: "generic_cli",
6761
- supportTier: "text",
6762
- command,
6763
- args: defaultArgs,
6764
- promptViaStdin: true
6765
- });
6766
- }
6767
6944
  function buildCodexEffortArgs(selectedEffortLevel) {
6768
6945
  const level = selectedEffortLevel?.trim();
6769
6946
  if (!level) return [];
@@ -6774,7 +6951,7 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
6774
6951
  kind: "codex_app_server",
6775
6952
  supportTier: "structured",
6776
6953
  async run(context) {
6777
- const { paths: imagePaths, cleanup } = writeImagesToTempFiles(context.config.images);
6954
+ const { paths: imagePaths, cleanup } = writeImagesToTempFiles(context.config.images, context.cwd);
6778
6955
  try {
6779
6956
  return await createGenericCliBackend({
6780
6957
  kind: "codex_app_server",
@@ -6785,7 +6962,7 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
6785
6962
  const resumeId = ctx.config.runtimeSessionId?.trim() || ctx.config.providerSessionId?.trim();
6786
6963
  const modelArgs = ctx.config.selectedModel?.trim() ? ["--model", ctx.config.selectedModel.trim()] : [];
6787
6964
  const effortArgs = buildCodexEffortArgs(ctx.config.selectedEffortLevel);
6788
- const permissionArgs = shouldUseReadOnlyRuntimePermissions(ctx.config) ? ["--ask-for-approval", "never", "--sandbox", "read-only"] : ["--dangerously-bypass-approvals-and-sandbox"];
6965
+ const permissionArgs = ["--dangerously-bypass-approvals-and-sandbox"];
6789
6966
  const imageArgs = imagePaths.flatMap((p) => ["--image", p]);
6790
6967
  const baseArgs = resumeId ? [...permissionArgs, "exec", "resume", "--json", "--skip-git-repo-check", ...modelArgs, ...effortArgs, ...imageArgs, resumeId, "-"] : [...permissionArgs, "exec", "--json", "--skip-git-repo-check", ...modelArgs, ...effortArgs, ...imageArgs, "-"];
6791
6968
  return [...baseArgs, ...defaultArgs];
@@ -6881,51 +7058,67 @@ function parseCopilotStructuredLine(line, context, state) {
6881
7058
  }
6882
7059
  }
6883
7060
  function createCopilotCliBackend(command = "copilot", defaultArgs = []) {
6884
- return createGenericCliBackend({
7061
+ return {
6885
7062
  kind: "copilot_cli",
6886
7063
  supportTier: "structured",
6887
- command,
6888
- args: [],
6889
- buildArgs: (context, prompt) => {
6890
- const args = [
6891
- "--autopilot",
6892
- "--yolo",
6893
- "--max-autopilot-continues",
6894
- "20",
6895
- "-s",
6896
- "--stream",
6897
- "on",
6898
- "--output-format",
6899
- "json",
6900
- "-p",
6901
- prompt
6902
- ];
6903
- if (context.config.selectedModel?.trim()) {
6904
- args.push("--model", context.config.selectedModel.trim());
7064
+ async run(context) {
7065
+ const { files: imageFiles, cleanup } = writeImagesToTempFiles(context.config.images, context.cwd);
7066
+ try {
7067
+ return await createGenericCliBackend({
7068
+ kind: "copilot_cli",
7069
+ supportTier: "structured",
7070
+ command,
7071
+ args: [],
7072
+ buildArgs: (ctx, prompt) => {
7073
+ const args = [
7074
+ "--autopilot",
7075
+ "--yolo",
7076
+ "--max-autopilot-continues",
7077
+ "20",
7078
+ "-s",
7079
+ "--stream",
7080
+ "on",
7081
+ "--output-format",
7082
+ "json",
7083
+ "-p",
7084
+ prompt
7085
+ ];
7086
+ if (ctx.config.selectedModel?.trim()) {
7087
+ args.push("--model", ctx.config.selectedModel.trim());
7088
+ }
7089
+ return [...args, ...defaultArgs];
7090
+ },
7091
+ augmentPrompt: (ctx) => buildPromptWithImagePathReferences(ctx, imageFiles),
7092
+ parseStructuredLine: parseCopilotStructuredLine
7093
+ }).run(context);
7094
+ } finally {
7095
+ cleanup();
6905
7096
  }
6906
- return [...args, ...defaultArgs];
6907
- },
6908
- parseStructuredLine: parseCopilotStructuredLine
6909
- });
7097
+ }
7098
+ };
6910
7099
  }
6911
7100
  var DEFAULT_MODEL = "claude-sonnet-4-6";
6912
7101
  var DROID_DEFAULT_MODEL = "claude-opus-4-7";
6913
7102
  function defaultSelectedModelForBackend(backendKind) {
6914
7103
  switch (backendKind) {
7104
+ case "claude_cli":
7105
+ return void 0;
6915
7106
  case "droid_cli":
6916
7107
  return DROID_DEFAULT_MODEL;
6917
7108
  case "copilot_cli":
6918
7109
  return "claude-sonnet-4.5";
6919
7110
  case "kimi_cli":
6920
7111
  return void 0;
7112
+ case "supatest_cli":
7113
+ return "medium";
6921
7114
  default:
6922
7115
  return DEFAULT_MODEL;
6923
7116
  }
6924
7117
  }
6925
7118
  function canonicalizePath(inputPath) {
6926
7119
  try {
6927
- const resolved = (0, import_path2.resolve)(inputPath);
6928
- return (0, import_fs2.existsSync)(resolved) ? (0, import_fs2.realpathSync)(resolved) : resolved;
7120
+ const resolved = (0, import_path3.resolve)(inputPath);
7121
+ return (0, import_fs4.existsSync)(resolved) ? (0, import_fs4.realpathSync)(resolved) : resolved;
6929
7122
  } catch {
6930
7123
  return null;
6931
7124
  }
@@ -6938,6 +7131,34 @@ function buildPromptWithFiles(task, files) {
6938
7131
  The user has attached the following files. You can read them using your available tools:
6939
7132
  ${fileList}`;
6940
7133
  }
7134
+ function buildTaskLifecyclePrompt(config) {
7135
+ if (config.mode === "plan" || config.mode === "ask" || config.mode === "review") {
7136
+ return void 0;
7137
+ }
7138
+ const taskId = config.taskId ?? config.taskMeta?.id;
7139
+ if (!taskId) return void 0;
7140
+ const statusLines = config.taskMeta?.statusOptions?.length ? [
7141
+ "",
7142
+ "Available task statuses for this task, in board order:",
7143
+ ...config.taskMeta.statusOptions.map(
7144
+ (status) => `- ${status.id}: ${status.label} (${status.group})`
7145
+ )
7146
+ ] : [];
7147
+ return [
7148
+ "## Task Lifecycle",
7149
+ `This session is attached to taskId "${taskId}". Keep the task status current by calling the Aiden MCP update_task tool as you move through work phases.`,
7150
+ ...statusLines,
7151
+ "",
7152
+ "Use exact status IDs from the available statuses when provided. Do not invent status IDs.",
7153
+ "Lifecycle mapping:",
7154
+ "- Research / discovery / code reading / planning: use the best matching status such as research, discovery, planning, or the first active status.",
7155
+ "- Implementation / coding / file edits: use the best matching status such as implementing, in_progress, working, or running.",
7156
+ "- Local validation / tests / QA: use the best matching status such as localtesting, local_testing, testing, qa, or validation.",
7157
+ "- Ready for human review: use the best matching review status such as in_review or review.",
7158
+ "- Done: use only when the requested work is fully complete and validated; otherwise leave it in the review/testing phase.",
7159
+ "If no suitable status exists for a phase, leave the current status unchanged rather than forcing an unrelated one."
7160
+ ].join("\n");
7161
+ }
6941
7162
  var BaseMachineAgent = class _BaseMachineAgent {
6942
7163
  static MAX_RETRIES = 2;
6943
7164
  static RETRY_BASE_DELAY_MS = 3e3;
@@ -7018,6 +7239,8 @@ var BaseMachineAgent = class _BaseMachineAgent {
7018
7239
  case "gemini_cli":
7019
7240
  case "opencode_cli":
7020
7241
  case "copilot_cli":
7242
+ case "supatest_cli":
7243
+ case "kimi_cli":
7021
7244
  return "structured";
7022
7245
  case "generic_cli":
7023
7246
  default:
@@ -7042,6 +7265,8 @@ var BaseMachineAgent = class _BaseMachineAgent {
7042
7265
  Prefer relative paths and keep your work scoped to this project.`
7043
7266
  );
7044
7267
  }
7268
+ const lifecyclePrompt = buildTaskLifecyclePrompt(config);
7269
+ if (lifecyclePrompt) systemPromptParts.push(lifecyclePrompt);
7045
7270
  const extra = this.buildExtraSystemPromptParts(config);
7046
7271
  systemPromptParts.push(...extra);
7047
7272
  return {
@@ -7056,9 +7281,9 @@ Prefer relative paths and keep your work scoped to this project.`
7056
7281
  }
7057
7282
  async runCliBackend(config, safeProjectPath) {
7058
7283
  const backendKind = this.resolveBackendKind(config);
7059
- const defaultCwd = (0, import_path2.join)((0, import_os2.homedir)(), "Documents", "aiden");
7060
- if (!(0, import_fs2.existsSync)(defaultCwd)) {
7061
- (0, import_fs2.mkdirSync)(defaultCwd, { recursive: true });
7284
+ const defaultCwd = (0, import_path3.join)((0, import_os3.homedir)(), "Documents", "aiden");
7285
+ if (!(0, import_fs4.existsSync)(defaultCwd)) {
7286
+ (0, import_fs4.mkdirSync)(defaultCwd, { recursive: true });
7062
7287
  }
7063
7288
  let runtimeConfig = this.buildRuntimeConfig(config, safeProjectPath);
7064
7289
  const cwd = runtimeConfig.worktreePath || safeProjectPath || runtimeConfig.cwd || defaultCwd;
@@ -7066,7 +7291,7 @@ Prefer relative paths and keep your work scoped to this project.`
7066
7291
  const promptText = this.buildPromptText(runtimeConfig);
7067
7292
  const runtimeCommand = runtimeConfig.runtimeCommand || this.runtime.runtimeCommand;
7068
7293
  const runtimeArgs = runtimeConfig.runtimeArgs || this.runtime.runtimeArgs || [];
7069
- const backend = backendKind === "claude_cli" ? createClaudeCliBackend(runtimeCommand || "claude", runtimeArgs) : backendKind === "codex_app_server" ? createCodexRuntimeBackend(runtimeCommand || "codex", runtimeArgs) : backendKind === "copilot_cli" ? createCopilotCliBackend(runtimeCommand || "copilot", runtimeArgs) : backendKind === "cursor_agent_cli" ? createCursorAgentCliBackend(runtimeCommand || "cursor-agent", runtimeArgs) : backendKind === "gemini_cli" ? createGeminiCliBackend(runtimeCommand || "gemini", runtimeArgs) : backendKind === "droid_cli" ? createDroidCliBackend(runtimeCommand || "droid", runtimeArgs) : backendKind === "opencode_cli" ? createOpencodeCliBackend(runtimeCommand || "opencode", runtimeArgs) : backendKind === "kimi_cli" ? createKimiCliBackend(runtimeCommand || "kimi", runtimeArgs) : createGenericCliPassthroughBackend(runtimeCommand || "generic-cli", runtimeArgs);
7294
+ const backend = backendKind === "claude_cli" ? createClaudeCliBackend(runtimeCommand || "claude", runtimeArgs) : backendKind === "codex_app_server" ? createCodexRuntimeBackend(runtimeCommand || "codex", runtimeArgs) : backendKind === "copilot_cli" ? createCopilotCliBackend(runtimeCommand || "copilot", runtimeArgs) : backendKind === "cursor_agent_cli" ? createCursorAgentCliBackend(runtimeCommand || "cursor-agent", runtimeArgs) : backendKind === "gemini_cli" ? createGeminiCliBackend(runtimeCommand || "gemini", runtimeArgs) : backendKind === "droid_cli" ? createDroidCliBackend(runtimeCommand || "droid", runtimeArgs) : backendKind === "opencode_cli" ? createOpencodeCliBackend(runtimeCommand || "opencode", runtimeArgs) : backendKind === "kimi_cli" ? createKimiCliBackend(runtimeCommand || "kimi", runtimeArgs) : backendKind === "supatest_cli" ? createSupatestCliBackend(runtimeCommand || "supatest", runtimeArgs) : createGenericCliPassthroughBackend(runtimeCommand || "generic-cli", runtimeArgs);
7070
7295
  let lastResult = null;
7071
7296
  for (let attempt = 0; attempt <= _BaseMachineAgent.MAX_RETRIES; attempt++) {
7072
7297
  if (this.abortController?.signal.aborted) break;
@@ -7162,7 +7387,7 @@ Prefer relative paths and keep your work scoped to this project.`
7162
7387
  const canonicalized = canonicalizePath(initialConfig.teamPath);
7163
7388
  if (canonicalized) safeProjectPath = canonicalized;
7164
7389
  }
7165
- if (safeProjectPath && !(0, import_fs2.existsSync)(safeProjectPath)) {
7390
+ if (safeProjectPath && !(0, import_fs4.existsSync)(safeProjectPath)) {
7166
7391
  const message = `Local path no longer exists: "${safeProjectPath}". Please update the team's local path in team settings before running the agent.`;
7167
7392
  this.presenter.onError(message, true);
7168
7393
  const result = {
@@ -7207,8 +7432,9 @@ Prefer relative paths and keep your work scoped to this project.`
7207
7432
  var import_node_os2 = require("os");
7208
7433
 
7209
7434
  // src/cli-executable.ts
7210
- var import_node_fs = require("fs");
7211
7435
  var import_node_child_process = require("child_process");
7436
+ var import_node_fs = require("fs");
7437
+ var import_node_child_process2 = require("child_process");
7212
7438
  var import_node_os = require("os");
7213
7439
  var import_node_path = require("path");
7214
7440
  var PROVIDER_CLI_COMMANDS = {
@@ -7219,7 +7445,8 @@ var PROVIDER_CLI_COMMANDS = {
7219
7445
  gemini_cli: { provider: "gemini_cli", command: "gemini", envVar: "AIDEN_GEMINI_PATH" },
7220
7446
  kimi_cli: { provider: "kimi_cli", command: "kimi", envVar: "AIDEN_KIMI_PATH" },
7221
7447
  opencode_cli: { provider: "opencode_cli", command: "opencode", envVar: "AIDEN_OPENCODE_PATH" },
7222
- droid_cli: { provider: "droid_cli", command: "droid", envVar: "AIDEN_DROID_PATH" }
7448
+ droid_cli: { provider: "droid_cli", command: "droid", envVar: "AIDEN_DROID_PATH" },
7449
+ supatest_cli: { provider: "supatest_cli", command: "supatest", envVar: "AIDEN_SUPATEST_PATH" }
7223
7450
  };
7224
7451
  var DISCOVERABLE_PROVIDER_KINDS = Object.keys(PROVIDER_CLI_COMMANDS);
7225
7452
  var PROVIDER_ALIASES = {
@@ -7236,11 +7463,47 @@ var BACKEND_CLI_COMMANDS = {
7236
7463
  gemini: "gemini",
7237
7464
  kimi_cli: "kimi",
7238
7465
  opencode_cli: "opencode",
7239
- droid_cli: "droid"
7466
+ droid_cli: "droid",
7467
+ supatest_cli: "supatest"
7240
7468
  };
7241
7469
  var resolvedCliCache = /* @__PURE__ */ new Map();
7470
+ var cachedLoginShellEnv = null;
7471
+ function parseEnvOutput(output) {
7472
+ const loginEnv = {};
7473
+ for (const line of output.split("\n")) {
7474
+ if (!line.trim()) continue;
7475
+ const eqIdx = line.indexOf("=");
7476
+ if (eqIdx <= 0) continue;
7477
+ const key = line.slice(0, eqIdx);
7478
+ if (/\s/.test(key)) continue;
7479
+ loginEnv[key] = line.slice(eqIdx + 1);
7480
+ }
7481
+ if (!loginEnv.HOME) loginEnv.HOME = process.env.HOME ?? (0, import_node_os.homedir)();
7482
+ if (!loginEnv.SHELL) loginEnv.SHELL = process.env.SHELL ?? "/bin/zsh";
7483
+ return loginEnv;
7484
+ }
7485
+ function loadLoginShellEnvironment() {
7486
+ if (cachedLoginShellEnv) return cachedLoginShellEnv;
7487
+ if ((0, import_node_os.platform)() === "win32") {
7488
+ cachedLoginShellEnv = process.env;
7489
+ return cachedLoginShellEnv;
7490
+ }
7491
+ try {
7492
+ const shell = (process.env.SHELL || "/bin/bash").replace(/'/g, "'\\''");
7493
+ const envOutput = (0, import_node_child_process.execSync)(`'${shell}' -ilc 'env'`, {
7494
+ encoding: "utf8",
7495
+ stdio: ["pipe", "pipe", "pipe"],
7496
+ timeout: 5e3
7497
+ });
7498
+ cachedLoginShellEnv = parseEnvOutput(envOutput);
7499
+ } catch {
7500
+ cachedLoginShellEnv = process.env;
7501
+ }
7502
+ return cachedLoginShellEnv;
7503
+ }
7242
7504
  function getDaemonCliEnvironment() {
7243
- return augmentCliPath(process.env);
7505
+ const base = (0, import_node_os.platform)() === "win32" ? process.env : loadLoginShellEnvironment();
7506
+ return augmentCliPath(base);
7244
7507
  }
7245
7508
  function cacheResolvedCli(command, resolvedPath) {
7246
7509
  resolvedCliCache.set(command, resolvedPath);
@@ -7252,7 +7515,7 @@ function resolveViaWhere(command, env) {
7252
7515
  if ((0, import_node_os.platform)() !== "win32") return null;
7253
7516
  const whereExe = (0, import_node_path.join)(env.SystemRoot ?? process.env.SystemRoot ?? "C:\\Windows", "System32", "where.exe");
7254
7517
  if (!(0, import_node_fs.existsSync)(whereExe)) return null;
7255
- const result = (0, import_node_child_process.spawnSync)(whereExe, [command], {
7518
+ const result = (0, import_node_child_process2.spawnSync)(whereExe, [command], {
7256
7519
  encoding: "utf8",
7257
7520
  env,
7258
7521
  windowsHide: true,
@@ -7266,26 +7529,6 @@ function resolveViaWhere(command, env) {
7266
7529
  }
7267
7530
  return null;
7268
7531
  }
7269
- function runCliVersionProbe(executable, env, args = ["--version"]) {
7270
- const isWin = (0, import_node_os.platform)() === "win32";
7271
- if (isWin && /\.(cmd|bat)$/i.test(executable)) {
7272
- const comSpec = env.ComSpec ?? process.env.ComSpec ?? "cmd.exe";
7273
- return (0, import_node_child_process.spawnSync)(comSpec, ["/d", "/s", "/c", executable, ...args], {
7274
- encoding: "utf8",
7275
- timeout: 3e3,
7276
- env,
7277
- windowsHide: true
7278
- });
7279
- }
7280
- const useShell = isWin && !/[\\/]/.test(executable);
7281
- return (0, import_node_child_process.spawnSync)(executable, args, {
7282
- encoding: "utf8",
7283
- timeout: 3e3,
7284
- env,
7285
- shell: useShell,
7286
- windowsHide: isWin ? true : void 0
7287
- });
7288
- }
7289
7532
  function pathSeparator() {
7290
7533
  return (0, import_node_os.platform)() === "win32" ? ";" : ":";
7291
7534
  }
@@ -7425,10 +7668,12 @@ function resolveBackendRuntimeCommand(backendKind, env = getDaemonCliEnvironment
7425
7668
  var CoreAgent = class _CoreAgent extends BaseMachineAgent {
7426
7669
  childProcess = null;
7427
7670
  _aborted = false;
7671
+ providerApiKey;
7428
7672
  /** How long to wait after SIGTERM before escalating to SIGKILL. */
7429
7673
  static SIGKILL_DELAY_MS = 3e3;
7430
7674
  constructor(presenter, runtime) {
7431
7675
  super(presenter, runtime);
7676
+ this.providerApiKey = runtime.providerApiKey;
7432
7677
  }
7433
7678
  abort() {
7434
7679
  this._aborted = true;
@@ -7553,6 +7798,9 @@ var CoreAgent = class _CoreAgent extends BaseMachineAgent {
7553
7798
  if (value2 !== void 0) env[key] = value2;
7554
7799
  }
7555
7800
  const augmented = getDaemonCliEnvironment();
7801
+ if (this.runtime.backendKind === "supatest_cli" && this.providerApiKey) {
7802
+ augmented.SUPATEST_API_KEY = this.providerApiKey;
7803
+ }
7556
7804
  if ((0, import_node_os2.platform)() !== "win32") {
7557
7805
  const home = augmented.HOME ?? "/home/user";
7558
7806
  const extraPaths = [`${home}/.local/node/bin`, `${home}/.local/bin`];
@@ -11387,7 +11635,7 @@ var WSClient = class {
11387
11635
  };
11388
11636
 
11389
11637
  // src/version.ts
11390
- var AGENT_VERSION = "0.1.14";
11638
+ var AGENT_VERSION = "0.1.15";
11391
11639
 
11392
11640
  // src/sandbox.ts
11393
11641
  async function runSandbox(config) {
@@ -11403,6 +11651,8 @@ async function runSandbox(config) {
11403
11651
  let currentConversationId = config.sessionId;
11404
11652
  let currentTeamId = config.teamId;
11405
11653
  let currentUser = config.currentUser;
11654
+ let currentTerminalContextRefs;
11655
+ let currentTerminalSnapshots;
11406
11656
  console.info("[sandbox] Connecting to WS server", { wsUrl: config.wsUrl, sessionId: config.sessionId });
11407
11657
  const wsClient = new WSClient(
11408
11658
  config.wsUrl,
@@ -11484,6 +11734,8 @@ async function runSandbox(config) {
11484
11734
  teamId: currentTeamId,
11485
11735
  currentUser,
11486
11736
  workflowTools: currentWorkflowTools,
11737
+ terminalContextRefs: currentTerminalContextRefs,
11738
+ terminalSnapshots: currentTerminalSnapshots,
11487
11739
  images: currentImages.length > 0 ? currentImages.map((img) => ({
11488
11740
  id: img.id,
11489
11741
  data: img.data,
@@ -11524,8 +11776,8 @@ async function runSandbox(config) {
11524
11776
  if (nextPayload.agentPrompt !== void 0) {
11525
11777
  activeAgentPrompt = nextPayload.agentPrompt;
11526
11778
  }
11527
- if (nextPayload.selectedModel) {
11528
- activeModel = nextPayload.selectedModel;
11779
+ if (nextPayload.selectedModel !== void 0) {
11780
+ activeModel = nextPayload.selectedModel.trim() || void 0;
11529
11781
  }
11530
11782
  if (nextPayload.selectedContextWindow !== void 0) {
11531
11783
  activeContextWindow = nextPayload.selectedContextWindow;
@@ -11557,6 +11809,12 @@ async function runSandbox(config) {
11557
11809
  if (nextPayload.teamId !== void 0) {
11558
11810
  currentTeamId = nextPayload.teamId;
11559
11811
  }
11812
+ if (nextPayload.terminalContextRefs !== void 0) {
11813
+ currentTerminalContextRefs = nextPayload.terminalContextRefs;
11814
+ }
11815
+ if (nextPayload.terminalSnapshots !== void 0) {
11816
+ currentTerminalSnapshots = nextPayload.terminalSnapshots;
11817
+ }
11560
11818
  }
11561
11819
  presenter.setSuppressSessionLifecycle(false);
11562
11820
  wsClient.close();
@@ -11572,6 +11830,26 @@ var PRODUCTION_API_URL = "https://api.aiden-platform.com";
11572
11830
  var PRODUCTION_WS_URL = "wss://ws.aiden-platform.com";
11573
11831
  var LOCAL_API_URL = "http://localhost:8400";
11574
11832
  var LOCAL_WS_URL = "ws://localhost:8401";
11833
+ async function probeLocalDaemonEndpoint(port, token, timeoutMs = 750) {
11834
+ try {
11835
+ const headers = {};
11836
+ if (token) headers.authorization = `Bearer ${token}`;
11837
+ const response = await fetch(`http://127.0.0.1:${port}/status`, {
11838
+ headers,
11839
+ signal: AbortSignal.timeout(timeoutMs)
11840
+ });
11841
+ if (response.status === 401) return { state: "listening_unauthorized" };
11842
+ if (!response.ok) return { state: "absent" };
11843
+ const body = await response.json();
11844
+ if (!body.runtimeId) return { state: "absent" };
11845
+ return { state: "running", status: body };
11846
+ } catch {
11847
+ return { state: "absent" };
11848
+ }
11849
+ }
11850
+ function describeOccupiedLocalDaemon(port) {
11851
+ return `local daemon API port ${port} is already in use. If Aiden desktop is open, it manages the daemon automatically. Quit Aiden desktop or stop the existing daemon before starting another.`;
11852
+ }
11575
11853
  function getConfigPath() {
11576
11854
  return process.env.AIDEN_AGENT_CONFIG_PATH ?? (0, import_node_path2.join)((0, import_node_os3.homedir)(), ".aiden", "agent", "config.json");
11577
11855
  }
@@ -11629,50 +11907,70 @@ function isLocalEndpoint(url2) {
11629
11907
  function getLocalApiPort(args, stored) {
11630
11908
  const raw = argValue(args, "--local-api-port") ?? process.env.AIDEN_AGENT_LOCAL_API_PORT;
11631
11909
  if (raw) return Number.parseInt(raw, 10);
11632
- return stored.localApiPort ?? 47831;
11910
+ const storedPort = stored.localApiPort;
11911
+ if (storedPort && storedPort > 0) return storedPort;
11912
+ return 47831;
11633
11913
  }
11634
11914
  function sleep(ms) {
11635
11915
  if (ms <= 0) return Promise.resolve();
11636
11916
  return new Promise((resolve2) => setTimeout(resolve2, ms));
11637
11917
  }
11638
- function commandVersion(command, args = ["--version"]) {
11639
- const env = getDaemonCliEnvironment();
11640
- const executable = resolveCliExecutable(command, env);
11641
- if (!executable) return void 0;
11642
- const result = runCliVersionProbe(executable, env, args);
11643
- if (result.error || result.status !== 0) return void 0;
11644
- return (result.stdout || result.stderr).split("\n")[0]?.trim() || void 0;
11645
- }
11646
- function commandVersionForProvider(provider) {
11647
- const env = getDaemonCliEnvironment();
11648
- const executable = resolveProviderCliCommand(provider, env);
11649
- if (!executable) return void 0;
11650
- const result = runCliVersionProbe(executable, env, ["--version"]);
11651
- if (result.error || result.status !== 0) return void 0;
11652
- return (result.stdout || result.stderr).split("\n")[0]?.trim() || void 0;
11653
- }
11654
11918
  function normalizeBackendKind(backendKind) {
11655
11919
  if (backendKind === "codex") return "codex_app_server";
11656
11920
  return backendKind;
11657
11921
  }
11658
11922
  function discoverCapabilities() {
11659
11923
  const now = (/* @__PURE__ */ new Date()).toISOString();
11924
+ const env = getDaemonCliEnvironment();
11660
11925
  return {
11661
11926
  agents: DISCOVERABLE_PROVIDER_KINDS.map((provider) => {
11662
- const version = commandVersionForProvider(provider);
11927
+ const executable = resolveProviderCliCommand(provider, env);
11663
11928
  return {
11664
11929
  provider,
11665
- available: Boolean(version),
11666
- ...version ? { version } : {},
11930
+ available: Boolean(executable),
11667
11931
  models: [],
11668
11932
  lastCheckedAt: now
11669
11933
  };
11670
11934
  }),
11671
- hasGit: Boolean(commandVersion("git")),
11935
+ hasGit: Boolean(resolveCliExecutable("git", env)),
11672
11936
  hasTerminal: true,
11673
11937
  supportsFilesystem: true
11674
11938
  };
11675
11939
  }
11940
+ var USER_INTERRUPTED_RESULT = {
11941
+ success: false,
11942
+ summary: "Interrupted by user",
11943
+ filesModified: [],
11944
+ planFilesCreated: [],
11945
+ iterations: 0,
11946
+ error: "Interrupted by user"
11947
+ };
11948
+ function abortActiveAgent(entry) {
11949
+ entry.presenter.onComplete(USER_INTERRUPTED_RESULT);
11950
+ entry.agent.kill();
11951
+ }
11952
+ function collectRuntimeMetadata() {
11953
+ const cpuList = (0, import_node_os3.cpus)();
11954
+ const metadata = {
11955
+ platform: (0, import_node_os3.platform)(),
11956
+ arch: (0, import_node_os3.arch)(),
11957
+ osVersion: (0, import_node_os3.release)(),
11958
+ agentVersion: AGENT_VERSION,
11959
+ memoryBytes: (0, import_node_os3.totalmem)(),
11960
+ memoryFreeBytes: (0, import_node_os3.freemem)()
11961
+ };
11962
+ if (cpuList.length > 0) {
11963
+ metadata.cpuModel = cpuList[0]?.model;
11964
+ metadata.cpuCores = cpuList.length;
11965
+ }
11966
+ try {
11967
+ const disk = (0, import_node_fs2.statfsSync)((0, import_node_os3.homedir)());
11968
+ metadata.diskFreeBytes = disk.bavail * disk.bsize;
11969
+ metadata.diskTotalBytes = disk.blocks * disk.bsize;
11970
+ } catch {
11971
+ }
11972
+ return metadata;
11973
+ }
11676
11974
  var RuntimePresenter = class {
11677
11975
  constructor(socket, conversationId, runId) {
11678
11976
  this.socket = socket;
@@ -11778,11 +12076,7 @@ async function setupDaemon(args) {
11778
12076
  ownerType: scope,
11779
12077
  visibility: scope,
11780
12078
  capabilities: discoverCapabilities(),
11781
- metadata: {
11782
- platform: (0, import_node_os3.platform)(),
11783
- arch: (0, import_node_os3.arch)(),
11784
- agentVersion: AGENT_VERSION
11785
- }
12079
+ metadata: collectRuntimeMetadata()
11786
12080
  })
11787
12081
  });
11788
12082
  if (!response.ok) {
@@ -11813,11 +12107,7 @@ async function loginWithDeviceCode(args) {
11813
12107
  displayName,
11814
12108
  hostname: (0, import_node_os3.hostname)(),
11815
12109
  capabilities: discoverCapabilities(),
11816
- metadata: {
11817
- platform: (0, import_node_os3.platform)(),
11818
- arch: (0, import_node_os3.arch)(),
11819
- agentVersion: AGENT_VERSION
11820
- }
12110
+ metadata: collectRuntimeMetadata()
11821
12111
  })
11822
12112
  });
11823
12113
  if (!start.ok) {
@@ -11865,8 +12155,9 @@ async function startDaemon(args) {
11865
12155
  if (!runtimeId || !runtimeToken || !wsUrl) {
11866
12156
  throw new Error("daemon requires runtimeId, runtime token, and wsUrl. Run setup first or pass --runtime-id/--token/--ws-url.");
11867
12157
  }
11868
- const capabilities = discoverCapabilities();
11869
12158
  const activeAgents = /* @__PURE__ */ new Map();
12159
+ const currentCapabilities = () => discoverCapabilities();
12160
+ const currentMetadata = () => collectRuntimeMetadata();
11870
12161
  const recentLogs = [];
11871
12162
  const pushLog = (message) => {
11872
12163
  recentLogs.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${message}`);
@@ -11888,16 +12179,21 @@ async function startDaemon(args) {
11888
12179
  extraHeaders: { "ngrok-skip-browser-warning": "1" }
11889
12180
  });
11890
12181
  const heartbeat = setInterval(() => {
11891
- if (socket.connected) socket.emit("runtime.heartbeat", { capabilities });
12182
+ if (socket.connected) socket.emit("runtime.heartbeat", { capabilities: currentCapabilities(), metadata: currentMetadata() });
11892
12183
  }, 15e3);
11893
12184
  socket.on("connect", () => {
11894
12185
  pushLog(`connected runtime=${runtimeId}`);
11895
12186
  console.info("[aiden-agent] Daemon connected", { runtimeId, wsUrl, version: AGENT_VERSION });
11896
- socket.emit("runtime.hello", { capabilities });
12187
+ socket.emit("runtime.hello", { capabilities: currentCapabilities(), metadata: currentMetadata() });
11897
12188
  });
12189
+ let lastConnectErrorLogAt = 0;
11898
12190
  socket.on("connect_error", (error) => {
11899
12191
  pushLog(`connect_error ${error.message}`);
11900
- console.warn("[aiden-agent] Daemon connection error:", error.message);
12192
+ const now = Date.now();
12193
+ if (now - lastConnectErrorLogAt < 5e3) return;
12194
+ lastConnectErrorLogAt = now;
12195
+ const wsHint = isLocalEndpoint(wsUrl) ? ` Nothing is listening at ${wsUrl}. For local dev run setup with --profile local and start the stack (WS on ws://localhost:8401).` : ` Verify wsUrl in ${getConfigPath()} and that you can reach ${wsUrl}.`;
12196
+ console.warn("[aiden-agent] Daemon connection error:", `${error.message}.${wsHint}`);
11901
12197
  });
11902
12198
  socket.on("disconnect", (reason) => {
11903
12199
  pushLog(`disconnected ${reason}`);
@@ -11925,10 +12221,11 @@ async function startDaemon(args) {
11925
12221
  const presenter = new RuntimePresenter(socket, payload.conversationId, payload.runId);
11926
12222
  const agent = new CoreAgent(presenter, {
11927
12223
  backendKind,
11928
- runtimeCommand
12224
+ runtimeCommand,
12225
+ providerApiKey: payload.providerApiKey
11929
12226
  });
11930
- activeAgents.set(payload.runId, agent);
11931
12227
  const cwd = payload.projectPath ?? process.cwd();
12228
+ activeAgents.set(payload.runId, { agent, presenter });
11932
12229
  void agent.run({
11933
12230
  task: payload.content,
11934
12231
  maxIterations: payload.maxIterations ?? 50,
@@ -11964,21 +12261,21 @@ async function startDaemon(args) {
11964
12261
  });
11965
12262
  socket.on("agent.abort", (payload) => {
11966
12263
  if (!payload?.runId) return;
11967
- const agent = activeAgents.get(payload.runId);
11968
- if (!agent) return;
11969
- agent.kill();
12264
+ const entry = activeAgents.get(payload.runId);
12265
+ if (!entry) return;
12266
+ abortActiveAgent(entry);
11970
12267
  activeAgents.delete(payload.runId);
11971
12268
  pushLog(`aborted run=${payload.runId}`);
11972
12269
  });
11973
12270
  socket.on("agent.tool_response", (payload) => {
11974
12271
  if (!payload?.toolId || typeof payload.response !== "string") return;
11975
- const agents = payload.runId ? [activeAgents.get(payload.runId)] : [...activeAgents.values()];
12272
+ const agents = payload.runId ? [activeAgents.get(payload.runId)?.agent] : [...activeAgents.values()].map((entry) => entry.agent);
11976
12273
  const delivered = agents.some((agent) => agent?.sendToolResponse(payload.toolId, payload.response) === true);
11977
12274
  if (!delivered) pushLog(`tool_response missed tool=${payload.toolId}`);
11978
12275
  });
11979
12276
  socket.on("agent.append_message", (payload) => {
11980
12277
  if (!payload?.runId || typeof payload.text !== "string") return;
11981
- const agent = activeAgents.get(payload.runId);
12278
+ const agent = activeAgents.get(payload.runId)?.agent;
11982
12279
  const delivered = agent?.sendUserMessage(payload.text) === true;
11983
12280
  socket.emit(delivered ? "agent.accepted" : "agent.rejected", {
11984
12281
  runId: payload.runId,
@@ -12012,21 +12309,47 @@ async function startDaemon(args) {
12012
12309
  return;
12013
12310
  }
12014
12311
  if (req.method === "POST" && req.url === "/stop") {
12015
- for (const agent of activeAgents.values()) agent.kill();
12312
+ for (const entry of activeAgents.values()) abortActiveAgent(entry);
12016
12313
  activeAgents.clear();
12017
12314
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true }));
12018
12315
  return;
12019
12316
  }
12317
+ if (req.method === "POST" && req.url === "/shutdown") {
12318
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true }));
12319
+ setImmediate(queueFullShutdown);
12320
+ return;
12321
+ }
12020
12322
  res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: "Not found" }));
12021
12323
  });
12324
+ const occupiedProbe = await probeLocalDaemonEndpoint(localApiPort, localApiToken);
12325
+ if (occupiedProbe.state === "running") {
12326
+ clearInterval(heartbeat);
12327
+ socket.disconnect();
12328
+ throw new Error(
12329
+ `${describeOccupiedLocalDaemon(localApiPort)} (runtime ${occupiedProbe.status.runtimeId})`
12330
+ );
12331
+ }
12332
+ if (occupiedProbe.state === "listening_unauthorized") {
12333
+ clearInterval(heartbeat);
12334
+ socket.disconnect();
12335
+ throw new Error(
12336
+ `${describeOccupiedLocalDaemon(localApiPort)} The token in ${getConfigPath()} no longer matches the running daemon; quit Aiden desktop or run setup again.`
12337
+ );
12338
+ }
12022
12339
  await new Promise((resolve2, reject) => {
12023
12340
  const onError = (error) => {
12024
12341
  clearInterval(heartbeat);
12025
12342
  socket.disconnect();
12026
12343
  if (error.code === "EADDRINUSE") {
12027
- reject(new Error(
12028
- `local daemon API port ${localApiPort} is already in use. Another aiden-agent daemon may already be running; use \`aiden-agent status\` or stop the existing daemon before starting a new one.`
12029
- ));
12344
+ void probeLocalDaemonEndpoint(localApiPort).then((probe) => {
12345
+ if (probe.state !== "absent") {
12346
+ reject(new Error(describeOccupiedLocalDaemon(localApiPort)));
12347
+ return;
12348
+ }
12349
+ reject(new Error(
12350
+ `local daemon API port ${localApiPort} is already in use by another process. Otherwise free the port or pass \`--local-api-port\` with a different value.`
12351
+ ));
12352
+ });
12030
12353
  return;
12031
12354
  }
12032
12355
  reject(error);
@@ -12046,8 +12369,8 @@ async function startDaemon(args) {
12046
12369
  if (stopped) return;
12047
12370
  stopped = true;
12048
12371
  clearInterval(heartbeat);
12049
- for (const agent of activeAgents.values()) {
12050
- agent.kill();
12372
+ for (const entry of activeAgents.values()) {
12373
+ abortActiveAgent(entry);
12051
12374
  }
12052
12375
  activeAgents.clear();
12053
12376
  socket.disconnect();
@@ -12055,13 +12378,74 @@ async function startDaemon(args) {
12055
12378
  localServer.close(() => resolve2());
12056
12379
  });
12057
12380
  };
12058
- return { runtimeId, wsUrl, localApiPort, localApiToken, stop };
12381
+ const controller = {
12382
+ runtimeId,
12383
+ wsUrl,
12384
+ localApiPort,
12385
+ localApiToken,
12386
+ stop
12387
+ };
12388
+ const queueFullShutdown = () => {
12389
+ void stop().then(() => controller.whenExternallyStopped?.());
12390
+ };
12391
+ return controller;
12392
+ }
12393
+ async function stopDaemon(args) {
12394
+ const stored = readConfig();
12395
+ const localApiPort = getLocalApiPort(args, stored);
12396
+ const localApiToken = stored.localApiToken;
12397
+ if (!localApiToken) {
12398
+ throw new Error("No local API token in config. Run setup or login first.");
12399
+ }
12400
+ const probe = await probeLocalDaemonEndpoint(localApiPort, localApiToken);
12401
+ if (probe.state === "listening_unauthorized") {
12402
+ throw new Error(
12403
+ `Daemon is listening on port ${localApiPort} but rejected this config token. Quit Aiden desktop or rerun setup/login to refresh config.`
12404
+ );
12405
+ }
12406
+ if (probe.state === "absent") {
12407
+ console.info("[aiden-agent] No daemon listening", { port: localApiPort });
12408
+ return;
12409
+ }
12410
+ const response = await fetch(`http://127.0.0.1:${localApiPort}/shutdown`, {
12411
+ method: "POST",
12412
+ headers: { authorization: `Bearer ${localApiToken}` },
12413
+ signal: AbortSignal.timeout(5e3)
12414
+ });
12415
+ if (!response.ok) {
12416
+ throw new Error(`Failed to stop daemon: HTTP ${response.status}`);
12417
+ }
12418
+ console.info("[aiden-agent] Daemon stopped", {
12419
+ port: localApiPort,
12420
+ runtimeId: probe.status.runtimeId
12421
+ });
12059
12422
  }
12060
12423
  async function runDaemon(args) {
12424
+ const stored = readConfig();
12425
+ const localApiPort = getLocalApiPort(args, stored);
12426
+ const localApiToken = stored.localApiToken;
12427
+ const existingProbe = await probeLocalDaemonEndpoint(localApiPort, localApiToken);
12428
+ if (existingProbe.state === "running") {
12429
+ console.info("[aiden-agent] Daemon already running", {
12430
+ port: localApiPort,
12431
+ runtimeId: existingProbe.status.runtimeId,
12432
+ connected: existingProbe.status.connected,
12433
+ hint: "If Aiden desktop is open, it manages the daemon automatically."
12434
+ });
12435
+ return;
12436
+ }
12437
+ if (existingProbe.state === "listening_unauthorized") {
12438
+ console.info("[aiden-agent] Daemon already running", {
12439
+ port: localApiPort,
12440
+ hint: "Aiden desktop is using a different local API token than config.json. Quit the desktop app or rerun setup if you need a fresh daemon."
12441
+ });
12442
+ return;
12443
+ }
12061
12444
  const controller = await startDaemon(args);
12062
12445
  const shutdown = () => {
12063
12446
  void controller.stop().finally(() => process.exit(0));
12064
12447
  };
12448
+ controller.whenExternallyStopped = shutdown;
12065
12449
  process.once("SIGINT", shutdown);
12066
12450
  process.once("SIGTERM", shutdown);
12067
12451
  await new Promise(() => {
@@ -12108,13 +12492,16 @@ function logoutDaemon() {
12108
12492
  removeConfig();
12109
12493
  console.info("[aiden-agent] Runtime config removed", { configPath: getConfigPath() });
12110
12494
  }
12111
- function printStatus() {
12495
+ async function printStatus() {
12112
12496
  const config = readConfig();
12113
12497
  const configured = Boolean(config.runtimeId && config.runtimeToken && config.wsUrl);
12114
12498
  const warnings = [];
12115
12499
  if ((isLocalEndpoint(config.apiUrl) || isLocalEndpoint(config.wsUrl)) && config.endpointProfile !== "local") {
12116
12500
  warnings.push("Configured endpoint points at localhost. Use --profile local only for local development.");
12117
12501
  }
12502
+ const localApiPort = config.localApiPort && config.localApiPort > 0 ? config.localApiPort : getLocalApiPort([], config);
12503
+ const localDaemonProbe = config.localApiToken ? await probeLocalDaemonEndpoint(localApiPort, config.localApiToken) : { state: "absent" };
12504
+ const localDaemonRunning = localDaemonProbe.state === "running";
12118
12505
  console.info(JSON.stringify({
12119
12506
  mode: "runtime",
12120
12507
  configured,
@@ -12123,9 +12510,10 @@ function printStatus() {
12123
12510
  wsUrl: config.wsUrl ?? null,
12124
12511
  endpointProfile: config.endpointProfile ?? null,
12125
12512
  localApiPort: config.localApiPort ?? null,
12513
+ localDaemonRunning,
12126
12514
  hasLocalApiToken: Boolean(config.localApiToken),
12127
12515
  configPath: getConfigPath(),
12128
- note: configured ? "Durable runtime config is present. Start it with: aiden-agent daemon" : "No durable runtime configured. Cloud sandbox sessions are launched by Aiden with session env vars and do not appear in this local config.",
12516
+ note: configured ? localDaemonRunning ? "Local daemon is running. Stop it with: aiden-agent stop" : "Durable runtime config is present. Start it with: aiden-agent daemon" : "No durable runtime configured. Cloud sandbox sessions are launched by Aiden with session env vars and do not appear in this local config.",
12129
12517
  warnings
12130
12518
  }, null, 2));
12131
12519
  }
@@ -12144,7 +12532,7 @@ async function printDoctor(args = []) {
12144
12532
  authorization: `Bearer ${config.runtimeToken}`,
12145
12533
  "content-type": "application/json"
12146
12534
  },
12147
- body: JSON.stringify({ capabilities })
12535
+ body: JSON.stringify({ capabilities, metadata: collectRuntimeMetadata() })
12148
12536
  });
12149
12537
  if (!response.ok) {
12150
12538
  syncError = `${response.status} ${await response.text()}`;
@@ -12177,6 +12565,7 @@ Commands:
12177
12565
  setup Register this computer with a setup token from Aiden
12178
12566
  login Register this computer through browser authorization
12179
12567
  daemon Start the durable runtime daemon
12568
+ stop Stop the local daemon (or active runs only via the local API /stop)
12180
12569
  status Show local runtime configuration without secrets
12181
12570
  doctor Check local CLI capabilities
12182
12571
  logout Remove local runtime configuration
@@ -12188,6 +12577,7 @@ Common flows:
12188
12577
  aiden-agent setup --setup-token <token>
12189
12578
  aiden-agent setup --setup-token <token> --scope user
12190
12579
  aiden-agent daemon
12580
+ aiden-agent stop
12191
12581
 
12192
12582
  Local development:
12193
12583
  aiden-agent login --profile local
@@ -12274,8 +12664,12 @@ async function main() {
12274
12664
  await runDaemon(commandArgs);
12275
12665
  return;
12276
12666
  }
12667
+ if (command === "stop") {
12668
+ await stopDaemon(commandArgs);
12669
+ return;
12670
+ }
12277
12671
  if (command === "status") {
12278
- printStatus();
12672
+ await printStatus();
12279
12673
  return;
12280
12674
  }
12281
12675
  if (command === "doctor") {