@codelia/runtime 0.1.3 → 0.1.12

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.
Files changed (3) hide show
  1. package/dist/index.cjs +2082 -331
  2. package/dist/index.js +2139 -374
  3. package/package.json +9 -9
package/dist/index.cjs CHANGED
@@ -23,11 +23,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
 
25
25
  // src/runtime.ts
26
- var import_node_path13 = __toESM(require("path"), 1);
26
+ var import_node_path15 = __toESM(require("path"), 1);
27
+ var import_storage9 = require("@codelia/storage");
27
28
 
28
29
  // src/agent-factory.ts
29
- var import_core16 = require("@codelia/core");
30
- var import_storage4 = require("@codelia/storage");
30
+ var import_node_fs14 = require("fs");
31
+ var import_core17 = require("@codelia/core");
32
+ var import_storage5 = require("@codelia/storage");
31
33
 
32
34
  // src/agents/context.ts
33
35
  var createAgentsResolverKey = (resolver) => ({
@@ -856,10 +858,13 @@ var describeRpcMessage = (msg) => {
856
858
  }
857
859
  return "unknown";
858
860
  };
861
+ var serializeMessage = (msg) => ({
862
+ payload: `${JSON.stringify(msg)}
863
+ `,
864
+ label: describeRpcMessage(msg)
865
+ });
859
866
  var send = (msg) => {
860
- const payload = `${JSON.stringify(msg)}
861
- `;
862
- const label = describeRpcMessage(msg);
867
+ const { payload, label } = serializeMessage(msg);
863
868
  const writable = process.stdout.write(payload, (error) => {
864
869
  if (error) {
865
870
  (0, import_logger.debugLog)(`transport.write.error label=${label} message=${error.message}`);
@@ -869,6 +874,20 @@ var send = (msg) => {
869
874
  (0, import_logger.debugLog)(`transport.backpressure label=${label} bytes=${payload.length}`);
870
875
  }
871
876
  };
877
+ var sendAsync = async (msg) => new Promise((resolve) => {
878
+ const { payload, label } = serializeMessage(msg);
879
+ const writable = process.stdout.write(payload, (error) => {
880
+ if (error) {
881
+ (0, import_logger.debugLog)(
882
+ `transport.write.error label=${label} message=${error.message}`
883
+ );
884
+ }
885
+ resolve();
886
+ });
887
+ if (!writable) {
888
+ (0, import_logger.debugLog)(`transport.backpressure label=${label} bytes=${payload.length}`);
889
+ }
890
+ });
872
891
  var sendError = (id, error) => {
873
892
  const response = { jsonrpc: "2.0", id, error };
874
893
  send(response);
@@ -892,6 +911,21 @@ var sendAgentEvent = (state, runId, event) => {
892
911
  send(notify);
893
912
  return seq;
894
913
  };
914
+ var sendAgentEventAsync = async (state, runId, event) => {
915
+ if (state.shouldSuppressEvent(runId)) return null;
916
+ const seq = state.nextSequence(runId);
917
+ const notify = {
918
+ jsonrpc: "2.0",
919
+ method: "agent.event",
920
+ params: {
921
+ run_id: runId,
922
+ seq,
923
+ event
924
+ }
925
+ };
926
+ await sendAsync(notify);
927
+ return seq;
928
+ };
895
929
  var sendRunStatus = (runId, status, message) => {
896
930
  const notify = {
897
931
  jsonrpc: "2.0",
@@ -904,6 +938,18 @@ var sendRunStatus = (runId, status, message) => {
904
938
  };
905
939
  send(notify);
906
940
  };
941
+ var sendRunStatusAsync = async (runId, status, message) => {
942
+ const notify = {
943
+ jsonrpc: "2.0",
944
+ method: "run.status",
945
+ params: {
946
+ run_id: runId,
947
+ status,
948
+ message
949
+ }
950
+ };
951
+ await sendAsync(notify);
952
+ };
907
953
  var sendRunContext = (runId, contextLeftPercent) => {
908
954
  const notify = {
909
955
  jsonrpc: "2.0",
@@ -982,10 +1028,15 @@ var AuthStore = class {
982
1028
  };
983
1029
 
984
1030
  // src/auth/resolver.ts
985
- var SUPPORTED_PROVIDERS = ["openai", "anthropic"];
1031
+ var SUPPORTED_PROVIDERS = [
1032
+ "openai",
1033
+ "anthropic",
1034
+ "openrouter"
1035
+ ];
986
1036
  var API_KEY_ENV = {
987
1037
  openai: "OPENAI_API_KEY",
988
- anthropic: "ANTHROPIC_API_KEY"
1038
+ anthropic: "ANTHROPIC_API_KEY",
1039
+ openrouter: "OPENROUTER_API_KEY"
989
1040
  };
990
1041
  var AuthResolver = class _AuthResolver {
991
1042
  auth;
@@ -1003,6 +1054,12 @@ var AuthResolver = class _AuthResolver {
1003
1054
  const auth = await store.load();
1004
1055
  return new _AuthResolver(state, log2, store, auth);
1005
1056
  }
1057
+ hasAnyAvailableAuth() {
1058
+ return SUPPORTED_PROVIDERS.some((provider) => {
1059
+ if (this.auth.providers[provider]) return true;
1060
+ return Boolean(readEnvValue(API_KEY_ENV[provider]));
1061
+ });
1062
+ }
1006
1063
  async resolveProvider(preferred) {
1007
1064
  const preferredProvider = preferred && SUPPORTED_PROVIDERS.includes(preferred) ? preferred : null;
1008
1065
  if (preferredProvider) return preferredProvider;
@@ -1021,10 +1078,11 @@ var AuthResolver = class _AuthResolver {
1021
1078
  return "openai";
1022
1079
  }
1023
1080
  const result = await requestUiPick(this.state, {
1024
- title: "Select provider",
1081
+ title: "Welcome! Choose your provider to get started.",
1025
1082
  items: SUPPORTED_PROVIDERS.map((provider) => ({
1026
1083
  id: provider,
1027
- label: provider
1084
+ label: provider,
1085
+ detail: provider === "openai" ? "OAuth (ChatGPT Plus/Pro) or API key" : provider === "openrouter" ? "API key (OpenRouter)" : "API key"
1028
1086
  })),
1029
1087
  multi: false
1030
1088
  });
@@ -1047,7 +1105,17 @@ var AuthResolver = class _AuthResolver {
1047
1105
  if (provider === "openai") {
1048
1106
  return this.promptOpenAiAuth();
1049
1107
  }
1050
- return this.promptApiKey(provider, "Anthropic API key");
1108
+ return this.promptApiKey(provider, this.getApiKeyPromptLabel(provider));
1109
+ }
1110
+ getApiKeyPromptLabel(provider) {
1111
+ switch (provider) {
1112
+ case "anthropic":
1113
+ return "Anthropic API key";
1114
+ case "openrouter":
1115
+ return "OpenRouter API key";
1116
+ default:
1117
+ return "API key";
1118
+ }
1051
1119
  }
1052
1120
  async getOpenAiAccessToken() {
1053
1121
  const entry = this.auth.providers.openai;
@@ -1076,10 +1144,18 @@ var AuthResolver = class _AuthResolver {
1076
1144
  throw new Error("UI does not support auth prompts");
1077
1145
  }
1078
1146
  const pick = await requestUiPick(this.state, {
1079
- title: "OpenAI auth method",
1147
+ title: "How would you like to connect OpenAI?",
1080
1148
  items: [
1081
- { id: "oauth", label: "ChatGPT Plus/Pro (OAuth)" },
1082
- { id: "api_key", label: "Manually enter API key" }
1149
+ {
1150
+ id: "oauth",
1151
+ label: "ChatGPT Plus/Pro (OAuth)",
1152
+ detail: "Recommended if you use ChatGPT subscription access"
1153
+ },
1154
+ {
1155
+ id: "api_key",
1156
+ label: "OpenAI API key",
1157
+ detail: "Use a standard OpenAI API key from platform settings"
1158
+ }
1083
1159
  ],
1084
1160
  multi: false
1085
1161
  });
@@ -1103,7 +1179,7 @@ var AuthResolver = class _AuthResolver {
1103
1179
  const session = await createOAuthSession();
1104
1180
  const confirm = await requestUiConfirm(this.state, {
1105
1181
  title: "OpenAI OAuth",
1106
- message: `Open your browser to authenticate.
1182
+ message: `You're almost done. Open your browser to continue sign in.
1107
1183
 
1108
1184
  ${session.authUrl}`,
1109
1185
  confirm_label: "Open browser",
@@ -1142,7 +1218,7 @@ ${session.authUrl}`,
1142
1218
  }
1143
1219
  const prompt = await requestUiPrompt(this.state, {
1144
1220
  title: label,
1145
- message: "Enter the API key.",
1221
+ message: "Paste your API key to continue. You can change it later with /logout.",
1146
1222
  secret: true
1147
1223
  });
1148
1224
  const value = prompt?.value?.trim() ?? "";
@@ -1343,7 +1419,8 @@ var createMcpOAuthSession = async (params) => {
1343
1419
  var import_core2 = require("@codelia/core");
1344
1420
  var import_model_metadata = require("@codelia/model-metadata");
1345
1421
  var import_storage3 = require("@codelia/storage");
1346
- var buildModelRegistry = async (llm) => {
1422
+ var buildModelRegistry = async (llm, options = {}) => {
1423
+ const strict = options.strict ?? true;
1347
1424
  const metadataService = new import_model_metadata.ModelMetadataServiceImpl({
1348
1425
  storagePathService: new import_storage3.StoragePathServiceImpl()
1349
1426
  });
@@ -1358,9 +1435,11 @@ var buildModelRegistry = async (llm) => {
1358
1435
  fullIdEntry = providerEntries?.[`${llm.provider}/${llm.model}`];
1359
1436
  }
1360
1437
  if (!directEntry && !fullIdEntry) {
1361
- throw new Error(
1362
- `Model metadata not found for ${llm.provider}/${llm.model} after refresh`
1363
- );
1438
+ if (strict) {
1439
+ throw new Error(
1440
+ `Model metadata not found for ${llm.provider}/${llm.model} after refresh`
1441
+ );
1442
+ }
1364
1443
  }
1365
1444
  return (0, import_core2.applyModelMetadata)(import_core2.DEFAULT_MODEL_REGISTRY, { models: entries });
1366
1445
  };
@@ -1717,6 +1796,8 @@ var SYSTEM_TOOL_ALLOWLIST = [
1717
1796
  "agents_resolve",
1718
1797
  "skill_search",
1719
1798
  "skill_load",
1799
+ "lane_list",
1800
+ "lane_status",
1720
1801
  "done"
1721
1802
  ];
1722
1803
  var SYSTEM_BASH_ALLOWLIST = [
@@ -1808,7 +1889,27 @@ var PermissionService = class {
1808
1889
  message: `${skillName ? skillName : explicitPath || `skill_load ${rawArgs}`}${rememberPreview}`
1809
1890
  };
1810
1891
  }
1811
- return { title: "Run tool?", message: `${toolName} ${rawArgs}` };
1892
+ if (toolName === "write") {
1893
+ const parsed = parseRawArgsForPrompt(rawArgs);
1894
+ const filePath = parsed && typeof parsed.file_path === "string" ? parsed.file_path.trim() : "";
1895
+ const content = parsed && typeof parsed.content === "string" ? parsed.content : "";
1896
+ const target = filePath || "(unknown path)";
1897
+ return {
1898
+ title: "Run tool?",
1899
+ message: `write ${target} (${content.length} bytes)${rememberPreview}`
1900
+ };
1901
+ }
1902
+ if (toolName === "edit") {
1903
+ const parsed = parseRawArgsForPrompt(rawArgs);
1904
+ const filePath = parsed && typeof parsed.file_path === "string" ? parsed.file_path.trim() : "";
1905
+ const target = filePath || "(unknown path)";
1906
+ const mode = parsed && typeof parsed.match_mode === "string" ? parsed.match_mode : "auto";
1907
+ return {
1908
+ title: "Run tool?",
1909
+ message: `edit ${target} (match=${mode})${rememberPreview}`
1910
+ };
1911
+ }
1912
+ return { title: "Run tool?", message: toolName };
1812
1913
  }
1813
1914
  evaluate(toolName, rawArgs) {
1814
1915
  if (toolName === "bash") {
@@ -3304,94 +3405,820 @@ var createGrepTool = (sandboxKey) => (0, import_core8.defineTool)({
3304
3405
  }
3305
3406
  });
3306
3407
 
3307
- // src/tools/read.ts
3308
- var import_node_fs10 = require("fs");
3408
+ // src/tools/lane.ts
3309
3409
  var import_core9 = require("@codelia/core");
3310
3410
  var import_zod8 = require("zod");
3311
- var DEFAULT_READ_LIMIT = 2e3;
3312
- var MAX_LINE_LENGTH = 2e3;
3313
- var MAX_BYTES = 50 * 1024;
3314
- var createReadTool = (sandboxKey) => (0, import_core9.defineTool)({
3315
- name: "read",
3316
- description: "Read a text file with optional 0-based line offset and line limit.",
3317
- input: import_zod8.z.object({
3318
- file_path: import_zod8.z.string().describe("File path under the sandbox root."),
3319
- offset: import_zod8.z.number().int().nonnegative().optional().describe("0-based start line. Default 0."),
3320
- limit: import_zod8.z.number().int().positive().optional().describe("Max lines to read. Default 2000.")
3321
- }),
3322
- execute: async (input, ctx) => {
3323
- let resolved;
3411
+
3412
+ // src/lanes/manager.ts
3413
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
3414
+ var import_node_fs11 = require("fs");
3415
+ var import_node_os2 = __toESM(require("os"), 1);
3416
+ var import_node_path11 = __toESM(require("path"), 1);
3417
+
3418
+ // src/lanes/command.ts
3419
+ var import_node_child_process3 = require("child_process");
3420
+ var DEFAULT_TIMEOUT_MS = 3e4;
3421
+ var MAX_CAPTURE_BYTES = 512 * 1024;
3422
+ var truncate2 = (value) => {
3423
+ if (value.length <= 8e3) return value;
3424
+ return `${value.slice(0, 8e3)}...[truncated]`;
3425
+ };
3426
+ var runCommand = async (command, args, options = {}) => {
3427
+ const timeoutMs = Math.max(1e3, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
3428
+ return new Promise((resolve, reject) => {
3429
+ const child = (0, import_node_child_process3.spawn)(command, args, {
3430
+ cwd: options.cwd,
3431
+ stdio: ["ignore", "pipe", "pipe"]
3432
+ });
3433
+ let stdout = "";
3434
+ let stderr = "";
3435
+ let bytes = 0;
3436
+ let done = false;
3437
+ const finish = (fn) => {
3438
+ if (done) return;
3439
+ done = true;
3440
+ clearTimeout(timer);
3441
+ fn();
3442
+ };
3443
+ const consume = (chunk, kind) => {
3444
+ const text = chunk.toString("utf8");
3445
+ bytes += Buffer.byteLength(text, "utf8");
3446
+ if (bytes > MAX_CAPTURE_BYTES) {
3447
+ finish(
3448
+ () => reject(
3449
+ new Error(
3450
+ `${command} output exceeded ${MAX_CAPTURE_BYTES} bytes while running ${args.join(" ")}`
3451
+ )
3452
+ )
3453
+ );
3454
+ try {
3455
+ child.kill("SIGTERM");
3456
+ } catch {
3457
+ }
3458
+ return;
3459
+ }
3460
+ if (kind === "stdout") {
3461
+ stdout += text;
3462
+ } else {
3463
+ stderr += text;
3464
+ }
3465
+ };
3466
+ child.stdout?.on("data", (chunk) => consume(chunk, "stdout"));
3467
+ child.stderr?.on("data", (chunk) => consume(chunk, "stderr"));
3468
+ child.on("error", (error) => {
3469
+ finish(() => reject(error));
3470
+ });
3471
+ child.on("close", (code) => {
3472
+ if (code === 0) {
3473
+ finish(() => resolve({ stdout, stderr }));
3474
+ return;
3475
+ }
3476
+ const detail = `${stderr}
3477
+ ${stdout}`.trim();
3478
+ const message = detail ? `${command} ${args.join(" ")} failed (exit ${String(code)}): ${truncate2(detail)}` : `${command} ${args.join(" ")} failed (exit ${String(code)})`;
3479
+ finish(() => reject(new Error(message)));
3480
+ });
3481
+ const timer = setTimeout(() => {
3482
+ try {
3483
+ child.kill("SIGTERM");
3484
+ } catch {
3485
+ }
3486
+ finish(
3487
+ () => reject(
3488
+ new Error(
3489
+ `${command} ${args.join(" ")} timed out after ${Math.floor(timeoutMs / 1e3)}s`
3490
+ )
3491
+ )
3492
+ );
3493
+ }, timeoutMs);
3494
+ });
3495
+ };
3496
+
3497
+ // src/lanes/registry.ts
3498
+ var import_node_fs10 = require("fs");
3499
+ var import_node_path10 = __toESM(require("path"), 1);
3500
+ var import_storage4 = require("@codelia/storage");
3501
+ var REGISTRY_DIRNAME = "lanes";
3502
+ var REGISTRY_FILENAME = "registry.json";
3503
+ var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
3504
+ var sortByUpdatedDesc = (lanes) => [...lanes].sort((a, b) => b.updated_at.localeCompare(a.updated_at));
3505
+ var readJsonFile = async (filePath) => {
3506
+ try {
3507
+ const raw = await import_node_fs10.promises.readFile(filePath, "utf8");
3508
+ const parsed = JSON.parse(raw);
3509
+ if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.lanes)) {
3510
+ return null;
3511
+ }
3512
+ return {
3513
+ version: 1,
3514
+ lanes: parsed.lanes
3515
+ };
3516
+ } catch {
3517
+ return null;
3518
+ }
3519
+ };
3520
+ var atomicWrite = async (filePath, payload) => {
3521
+ const dir = import_node_path10.default.dirname(filePath);
3522
+ const base = import_node_path10.default.basename(filePath);
3523
+ const tmp = import_node_path10.default.join(
3524
+ dir,
3525
+ `${base}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
3526
+ );
3527
+ await import_node_fs10.promises.writeFile(tmp, payload, "utf8");
3528
+ await import_node_fs10.promises.rename(tmp, filePath);
3529
+ };
3530
+ var LaneRegistryStore = class {
3531
+ registryPath;
3532
+ constructor(registryPath) {
3533
+ if (registryPath) {
3534
+ this.registryPath = registryPath;
3535
+ return;
3536
+ }
3537
+ const root = (0, import_storage4.resolveStoragePaths)().root;
3538
+ this.registryPath = import_node_path10.default.join(root, REGISTRY_DIRNAME, REGISTRY_FILENAME);
3539
+ }
3540
+ async ensureDir() {
3541
+ await import_node_fs10.promises.mkdir(import_node_path10.default.dirname(this.registryPath), { recursive: true });
3542
+ }
3543
+ async readAll() {
3544
+ await this.ensureDir();
3545
+ const data = await readJsonFile(this.registryPath);
3546
+ if (!data) return [];
3547
+ return sortByUpdatedDesc(data.lanes);
3548
+ }
3549
+ async writeAll(lanes) {
3550
+ await this.ensureDir();
3551
+ const payload = {
3552
+ version: 1,
3553
+ lanes: sortByUpdatedDesc(lanes)
3554
+ };
3555
+ await atomicWrite(
3556
+ this.registryPath,
3557
+ `${JSON.stringify(payload, null, 2)}
3558
+ `
3559
+ );
3560
+ }
3561
+ async list() {
3562
+ return this.readAll();
3563
+ }
3564
+ async get(laneId) {
3565
+ const lanes = await this.readAll();
3566
+ return lanes.find((lane) => lane.lane_id === laneId) ?? null;
3567
+ }
3568
+ async upsert(record) {
3569
+ const lanes = await this.readAll();
3570
+ const idx = lanes.findIndex((lane) => lane.lane_id === record.lane_id);
3571
+ const next = {
3572
+ ...record,
3573
+ updated_at: nowIso()
3574
+ };
3575
+ if (idx >= 0) {
3576
+ lanes[idx] = next;
3577
+ } else {
3578
+ lanes.push(next);
3579
+ }
3580
+ await this.writeAll(lanes);
3581
+ }
3582
+ async patch(laneId, patch) {
3583
+ const lanes = await this.readAll();
3584
+ const idx = lanes.findIndex((lane) => lane.lane_id === laneId);
3585
+ if (idx < 0) return null;
3586
+ const current = lanes[idx];
3587
+ const next = {
3588
+ ...current,
3589
+ ...patch,
3590
+ lane_id: current.lane_id,
3591
+ created_at: current.created_at,
3592
+ updated_at: nowIso()
3593
+ };
3594
+ lanes[idx] = next;
3595
+ await this.writeAll(lanes);
3596
+ return next;
3597
+ }
3598
+ };
3599
+
3600
+ // src/lanes/manager.ts
3601
+ var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
3602
+ var INITIAL_MESSAGE_FLAG = "--initial-message";
3603
+ var toSlug = (value) => {
3604
+ const normalized = value.trim().toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
3605
+ return normalized || "task";
3606
+ };
3607
+ var parseDate = (value) => {
3608
+ const ts = Date.parse(value);
3609
+ if (Number.isNaN(ts)) return 0;
3610
+ return ts;
3611
+ };
3612
+ var isNotFoundError = (error) => {
3613
+ const message = error instanceof Error ? error.message : String(error);
3614
+ return message.includes("command not found") || message.includes("ENOENT") || message.includes("not found");
3615
+ };
3616
+ var isExitFailure = (error) => {
3617
+ const message = error instanceof Error ? error.message : String(error);
3618
+ return message.includes("failed (exit");
3619
+ };
3620
+ var LaneManagerError = class extends Error {
3621
+ code;
3622
+ constructor(code, message) {
3623
+ super(message);
3624
+ this.code = code;
3625
+ this.name = "LaneManagerError";
3626
+ }
3627
+ };
3628
+ var LaneManager = class {
3629
+ runner;
3630
+ registry;
3631
+ now;
3632
+ randomId;
3633
+ launchCommand;
3634
+ defaultWorktreeRoot;
3635
+ constructor(options = {}) {
3636
+ this.runner = options.runner ?? runCommand;
3637
+ this.registry = options.registry ?? new LaneRegistryStore();
3638
+ this.now = options.now ?? nowIso2;
3639
+ this.randomId = options.randomId ?? (() => import_node_crypto4.default.randomUUID());
3640
+ this.launchCommand = options.launchCommand ?? process.env.CODELIA_LANE_LAUNCH_COMMAND?.trim() ?? "codelia";
3641
+ this.defaultWorktreeRoot = options.defaultWorktreeRoot ?? import_node_path11.default.join(import_node_os2.default.homedir(), ".codelia", "worktrees");
3642
+ }
3643
+ async cmd(command, args, options) {
3644
+ (0, import_logger.debugLog)(`lane.cmd ${command} ${args.join(" ")}`);
3645
+ return this.runner(command, args, options);
3646
+ }
3647
+ async resolveRepoRoot(workingDir) {
3648
+ const { stdout } = await this.cmd(
3649
+ "git",
3650
+ ["-C", workingDir, "rev-parse", "--show-toplevel"],
3651
+ { cwd: workingDir }
3652
+ );
3653
+ const root = stdout.trim();
3654
+ if (!root) {
3655
+ throw new LaneManagerError(
3656
+ "backend_command_failed",
3657
+ "Could not resolve git repository root."
3658
+ );
3659
+ }
3660
+ return root;
3661
+ }
3662
+ async preflightBackend(backend) {
3663
+ if (backend === "tmux") {
3664
+ try {
3665
+ await this.cmd("tmux", ["-V"]);
3666
+ return;
3667
+ } catch (error) {
3668
+ if (isNotFoundError(error)) {
3669
+ throw new LaneManagerError(
3670
+ "backend_not_found",
3671
+ "tmux not found in PATH."
3672
+ );
3673
+ }
3674
+ throw new LaneManagerError(
3675
+ "backend_command_failed",
3676
+ `tmux preflight failed: ${String(error)}`
3677
+ );
3678
+ }
3679
+ }
3324
3680
  try {
3325
- const sandbox = await getSandboxContext(ctx, sandboxKey);
3326
- resolved = sandbox.resolvePath(input.file_path);
3681
+ await this.cmd("zellij", ["--version"]);
3327
3682
  } catch (error) {
3328
- return `Security error: ${String(error)}`;
3683
+ if (isNotFoundError(error)) {
3684
+ throw new LaneManagerError(
3685
+ "backend_not_found",
3686
+ "zellij not found in PATH."
3687
+ );
3688
+ }
3689
+ throw new LaneManagerError(
3690
+ "backend_command_failed",
3691
+ `zellij preflight failed: ${String(error)}`
3692
+ );
3329
3693
  }
3694
+ throw new LaneManagerError(
3695
+ "backend_command_failed",
3696
+ "zellij backend is not supported yet in lane MVP. Use tmux."
3697
+ );
3698
+ }
3699
+ async startTmuxLane(target, worktreePath, seedContext) {
3700
+ const launchCommand = this.buildLaunchCommand(seedContext);
3701
+ await this.cmd("tmux", [
3702
+ "new-session",
3703
+ "-d",
3704
+ "-s",
3705
+ target,
3706
+ "-c",
3707
+ worktreePath
3708
+ ]);
3709
+ const paneTarget = `${target}:0.0`;
3710
+ await this.cmd("tmux", [
3711
+ "send-keys",
3712
+ "-t",
3713
+ paneTarget,
3714
+ "-l",
3715
+ launchCommand
3716
+ ]);
3717
+ await this.cmd("tmux", ["send-keys", "-t", paneTarget, "Enter"]);
3718
+ }
3719
+ shellQuote(value) {
3720
+ if (!value) return "''";
3721
+ return `'${value.replaceAll("'", "'\\''")}'`;
3722
+ }
3723
+ buildLaunchCommand(seedContext) {
3724
+ const seed = seedContext?.trim();
3725
+ if (!seed) {
3726
+ return this.launchCommand;
3727
+ }
3728
+ return `${this.launchCommand} ${INITIAL_MESSAGE_FLAG} ${this.shellQuote(seed)}`;
3729
+ }
3730
+ async isTmuxAlive(target) {
3330
3731
  try {
3331
- const stat = await import_node_fs10.promises.stat(resolved);
3332
- if (stat.isDirectory()) {
3333
- return `Path is a directory: ${input.file_path}`;
3334
- }
3732
+ await this.cmd("tmux", ["has-session", "-t", target], {
3733
+ timeoutMs: 5e3
3734
+ });
3735
+ return true;
3736
+ } catch (error) {
3737
+ if (isExitFailure(error)) return false;
3738
+ throw new LaneManagerError(
3739
+ "backend_command_failed",
3740
+ `tmux status failed: ${String(error)}`
3741
+ );
3742
+ }
3743
+ }
3744
+ async stopTmuxLane(target) {
3745
+ try {
3746
+ await this.cmd("tmux", ["kill-session", "-t", target], {
3747
+ timeoutMs: 1e4
3748
+ });
3749
+ } catch (error) {
3750
+ if (isExitFailure(error)) return;
3751
+ throw new LaneManagerError(
3752
+ "backend_command_failed",
3753
+ `tmux close failed: ${String(error)}`
3754
+ );
3755
+ }
3756
+ }
3757
+ async resolveWorktreeMainRoot(worktreePath) {
3758
+ const { stdout } = await this.cmd("git", [
3759
+ "-C",
3760
+ worktreePath,
3761
+ "rev-parse",
3762
+ "--git-common-dir"
3763
+ ]);
3764
+ const commonDir = import_node_path11.default.resolve(worktreePath, stdout.trim());
3765
+ if (import_node_path11.default.basename(commonDir) === ".git") {
3766
+ return import_node_path11.default.dirname(commonDir);
3767
+ }
3768
+ return await this.resolveRepoRoot(worktreePath);
3769
+ }
3770
+ async isWorktreeDirty(worktreePath) {
3771
+ try {
3772
+ await import_node_fs11.promises.access(worktreePath);
3335
3773
  } catch {
3336
- return `File not found: ${input.file_path}`;
3774
+ return false;
3337
3775
  }
3776
+ const { stdout } = await this.cmd("git", [
3777
+ "-C",
3778
+ worktreePath,
3779
+ "status",
3780
+ "--porcelain"
3781
+ ]);
3782
+ return stdout.trim().length > 0;
3783
+ }
3784
+ async removeWorktree(worktreePath, force) {
3338
3785
  try {
3339
- const content = await import_node_fs10.promises.readFile(resolved, "utf8");
3340
- const lines = content.split(/\r?\n/);
3341
- if (lines.length === 0) {
3342
- return "";
3786
+ await import_node_fs11.promises.access(worktreePath);
3787
+ } catch {
3788
+ return;
3789
+ }
3790
+ const repoRoot = await this.resolveWorktreeMainRoot(worktreePath);
3791
+ const args = ["-C", repoRoot, "worktree", "remove"];
3792
+ if (force) args.push("--force");
3793
+ args.push(worktreePath);
3794
+ await this.cmd("git", args);
3795
+ }
3796
+ async create(input, options) {
3797
+ const taskId = input.task_id.trim();
3798
+ if (!taskId) {
3799
+ throw new LaneManagerError(
3800
+ "backend_command_failed",
3801
+ "task_id is required."
3802
+ );
3803
+ }
3804
+ const backend = input.mux_backend ?? "tmux";
3805
+ await this.preflightBackend(backend);
3806
+ const repoRoot = await this.resolveRepoRoot(options.workingDir);
3807
+ const laneId = this.randomId();
3808
+ const slug = toSlug(taskId);
3809
+ const shortId = laneId.slice(0, 8);
3810
+ const branchName = `lane/${slug}-${shortId}`;
3811
+ const worktreePath = input.worktree_path ? import_node_path11.default.resolve(options.workingDir, input.worktree_path) : import_node_path11.default.join(this.defaultWorktreeRoot, `${slug}-${shortId}`);
3812
+ const muxTarget = `codelia-lane-${shortId}`;
3813
+ const createdAt = this.now();
3814
+ const record = {
3815
+ lane_id: laneId,
3816
+ task_id: taskId,
3817
+ state: "creating",
3818
+ mux_backend: backend,
3819
+ mux_target: muxTarget,
3820
+ worktree_path: worktreePath,
3821
+ branch_name: branchName,
3822
+ session_id: import_node_crypto4.default.randomUUID(),
3823
+ created_at: createdAt,
3824
+ updated_at: createdAt,
3825
+ last_activity_at: createdAt
3826
+ };
3827
+ await this.registry.upsert(record);
3828
+ try {
3829
+ await import_node_fs11.promises.mkdir(import_node_path11.default.dirname(worktreePath), { recursive: true });
3830
+ await this.cmd("git", [
3831
+ "-C",
3832
+ repoRoot,
3833
+ "worktree",
3834
+ "add",
3835
+ "-b",
3836
+ branchName,
3837
+ worktreePath,
3838
+ input.base_ref?.trim() || "HEAD"
3839
+ ]);
3840
+ if (backend === "tmux") {
3841
+ await this.startTmuxLane(muxTarget, worktreePath, input.seed_context);
3343
3842
  }
3344
- const offset = input.offset ?? 0;
3345
- const limit = input.limit ?? DEFAULT_READ_LIMIT;
3346
- if (offset >= lines.length) {
3347
- return `Offset exceeds file length: ${input.file_path}`;
3843
+ const running = await this.registry.patch(laneId, {
3844
+ state: "running",
3845
+ last_activity_at: this.now(),
3846
+ last_error: void 0
3847
+ });
3848
+ if (!running) {
3849
+ throw new Error("lane record disappeared during create");
3348
3850
  }
3349
- const raw = [];
3350
- let bytes = 0;
3351
- let truncatedByBytes = false;
3352
- for (let index = offset; index < Math.min(lines.length, offset + limit); index += 1) {
3353
- const line = lines[index].length > MAX_LINE_LENGTH ? `${lines[index].slice(0, MAX_LINE_LENGTH)}...` : lines[index];
3354
- const size = Buffer.byteLength(line, "utf8") + (raw.length > 0 ? 1 : 0);
3355
- if (bytes + size > MAX_BYTES) {
3356
- truncatedByBytes = true;
3357
- break;
3851
+ return running;
3852
+ } catch (error) {
3853
+ await this.registry.patch(laneId, {
3854
+ state: "error",
3855
+ last_error: String(error),
3856
+ last_activity_at: this.now()
3857
+ });
3858
+ throw new LaneManagerError(
3859
+ "backend_command_failed",
3860
+ `lane.create failed (${laneId}): ${String(error)}`
3861
+ );
3862
+ }
3863
+ }
3864
+ async list(input) {
3865
+ const all = await this.registry.list();
3866
+ if (input?.include_closed) return all;
3867
+ return all.filter((lane) => lane.state !== "closed");
3868
+ }
3869
+ async status(laneId) {
3870
+ const lane = await this.registry.get(laneId);
3871
+ if (!lane) {
3872
+ throw new LaneManagerError("lane_not_found", `Lane not found: ${laneId}`);
3873
+ }
3874
+ let alive = false;
3875
+ if (lane.state !== "closed") {
3876
+ if (lane.mux_backend === "tmux") {
3877
+ alive = await this.isTmuxAlive(lane.mux_target);
3878
+ }
3879
+ if (lane.state === "running" && !alive) {
3880
+ const patched = await this.registry.patch(lane.lane_id, {
3881
+ state: "finished",
3882
+ last_activity_at: this.now()
3883
+ });
3884
+ if (patched) {
3885
+ return { lane: patched, backend_alive: false };
3358
3886
  }
3359
- raw.push(line);
3360
- bytes += size;
3361
3887
  }
3362
- const numbered = raw.map(
3363
- (line, index) => `${String(index + offset + 1).padStart(5, " ")} ${line}`
3888
+ }
3889
+ return { lane, backend_alive: alive };
3890
+ }
3891
+ async close(input) {
3892
+ const lane = await this.registry.get(input.lane_id);
3893
+ if (!lane) {
3894
+ throw new LaneManagerError(
3895
+ "lane_not_found",
3896
+ `Lane not found: ${input.lane_id}`
3364
3897
  );
3365
- const lastReadLine = offset + raw.length;
3366
- const hasMoreLines = lines.length > lastReadLine;
3367
- const truncated = truncatedByBytes || hasMoreLines;
3368
- let output = numbered.join("\n");
3369
- if (truncated) {
3370
- const reason = truncatedByBytes ? `Output truncated at ${MAX_BYTES} bytes.` : "File has more lines.";
3371
- output += `
3372
-
3373
- ${reason} Use offset to read beyond line ${lastReadLine}.`;
3898
+ }
3899
+ if (lane.state === "closed") {
3900
+ return lane;
3901
+ }
3902
+ const force = input.force === true;
3903
+ const removeWorktree = input.remove_worktree !== false;
3904
+ const status = await this.status(lane.lane_id);
3905
+ const refreshed = status.lane;
3906
+ if (status.backend_alive && !force) {
3907
+ throw new LaneManagerError(
3908
+ "lane_running",
3909
+ `Lane is still running: ${lane.lane_id}`
3910
+ );
3911
+ }
3912
+ if (status.backend_alive && refreshed.mux_backend === "tmux") {
3913
+ await this.stopTmuxLane(refreshed.mux_target);
3914
+ }
3915
+ if (removeWorktree) {
3916
+ const dirty = await this.isWorktreeDirty(refreshed.worktree_path);
3917
+ if (dirty && !force) {
3918
+ throw new LaneManagerError(
3919
+ "worktree_dirty",
3920
+ `Worktree has uncommitted changes: ${refreshed.worktree_path}`
3921
+ );
3374
3922
  }
3375
- return output;
3376
- } catch (error) {
3377
- return `Error reading file: ${String(error)}`;
3923
+ await this.removeWorktree(refreshed.worktree_path, force);
3924
+ }
3925
+ const closed = await this.registry.patch(refreshed.lane_id, {
3926
+ state: "closed",
3927
+ last_activity_at: this.now()
3928
+ });
3929
+ if (!closed) {
3930
+ throw new LaneManagerError(
3931
+ "backend_command_failed",
3932
+ "Failed to persist lane close state."
3933
+ );
3378
3934
  }
3935
+ return closed;
3379
3936
  }
3380
- });
3381
-
3382
- // src/tools/skill-load.ts
3383
- var import_core10 = require("@codelia/core");
3384
- var import_zod9 = require("zod");
3385
- var SkillLoadInputSchema = import_zod9.z.object({
3386
- name: import_zod9.z.string().min(1).optional().describe("Exact skill name to load."),
3387
- path: import_zod9.z.string().min(1).optional().describe("Absolute or workspace-relative path to SKILL.md.")
3388
- }).refine((value) => !!value.name || !!value.path, {
3389
- message: "name or path is required",
3390
- path: ["name"]
3391
- });
3392
- var createSkillLoadTool = (skillsResolverKey) => (0, import_core10.defineTool)({
3393
- name: "skill_load",
3394
- description: "Load full SKILL.md content by exact name or path.",
3937
+ async gc(input) {
3938
+ const ttlMs = Math.max(1, input.idle_ttl_minutes) * 6e4;
3939
+ const now = Date.now();
3940
+ const lanes = await this.registry.list();
3941
+ let closed = 0;
3942
+ let skipped = 0;
3943
+ const errors = [];
3944
+ for (const lane of lanes) {
3945
+ try {
3946
+ const status = await this.status(lane.lane_id);
3947
+ const current = status.lane;
3948
+ if (current.state === "running" || current.state === "closed") {
3949
+ skipped += 1;
3950
+ continue;
3951
+ }
3952
+ if (current.state !== "finished" && current.state !== "error") {
3953
+ skipped += 1;
3954
+ continue;
3955
+ }
3956
+ const idleSince = parseDate(current.last_activity_at) || parseDate(current.updated_at);
3957
+ if (idleSince <= 0 || now - idleSince < ttlMs) {
3958
+ skipped += 1;
3959
+ continue;
3960
+ }
3961
+ await this.close({
3962
+ lane_id: current.lane_id,
3963
+ remove_worktree: input.remove_worktree,
3964
+ force: input.force
3965
+ });
3966
+ closed += 1;
3967
+ } catch (error) {
3968
+ errors.push(`${lane.lane_id}: ${String(error)}`);
3969
+ }
3970
+ }
3971
+ return {
3972
+ checked: lanes.length,
3973
+ closed,
3974
+ skipped,
3975
+ errors
3976
+ };
3977
+ }
3978
+ };
3979
+
3980
+ // src/tools/lane.ts
3981
+ var laneManager = new LaneManager();
3982
+ var backendSchema = import_zod8.z.enum(["tmux", "zellij"]);
3983
+ var formatError = (error) => {
3984
+ if (error instanceof LaneManagerError) {
3985
+ return new Error(`${error.code}: ${error.message}`);
3986
+ }
3987
+ if (error instanceof Error) return error;
3988
+ return new Error(String(error));
3989
+ };
3990
+ var shQuote = (value) => {
3991
+ if (!value) return "''";
3992
+ return `'${value.replaceAll("'", "'\\''")}'`;
3993
+ };
3994
+ var buildLaneHints = (lane) => {
3995
+ const attachCommand = lane.mux_backend === "tmux" ? `tmux attach -t ${shQuote(lane.mux_target)}` : lane.mux_backend === "zellij" ? `zellij attach ${shQuote(lane.mux_target)}` : null;
3996
+ return {
3997
+ attach_command: attachCommand,
3998
+ enter_worktree_command: `cd ${shQuote(lane.worktree_path)}`,
3999
+ status_tool: {
4000
+ name: "lane_status",
4001
+ args: { lane_id: lane.lane_id }
4002
+ },
4003
+ close_tool: {
4004
+ name: "lane_close",
4005
+ args: { lane_id: lane.lane_id }
4006
+ }
4007
+ };
4008
+ };
4009
+ var createLaneCreateTool = (sandboxKey) => (0, import_core9.defineTool)({
4010
+ name: "lane_create",
4011
+ description: "Create a Task lane (worktree slot) and start autonomous execution in tmux/zellij.",
4012
+ input: import_zod8.z.object({
4013
+ task_id: import_zod8.z.string().describe("Task identifier."),
4014
+ base_ref: import_zod8.z.string().optional().describe("Base git ref. Default: HEAD."),
4015
+ worktree_path: import_zod8.z.string().optional().describe(
4016
+ "Optional worktree path override. Default: ~/.codelia/worktrees/<task-slug>-<lane-id8>."
4017
+ ),
4018
+ mux_backend: backendSchema.optional().describe("Multiplexer backend. Default: tmux."),
4019
+ seed_context: import_zod8.z.string().optional().describe("Optional initial text context.")
4020
+ }),
4021
+ execute: async (input, ctx) => {
4022
+ const sandbox = await getSandboxContext(ctx, sandboxKey);
4023
+ try {
4024
+ const lane = await laneManager.create(
4025
+ {
4026
+ task_id: input.task_id,
4027
+ base_ref: input.base_ref,
4028
+ worktree_path: input.worktree_path,
4029
+ mux_backend: input.mux_backend,
4030
+ seed_context: input.seed_context
4031
+ },
4032
+ { workingDir: sandbox.workingDir }
4033
+ );
4034
+ return {
4035
+ ok: true,
4036
+ lane,
4037
+ hints: buildLaneHints(lane)
4038
+ };
4039
+ } catch (error) {
4040
+ throw formatError(error);
4041
+ }
4042
+ }
4043
+ });
4044
+ var createLaneListTool = (sandboxKey) => (0, import_core9.defineTool)({
4045
+ name: "lane_list",
4046
+ description: "List Task lanes (worktree slots) and current states.",
4047
+ input: import_zod8.z.object({
4048
+ include_closed: import_zod8.z.boolean().optional().describe("Include closed lanes. Default: false.")
4049
+ }),
4050
+ execute: async (input, ctx) => {
4051
+ await getSandboxContext(ctx, sandboxKey);
4052
+ try {
4053
+ const lanes = await laneManager.list({
4054
+ include_closed: input.include_closed
4055
+ });
4056
+ return {
4057
+ lanes,
4058
+ hints: lanes.map((lane) => ({
4059
+ lane_id: lane.lane_id,
4060
+ ...buildLaneHints(lane)
4061
+ }))
4062
+ };
4063
+ } catch (error) {
4064
+ throw formatError(error);
4065
+ }
4066
+ }
4067
+ });
4068
+ var createLaneStatusTool = (sandboxKey) => (0, import_core9.defineTool)({
4069
+ name: "lane_status",
4070
+ description: "Get Task lane status and backend liveness.",
4071
+ input: import_zod8.z.object({
4072
+ lane_id: import_zod8.z.string().describe("Lane id.")
4073
+ }),
4074
+ execute: async (input, ctx) => {
4075
+ await getSandboxContext(ctx, sandboxKey);
4076
+ try {
4077
+ const result = await laneManager.status(input.lane_id);
4078
+ return {
4079
+ ...result,
4080
+ hints: buildLaneHints(result.lane)
4081
+ };
4082
+ } catch (error) {
4083
+ throw formatError(error);
4084
+ }
4085
+ }
4086
+ });
4087
+ var createLaneCloseTool = (sandboxKey) => (0, import_core9.defineTool)({
4088
+ name: "lane_close",
4089
+ description: "Close a Task lane and optionally remove its worktree.",
4090
+ input: import_zod8.z.object({
4091
+ lane_id: import_zod8.z.string().describe("Lane id."),
4092
+ remove_worktree: import_zod8.z.boolean().optional().describe("Remove worktree. Default: true."),
4093
+ force: import_zod8.z.boolean().optional().describe("Force close/cleanup. Default: false.")
4094
+ }),
4095
+ execute: async (input, ctx) => {
4096
+ await getSandboxContext(ctx, sandboxKey);
4097
+ try {
4098
+ const lane = await laneManager.close({
4099
+ lane_id: input.lane_id,
4100
+ remove_worktree: input.remove_worktree,
4101
+ force: input.force
4102
+ });
4103
+ return {
4104
+ ok: true,
4105
+ lane
4106
+ };
4107
+ } catch (error) {
4108
+ throw formatError(error);
4109
+ }
4110
+ }
4111
+ });
4112
+ var createLaneGcTool = (sandboxKey) => (0, import_core9.defineTool)({
4113
+ name: "lane_gc",
4114
+ description: "Close stale finished/error lanes after idle TTL.",
4115
+ input: import_zod8.z.object({
4116
+ idle_ttl_minutes: import_zod8.z.number().int().positive().describe("Idle TTL in minutes."),
4117
+ remove_worktree: import_zod8.z.boolean().optional().describe("Remove worktree on close. Default: false."),
4118
+ force: import_zod8.z.boolean().optional().describe("Force cleanup for dirty worktrees. Default: false.")
4119
+ }),
4120
+ execute: async (input, ctx) => {
4121
+ await getSandboxContext(ctx, sandboxKey);
4122
+ try {
4123
+ return await laneManager.gc({
4124
+ idle_ttl_minutes: input.idle_ttl_minutes,
4125
+ remove_worktree: input.remove_worktree,
4126
+ force: input.force
4127
+ });
4128
+ } catch (error) {
4129
+ throw formatError(error);
4130
+ }
4131
+ }
4132
+ });
4133
+
4134
+ // src/tools/read.ts
4135
+ var import_node_fs12 = require("fs");
4136
+ var import_core10 = require("@codelia/core");
4137
+ var import_zod9 = require("zod");
4138
+ var DEFAULT_READ_LIMIT = 2e3;
4139
+ var MAX_LINE_LENGTH = 2e3;
4140
+ var MAX_BYTES = 50 * 1024;
4141
+ var createReadTool = (sandboxKey) => (0, import_core10.defineTool)({
4142
+ name: "read",
4143
+ description: "Read a text file with optional 0-based line offset and line limit.",
4144
+ input: import_zod9.z.object({
4145
+ file_path: import_zod9.z.string().describe("File path under the sandbox root."),
4146
+ offset: import_zod9.z.number().int().nonnegative().optional().describe("0-based start line. Default 0."),
4147
+ limit: import_zod9.z.number().int().positive().optional().describe("Max lines to read. Default 2000.")
4148
+ }),
4149
+ execute: async (input, ctx) => {
4150
+ let resolved;
4151
+ try {
4152
+ const sandbox = await getSandboxContext(ctx, sandboxKey);
4153
+ resolved = sandbox.resolvePath(input.file_path);
4154
+ } catch (error) {
4155
+ return `Security error: ${String(error)}`;
4156
+ }
4157
+ try {
4158
+ const stat = await import_node_fs12.promises.stat(resolved);
4159
+ if (stat.isDirectory()) {
4160
+ return `Path is a directory: ${input.file_path}`;
4161
+ }
4162
+ } catch {
4163
+ return `File not found: ${input.file_path}`;
4164
+ }
4165
+ try {
4166
+ const content = await import_node_fs12.promises.readFile(resolved, "utf8");
4167
+ const lines = content.split(/\r?\n/);
4168
+ if (lines.length === 0) {
4169
+ return "";
4170
+ }
4171
+ const offset = input.offset ?? 0;
4172
+ const limit = input.limit ?? DEFAULT_READ_LIMIT;
4173
+ if (offset >= lines.length) {
4174
+ return `Offset exceeds file length: ${input.file_path}`;
4175
+ }
4176
+ const raw = [];
4177
+ let bytes = 0;
4178
+ let truncatedByBytes = false;
4179
+ for (let index = offset; index < Math.min(lines.length, offset + limit); index += 1) {
4180
+ const line = lines[index].length > MAX_LINE_LENGTH ? `${lines[index].slice(0, MAX_LINE_LENGTH)}...` : lines[index];
4181
+ const size = Buffer.byteLength(line, "utf8") + (raw.length > 0 ? 1 : 0);
4182
+ if (bytes + size > MAX_BYTES) {
4183
+ truncatedByBytes = true;
4184
+ break;
4185
+ }
4186
+ raw.push(line);
4187
+ bytes += size;
4188
+ }
4189
+ const numbered = raw.map(
4190
+ (line, index) => `${String(index + offset + 1).padStart(5, " ")} ${line}`
4191
+ );
4192
+ const lastReadLine = offset + raw.length;
4193
+ const hasMoreLines = lines.length > lastReadLine;
4194
+ const truncated = truncatedByBytes || hasMoreLines;
4195
+ let output = numbered.join("\n");
4196
+ if (truncated) {
4197
+ const reason = truncatedByBytes ? `Output truncated at ${MAX_BYTES} bytes.` : "File has more lines.";
4198
+ output += `
4199
+
4200
+ ${reason} Use offset to read beyond line ${lastReadLine}.`;
4201
+ }
4202
+ return output;
4203
+ } catch (error) {
4204
+ return `Error reading file: ${String(error)}`;
4205
+ }
4206
+ }
4207
+ });
4208
+
4209
+ // src/tools/skill-load.ts
4210
+ var import_core11 = require("@codelia/core");
4211
+ var import_zod10 = require("zod");
4212
+ var SkillLoadInputSchema = import_zod10.z.object({
4213
+ name: import_zod10.z.string().min(1).optional().describe("Exact skill name to load."),
4214
+ path: import_zod10.z.string().min(1).optional().describe("Absolute or workspace-relative path to SKILL.md.")
4215
+ }).refine((value) => !!value.name || !!value.path, {
4216
+ message: "name or path is required",
4217
+ path: ["name"]
4218
+ });
4219
+ var createSkillLoadTool = (skillsResolverKey) => (0, import_core11.defineTool)({
4220
+ name: "skill_load",
4221
+ description: "Load full SKILL.md content by exact name or path.",
3395
4222
  input: SkillLoadInputSchema,
3396
4223
  execute: async (input, ctx) => {
3397
4224
  try {
@@ -3435,15 +4262,15 @@ var createSkillLoadTool = (skillsResolverKey) => (0, import_core10.defineTool)({
3435
4262
  });
3436
4263
 
3437
4264
  // src/tools/skill-search.ts
3438
- var import_core11 = require("@codelia/core");
3439
- var import_zod10 = require("zod");
3440
- var createSkillSearchTool = (skillsResolverKey) => (0, import_core11.defineTool)({
4265
+ var import_core12 = require("@codelia/core");
4266
+ var import_zod11 = require("zod");
4267
+ var createSkillSearchTool = (skillsResolverKey) => (0, import_core12.defineTool)({
3441
4268
  name: "skill_search",
3442
4269
  description: "Search installed local skills by name, description, or path.",
3443
- input: import_zod10.z.object({
3444
- query: import_zod10.z.string().min(1).describe("Search query text."),
3445
- limit: import_zod10.z.number().int().positive().optional().describe("Optional max result count (clamped by config)."),
3446
- scope: import_zod10.z.enum(["repo", "user"]).optional().describe("Optional scope filter.")
4270
+ input: import_zod11.z.object({
4271
+ query: import_zod11.z.string().min(1).describe("Search query text."),
4272
+ limit: import_zod11.z.number().int().positive().optional().describe("Optional max result count (clamped by config)."),
4273
+ scope: import_zod11.z.enum(["repo", "user"]).optional().describe("Optional scope filter.")
3447
4274
  }),
3448
4275
  execute: async (input, ctx) => {
3449
4276
  try {
@@ -3466,17 +4293,17 @@ var createSkillSearchTool = (skillsResolverKey) => (0, import_core11.defineTool)
3466
4293
  });
3467
4294
 
3468
4295
  // src/tools/todo-read.ts
3469
- var import_core12 = require("@codelia/core");
3470
- var import_zod11 = require("zod");
4296
+ var import_core13 = require("@codelia/core");
4297
+ var import_zod12 = require("zod");
3471
4298
 
3472
4299
  // src/tools/todo-store.ts
3473
4300
  var todoStore = /* @__PURE__ */ new Map();
3474
4301
 
3475
4302
  // src/tools/todo-read.ts
3476
- var createTodoReadTool = (sandboxKey) => (0, import_core12.defineTool)({
4303
+ var createTodoReadTool = (sandboxKey) => (0, import_core13.defineTool)({
3477
4304
  name: "todo_read",
3478
4305
  description: "Read the in-session todo list.",
3479
- input: import_zod11.z.object({}),
4306
+ input: import_zod12.z.object({}),
3480
4307
  execute: async (_input, ctx) => {
3481
4308
  const sandbox = await getSandboxContext(ctx, sandboxKey);
3482
4309
  const todos = todoStore.get(sandbox.sessionId) ?? [];
@@ -3489,17 +4316,17 @@ var createTodoReadTool = (sandboxKey) => (0, import_core12.defineTool)({
3489
4316
  });
3490
4317
 
3491
4318
  // src/tools/todo-write.ts
3492
- var import_core13 = require("@codelia/core");
3493
- var import_zod12 = require("zod");
3494
- var createTodoWriteTool = (sandboxKey) => (0, import_core13.defineTool)({
4319
+ var import_core14 = require("@codelia/core");
4320
+ var import_zod13 = require("zod");
4321
+ var createTodoWriteTool = (sandboxKey) => (0, import_core14.defineTool)({
3495
4322
  name: "todo_write",
3496
4323
  description: "Replace the in-session todo list.",
3497
- input: import_zod12.z.object({
3498
- todos: import_zod12.z.array(
3499
- import_zod12.z.object({
3500
- content: import_zod12.z.string().describe("Todo item text."),
3501
- status: import_zod12.z.enum(["pending", "in_progress", "completed"]).describe("Todo status."),
3502
- activeForm: import_zod12.z.string().optional().describe("Optional in-progress phrasing for UI display.")
4324
+ input: import_zod13.z.object({
4325
+ todos: import_zod13.z.array(
4326
+ import_zod13.z.object({
4327
+ content: import_zod13.z.string().describe("Todo item text."),
4328
+ status: import_zod13.z.enum(["pending", "in_progress", "completed"]).describe("Todo status."),
4329
+ activeForm: import_zod13.z.string().optional().describe("Optional in-progress phrasing for UI display.")
3503
4330
  })
3504
4331
  )
3505
4332
  }),
@@ -3516,15 +4343,15 @@ var createTodoWriteTool = (sandboxKey) => (0, import_core13.defineTool)({
3516
4343
  });
3517
4344
 
3518
4345
  // src/tools/tool-output-cache.ts
3519
- var import_core14 = require("@codelia/core");
3520
- var import_zod13 = require("zod");
3521
- var createToolOutputCacheTool = (store) => (0, import_core14.defineTool)({
4346
+ var import_core15 = require("@codelia/core");
4347
+ var import_zod14 = require("zod");
4348
+ var createToolOutputCacheTool = (store) => (0, import_core15.defineTool)({
3522
4349
  name: "tool_output_cache",
3523
4350
  description: "Read cached tool output by ref_id.",
3524
- input: import_zod13.z.object({
3525
- ref_id: import_zod13.z.string().describe("Tool output reference ID."),
3526
- offset: import_zod13.z.number().int().nonnegative().optional().describe("Optional 0-based line offset."),
3527
- limit: import_zod13.z.number().int().positive().optional().describe("Optional max number of lines to return.")
4351
+ input: import_zod14.z.object({
4352
+ ref_id: import_zod14.z.string().describe("Tool output reference ID."),
4353
+ offset: import_zod14.z.number().int().nonnegative().optional().describe("Optional 0-based line offset."),
4354
+ limit: import_zod14.z.number().int().positive().optional().describe("Optional max number of lines to return.")
3528
4355
  }),
3529
4356
  execute: async (input) => {
3530
4357
  if (!store.read) {
@@ -3540,16 +4367,16 @@ var createToolOutputCacheTool = (store) => (0, import_core14.defineTool)({
3540
4367
  }
3541
4368
  }
3542
4369
  });
3543
- var createToolOutputCacheGrepTool = (store) => (0, import_core14.defineTool)({
4370
+ var createToolOutputCacheGrepTool = (store) => (0, import_core15.defineTool)({
3544
4371
  name: "tool_output_cache_grep",
3545
4372
  description: "Search cached tool output by ref_id.",
3546
- input: import_zod13.z.object({
3547
- ref_id: import_zod13.z.string().describe("Tool output reference ID."),
3548
- pattern: import_zod13.z.string().describe("Text or regex pattern to search for."),
3549
- regex: import_zod13.z.boolean().optional().describe("Interpret pattern as regex when true. Default false."),
3550
- before: import_zod13.z.number().int().nonnegative().optional().describe("Context lines before each match. Default 0."),
3551
- after: import_zod13.z.number().int().nonnegative().optional().describe("Context lines after each match. Default 0."),
3552
- max_matches: import_zod13.z.number().int().positive().optional().describe("Maximum number of matches to return.")
4373
+ input: import_zod14.z.object({
4374
+ ref_id: import_zod14.z.string().describe("Tool output reference ID."),
4375
+ pattern: import_zod14.z.string().describe("Text or regex pattern to search for."),
4376
+ regex: import_zod14.z.boolean().optional().describe("Interpret pattern as regex when true. Default false."),
4377
+ before: import_zod14.z.number().int().nonnegative().optional().describe("Context lines before each match. Default 0."),
4378
+ after: import_zod14.z.number().int().nonnegative().optional().describe("Context lines after each match. Default 0."),
4379
+ max_matches: import_zod14.z.number().int().positive().optional().describe("Maximum number of matches to return.")
3553
4380
  }),
3554
4381
  execute: async (input) => {
3555
4382
  if (!store.grep) {
@@ -3570,16 +4397,16 @@ var createToolOutputCacheGrepTool = (store) => (0, import_core14.defineTool)({
3570
4397
  });
3571
4398
 
3572
4399
  // src/tools/write.ts
3573
- var import_node_fs11 = require("fs");
3574
- var import_node_path10 = __toESM(require("path"), 1);
3575
- var import_core15 = require("@codelia/core");
3576
- var import_zod14 = require("zod");
3577
- var createWriteTool = (sandboxKey) => (0, import_core15.defineTool)({
4400
+ var import_node_fs13 = require("fs");
4401
+ var import_node_path12 = __toESM(require("path"), 1);
4402
+ var import_core16 = require("@codelia/core");
4403
+ var import_zod15 = require("zod");
4404
+ var createWriteTool = (sandboxKey) => (0, import_core16.defineTool)({
3578
4405
  name: "write",
3579
4406
  description: "Write text to a file, creating parent directories if needed.",
3580
- input: import_zod14.z.object({
3581
- file_path: import_zod14.z.string().describe("File path under the sandbox root."),
3582
- content: import_zod14.z.string().describe("UTF-8 text content to write.")
4407
+ input: import_zod15.z.object({
4408
+ file_path: import_zod15.z.string().describe("File path under the sandbox root."),
4409
+ content: import_zod15.z.string().describe("UTF-8 text content to write.")
3583
4410
  }),
3584
4411
  execute: async (input, ctx) => {
3585
4412
  let resolved;
@@ -3590,8 +4417,8 @@ var createWriteTool = (sandboxKey) => (0, import_core15.defineTool)({
3590
4417
  return `Security error: ${String(error)}`;
3591
4418
  }
3592
4419
  try {
3593
- await import_node_fs11.promises.mkdir(import_node_path10.default.dirname(resolved), { recursive: true });
3594
- await import_node_fs11.promises.writeFile(resolved, input.content, "utf8");
4420
+ await import_node_fs13.promises.mkdir(import_node_path12.default.dirname(resolved), { recursive: true });
4421
+ await import_node_fs13.promises.writeFile(resolved, input.content, "utf8");
3595
4422
  return `Wrote ${input.content.length} bytes to ${input.file_path}`;
3596
4423
  } catch (error) {
3597
4424
  return `Error writing file: ${String(error)}`;
@@ -3616,9 +4443,106 @@ var createTools = (sandboxKey, agentsResolverKey, skillsResolverKey, options = {
3616
4443
  ] : [],
3617
4444
  createTodoReadTool(sandboxKey),
3618
4445
  createTodoWriteTool(sandboxKey),
4446
+ createLaneCreateTool(sandboxKey),
4447
+ createLaneListTool(sandboxKey),
4448
+ createLaneStatusTool(sandboxKey),
4449
+ createLaneCloseTool(sandboxKey),
4450
+ createLaneGcTool(sandboxKey),
3619
4451
  createDoneTool()
3620
4452
  ];
3621
4453
 
4454
+ // src/utils/language.ts
4455
+ var normalizeLanguage = (value) => {
4456
+ const token = value.trim().toLowerCase();
4457
+ if (!token) return "";
4458
+ switch (token) {
4459
+ case "yml":
4460
+ return "yaml";
4461
+ case "mjs":
4462
+ case "cjs":
4463
+ case "node":
4464
+ return "javascript";
4465
+ case "mts":
4466
+ case "cts":
4467
+ case "ts-node":
4468
+ case "deno":
4469
+ return "typescript";
4470
+ case "py":
4471
+ case "python3":
4472
+ return "python";
4473
+ case "rb":
4474
+ return "ruby";
4475
+ case "zsh":
4476
+ case "sh":
4477
+ return "bash";
4478
+ case "ps1":
4479
+ return "powershell";
4480
+ default:
4481
+ return token;
4482
+ }
4483
+ };
4484
+ var languageFromFilePath = (filePath) => {
4485
+ if (!filePath) return void 0;
4486
+ const path16 = filePath.trim().replace(/^["']|["']$/g, "");
4487
+ if (!path16 || path16 === "/dev/null") return void 0;
4488
+ const normalizedPath = path16.replace(/^a\//, "").replace(/^b\//, "");
4489
+ const lastSlash = Math.max(
4490
+ normalizedPath.lastIndexOf("/"),
4491
+ normalizedPath.lastIndexOf("\\")
4492
+ );
4493
+ const basename = lastSlash >= 0 ? normalizedPath.slice(lastSlash + 1) : normalizedPath;
4494
+ if (!basename) return void 0;
4495
+ const dotIndex = basename.lastIndexOf(".");
4496
+ if (dotIndex > 0 && dotIndex < basename.length - 1) {
4497
+ return normalizeLanguage(basename.slice(dotIndex + 1));
4498
+ }
4499
+ if (basename === "Dockerfile") return "dockerfile";
4500
+ if (/^Makefile(\..+)?$/i.test(basename)) return "makefile";
4501
+ return void 0;
4502
+ };
4503
+ var languageFromDiffHeaders = (diff) => {
4504
+ if (!diff) return void 0;
4505
+ for (const line of diff.split("\n")) {
4506
+ if (!line.startsWith("--- ") && !line.startsWith("+++ ")) continue;
4507
+ const rawPath = line.slice(4).trim().split(/\s+/)[0] ?? "";
4508
+ const language = languageFromFilePath(rawPath);
4509
+ if (language) return language;
4510
+ }
4511
+ return void 0;
4512
+ };
4513
+ var languageFromShebang = (content) => {
4514
+ if (!content) return void 0;
4515
+ const firstLine = content.split("\n", 1)[0]?.trim() ?? "";
4516
+ if (!firstLine.startsWith("#!")) return void 0;
4517
+ const body = firstLine.slice(2).trim();
4518
+ if (!body) return void 0;
4519
+ const parts = body.split(/\s+/).filter(Boolean);
4520
+ if (!parts.length) return void 0;
4521
+ let command = parts[0];
4522
+ if (command.endsWith("/env")) {
4523
+ let index = 1;
4524
+ if (parts[index] === "-S") index += 1;
4525
+ while (index < parts.length && parts[index].includes("=")) {
4526
+ index += 1;
4527
+ }
4528
+ command = parts[index] ?? "";
4529
+ }
4530
+ if (!command) return void 0;
4531
+ const last = command.split("/").pop() ?? command;
4532
+ const simplified = last.replace(/[0-9.]+$/g, "");
4533
+ const normalized = normalizeLanguage(simplified);
4534
+ return normalized || void 0;
4535
+ };
4536
+ var resolvePreviewLanguageHint = (input) => {
4537
+ const explicit = input.language ? normalizeLanguage(input.language) : "";
4538
+ if (explicit) return explicit;
4539
+ const shebang = languageFromShebang(input.content);
4540
+ if (shebang) return shebang;
4541
+ const fromDiff = languageFromDiffHeaders(input.diff);
4542
+ if (fromDiff) return fromDiff;
4543
+ return languageFromFilePath(input.filePath);
4544
+ };
4545
+
3622
4546
  // src/agent-factory.ts
3623
4547
  var requireApiKeyAuth = (provider, auth) => {
3624
4548
  if (auth.method !== "api_key") {
@@ -3631,6 +4555,7 @@ var envTruthy = (value) => {
3631
4555
  const normalized = value.trim().toLowerCase();
3632
4556
  return normalized === "1" || normalized === "true";
3633
4557
  };
4558
+ var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
3634
4559
  var buildOpenAiClientOptions = (authResolver, auth) => {
3635
4560
  if (auth.method === "api_key") {
3636
4561
  return { apiKey: auth.api_key };
@@ -3674,6 +4599,22 @@ var buildOpenAiClientOptions = (authResolver, auth) => {
3674
4599
  fetch: fetchWithAccount
3675
4600
  };
3676
4601
  };
4602
+ var buildOpenRouterClientOptions = (auth) => {
4603
+ const headers = {};
4604
+ const referer = readEnvValue("OPENROUTER_HTTP_REFERER");
4605
+ if (referer) {
4606
+ headers["HTTP-Referer"] = referer;
4607
+ }
4608
+ const title = readEnvValue("OPENROUTER_X_TITLE");
4609
+ if (title) {
4610
+ headers["X-Title"] = title;
4611
+ }
4612
+ return {
4613
+ apiKey: requireApiKeyAuth("OpenRouter", auth),
4614
+ baseURL: OPENROUTER_BASE_URL,
4615
+ ...Object.keys(headers).length ? { defaultHeaders: headers } : {}
4616
+ };
4617
+ };
3677
4618
  var sleep = (ms) => new Promise((resolve) => {
3678
4619
  setTimeout(resolve, ms);
3679
4620
  });
@@ -3687,6 +4628,41 @@ var waitForUiConfirmSupport = async (state, timeoutMs = 5e3) => {
3687
4628
  }
3688
4629
  return !!state.uiCapabilities?.supports_confirm;
3689
4630
  };
4631
+ var MAX_CONFIRM_PREVIEW_LINES = 120;
4632
+ var splitLines = (value) => value.split("\n").map((line) => line.replace(/\r$/, ""));
4633
+ var buildBoundedDiffPreview = (diff, maxLines = MAX_CONFIRM_PREVIEW_LINES) => {
4634
+ if (!diff.trim()) return { diff: null, truncated: false };
4635
+ const lines = splitLines(diff);
4636
+ if (!lines.length) return { diff: null, truncated: false };
4637
+ if (lines.length <= maxLines) {
4638
+ return { diff: lines.join("\n"), truncated: false };
4639
+ }
4640
+ return {
4641
+ diff: lines.slice(0, maxLines).join("\n"),
4642
+ truncated: true
4643
+ };
4644
+ };
4645
+ var parseToolArgsObject = (rawArgs) => {
4646
+ try {
4647
+ const parsed = JSON.parse(rawArgs);
4648
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4649
+ return null;
4650
+ }
4651
+ return parsed;
4652
+ } catch {
4653
+ return null;
4654
+ }
4655
+ };
4656
+ var unwrapToolJsonObject = (result) => {
4657
+ if (!result || typeof result !== "object") return null;
4658
+ const typed = result;
4659
+ if (typed.type !== "json") return null;
4660
+ const value = typed.value;
4661
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4662
+ return null;
4663
+ }
4664
+ return value;
4665
+ };
3690
4666
  var requestMcpOAuthTokens = async (state, serverId, oauth2, errorMessage) => {
3691
4667
  const canConfirm = state.uiCapabilities?.supports_confirm ? true : await waitForUiConfirmSupport(state);
3692
4668
  if (!canConfirm) {
@@ -3815,7 +4791,7 @@ var createAgentFactory = (state, options = {}) => {
3815
4791
  const sandboxKey = createSandboxKey(ctx);
3816
4792
  const agentsResolverKey = createAgentsResolverKey(agentsResolver);
3817
4793
  const skillsResolverKey = createSkillsResolverKey(skillsResolver);
3818
- const toolOutputCacheStore = new import_storage4.ToolOutputCacheStoreImpl();
4794
+ const toolOutputCacheStore = new import_storage5.ToolOutputCacheStoreImpl();
3819
4795
  const localTools = createTools(
3820
4796
  sandboxKey,
3821
4797
  agentsResolverKey,
@@ -3824,6 +4800,9 @@ var createAgentFactory = (state, options = {}) => {
3824
4800
  toolOutputCacheStore
3825
4801
  }
3826
4802
  );
4803
+ const editTool = localTools.find(
4804
+ (tool) => tool.definition.name === "edit"
4805
+ );
3827
4806
  let mcpTools = [];
3828
4807
  if (options.mcpManager) {
3829
4808
  try {
@@ -3845,6 +4824,7 @@ var createAgentFactory = (state, options = {}) => {
3845
4824
  }
3846
4825
  }
3847
4826
  const tools = [...localTools, ...mcpTools];
4827
+ state.tools = tools;
3848
4828
  state.toolDefinitions = tools.map((tool) => tool.definition);
3849
4829
  const baseSystemPrompt = await loadSystemPrompt(ctx.workingDir);
3850
4830
  const withAgentsContext = appendInitialAgentsContext(
@@ -3886,7 +4866,7 @@ var createAgentFactory = (state, options = {}) => {
3886
4866
  case "openai": {
3887
4867
  const reasoningEffort = resolveReasoningEffort(modelConfig.reasoning);
3888
4868
  const textVerbosity = resolveTextVerbosity(modelConfig.verbosity);
3889
- llm = new import_core16.ChatOpenAI({
4869
+ llm = new import_core17.ChatOpenAI({
3890
4870
  clientOptions: buildOpenAiClientOptions(authResolver, providerAuth),
3891
4871
  ...modelConfig.name ? { model: modelConfig.name } : {},
3892
4872
  ...reasoningEffort ? { reasoningEffort } : {},
@@ -3894,8 +4874,19 @@ var createAgentFactory = (state, options = {}) => {
3894
4874
  });
3895
4875
  break;
3896
4876
  }
4877
+ case "openrouter": {
4878
+ const reasoningEffort = resolveReasoningEffort(modelConfig.reasoning);
4879
+ const textVerbosity = resolveTextVerbosity(modelConfig.verbosity);
4880
+ llm = new import_core17.ChatOpenAI({
4881
+ clientOptions: buildOpenRouterClientOptions(providerAuth),
4882
+ ...modelConfig.name ? { model: modelConfig.name } : {},
4883
+ ...reasoningEffort ? { reasoningEffort } : {},
4884
+ ...textVerbosity ? { textVerbosity } : {}
4885
+ });
4886
+ break;
4887
+ }
3897
4888
  case "anthropic": {
3898
- llm = new import_core16.ChatAnthropic({
4889
+ llm = new import_core17.ChatAnthropic({
3899
4890
  clientOptions: {
3900
4891
  apiKey: requireApiKeyAuth("Anthropic", providerAuth)
3901
4892
  },
@@ -3906,14 +4897,16 @@ var createAgentFactory = (state, options = {}) => {
3906
4897
  default:
3907
4898
  throw new Error(`Unsupported model.provider: ${provider}`);
3908
4899
  }
3909
- const modelRegistry = await buildModelRegistry(llm);
3910
- const agent = new import_core16.Agent({
4900
+ const modelRegistry = await buildModelRegistry(llm, {
4901
+ strict: provider !== "openrouter"
4902
+ });
4903
+ const agent = new import_core17.Agent({
3911
4904
  llm,
3912
4905
  tools,
3913
4906
  systemPrompt,
3914
- modelRegistry: modelRegistry ?? import_core16.DEFAULT_MODEL_REGISTRY,
4907
+ modelRegistry: modelRegistry ?? import_core17.DEFAULT_MODEL_REGISTRY,
3915
4908
  services: { toolOutputCacheStore },
3916
- canExecuteTool: async (call, rawArgs) => {
4909
+ canExecuteTool: async (call, rawArgs, toolCtx) => {
3917
4910
  const decision = permissionService.evaluate(
3918
4911
  call.function.name,
3919
4912
  rawArgs
@@ -3935,12 +4928,129 @@ var createAgentFactory = (state, options = {}) => {
3935
4928
  };
3936
4929
  }
3937
4930
  const runId = state.activeRunId ?? void 0;
4931
+ (0, import_logger.debugLog)(
4932
+ `permission.request tool=${call.function.name} args=${rawArgs}`
4933
+ );
3938
4934
  const prompt = permissionService.getConfirmPrompt(
3939
4935
  call.function.name,
3940
4936
  rawArgs
3941
4937
  );
4938
+ let previewDiff = null;
4939
+ let previewSummary = null;
4940
+ let previewTruncated = false;
4941
+ let previewFilePath = null;
4942
+ let previewLanguage = null;
4943
+ if (call.function.name === "write") {
4944
+ const parsed = parseToolArgsObject(rawArgs);
4945
+ const filePath = typeof parsed?.file_path === "string" ? parsed.file_path : "";
4946
+ const content = typeof parsed?.content === "string" ? parsed.content : "";
4947
+ const language = typeof parsed?.language === "string" ? parsed.language : "";
4948
+ if (filePath) {
4949
+ previewFilePath = filePath;
4950
+ previewLanguage = resolvePreviewLanguageHint({
4951
+ language,
4952
+ filePath,
4953
+ content
4954
+ }) ?? previewLanguage;
4955
+ try {
4956
+ const sandbox = await getSandboxContext(toolCtx, sandboxKey);
4957
+ const resolved = sandbox.resolvePath(filePath);
4958
+ let before = "";
4959
+ try {
4960
+ const stat = await import_node_fs14.promises.stat(resolved);
4961
+ if (!stat.isDirectory()) {
4962
+ before = await import_node_fs14.promises.readFile(resolved, "utf8");
4963
+ }
4964
+ } catch {
4965
+ before = "";
4966
+ }
4967
+ const preview = buildBoundedDiffPreview(
4968
+ createUnifiedDiff(filePath, before, content)
4969
+ );
4970
+ previewDiff = preview.diff;
4971
+ previewTruncated = preview.truncated;
4972
+ } catch {
4973
+ previewDiff = null;
4974
+ previewSummary = null;
4975
+ previewTruncated = false;
4976
+ }
4977
+ }
4978
+ } else if (call.function.name === "edit" && editTool) {
4979
+ const parsed = parseToolArgsObject(rawArgs);
4980
+ if (parsed) {
4981
+ const filePath = typeof parsed.file_path === "string" ? parsed.file_path : "";
4982
+ const language = typeof parsed.language === "string" ? parsed.language : "";
4983
+ if (filePath) {
4984
+ previewFilePath = filePath;
4985
+ }
4986
+ previewLanguage = resolvePreviewLanguageHint({
4987
+ language,
4988
+ filePath
4989
+ }) ?? previewLanguage;
4990
+ const dryRunInput = { ...parsed, dry_run: true };
4991
+ try {
4992
+ const result = await editTool.executeRaw(
4993
+ JSON.stringify(dryRunInput),
4994
+ toolCtx
4995
+ );
4996
+ if (result && typeof result === "object") {
4997
+ const obj = unwrapToolJsonObject(result);
4998
+ if (!obj) {
4999
+ previewSummary = "Preview unavailable: unexpected dry-run output";
5000
+ } else {
5001
+ const resultFilePath = typeof obj.file_path === "string" ? obj.file_path : "";
5002
+ const resultLanguage = typeof obj.language === "string" ? obj.language : "";
5003
+ if (!previewFilePath && resultFilePath) {
5004
+ previewFilePath = resultFilePath;
5005
+ }
5006
+ const diff = typeof obj.diff === "string" ? obj.diff : "";
5007
+ const summary = typeof obj.summary === "string" ? obj.summary : "";
5008
+ previewLanguage = resolvePreviewLanguageHint({
5009
+ language: resultLanguage || previewLanguage,
5010
+ filePath: previewFilePath || resultFilePath,
5011
+ diff
5012
+ }) ?? previewLanguage;
5013
+ const preview = buildBoundedDiffPreview(diff);
5014
+ previewDiff = preview.diff;
5015
+ previewTruncated = preview.truncated;
5016
+ if (!previewDiff) {
5017
+ previewSummary = summary || "Preview: no diff content";
5018
+ }
5019
+ }
5020
+ }
5021
+ } catch {
5022
+ previewSummary = "Preview unavailable: dry-run failed";
5023
+ }
5024
+ }
5025
+ }
3942
5026
  if (runId) {
3943
- sendRunStatus(runId, "awaiting_ui", "waiting for confirmation");
5027
+ if (previewDiff || previewSummary) {
5028
+ previewLanguage = resolvePreviewLanguageHint({
5029
+ language: previewLanguage,
5030
+ filePath: previewFilePath,
5031
+ diff: previewDiff
5032
+ }) ?? previewLanguage;
5033
+ await sendAgentEventAsync(state, runId, {
5034
+ type: "permission.preview",
5035
+ tool: call.function.name,
5036
+ tool_call_id: call.id,
5037
+ ...previewFilePath ? { file_path: previewFilePath } : {},
5038
+ ...previewLanguage ? { language: previewLanguage } : {},
5039
+ ...previewDiff ? { diff: previewDiff } : {},
5040
+ ...previewSummary ? { summary: previewSummary } : {},
5041
+ ...previewTruncated ? { truncated: true } : {}
5042
+ });
5043
+ }
5044
+ await sendAgentEventAsync(state, runId, {
5045
+ type: "permission.ready",
5046
+ tool: call.function.name,
5047
+ tool_call_id: call.id
5048
+ });
5049
+ await sendRunStatusAsync(
5050
+ runId,
5051
+ "awaiting_ui",
5052
+ "waiting for confirmation"
5053
+ );
3944
5054
  }
3945
5055
  const confirmResult = await requestUiConfirm(state, {
3946
5056
  run_id: runId,
@@ -3994,7 +5104,7 @@ var createAgentFactory = (state, options = {}) => {
3994
5104
  };
3995
5105
 
3996
5106
  // src/mcp/auth-store.ts
3997
- var import_storage5 = require("@codelia/storage");
5107
+ var import_storage6 = require("@codelia/storage");
3998
5108
 
3999
5109
  // src/mcp/manager.ts
4000
5110
  var import_protocol = require("@codelia/protocol");
@@ -4277,7 +5387,7 @@ var HttpMcpClient = class {
4277
5387
  };
4278
5388
 
4279
5389
  // src/mcp/stdio-client.ts
4280
- var import_node_child_process3 = require("child_process");
5390
+ var import_node_child_process4 = require("child_process");
4281
5391
  var import_node_readline = require("readline");
4282
5392
  var StdioMcpClient = class {
4283
5393
  constructor(options) {
@@ -4286,7 +5396,7 @@ var StdioMcpClient = class {
4286
5396
  ...process.env,
4287
5397
  ...options.env ?? {}
4288
5398
  };
4289
- this.child = (0, import_node_child_process3.spawn)(options.command, options.args, {
5399
+ this.child = (0, import_node_child_process4.spawn)(options.command, options.args, {
4290
5400
  cwd: options.cwd,
4291
5401
  env,
4292
5402
  stdio: ["pipe", "pipe", "pipe"]
@@ -4496,8 +5606,8 @@ var parseAuthorizationServerMetadata = (value) => {
4496
5606
  var buildAuthorizationServerMetadataUrl = (issuer) => {
4497
5607
  try {
4498
5608
  const parsed = new URL(issuer);
4499
- const path14 = parsed.pathname === "/" ? "" : parsed.pathname;
4500
- const basePath = path14.startsWith("/") ? path14 : `/${path14}`;
5609
+ const path16 = parsed.pathname === "/" ? "" : parsed.pathname;
5610
+ const basePath = path16.startsWith("/") ? path16 : `/${path16}`;
4501
5611
  const metadataPath = `/.well-known/oauth-authorization-server${basePath}`;
4502
5612
  return new URL(metadataPath, `${parsed.origin}/`).toString();
4503
5613
  } catch {
@@ -4594,24 +5704,24 @@ var fetchDiscoveredOAuthConfig = async (serverUrl) => {
4594
5704
  };
4595
5705
 
4596
5706
  // src/mcp/tooling.ts
4597
- var import_node_crypto4 = __toESM(require("crypto"), 1);
5707
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
4598
5708
  var MAX_SCHEMA_SIZE_BYTES = 64 * 1024;
4599
5709
  var MAX_TOOL_OUTPUT_CHARS = 1e5;
4600
5710
  var MAX_TOOLS_LIST_PAGES = 100;
4601
5711
  var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4602
5712
  var isMcpToolDescriptor = (value) => isRecord4(value) && typeof value.name === "string";
4603
- var toSlug = (value) => {
5713
+ var toSlug2 = (value) => {
4604
5714
  const slug = value.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
4605
5715
  return slug || "tool";
4606
5716
  };
4607
- var hash8 = (value) => import_node_crypto4.default.createHash("sha256").update(value).digest("hex").slice(0, 8);
5717
+ var hash8 = (value) => import_node_crypto5.default.createHash("sha256").update(value).digest("hex").slice(0, 8);
4608
5718
  var clampToolName = (value) => {
4609
5719
  if (value.length <= 63) return value;
4610
5720
  return value.slice(0, 63);
4611
5721
  };
4612
5722
  var buildToolName = (serverId, toolName) => {
4613
- const serverSlug = toSlug(serverId);
4614
- const toolSlug = toSlug(toolName);
5723
+ const serverSlug = toSlug2(serverId);
5724
+ const toolSlug = toSlug2(toolName);
4615
5725
  const fingerprint = hash8(`${serverId}:${toolName}`);
4616
5726
  const maxPrefixLength = 63 - (fingerprint.length + 1);
4617
5727
  const prefix = `mcp_${serverSlug}_${toolSlug}`.slice(0, maxPrefixLength);
@@ -4769,7 +5879,7 @@ var createMcpToolAdapter = (params) => {
4769
5879
 
4770
5880
  // src/mcp/manager.ts
4771
5881
  var OAUTH_TOKEN_EXPIRY_SKEW_MS = 6e4;
4772
- var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
5882
+ var nowIso3 = () => (/* @__PURE__ */ new Date()).toISOString();
4773
5883
  var describeError2 = (error) => error instanceof Error ? error.message : String(error);
4774
5884
  var toListServer = (id, runtime) => ({
4775
5885
  id,
@@ -4791,7 +5901,7 @@ var McpManager = class {
4791
5901
  this.options = options;
4792
5902
  }
4793
5903
  servers = /* @__PURE__ */ new Map();
4794
- authStore = new import_storage5.McpAuthStore();
5904
+ authStore = new import_storage6.McpAuthStore();
4795
5905
  startPromise = null;
4796
5906
  authFile = { version: 1, servers: {} };
4797
5907
  discoveredOAuth = /* @__PURE__ */ new Map();
@@ -4901,7 +6011,7 @@ var McpManager = class {
4901
6011
  runtime.tools_count = runtime.toolAdapters.length;
4902
6012
  runtime.state = "ready";
4903
6013
  runtime.last_error = void 0;
4904
- runtime.last_connected_at = nowIso();
6014
+ runtime.last_connected_at = nowIso3();
4905
6015
  connectOptions?.onStatus?.(
4906
6016
  `MCP server ready: ${serverId} (${runtime.tools_count ?? 0} tools)`
4907
6017
  );
@@ -5154,7 +6264,9 @@ var McpManager = class {
5154
6264
  };
5155
6265
 
5156
6266
  // src/rpc/handlers.ts
5157
- var import_storage7 = require("@codelia/storage");
6267
+ var import_config_loader3 = require("@codelia/config-loader");
6268
+ var import_protocol8 = require("@codelia/protocol");
6269
+ var import_storage8 = require("@codelia/storage");
5158
6270
 
5159
6271
  // src/constants.ts
5160
6272
  var SERVER_NAME = "codelia-runtime";
@@ -5162,6 +6274,7 @@ var SERVER_VERSION = "0.1.0";
5162
6274
  var PROTOCOL_VERSION = "0";
5163
6275
 
5164
6276
  // src/rpc/context.ts
6277
+ var import_protocol2 = require("@codelia/protocol");
5165
6278
  var createContextHandlers = ({
5166
6279
  state,
5167
6280
  log: log2
@@ -5251,7 +6364,7 @@ var createContextHandlers = ({
5251
6364
  sendResult(id, result);
5252
6365
  } catch (error) {
5253
6366
  sendError(id, {
5254
- code: -32e3,
6367
+ code: import_protocol2.RPC_ERROR_CODE.RUNTIME_INTERNAL,
5255
6368
  message: `context.inspect failed: ${String(error)}`
5256
6369
  });
5257
6370
  }
@@ -5260,14 +6373,50 @@ var createContextHandlers = ({
5260
6373
  };
5261
6374
 
5262
6375
  // src/rpc/history.ts
5263
- var import_node_fs12 = require("fs");
5264
- var import_node_path11 = __toESM(require("path"), 1);
6376
+ var import_node_fs15 = require("fs");
6377
+ var import_node_path13 = __toESM(require("path"), 1);
5265
6378
  var import_node_readline2 = require("readline");
5266
- var import_storage6 = require("@codelia/storage");
6379
+ var import_protocol3 = require("@codelia/protocol");
6380
+ var import_storage7 = require("@codelia/storage");
5267
6381
  var DEFAULT_HISTORY_RUNS = 20;
5268
6382
  var DEFAULT_HISTORY_EVENTS = 1500;
6383
+ var runStartInputToHiddenMessage = (input) => {
6384
+ if (!input) {
6385
+ return "";
6386
+ }
6387
+ if (input.type === "text") {
6388
+ return input.text;
6389
+ }
6390
+ if (!Array.isArray(input.parts)) {
6391
+ return "";
6392
+ }
6393
+ let message = "";
6394
+ for (const part of input.parts) {
6395
+ if (!part || typeof part !== "object") {
6396
+ continue;
6397
+ }
6398
+ if (part.type === "text") {
6399
+ message += part.text;
6400
+ continue;
6401
+ }
6402
+ if (part.type === "image_url") {
6403
+ message += "[image]";
6404
+ }
6405
+ }
6406
+ return message;
6407
+ };
6408
+ var parseRunHeader = (headerLine) => {
6409
+ try {
6410
+ const header = JSON.parse(headerLine);
6411
+ if (!header || typeof header !== "object") return null;
6412
+ if (header.type !== "header") return null;
6413
+ return header;
6414
+ } catch {
6415
+ return null;
6416
+ }
6417
+ };
5269
6418
  var readFirstLine = async (filePath) => {
5270
- const stream = (0, import_node_fs12.createReadStream)(filePath, { encoding: "utf8" });
6419
+ const stream = (0, import_node_fs15.createReadStream)(filePath, { encoding: "utf8" });
5271
6420
  const reader = (0, import_node_readline2.createInterface)({
5272
6421
  input: stream,
5273
6422
  crlfDelay: Number.POSITIVE_INFINITY
@@ -5282,12 +6431,12 @@ var readFirstLine = async (filePath) => {
5282
6431
  stream.destroy();
5283
6432
  }
5284
6433
  };
5285
- var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5286
- const paths = (0, import_storage6.resolveStoragePaths)();
6434
+ var collectRunCandidates = async (log2) => {
6435
+ const paths = (0, import_storage7.resolveStoragePaths)();
5287
6436
  const sessionsDir = paths.sessionsDir;
5288
6437
  let yearEntries;
5289
6438
  try {
5290
- yearEntries = await import_node_fs12.promises.readdir(sessionsDir, {
6439
+ yearEntries = await import_node_fs15.promises.readdir(sessionsDir, {
5291
6440
  withFileTypes: true,
5292
6441
  encoding: "utf8"
5293
6442
  });
@@ -5299,10 +6448,10 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5299
6448
  for (const yearEntry of yearEntries) {
5300
6449
  if (!yearEntry.isDirectory()) continue;
5301
6450
  if (!/^\d{4}$/.test(yearEntry.name)) continue;
5302
- const yearPath = import_node_path11.default.join(sessionsDir, yearEntry.name);
6451
+ const yearPath = import_node_path13.default.join(sessionsDir, yearEntry.name);
5303
6452
  let monthEntries;
5304
6453
  try {
5305
- monthEntries = await import_node_fs12.promises.readdir(yearPath, {
6454
+ monthEntries = await import_node_fs15.promises.readdir(yearPath, {
5306
6455
  withFileTypes: true,
5307
6456
  encoding: "utf8"
5308
6457
  });
@@ -5312,10 +6461,10 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5312
6461
  for (const monthEntry of monthEntries) {
5313
6462
  if (!monthEntry.isDirectory()) continue;
5314
6463
  if (!/^\d{2}$/.test(monthEntry.name)) continue;
5315
- const monthPath = import_node_path11.default.join(yearPath, monthEntry.name);
6464
+ const monthPath = import_node_path13.default.join(yearPath, monthEntry.name);
5316
6465
  let dayEntries;
5317
6466
  try {
5318
- dayEntries = await import_node_fs12.promises.readdir(monthPath, {
6467
+ dayEntries = await import_node_fs15.promises.readdir(monthPath, {
5319
6468
  withFileTypes: true,
5320
6469
  encoding: "utf8"
5321
6470
  });
@@ -5325,10 +6474,10 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5325
6474
  for (const dayEntry of dayEntries) {
5326
6475
  if (!dayEntry.isDirectory()) continue;
5327
6476
  if (!/^\d{2}$/.test(dayEntry.name)) continue;
5328
- const dayPath = import_node_path11.default.join(monthPath, dayEntry.name);
6477
+ const dayPath = import_node_path13.default.join(monthPath, dayEntry.name);
5329
6478
  let files;
5330
6479
  try {
5331
- files = await import_node_fs12.promises.readdir(dayPath, {
6480
+ files = await import_node_fs15.promises.readdir(dayPath, {
5332
6481
  withFileTypes: true,
5333
6482
  encoding: "utf8"
5334
6483
  });
@@ -5338,9 +6487,9 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5338
6487
  for (const file of files) {
5339
6488
  if (!file.isFile()) continue;
5340
6489
  if (!file.name.endsWith(".jsonl")) continue;
5341
- const filePath = import_node_path11.default.join(dayPath, file.name);
6490
+ const filePath = import_node_path13.default.join(dayPath, file.name);
5342
6491
  try {
5343
- const stat = await import_node_fs12.promises.stat(filePath);
6492
+ const stat = await import_node_fs15.promises.stat(filePath);
5344
6493
  candidates.push({ path: filePath, mtimeMs: stat.mtimeMs });
5345
6494
  } catch {
5346
6495
  }
@@ -5349,6 +6498,10 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5349
6498
  }
5350
6499
  }
5351
6500
  candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
6501
+ return candidates;
6502
+ };
6503
+ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
6504
+ const candidates = await collectRunCandidates(log2);
5352
6505
  const runs = [];
5353
6506
  for (const candidate of candidates) {
5354
6507
  if (runs.length >= maxRuns) break;
@@ -5359,25 +6512,22 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5359
6512
  continue;
5360
6513
  }
5361
6514
  if (!headerLine) continue;
5362
- try {
5363
- const header = JSON.parse(headerLine);
5364
- if (header.type !== "header") continue;
5365
- if (header.session_id !== sessionId) continue;
5366
- const runId = header.run_id ?? import_node_path11.default.basename(candidate.path, ".jsonl");
5367
- const startedAt = header.started_at ?? new Date(candidate.mtimeMs).toISOString();
5368
- runs.push({
5369
- path: candidate.path,
5370
- run_id: runId,
5371
- started_at: startedAt
5372
- });
5373
- } catch {
5374
- }
6515
+ const header = parseRunHeader(headerLine);
6516
+ if (!header) continue;
6517
+ if (header.session_id !== sessionId) continue;
6518
+ const runId = header.run_id ?? import_node_path13.default.basename(candidate.path, ".jsonl");
6519
+ const startedAt = header.started_at ?? new Date(candidate.mtimeMs).toISOString();
6520
+ runs.push({
6521
+ path: candidate.path,
6522
+ run_id: runId,
6523
+ started_at: startedAt
6524
+ });
5375
6525
  }
5376
6526
  runs.sort((a, b) => a.started_at.localeCompare(b.started_at));
5377
6527
  return runs;
5378
6528
  };
5379
6529
  var readJsonl = async function* (filePath) {
5380
- const stream = (0, import_node_fs12.createReadStream)(filePath, { encoding: "utf8" });
6530
+ const stream = (0, import_node_fs15.createReadStream)(filePath, { encoding: "utf8" });
5381
6531
  let buffer = "";
5382
6532
  for await (const chunk of stream) {
5383
6533
  buffer += chunk;
@@ -5419,7 +6569,7 @@ var createHistoryHandlers = ({
5419
6569
  sessions = await sessionStateStore.list();
5420
6570
  } catch (error) {
5421
6571
  sendError(id, {
5422
- code: -32006,
6572
+ code: import_protocol3.RPC_ERROR_CODE.SESSION_LIST_FAILED,
5423
6573
  message: `session list failed: ${String(error)}`
5424
6574
  });
5425
6575
  return;
@@ -5434,7 +6584,7 @@ var createHistoryHandlers = ({
5434
6584
  const handleSessionHistory = async (id, params) => {
5435
6585
  const sessionId = params?.session_id?.trim();
5436
6586
  if (!sessionId) {
5437
- sendError(id, { code: -32602, message: "session_id is required" });
6587
+ sendError(id, { code: import_protocol3.RPC_ERROR_CODE.INVALID_PARAMS, message: "session_id is required" });
5438
6588
  return;
5439
6589
  }
5440
6590
  const maxRuns = Math.max(0, params?.max_runs ?? DEFAULT_HISTORY_RUNS);
@@ -5464,7 +6614,7 @@ var createHistoryHandlers = ({
5464
6614
  }
5465
6615
  if (!record || typeof record !== "object") continue;
5466
6616
  if (record.type === "run.start") {
5467
- const input = record.input?.type === "text" ? record.input.text : "";
6617
+ const input = runStartInputToHiddenMessage(record.input);
5468
6618
  if (input) {
5469
6619
  sendHistoryEvent(record.run_id, -1, {
5470
6620
  type: "hidden_user_message",
@@ -5493,8 +6643,268 @@ var createHistoryHandlers = ({
5493
6643
 
5494
6644
  // src/rpc/model.ts
5495
6645
  var import_config_loader2 = require("@codelia/config-loader");
5496
- var import_core17 = require("@codelia/core");
6646
+ var import_core18 = require("@codelia/core");
5497
6647
  var import_model_metadata2 = require("@codelia/model-metadata");
6648
+ var import_protocol4 = require("@codelia/protocol");
6649
+ var isSupportedProvider = (provider) => provider === "openai" || provider === "anthropic" || provider === "openrouter";
6650
+ var resolveProviderModelEntry = (providerEntries, provider, model) => {
6651
+ if (!providerEntries) return null;
6652
+ return providerEntries[model] ?? providerEntries[`${provider}/${model}`] ?? null;
6653
+ };
6654
+ var parseReleaseTimestamp = (entry) => {
6655
+ const releaseDate = entry?.releaseDate?.trim();
6656
+ if (!releaseDate) return null;
6657
+ const timestamp = Date.parse(releaseDate);
6658
+ if (Number.isNaN(timestamp)) return null;
6659
+ return timestamp;
6660
+ };
6661
+ var normalizeUsdPer1M = (value) => {
6662
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
6663
+ return void 0;
6664
+ }
6665
+ return value;
6666
+ };
6667
+ var buildModelListDetail = (entry) => {
6668
+ if (!entry) return null;
6669
+ const detail = {};
6670
+ if (entry.releaseDate?.trim()) {
6671
+ detail.release_date = entry.releaseDate.trim();
6672
+ }
6673
+ const limits = entry.limits;
6674
+ if (typeof limits?.contextWindow === "number" && Number.isFinite(limits.contextWindow) && limits.contextWindow > 0) {
6675
+ detail.context_window = limits.contextWindow;
6676
+ }
6677
+ if (typeof limits?.inputTokens === "number" && Number.isFinite(limits.inputTokens) && limits.inputTokens > 0) {
6678
+ detail.max_input_tokens = limits.inputTokens;
6679
+ }
6680
+ if (typeof limits?.outputTokens === "number" && Number.isFinite(limits.outputTokens) && limits.outputTokens > 0) {
6681
+ detail.max_output_tokens = limits.outputTokens;
6682
+ }
6683
+ const inputCost = normalizeUsdPer1M(entry.cost?.input);
6684
+ if (inputCost !== void 0) {
6685
+ detail.cost_per_1m_input_tokens_usd = inputCost;
6686
+ }
6687
+ const outputCost = normalizeUsdPer1M(entry.cost?.output);
6688
+ if (outputCost !== void 0) {
6689
+ detail.cost_per_1m_output_tokens_usd = outputCost;
6690
+ }
6691
+ return Object.keys(detail).length ? detail : null;
6692
+ };
6693
+ var sortModelsByReleaseDate = (models, provider, providerEntries) => {
6694
+ const sortable = models.map((model, index) => ({
6695
+ model,
6696
+ index,
6697
+ releaseTimestamp: parseReleaseTimestamp(
6698
+ resolveProviderModelEntry(providerEntries, provider, model)
6699
+ )
6700
+ }));
6701
+ sortable.sort((left, right) => {
6702
+ if (left.releaseTimestamp !== null && right.releaseTimestamp !== null && left.releaseTimestamp !== right.releaseTimestamp) {
6703
+ return right.releaseTimestamp - left.releaseTimestamp;
6704
+ }
6705
+ if (left.releaseTimestamp !== null && right.releaseTimestamp === null) {
6706
+ return -1;
6707
+ }
6708
+ if (left.releaseTimestamp === null && right.releaseTimestamp !== null) {
6709
+ return 1;
6710
+ }
6711
+ return left.index - right.index;
6712
+ });
6713
+ return sortable.map((item) => item.model);
6714
+ };
6715
+ var loadProviderModelEntries = async (provider) => {
6716
+ const metadataService = new import_model_metadata2.ModelMetadataServiceImpl();
6717
+ const allEntries = await metadataService.getAllModelEntries();
6718
+ return allEntries[provider] ?? null;
6719
+ };
6720
+ var OPENROUTER_BASE_URL2 = "https://openrouter.ai/api/v1";
6721
+ var parseUnixSecondsToDate = (value) => {
6722
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
6723
+ return void 0;
6724
+ }
6725
+ const date = new Date(Math.floor(value) * 1e3);
6726
+ const timestamp = date.getTime();
6727
+ if (!Number.isFinite(timestamp)) {
6728
+ return void 0;
6729
+ }
6730
+ return date.toISOString().slice(0, 10);
6731
+ };
6732
+ var parsePositiveNumber = (value) => {
6733
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
6734
+ return void 0;
6735
+ }
6736
+ return value;
6737
+ };
6738
+ var buildOpenRouterHeaders = (apiKey) => {
6739
+ const headers = new Headers();
6740
+ headers.set("Authorization", `Bearer ${apiKey}`);
6741
+ const referer = readEnvValue("OPENROUTER_HTTP_REFERER");
6742
+ if (referer) {
6743
+ headers.set("HTTP-Referer", referer);
6744
+ }
6745
+ const title = readEnvValue("OPENROUTER_X_TITLE");
6746
+ if (title) {
6747
+ headers.set("X-Title", title);
6748
+ }
6749
+ return headers;
6750
+ };
6751
+ var resolveOpenRouterApiKey = async () => {
6752
+ const envKey = readEnvValue("OPENROUTER_API_KEY");
6753
+ if (envKey) {
6754
+ return envKey;
6755
+ }
6756
+ const store = new AuthStore();
6757
+ const auth = await store.load();
6758
+ const providerAuth = auth.providers.openrouter;
6759
+ if (providerAuth?.method !== "api_key") {
6760
+ return null;
6761
+ }
6762
+ const value = providerAuth.api_key.trim();
6763
+ return value ? value : null;
6764
+ };
6765
+ var resolveOpenRouterApiKeyWithPrompt = async ({
6766
+ state,
6767
+ log: log2
6768
+ }) => {
6769
+ const existing = await resolveOpenRouterApiKey();
6770
+ if (existing) {
6771
+ return existing;
6772
+ }
6773
+ if (!state) {
6774
+ throw new Error("OpenRouter API key is required");
6775
+ }
6776
+ const authResolver = await AuthResolver.create(state, log2);
6777
+ const auth = await authResolver.resolveProviderAuth("openrouter");
6778
+ if (auth.method !== "api_key") {
6779
+ throw new Error("OpenRouter API key is required");
6780
+ }
6781
+ const value = auth.api_key.trim();
6782
+ if (!value) {
6783
+ throw new Error("OpenRouter API key is required");
6784
+ }
6785
+ return value;
6786
+ };
6787
+ var parseOpenRouterModel = (value) => {
6788
+ if (!value || typeof value !== "object") {
6789
+ return null;
6790
+ }
6791
+ const entry = value;
6792
+ const id = typeof entry.id === "string" ? entry.id.trim() : "";
6793
+ if (!id) {
6794
+ return null;
6795
+ }
6796
+ const topProvider = entry.top_provider && typeof entry.top_provider === "object" ? entry.top_provider : null;
6797
+ return {
6798
+ id,
6799
+ created: typeof entry.created === "number" && Number.isFinite(entry.created) ? entry.created : void 0,
6800
+ context_length: parsePositiveNumber(entry.context_length),
6801
+ top_provider: topProvider ? {
6802
+ context_length: parsePositiveNumber(topProvider.context_length),
6803
+ max_completion_tokens: parsePositiveNumber(
6804
+ topProvider.max_completion_tokens
6805
+ )
6806
+ } : void 0
6807
+ };
6808
+ };
6809
+ var fetchOpenRouterModels = async (apiKey) => {
6810
+ const response = await fetch(`${OPENROUTER_BASE_URL2}/models`, {
6811
+ headers: buildOpenRouterHeaders(apiKey)
6812
+ });
6813
+ if (!response.ok) {
6814
+ const body = await response.text().catch(() => "");
6815
+ const snippet = body ? body.slice(0, 300) : "(empty)";
6816
+ throw new Error(
6817
+ `OpenRouter models request failed (${response.status}): ${snippet}`
6818
+ );
6819
+ }
6820
+ const payload = await response.json();
6821
+ if (!payload || typeof payload !== "object") {
6822
+ throw new Error("OpenRouter models response is not an object");
6823
+ }
6824
+ const data = payload.data;
6825
+ if (!Array.isArray(data)) {
6826
+ throw new Error("OpenRouter models response has no data array");
6827
+ }
6828
+ const models = data.map((entry) => parseOpenRouterModel(entry)).filter((entry) => !!entry);
6829
+ models.sort((left, right) => {
6830
+ const leftCreated = left.created ?? 0;
6831
+ const rightCreated = right.created ?? 0;
6832
+ if (leftCreated !== rightCreated) {
6833
+ return rightCreated - leftCreated;
6834
+ }
6835
+ return left.id.localeCompare(right.id);
6836
+ });
6837
+ return models;
6838
+ };
6839
+ var buildOpenRouterModelList = async ({
6840
+ includeDetails,
6841
+ state,
6842
+ log: log2
6843
+ }) => {
6844
+ const apiKey = await resolveOpenRouterApiKeyWithPrompt({ state, log: log2 });
6845
+ const models = await fetchOpenRouterModels(apiKey);
6846
+ const ids = models.map((model) => model.id);
6847
+ if (!includeDetails) {
6848
+ return { models: ids };
6849
+ }
6850
+ const details = {};
6851
+ for (const model of models) {
6852
+ const detail = {};
6853
+ const releaseDate = parseUnixSecondsToDate(model.created);
6854
+ if (releaseDate) {
6855
+ detail.release_date = releaseDate;
6856
+ }
6857
+ const contextWindow = model.context_length ?? model.top_provider?.context_length;
6858
+ if (contextWindow && contextWindow > 0) {
6859
+ detail.context_window = contextWindow;
6860
+ detail.max_input_tokens = contextWindow;
6861
+ }
6862
+ const maxOutputTokens = model.top_provider?.max_completion_tokens;
6863
+ if (maxOutputTokens && maxOutputTokens > 0) {
6864
+ detail.max_output_tokens = maxOutputTokens;
6865
+ }
6866
+ if (Object.keys(detail).length) {
6867
+ details[model.id] = detail;
6868
+ }
6869
+ }
6870
+ return Object.keys(details).length ? { models: ids, details } : { models: ids };
6871
+ };
6872
+ var buildProviderModelList = async ({
6873
+ provider,
6874
+ includeDetails,
6875
+ state,
6876
+ log: log2
6877
+ }) => {
6878
+ if (provider === "openrouter") {
6879
+ return buildOpenRouterModelList({ includeDetails, state, log: log2 });
6880
+ }
6881
+ let providerEntries = null;
6882
+ try {
6883
+ providerEntries = await loadProviderModelEntries(provider);
6884
+ } catch (error) {
6885
+ if (includeDetails) {
6886
+ log2(`model.list details error: ${error}`);
6887
+ }
6888
+ }
6889
+ const models = sortModelsByReleaseDate(
6890
+ (0, import_core18.listModels)(import_core18.DEFAULT_MODEL_REGISTRY, provider).map((model) => model.id),
6891
+ provider,
6892
+ providerEntries
6893
+ );
6894
+ if (!includeDetails || !providerEntries) {
6895
+ return { models };
6896
+ }
6897
+ const details = {};
6898
+ for (const model of models) {
6899
+ const detail = buildModelListDetail(
6900
+ resolveProviderModelEntry(providerEntries, provider, model)
6901
+ );
6902
+ if (detail) {
6903
+ details[model] = detail;
6904
+ }
6905
+ }
6906
+ return Object.keys(details).length ? { models, details } : { models };
6907
+ };
5498
6908
  var createModelHandlers = ({
5499
6909
  state,
5500
6910
  log: log2
@@ -5502,9 +6912,9 @@ var createModelHandlers = ({
5502
6912
  const handleModelList = async (id, params) => {
5503
6913
  const requestedProvider = params?.provider;
5504
6914
  const includeDetails = params?.include_details ?? false;
5505
- if (requestedProvider && requestedProvider !== "openai" && requestedProvider !== "anthropic") {
6915
+ if (requestedProvider && !isSupportedProvider(requestedProvider)) {
5506
6916
  sendError(id, {
5507
- code: -32602,
6917
+ code: import_protocol4.RPC_ERROR_CODE.INVALID_PARAMS,
5508
6918
  message: `unsupported provider: ${requestedProvider}`
5509
6919
  });
5510
6920
  return;
@@ -5518,54 +6928,35 @@ var createModelHandlers = ({
5518
6928
  current = config.name;
5519
6929
  }
5520
6930
  } catch (error) {
5521
- sendError(id, { code: -32e3, message: String(error) });
6931
+ sendError(id, { code: import_protocol4.RPC_ERROR_CODE.RUNTIME_INTERNAL, message: String(error) });
5522
6932
  return;
5523
6933
  }
5524
6934
  const provider = requestedProvider ?? configuredProvider ?? "openai";
5525
- if (provider !== "openai" && provider !== "anthropic") {
6935
+ if (!isSupportedProvider(provider)) {
5526
6936
  sendError(id, {
5527
- code: -32602,
6937
+ code: import_protocol4.RPC_ERROR_CODE.INVALID_PARAMS,
5528
6938
  message: `unsupported provider: ${provider}`
5529
6939
  });
5530
6940
  return;
5531
6941
  }
5532
- const models = (0, import_core17.listModels)(import_core17.DEFAULT_MODEL_REGISTRY, provider).map((model) => model.id).sort();
6942
+ let models;
6943
+ let details;
6944
+ try {
6945
+ const result2 = await buildProviderModelList({
6946
+ provider,
6947
+ includeDetails,
6948
+ state,
6949
+ log: log2
6950
+ });
6951
+ models = result2.models;
6952
+ details = result2.details;
6953
+ } catch (error) {
6954
+ sendError(id, { code: import_protocol4.RPC_ERROR_CODE.RUNTIME_INTERNAL, message: String(error) });
6955
+ return;
6956
+ }
5533
6957
  if (current && !models.includes(current)) {
5534
6958
  current = void 0;
5535
6959
  }
5536
- let details;
5537
- if (includeDetails) {
5538
- try {
5539
- const metadataService = new import_model_metadata2.ModelMetadataServiceImpl();
5540
- const entries = await metadataService.getAllModelEntries();
5541
- const providerEntries = entries[provider];
5542
- if (providerEntries) {
5543
- const nextDetails = {};
5544
- for (const model of models) {
5545
- const limits = providerEntries[model]?.limits;
5546
- if (!limits) continue;
5547
- const detail = {};
5548
- if (typeof limits.contextWindow === "number" && limits.contextWindow > 0) {
5549
- detail.context_window = limits.contextWindow;
5550
- }
5551
- if (typeof limits.inputTokens === "number" && limits.inputTokens > 0) {
5552
- detail.max_input_tokens = limits.inputTokens;
5553
- }
5554
- if (typeof limits.outputTokens === "number" && limits.outputTokens > 0) {
5555
- detail.max_output_tokens = limits.outputTokens;
5556
- }
5557
- if (Object.keys(detail).length > 0) {
5558
- nextDetails[model] = detail;
5559
- }
5560
- }
5561
- if (Object.keys(nextDetails).length > 0) {
5562
- details = nextDetails;
5563
- }
5564
- }
5565
- } catch (error) {
5566
- log2(`model.list details error: ${error}`);
5567
- }
5568
- }
5569
6960
  const result = {
5570
6961
  provider,
5571
6962
  models,
@@ -5576,26 +6967,28 @@ var createModelHandlers = ({
5576
6967
  };
5577
6968
  const handleModelSet = async (id, params) => {
5578
6969
  if (state.activeRunId) {
5579
- sendError(id, { code: -32001, message: "runtime busy" });
6970
+ sendError(id, { code: import_protocol4.RPC_ERROR_CODE.RUNTIME_BUSY, message: "runtime busy" });
5580
6971
  return;
5581
6972
  }
5582
6973
  const provider = params?.provider ?? "openai";
5583
6974
  const name = params?.name?.trim();
5584
6975
  if (!name) {
5585
- sendError(id, { code: -32602, message: "model name is required" });
6976
+ sendError(id, { code: import_protocol4.RPC_ERROR_CODE.INVALID_PARAMS, message: "model name is required" });
5586
6977
  return;
5587
6978
  }
5588
- if (provider !== "openai" && provider !== "anthropic") {
6979
+ if (provider !== "openai" && provider !== "anthropic" && provider !== "openrouter") {
5589
6980
  sendError(id, {
5590
- code: -32602,
6981
+ code: import_protocol4.RPC_ERROR_CODE.INVALID_PARAMS,
5591
6982
  message: `unsupported provider: ${provider}`
5592
6983
  });
5593
6984
  return;
5594
6985
  }
5595
- const spec = (0, import_core17.resolveModel)(import_core17.DEFAULT_MODEL_REGISTRY, name, provider);
5596
- if (!spec) {
5597
- sendError(id, { code: -32602, message: `unknown model: ${name}` });
5598
- return;
6986
+ if (provider !== "openrouter") {
6987
+ const spec = (0, import_core18.resolveModel)(import_core18.DEFAULT_MODEL_REGISTRY, name, provider);
6988
+ if (!spec) {
6989
+ sendError(id, { code: import_protocol4.RPC_ERROR_CODE.INVALID_PARAMS, message: `unknown model: ${name}` });
6990
+ return;
6991
+ }
5599
6992
  }
5600
6993
  try {
5601
6994
  const configPath = resolveConfigPath();
@@ -5605,17 +6998,21 @@ var createModelHandlers = ({
5605
6998
  sendResult(id, result);
5606
6999
  log2(`model.set ${provider}/${name}`);
5607
7000
  } catch (error) {
5608
- sendError(id, { code: -32e3, message: String(error) });
7001
+ sendError(id, { code: import_protocol4.RPC_ERROR_CODE.RUNTIME_INTERNAL, message: String(error) });
5609
7002
  }
5610
7003
  };
5611
7004
  return { handleModelList, handleModelSet };
5612
7005
  };
5613
7006
 
7007
+ // src/rpc/run.ts
7008
+ var import_protocol5 = require("@codelia/protocol");
7009
+
5614
7010
  // src/rpc/run-debug.ts
5615
- var import_core18 = require("@codelia/core");
7011
+ var import_core19 = require("@codelia/core");
5616
7012
  var DEBUG_MAX_CONTENT_CHARS = 2e3;
5617
7013
  var DEBUG_MAX_LOG_CHARS = 2e4;
5618
7014
  var DEBUG_MAX_EVENT_RESULT_CHARS = 500;
7015
+ var DEBUG_MAX_EVENT_ARGS_CHARS = 2e3;
5619
7016
  var truncateText2 = (value, maxChars) => {
5620
7017
  if (value.length <= maxChars) return value;
5621
7018
  return `${value.slice(0, maxChars)}...[truncated]`;
@@ -5628,7 +7025,7 @@ var stringifyUnknown = (value) => {
5628
7025
  }
5629
7026
  };
5630
7027
  var contentToDebugText = (content, maxChars) => {
5631
- const text = (0, import_core18.stringifyContent)(content, {
7028
+ const text = (0, import_core19.stringifyContent)(content, {
5632
7029
  mode: "log",
5633
7030
  joiner: "\n",
5634
7031
  includeOtherPayload: true
@@ -5687,7 +7084,9 @@ var summarizeRunEvent = (event) => {
5687
7084
  case "tool_call": {
5688
7085
  const tool = typeof event.tool === "string" ? event.tool : "unknown";
5689
7086
  const toolCallId = typeof event.tool_call_id === "string" ? event.tool_call_id : "unknown";
5690
- return `type=tool_call tool=${tool} tool_call_id=${toolCallId}`;
7087
+ const rawArgs = typeof event.raw_args === "string" ? event.raw_args : stringifyUnknown(event.args);
7088
+ const rawArgsSnippet = truncateText2(rawArgs, DEBUG_MAX_EVENT_ARGS_CHARS);
7089
+ return `type=tool_call tool=${tool} tool_call_id=${toolCallId} raw_args=${stringifyUnknown(rawArgsSnippet)}`;
5691
7090
  }
5692
7091
  case "tool_result": {
5693
7092
  const tool = typeof event.tool === "string" ? event.tool : "unknown";
@@ -5722,7 +7121,7 @@ var logRunDebug = (log2, runId, message) => {
5722
7121
  if (!(0, import_logger.isDebugEnabled)()) return;
5723
7122
  log2(`run debug run_id=${runId} ${message}`);
5724
7123
  };
5725
- var normalizeCancelledHistory = (messages) => {
7124
+ var normalizeToolCallHistory = (messages) => {
5726
7125
  const assistantCallIds = /* @__PURE__ */ new Set();
5727
7126
  const toolOutputCallIds = /* @__PURE__ */ new Set();
5728
7127
  for (const message of messages) {
@@ -5806,8 +7205,92 @@ var prepareRunInputText = (inputText) => {
5806
7205
  return `${inputText}${buildSkillMentionHint(mentions)}`;
5807
7206
  };
5808
7207
 
7208
+ // src/rpc/run-input.ts
7209
+ var isRunInputImageMediaType = (value) => value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
7210
+ var isRunInputImageDetail = (value) => value === "auto" || value === "low" || value === "high";
7211
+ var normalizeRunInputTextPart = (part) => {
7212
+ if (typeof part.text !== "string") {
7213
+ throw new Error("run.start input.parts[text].text must be a string");
7214
+ }
7215
+ return {
7216
+ type: "text",
7217
+ text: prepareRunInputText(part.text)
7218
+ };
7219
+ };
7220
+ var normalizeRunInputImagePart = (part) => {
7221
+ const imageUrl = part.image_url;
7222
+ if (!imageUrl || typeof imageUrl !== "object") {
7223
+ throw new Error("run.start input.parts[image_url].image_url is required");
7224
+ }
7225
+ if (typeof imageUrl.url !== "string" || imageUrl.url.length === 0) {
7226
+ throw new Error(
7227
+ "run.start input.parts[image_url].image_url.url must be a non-empty string"
7228
+ );
7229
+ }
7230
+ if (imageUrl.media_type !== void 0 && !isRunInputImageMediaType(imageUrl.media_type)) {
7231
+ throw new Error(
7232
+ "run.start input.parts[image_url].image_url.media_type must be png/jpeg/webp/gif"
7233
+ );
7234
+ }
7235
+ if (imageUrl.detail !== void 0 && !isRunInputImageDetail(imageUrl.detail)) {
7236
+ throw new Error(
7237
+ "run.start input.parts[image_url].image_url.detail must be auto/low/high"
7238
+ );
7239
+ }
7240
+ return {
7241
+ type: "image_url",
7242
+ image_url: {
7243
+ url: imageUrl.url,
7244
+ ...imageUrl.media_type ? { media_type: imageUrl.media_type } : {},
7245
+ ...imageUrl.detail ? { detail: imageUrl.detail } : {}
7246
+ }
7247
+ };
7248
+ };
7249
+ var normalizeRunInput = (input) => {
7250
+ if (input.type === "text") {
7251
+ if (typeof input.text !== "string") {
7252
+ throw new Error("run.start input.text must be a string");
7253
+ }
7254
+ return prepareRunInputText(input.text);
7255
+ }
7256
+ if (!Array.isArray(input.parts)) {
7257
+ throw new Error("run.start input.parts must be an array");
7258
+ }
7259
+ return input.parts.map((part) => {
7260
+ if (!part || typeof part !== "object") {
7261
+ throw new Error("run.start input.parts entry must be an object");
7262
+ }
7263
+ if (part.type === "text") {
7264
+ return normalizeRunInputTextPart(part);
7265
+ }
7266
+ if (part.type === "image_url") {
7267
+ return normalizeRunInputImagePart(part);
7268
+ }
7269
+ throw new Error(
7270
+ `run.start input.parts type is not supported: ${String(part.type)}`
7271
+ );
7272
+ });
7273
+ };
7274
+ var runInputLengthForDebug = (input) => {
7275
+ if (typeof input === "string") {
7276
+ return input.length;
7277
+ }
7278
+ let total = 0;
7279
+ for (const part of input) {
7280
+ if (part.type === "text") {
7281
+ total += part.text.length;
7282
+ continue;
7283
+ }
7284
+ if (part.type === "image_url") {
7285
+ total += part.image_url.url.length;
7286
+ }
7287
+ }
7288
+ return total;
7289
+ };
7290
+
5809
7291
  // src/rpc/run.ts
5810
- var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
7292
+ var nowIso4 = () => (/* @__PURE__ */ new Date()).toISOString();
7293
+ var SESSION_STATE_SAVE_DEBOUNCE_MS = 1500;
5811
7294
  var createSessionAppender = (store, onError) => {
5812
7295
  let chain = Promise.resolve();
5813
7296
  return (record) => {
@@ -5819,7 +7302,7 @@ var createSessionAppender = (store, onError) => {
5819
7302
  var buildSessionState = (sessionId, runId, messages, invokeSeq) => ({
5820
7303
  schema_version: 1,
5821
7304
  session_id: sessionId,
5822
- updated_at: nowIso2(),
7305
+ updated_at: nowIso4(),
5823
7306
  run_id: runId,
5824
7307
  invoke_seq: invokeSeq,
5825
7308
  messages
@@ -5830,18 +7313,26 @@ var createRunHandlers = ({
5830
7313
  log: log2,
5831
7314
  runEventStoreFactory,
5832
7315
  sessionStateStore,
5833
- appendSession
7316
+ appendSession,
7317
+ beforeRunStart
5834
7318
  }) => {
5835
7319
  let activeRunAbort = null;
5836
7320
  let runStartQueue = Promise.resolve();
5837
7321
  const normalizeRunHistoryAfterCancel = (runId, runtimeAgent) => {
5838
7322
  const currentMessages = runtimeAgent.getHistoryMessages();
5839
- const normalizedMessages = normalizeCancelledHistory(currentMessages);
7323
+ const normalizedMessages = normalizeToolCallHistory(currentMessages);
5840
7324
  if (normalizedMessages !== currentMessages) {
5841
7325
  runtimeAgent.replaceHistoryMessages(normalizedMessages);
5842
7326
  log2(`run.cancel normalized history ${runId}`);
5843
7327
  }
5844
7328
  };
7329
+ const normalizeRestoredSessionMessages = (messages, sessionId) => {
7330
+ const normalizedMessages = normalizeToolCallHistory(messages);
7331
+ if (normalizedMessages !== messages) {
7332
+ log2(`session restore normalized history ${sessionId}`);
7333
+ }
7334
+ return normalizedMessages;
7335
+ };
5845
7336
  const emitRunStatus = (runId, status, message) => {
5846
7337
  sendRunStatus(runId, status, message);
5847
7338
  const suffix = message ? ` message=${message}` : "";
@@ -5849,7 +7340,7 @@ var createRunHandlers = ({
5849
7340
  appendSession({
5850
7341
  type: "run.status",
5851
7342
  run_id: runId,
5852
- ts: nowIso2(),
7343
+ ts: nowIso4(),
5853
7344
  status,
5854
7345
  ...message ? { message } : {}
5855
7346
  });
@@ -5858,22 +7349,43 @@ var createRunHandlers = ({
5858
7349
  appendSession({
5859
7350
  type: "run.end",
5860
7351
  run_id: runId,
5861
- ts: nowIso2(),
7352
+ ts: nowIso4(),
5862
7353
  outcome,
5863
7354
  ...final !== void 0 ? { final } : {}
5864
7355
  });
5865
7356
  };
5866
7357
  const handleRunStart = (id, params) => {
5867
7358
  const run = async () => {
7359
+ let normalizedInput;
7360
+ try {
7361
+ normalizedInput = normalizeRunInput(params.input);
7362
+ } catch (error) {
7363
+ sendError(id, {
7364
+ code: import_protocol5.RPC_ERROR_CODE.INVALID_PARAMS,
7365
+ message: String(error)
7366
+ });
7367
+ return;
7368
+ }
7369
+ if (beforeRunStart) {
7370
+ try {
7371
+ await beforeRunStart();
7372
+ } catch (error) {
7373
+ sendError(id, {
7374
+ code: import_protocol5.RPC_ERROR_CODE.RUNTIME_INTERNAL,
7375
+ message: `startup onboarding failed: ${String(error)}`
7376
+ });
7377
+ return;
7378
+ }
7379
+ }
5868
7380
  if (state.activeRunId) {
5869
- sendError(id, { code: -32001, message: "runtime busy" });
7381
+ sendError(id, { code: import_protocol5.RPC_ERROR_CODE.RUNTIME_BUSY, message: "runtime busy" });
5870
7382
  return;
5871
7383
  }
5872
7384
  let runtimeAgent;
5873
7385
  try {
5874
7386
  runtimeAgent = await getAgent();
5875
7387
  } catch (error) {
5876
- sendError(id, { code: -32e3, message: String(error) });
7388
+ sendError(id, { code: import_protocol5.RPC_ERROR_CODE.RUNTIME_INTERNAL, message: String(error) });
5877
7389
  return;
5878
7390
  }
5879
7391
  const requestedSessionId = params.session_id?.trim() || void 0;
@@ -5884,16 +7396,20 @@ var createRunHandlers = ({
5884
7396
  resumeState = await sessionStateStore.load(requestedSessionId);
5885
7397
  } catch (error) {
5886
7398
  sendError(id, {
5887
- code: -32005,
7399
+ code: import_protocol5.RPC_ERROR_CODE.SESSION_LOAD_FAILED,
5888
7400
  message: `session load failed: ${String(error)}`
5889
7401
  });
5890
7402
  return;
5891
7403
  }
5892
7404
  if (!resumeState) {
5893
- sendError(id, { code: -32004, message: "session not found" });
7405
+ sendError(id, { code: import_protocol5.RPC_ERROR_CODE.SESSION_NOT_FOUND, message: "session not found" });
5894
7406
  return;
5895
7407
  }
5896
- const messages = Array.isArray(resumeState.messages) ? resumeState.messages : [];
7408
+ const restoredMessages = Array.isArray(resumeState.messages) ? resumeState.messages : [];
7409
+ const messages = normalizeRestoredSessionMessages(
7410
+ restoredMessages,
7411
+ requestedSessionId
7412
+ );
5897
7413
  runtimeAgent.replaceHistoryMessages(messages);
5898
7414
  sessionId = requestedSessionId;
5899
7415
  state.sessionId = sessionId;
@@ -5901,17 +7417,25 @@ var createRunHandlers = ({
5901
7417
  try {
5902
7418
  resumeState = await sessionStateStore.load(state.sessionId);
5903
7419
  } catch (error) {
5904
- log2(`session state reload error: ${String(error)}`);
7420
+ sendError(id, {
7421
+ code: import_protocol5.RPC_ERROR_CODE.SESSION_LOAD_FAILED,
7422
+ message: `session reload failed: ${String(error)}`
7423
+ });
7424
+ return;
5905
7425
  }
5906
7426
  if (resumeState) {
5907
- const messages = Array.isArray(resumeState.messages) ? resumeState.messages : [];
7427
+ const restoredMessages = Array.isArray(resumeState.messages) ? resumeState.messages : [];
7428
+ const messages = normalizeRestoredSessionMessages(
7429
+ restoredMessages,
7430
+ state.sessionId
7431
+ );
5908
7432
  runtimeAgent.replaceHistoryMessages(messages);
5909
7433
  }
5910
7434
  } else if (!state.sessionId) {
5911
7435
  state.sessionId = sessionId;
5912
7436
  }
5913
7437
  const runId = state.nextRunId();
5914
- const startedAt = nowIso2();
7438
+ const startedAt = nowIso4();
5915
7439
  state.beginRun(runId, params.ui_context ?? state.lastUiContext);
5916
7440
  const runAbortController = new AbortController();
5917
7441
  activeRunAbort = { runId, controller: runAbortController };
@@ -5961,7 +7485,7 @@ var createRunHandlers = ({
5961
7485
  type: "run.start",
5962
7486
  run_id: runId,
5963
7487
  session_id: sessionId,
5964
- ts: nowIso2(),
7488
+ ts: nowIso4(),
5965
7489
  input: params.input,
5966
7490
  ui_context: params.ui_context,
5967
7491
  meta: params.meta
@@ -5972,25 +7496,58 @@ var createRunHandlers = ({
5972
7496
  emitRunStatus(runId, "running");
5973
7497
  void (async () => {
5974
7498
  let finalResponse;
5975
- let cancelledWhileStreaming = false;
5976
- const preparedInputText = prepareRunInputText(params.input.text);
7499
+ let sessionSaveChain = Promise.resolve();
7500
+ let lastSessionSaveAt = 0;
7501
+ const queueSessionSave = async (reason) => {
7502
+ sessionSaveChain = sessionSaveChain.then(async () => {
7503
+ if (!sessionId) return;
7504
+ const messages = runtimeAgent.getHistoryMessages();
7505
+ const snapshotMessages = normalizeToolCallHistory(messages);
7506
+ if (snapshotMessages !== messages) {
7507
+ logRunDebug(
7508
+ log2,
7509
+ runId,
7510
+ `session.save normalized reason=${reason}`
7511
+ );
7512
+ }
7513
+ const snapshot = buildSessionState(
7514
+ sessionId,
7515
+ runId,
7516
+ snapshotMessages,
7517
+ session.invoke_seq
7518
+ );
7519
+ await sessionStateStore.save(snapshot);
7520
+ logRunDebug(log2, runId, `session.save ${reason}`);
7521
+ }).catch((error) => {
7522
+ log2(`Error: session-state save failed: ${String(error)}`);
7523
+ });
7524
+ await sessionSaveChain;
7525
+ };
7526
+ const maybeDebouncedSessionSave = () => {
7527
+ const now = Date.now();
7528
+ if (now - lastSessionSaveAt < SESSION_STATE_SAVE_DEBOUNCE_MS) {
7529
+ return;
7530
+ }
7531
+ lastSessionSaveAt = now;
7532
+ void queueSessionSave("debounced");
7533
+ };
5977
7534
  try {
5978
7535
  logRunDebug(
5979
7536
  log2,
5980
7537
  runId,
5981
- `stream.start input_chars=${preparedInputText.length}`
7538
+ `stream.start input_chars=${runInputLengthForDebug(normalizedInput)}`
5982
7539
  );
5983
- for await (const event of runtimeAgent.runStream(preparedInputText, {
7540
+ for await (const event of runtimeAgent.runStream(normalizedInput, {
5984
7541
  session,
5985
7542
  signal: runAbortController.signal,
5986
7543
  forceCompaction: params.force_compaction
5987
7544
  })) {
5988
7545
  if (state.cancelRequested) {
5989
- cancelledWhileStreaming = true;
5990
7546
  break;
5991
7547
  }
5992
7548
  if (event.type === "final") {
5993
7549
  finalResponse = event.content;
7550
+ await queueSessionSave("final");
5994
7551
  }
5995
7552
  if (event.type === "compaction_complete") {
5996
7553
  logCompactionSnapshot(log2, runId, runtimeAgent, event.compacted);
@@ -6014,7 +7571,7 @@ var createRunHandlers = ({
6014
7571
  appendSession({
6015
7572
  type: "agent.event",
6016
7573
  run_id: runId,
6017
- ts: nowIso2(),
7574
+ ts: nowIso4(),
6018
7575
  seq,
6019
7576
  event
6020
7577
  });
@@ -6025,14 +7582,16 @@ var createRunHandlers = ({
6025
7582
  appendSession({
6026
7583
  type: "run.context",
6027
7584
  run_id: runId,
6028
- ts: nowIso2(),
7585
+ ts: nowIso4(),
6029
7586
  context_left_percent: contextLeftPercent
6030
7587
  });
6031
7588
  }
7589
+ maybeDebouncedSessionSave();
6032
7590
  }
6033
- if (cancelledWhileStreaming) {
7591
+ if (state.cancelRequested) {
6034
7592
  normalizeRunHistoryAfterCancel(runId, runtimeAgent);
6035
7593
  }
7594
+ await queueSessionSave("terminal");
6036
7595
  const status = state.cancelRequested ? "cancelled" : "completed";
6037
7596
  emitRunStatus(runId, status);
6038
7597
  emitRunEnd(
@@ -6044,15 +7603,17 @@ var createRunHandlers = ({
6044
7603
  const err = error instanceof Error ? error : new Error(String(error));
6045
7604
  if (state.cancelRequested || isAbortLikeError(err)) {
6046
7605
  normalizeRunHistoryAfterCancel(runId, runtimeAgent);
7606
+ await queueSessionSave("cancelled");
6047
7607
  emitRunStatus(runId, "cancelled", err.message || "cancelled");
6048
7608
  emitRunEnd(runId, "cancelled", finalResponse);
6049
7609
  return;
6050
7610
  }
7611
+ await queueSessionSave("error");
6051
7612
  emitRunStatus(runId, "error", err.message);
6052
7613
  appendSession({
6053
7614
  type: "run.error",
6054
7615
  run_id: runId,
6055
- ts: nowIso2(),
7616
+ ts: nowIso4(),
6056
7617
  error: {
6057
7618
  name: err.name,
6058
7619
  message: err.message,
@@ -6062,18 +7623,10 @@ var createRunHandlers = ({
6062
7623
  emitRunEnd(runId, "error");
6063
7624
  } finally {
6064
7625
  logRunDebug(log2, runId, "stream.finally");
6065
- if (state.sessionId) {
6066
- const messages = runtimeAgent.getHistoryMessages();
6067
- const snapshot = buildSessionState(
6068
- sessionId,
6069
- runId,
6070
- messages,
6071
- session.invoke_seq
6072
- );
6073
- void sessionStateStore.save(snapshot).catch((error) => {
6074
- log2(`session-state error: ${String(error)}`);
6075
- });
7626
+ if (state.cancelRequested) {
7627
+ normalizeRunHistoryAfterCancel(runId, runtimeAgent);
6076
7628
  }
7629
+ await queueSessionSave("finally");
6077
7630
  if (activeRunAbort?.runId === runId) {
6078
7631
  activeRunAbort = null;
6079
7632
  }
@@ -6099,19 +7652,20 @@ var createRunHandlers = ({
6099
7652
  log2(`run.cancel ${params.run_id} (${params.reason ?? "no reason"})`);
6100
7653
  return;
6101
7654
  }
6102
- sendError(id, { code: -32002, message: "run not found" });
7655
+ sendError(id, { code: import_protocol5.RPC_ERROR_CODE.RUN_NOT_FOUND, message: "run not found" });
6103
7656
  };
6104
7657
  return { handleRunStart, handleRunCancel };
6105
7658
  };
6106
7659
 
6107
7660
  // src/rpc/skills.ts
6108
- var import_node_path12 = __toESM(require("path"), 1);
7661
+ var import_node_path14 = __toESM(require("path"), 1);
7662
+ var import_protocol6 = require("@codelia/protocol");
6109
7663
  var createSkillsHandlers = ({
6110
7664
  state,
6111
7665
  log: log2
6112
7666
  }) => {
6113
7667
  const handleSkillsList = async (id, params) => {
6114
- const requestedCwd = params?.cwd ? import_node_path12.default.resolve(params.cwd) : void 0;
7668
+ const requestedCwd = params?.cwd ? import_node_path14.default.resolve(params.cwd) : void 0;
6115
7669
  const workingDir = requestedCwd ?? state.runtimeWorkingDir ?? state.lastUiContext?.cwd ?? process.cwd();
6116
7670
  const forceReload = params?.force_reload ?? false;
6117
7671
  if (!forceReload) {
@@ -6135,7 +7689,7 @@ var createSkillsHandlers = ({
6135
7689
  });
6136
7690
  } catch (error) {
6137
7691
  sendError(id, {
6138
- code: -32e3,
7692
+ code: import_protocol6.RPC_ERROR_CODE.RUNTIME_INTERNAL,
6139
7693
  message: `skills resolver init failed: ${String(error)}`
6140
7694
  });
6141
7695
  return;
@@ -6157,7 +7711,7 @@ var createSkillsHandlers = ({
6157
7711
  } catch (error) {
6158
7712
  log2(`skills.list error: ${String(error)}`);
6159
7713
  sendError(id, {
6160
- code: -32e3,
7714
+ code: import_protocol6.RPC_ERROR_CODE.RUNTIME_INTERNAL,
6161
7715
  message: `skills list failed: ${String(error)}`
6162
7716
  });
6163
7717
  }
@@ -6165,6 +7719,75 @@ var createSkillsHandlers = ({
6165
7719
  return { handleSkillsList };
6166
7720
  };
6167
7721
 
7722
+ // src/rpc/tool.ts
7723
+ var import_protocol7 = require("@codelia/protocol");
7724
+ var normalizeToolResult = (result) => {
7725
+ switch (result.type) {
7726
+ case "json":
7727
+ return result.value;
7728
+ case "text":
7729
+ return result.text;
7730
+ case "parts":
7731
+ return result.parts;
7732
+ }
7733
+ };
7734
+ var createToolContext = () => {
7735
+ const deps = /* @__PURE__ */ Object.create(null);
7736
+ const cache = /* @__PURE__ */ new Map();
7737
+ const resolve = async (key) => {
7738
+ if (cache.has(key.id)) {
7739
+ return cache.get(key.id);
7740
+ }
7741
+ const value = await key.create();
7742
+ cache.set(key.id, value);
7743
+ return value;
7744
+ };
7745
+ return {
7746
+ deps,
7747
+ resolve,
7748
+ now: () => /* @__PURE__ */ new Date()
7749
+ };
7750
+ };
7751
+ var createToolHandlers = ({
7752
+ state,
7753
+ getAgent
7754
+ }) => {
7755
+ const handleToolCall = async (id, params) => {
7756
+ const toolName = params?.name;
7757
+ if (!toolName) {
7758
+ sendError(id, { code: import_protocol7.RPC_ERROR_CODE.INVALID_PARAMS, message: "tool name is required" });
7759
+ return;
7760
+ }
7761
+ if (!state.tools) {
7762
+ await getAgent();
7763
+ }
7764
+ const tool = state.tools?.find((entry) => entry.name === toolName);
7765
+ if (!tool) {
7766
+ sendError(id, { code: import_protocol7.RPC_ERROR_CODE.INVALID_PARAMS, message: `unknown tool: ${toolName}` });
7767
+ return;
7768
+ }
7769
+ try {
7770
+ const result = await tool.executeRaw(
7771
+ JSON.stringify(params?.arguments ?? {}),
7772
+ createToolContext()
7773
+ );
7774
+ const response = {
7775
+ ok: true,
7776
+ result: normalizeToolResult(result)
7777
+ };
7778
+ sendResult(id, response);
7779
+ } catch (error) {
7780
+ sendError(id, {
7781
+ code: import_protocol7.RPC_ERROR_CODE.RUNTIME_INTERNAL,
7782
+ message: `tool call failed: ${String(error)}`
7783
+ });
7784
+ }
7785
+ };
7786
+ return {
7787
+ handleToolCall
7788
+ };
7789
+ };
7790
+
6168
7791
  // src/rpc/handlers.ts
6169
7792
  var createRuntimeHandlers = ({
6170
7793
  state,
@@ -6172,31 +7795,132 @@ var createRuntimeHandlers = ({
6172
7795
  log: log2,
6173
7796
  mcpManager: injectedMcpManager,
6174
7797
  sessionStateStore: injectedSessionStateStore,
6175
- runEventStoreFactory: injectedRunEventStoreFactory
7798
+ runEventStoreFactory: injectedRunEventStoreFactory,
7799
+ buildProviderModelList: injectedBuildProviderModelList
6176
7800
  }) => {
6177
- const sessionStateStore = injectedSessionStateStore ?? new import_storage7.SessionStateStoreImpl({
7801
+ const sessionStateStore = injectedSessionStateStore ?? new import_storage8.SessionStateStoreImpl({
6178
7802
  onError: (error, context) => {
6179
7803
  log2(
6180
7804
  `session-state ${context.action} error${context.detail ? ` (${context.detail})` : ""}: ${String(error)}`
6181
7805
  );
6182
7806
  }
6183
7807
  });
6184
- const runEventStoreFactory = injectedRunEventStoreFactory ?? new import_storage7.RunEventStoreFactoryImpl();
7808
+ const runEventStoreFactory = injectedRunEventStoreFactory ?? new import_storage8.RunEventStoreFactoryImpl();
6185
7809
  const mcpManager = injectedMcpManager ?? {
6186
7810
  start: async () => void 0,
6187
7811
  list: () => ({ servers: [] })
6188
7812
  };
7813
+ const buildProviderModelList2 = injectedBuildProviderModelList ?? buildProviderModelList;
7814
+ let startupOnboardingPromise = null;
7815
+ let startupOnboardingStarted = false;
6189
7816
  const appendSession = (record) => {
6190
7817
  if (!state.sessionAppend) return;
6191
7818
  state.sessionAppend(record);
6192
7819
  };
7820
+ const formatUsdPer1M = (value) => {
7821
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
7822
+ return null;
7823
+ }
7824
+ const fixed = value.toFixed(4);
7825
+ return fixed.replace(/\.?0+$/, "");
7826
+ };
7827
+ const buildModelPickDetail = (details) => {
7828
+ const parts = [];
7829
+ if (details?.release_date) {
7830
+ parts.push(`released ${details.release_date}`);
7831
+ }
7832
+ const inputCost = formatUsdPer1M(details?.cost_per_1m_input_tokens_usd);
7833
+ const outputCost = formatUsdPer1M(details?.cost_per_1m_output_tokens_usd);
7834
+ if (inputCost !== null || outputCost !== null) {
7835
+ parts.push(
7836
+ `cost in/out ${inputCost ?? "-"}/${outputCost ?? "-"} USD per 1M`
7837
+ );
7838
+ }
7839
+ return parts.join(" \u2022 ");
7840
+ };
7841
+ const runStartupOnboarding = async () => {
7842
+ const supportsPick = !!state.uiCapabilities?.supports_pick;
7843
+ const supportsPrompt = !!state.uiCapabilities?.supports_prompt;
7844
+ if (!supportsPick || !supportsPrompt) {
7845
+ return;
7846
+ }
7847
+ const authResolver = await AuthResolver.create(state, log2);
7848
+ if (authResolver.hasAnyAvailableAuth()) {
7849
+ return;
7850
+ }
7851
+ const providerPick = await requestUiPick(state, {
7852
+ title: "Let's set up your provider to get started.",
7853
+ items: SUPPORTED_PROVIDERS.map((provider2) => ({
7854
+ id: provider2,
7855
+ label: provider2,
7856
+ detail: provider2 === "openai" ? "OAuth (ChatGPT Plus/Pro) or API key" : provider2 === "openrouter" ? "API key (OpenRouter)" : "API key"
7857
+ })),
7858
+ multi: false
7859
+ });
7860
+ const pickedProvider = providerPick?.ids?.[0];
7861
+ if (!pickedProvider || !SUPPORTED_PROVIDERS.includes(pickedProvider)) {
7862
+ log2("startup onboarding skipped (provider not selected)");
7863
+ return;
7864
+ }
7865
+ const provider = pickedProvider;
7866
+ try {
7867
+ await authResolver.resolveProviderAuth(provider);
7868
+ } catch (error) {
7869
+ log2(`startup onboarding auth failed: ${String(error)}`);
7870
+ return;
7871
+ }
7872
+ const { models, details } = await buildProviderModelList2({
7873
+ provider,
7874
+ includeDetails: true,
7875
+ state,
7876
+ log: log2
7877
+ });
7878
+ if (!models.length) {
7879
+ log2(`startup onboarding model list returned no models for ${provider}`);
7880
+ return;
7881
+ }
7882
+ const modelPick = await requestUiPick(state, {
7883
+ title: `Select model (${provider})`,
7884
+ items: models.map((model) => ({
7885
+ id: model,
7886
+ label: model,
7887
+ detail: buildModelPickDetail(details?.[model]) || void 0
7888
+ })),
7889
+ multi: false
7890
+ });
7891
+ const selectedModel = modelPick?.ids?.[0];
7892
+ if (!selectedModel || !models.includes(selectedModel)) {
7893
+ log2("startup onboarding skipped (model not selected)");
7894
+ return;
7895
+ }
7896
+ const configPath = resolveConfigPath();
7897
+ await (0, import_config_loader3.updateModelConfig)(configPath, { provider, name: selectedModel });
7898
+ state.agent = null;
7899
+ log2(`startup onboarding completed: ${provider}/${selectedModel}`);
7900
+ };
7901
+ const launchStartupOnboarding = () => {
7902
+ if (startupOnboardingStarted) {
7903
+ return;
7904
+ }
7905
+ startupOnboardingStarted = true;
7906
+ startupOnboardingPromise = runStartupOnboarding().catch((error) => {
7907
+ log2(`startup onboarding failed: ${String(error)}`);
7908
+ }).finally(() => {
7909
+ startupOnboardingPromise = null;
7910
+ });
7911
+ };
7912
+ const waitForStartupOnboarding = async () => {
7913
+ if (!startupOnboardingPromise) return;
7914
+ await startupOnboardingPromise;
7915
+ };
6193
7916
  const { handleRunStart, handleRunCancel } = createRunHandlers({
6194
7917
  state,
6195
7918
  getAgent,
6196
7919
  log: log2,
6197
7920
  runEventStoreFactory,
6198
7921
  sessionStateStore,
6199
- appendSession
7922
+ appendSession,
7923
+ beforeRunStart: waitForStartupOnboarding
6200
7924
  });
6201
7925
  const { handleSessionList, handleSessionHistory } = createHistoryHandlers({
6202
7926
  sessionStateStore,
@@ -6214,6 +7938,10 @@ var createRuntimeHandlers = ({
6214
7938
  state,
6215
7939
  log: log2
6216
7940
  });
7941
+ const { handleToolCall } = createToolHandlers({
7942
+ state,
7943
+ getAgent
7944
+ });
6217
7945
  const handleInitialize = (id, params) => {
6218
7946
  const result = {
6219
7947
  protocol_version: PROTOCOL_VERSION,
@@ -6223,13 +7951,16 @@ var createRuntimeHandlers = ({
6223
7951
  supports_ui_requests: true,
6224
7952
  supports_mcp_list: true,
6225
7953
  supports_skills_list: true,
6226
- supports_context_inspect: true
7954
+ supports_context_inspect: true,
7955
+ supports_tool_call: true,
7956
+ supports_permission_preflight_events: true
6227
7957
  }
6228
7958
  };
6229
7959
  sendResult(id, result);
6230
7960
  log2(`initialize from ${params.client?.name ?? "unknown"}`);
6231
7961
  state.lastClientInfo = params.client ?? null;
6232
7962
  state.setUiCapabilities(params.ui_capabilities);
7963
+ launchStartupOnboarding();
6233
7964
  };
6234
7965
  const handleUiContextUpdate = (params) => {
6235
7966
  state.updateUiContext(params);
@@ -6239,7 +7970,7 @@ var createRuntimeHandlers = ({
6239
7970
  };
6240
7971
  const handleAuthLogout = async (id, params) => {
6241
7972
  if (state.activeRunId) {
6242
- sendError(id, { code: -32001, message: "runtime busy" });
7973
+ sendError(id, { code: import_protocol8.RPC_ERROR_CODE.RUNTIME_BUSY, message: "runtime busy" });
6243
7974
  return;
6244
7975
  }
6245
7976
  const clearSession = params?.clear_session ?? true;
@@ -6247,7 +7978,7 @@ var createRuntimeHandlers = ({
6247
7978
  const supportsConfirm = !!state.uiCapabilities?.supports_confirm;
6248
7979
  if (!supportsConfirm) {
6249
7980
  sendError(id, {
6250
- code: -32e3,
7981
+ code: import_protocol8.RPC_ERROR_CODE.RUNTIME_INTERNAL,
6251
7982
  message: "UI confirmation is required for logout"
6252
7983
  });
6253
7984
  return;
@@ -6289,7 +8020,7 @@ var createRuntimeHandlers = ({
6289
8020
  log2(`auth.logout session_cleared=${clearSession}`);
6290
8021
  } catch (error) {
6291
8022
  sendError(id, {
6292
- code: -32e3,
8023
+ code: import_protocol8.RPC_ERROR_CODE.RUNTIME_INTERNAL,
6293
8024
  message: `auth logout failed: ${String(error)}`
6294
8025
  });
6295
8026
  }
@@ -6312,6 +8043,8 @@ var createRuntimeHandlers = ({
6312
8043
  return handleModelList(req.id, req.params);
6313
8044
  case "model.set":
6314
8045
  return handleModelSet(req.id, req.params);
8046
+ case "tool.call":
8047
+ return handleToolCall(req.id, req.params);
6315
8048
  case "mcp.list":
6316
8049
  await mcpManager.start?.();
6317
8050
  return sendResult(
@@ -6323,7 +8056,7 @@ var createRuntimeHandlers = ({
6323
8056
  case "context.inspect":
6324
8057
  return handleContextInspect(req.id, req.params);
6325
8058
  default:
6326
- return sendError(req.id, { code: -32601, message: "method not found" });
8059
+ return sendError(req.id, { code: import_protocol8.RPC_ERROR_CODE.METHOD_NOT_FOUND, message: "method not found" });
6327
8060
  }
6328
8061
  };
6329
8062
  const handleNotification = (note) => {
@@ -6350,7 +8083,7 @@ var createRuntimeHandlers = ({
6350
8083
  };
6351
8084
 
6352
8085
  // src/runtime-state.ts
6353
- var import_node_crypto5 = __toESM(require("crypto"), 1);
8086
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
6354
8087
  var RuntimeState = class {
6355
8088
  runSeq = /* @__PURE__ */ new Map();
6356
8089
  uiRequestCounter = 0;
@@ -6363,6 +8096,7 @@ var RuntimeState = class {
6363
8096
  uiCapabilities = null;
6364
8097
  systemPrompt = null;
6365
8098
  toolDefinitions = null;
8099
+ tools = null;
6366
8100
  sessionId = null;
6367
8101
  sessionAppend = null;
6368
8102
  agent = null;
@@ -6373,10 +8107,10 @@ var RuntimeState = class {
6373
8107
  runtimeWorkingDir = null;
6374
8108
  runtimeSandboxRoot = null;
6375
8109
  nextRunId() {
6376
- return import_node_crypto5.default.randomUUID();
8110
+ return import_node_crypto6.default.randomUUID();
6377
8111
  }
6378
8112
  nextSessionId() {
6379
- return import_node_crypto5.default.randomUUID();
8113
+ return import_node_crypto6.default.randomUUID();
6380
8114
  }
6381
8115
  beginRun(runId, uiContext) {
6382
8116
  this.activeRunId = runId;
@@ -6466,42 +8200,59 @@ var RuntimeState = class {
6466
8200
 
6467
8201
  // src/runtime.ts
6468
8202
  var startRuntime = () => {
6469
- const state = new RuntimeState();
6470
- const workingDir = process.env.CODELIA_SANDBOX_ROOT ? import_node_path13.default.resolve(process.env.CODELIA_SANDBOX_ROOT) : process.cwd();
6471
- state.runtimeWorkingDir = workingDir;
6472
- state.runtimeSandboxRoot = workingDir;
6473
- const mcpManager = new McpManager({ workingDir, log: import_logger.log });
6474
- void mcpManager.start({
6475
- onStatus: (message) => (0, import_logger.log)(`mcp: ${message}`),
6476
- requestOAuthTokens: ({ server_id, oauth: oauth2, error }) => requestMcpOAuthTokensWithRunStatus(state, server_id, oauth2, error)
6477
- });
6478
- const getAgent = createAgentFactory(state, { mcpManager });
6479
- const { processMessage } = createRuntimeHandlers({
6480
- state,
6481
- getAgent,
6482
- log: import_logger.log,
6483
- mcpManager
6484
- });
6485
- (0, import_logger.log)("runtime started");
6486
- process.stdin.setEncoding("utf8");
6487
- let buffer = "";
6488
- process.stdin.on("data", (chunk) => {
6489
- buffer += chunk;
6490
- let index = buffer.indexOf("\n");
6491
- while (index >= 0) {
6492
- const line = buffer.slice(0, index).trim();
6493
- buffer = buffer.slice(index + 1);
6494
- if (line) {
6495
- try {
6496
- const msg = JSON.parse(line);
6497
- processMessage(msg);
6498
- } catch (error) {
6499
- (0, import_logger.log)(`invalid json: ${String(error)}`);
6500
- }
8203
+ void (async () => {
8204
+ const state = new RuntimeState();
8205
+ const workingDir = process.env.CODELIA_SANDBOX_ROOT ? import_node_path15.default.resolve(process.env.CODELIA_SANDBOX_ROOT) : process.cwd();
8206
+ state.runtimeWorkingDir = workingDir;
8207
+ state.runtimeSandboxRoot = workingDir;
8208
+ const sessionStateStore = new import_storage9.SessionStateStoreImpl({
8209
+ onError: (error, context) => {
8210
+ (0, import_logger.log)(
8211
+ `Error: session-state ${context.action} error${context.detail ? ` (${context.detail})` : ""}: ${String(error)}`
8212
+ );
6501
8213
  }
6502
- index = buffer.indexOf("\n");
8214
+ });
8215
+ try {
8216
+ await sessionStateStore.list();
8217
+ } catch (error) {
8218
+ (0, import_logger.log)(`Error: session index database is not available: ${String(error)}`);
8219
+ process.exit(1);
8220
+ return;
6503
8221
  }
6504
- });
8222
+ const mcpManager = new McpManager({ workingDir, log: import_logger.log });
8223
+ void mcpManager.start({
8224
+ onStatus: (message) => (0, import_logger.log)(`mcp: ${message}`),
8225
+ requestOAuthTokens: ({ server_id, oauth: oauth2, error }) => requestMcpOAuthTokensWithRunStatus(state, server_id, oauth2, error)
8226
+ });
8227
+ const getAgent = createAgentFactory(state, { mcpManager });
8228
+ const { processMessage } = createRuntimeHandlers({
8229
+ state,
8230
+ getAgent,
8231
+ log: import_logger.log,
8232
+ mcpManager,
8233
+ sessionStateStore
8234
+ });
8235
+ (0, import_logger.log)("runtime started");
8236
+ process.stdin.setEncoding("utf8");
8237
+ let buffer = "";
8238
+ process.stdin.on("data", (chunk) => {
8239
+ buffer += chunk;
8240
+ let index = buffer.indexOf("\n");
8241
+ while (index >= 0) {
8242
+ const line = buffer.slice(0, index).trim();
8243
+ buffer = buffer.slice(index + 1);
8244
+ if (line) {
8245
+ try {
8246
+ const msg = JSON.parse(line);
8247
+ processMessage(msg);
8248
+ } catch (error) {
8249
+ (0, import_logger.log)(`invalid json: ${String(error)}`);
8250
+ }
8251
+ }
8252
+ index = buffer.indexOf("\n");
8253
+ }
8254
+ });
8255
+ })();
6505
8256
  };
6506
8257
 
6507
8258
  // src/index.ts