@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.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // src/runtime.ts
2
- import path13 from "path";
2
+ import path15 from "path";
3
+ import { SessionStateStoreImpl as SessionStateStoreImpl2 } from "@codelia/storage";
3
4
 
4
5
  // src/agent-factory.ts
6
+ import { promises as fs14 } from "fs";
5
7
  import {
6
8
  Agent,
7
9
  ChatAnthropic,
@@ -840,10 +842,13 @@ var describeRpcMessage = (msg) => {
840
842
  }
841
843
  return "unknown";
842
844
  };
845
+ var serializeMessage = (msg) => ({
846
+ payload: `${JSON.stringify(msg)}
847
+ `,
848
+ label: describeRpcMessage(msg)
849
+ });
843
850
  var send = (msg) => {
844
- const payload = `${JSON.stringify(msg)}
845
- `;
846
- const label = describeRpcMessage(msg);
851
+ const { payload, label } = serializeMessage(msg);
847
852
  const writable = process.stdout.write(payload, (error) => {
848
853
  if (error) {
849
854
  debugLog(`transport.write.error label=${label} message=${error.message}`);
@@ -853,6 +858,20 @@ var send = (msg) => {
853
858
  debugLog(`transport.backpressure label=${label} bytes=${payload.length}`);
854
859
  }
855
860
  };
861
+ var sendAsync = async (msg) => new Promise((resolve) => {
862
+ const { payload, label } = serializeMessage(msg);
863
+ const writable = process.stdout.write(payload, (error) => {
864
+ if (error) {
865
+ debugLog(
866
+ `transport.write.error label=${label} message=${error.message}`
867
+ );
868
+ }
869
+ resolve();
870
+ });
871
+ if (!writable) {
872
+ debugLog(`transport.backpressure label=${label} bytes=${payload.length}`);
873
+ }
874
+ });
856
875
  var sendError = (id, error) => {
857
876
  const response = { jsonrpc: "2.0", id, error };
858
877
  send(response);
@@ -876,6 +895,21 @@ var sendAgentEvent = (state, runId, event) => {
876
895
  send(notify);
877
896
  return seq;
878
897
  };
898
+ var sendAgentEventAsync = async (state, runId, event) => {
899
+ if (state.shouldSuppressEvent(runId)) return null;
900
+ const seq = state.nextSequence(runId);
901
+ const notify = {
902
+ jsonrpc: "2.0",
903
+ method: "agent.event",
904
+ params: {
905
+ run_id: runId,
906
+ seq,
907
+ event
908
+ }
909
+ };
910
+ await sendAsync(notify);
911
+ return seq;
912
+ };
879
913
  var sendRunStatus = (runId, status, message) => {
880
914
  const notify = {
881
915
  jsonrpc: "2.0",
@@ -888,6 +922,18 @@ var sendRunStatus = (runId, status, message) => {
888
922
  };
889
923
  send(notify);
890
924
  };
925
+ var sendRunStatusAsync = async (runId, status, message) => {
926
+ const notify = {
927
+ jsonrpc: "2.0",
928
+ method: "run.status",
929
+ params: {
930
+ run_id: runId,
931
+ status,
932
+ message
933
+ }
934
+ };
935
+ await sendAsync(notify);
936
+ };
891
937
  var sendRunContext = (runId, contextLeftPercent) => {
892
938
  const notify = {
893
939
  jsonrpc: "2.0",
@@ -966,10 +1012,15 @@ var AuthStore = class {
966
1012
  };
967
1013
 
968
1014
  // src/auth/resolver.ts
969
- var SUPPORTED_PROVIDERS = ["openai", "anthropic"];
1015
+ var SUPPORTED_PROVIDERS = [
1016
+ "openai",
1017
+ "anthropic",
1018
+ "openrouter"
1019
+ ];
970
1020
  var API_KEY_ENV = {
971
1021
  openai: "OPENAI_API_KEY",
972
- anthropic: "ANTHROPIC_API_KEY"
1022
+ anthropic: "ANTHROPIC_API_KEY",
1023
+ openrouter: "OPENROUTER_API_KEY"
973
1024
  };
974
1025
  var AuthResolver = class _AuthResolver {
975
1026
  auth;
@@ -987,6 +1038,12 @@ var AuthResolver = class _AuthResolver {
987
1038
  const auth = await store.load();
988
1039
  return new _AuthResolver(state, log2, store, auth);
989
1040
  }
1041
+ hasAnyAvailableAuth() {
1042
+ return SUPPORTED_PROVIDERS.some((provider) => {
1043
+ if (this.auth.providers[provider]) return true;
1044
+ return Boolean(readEnvValue(API_KEY_ENV[provider]));
1045
+ });
1046
+ }
990
1047
  async resolveProvider(preferred) {
991
1048
  const preferredProvider = preferred && SUPPORTED_PROVIDERS.includes(preferred) ? preferred : null;
992
1049
  if (preferredProvider) return preferredProvider;
@@ -1005,10 +1062,11 @@ var AuthResolver = class _AuthResolver {
1005
1062
  return "openai";
1006
1063
  }
1007
1064
  const result = await requestUiPick(this.state, {
1008
- title: "Select provider",
1065
+ title: "Welcome! Choose your provider to get started.",
1009
1066
  items: SUPPORTED_PROVIDERS.map((provider) => ({
1010
1067
  id: provider,
1011
- label: provider
1068
+ label: provider,
1069
+ detail: provider === "openai" ? "OAuth (ChatGPT Plus/Pro) or API key" : provider === "openrouter" ? "API key (OpenRouter)" : "API key"
1012
1070
  })),
1013
1071
  multi: false
1014
1072
  });
@@ -1031,7 +1089,17 @@ var AuthResolver = class _AuthResolver {
1031
1089
  if (provider === "openai") {
1032
1090
  return this.promptOpenAiAuth();
1033
1091
  }
1034
- return this.promptApiKey(provider, "Anthropic API key");
1092
+ return this.promptApiKey(provider, this.getApiKeyPromptLabel(provider));
1093
+ }
1094
+ getApiKeyPromptLabel(provider) {
1095
+ switch (provider) {
1096
+ case "anthropic":
1097
+ return "Anthropic API key";
1098
+ case "openrouter":
1099
+ return "OpenRouter API key";
1100
+ default:
1101
+ return "API key";
1102
+ }
1035
1103
  }
1036
1104
  async getOpenAiAccessToken() {
1037
1105
  const entry = this.auth.providers.openai;
@@ -1060,10 +1128,18 @@ var AuthResolver = class _AuthResolver {
1060
1128
  throw new Error("UI does not support auth prompts");
1061
1129
  }
1062
1130
  const pick = await requestUiPick(this.state, {
1063
- title: "OpenAI auth method",
1131
+ title: "How would you like to connect OpenAI?",
1064
1132
  items: [
1065
- { id: "oauth", label: "ChatGPT Plus/Pro (OAuth)" },
1066
- { id: "api_key", label: "Manually enter API key" }
1133
+ {
1134
+ id: "oauth",
1135
+ label: "ChatGPT Plus/Pro (OAuth)",
1136
+ detail: "Recommended if you use ChatGPT subscription access"
1137
+ },
1138
+ {
1139
+ id: "api_key",
1140
+ label: "OpenAI API key",
1141
+ detail: "Use a standard OpenAI API key from platform settings"
1142
+ }
1067
1143
  ],
1068
1144
  multi: false
1069
1145
  });
@@ -1087,7 +1163,7 @@ var AuthResolver = class _AuthResolver {
1087
1163
  const session = await createOAuthSession();
1088
1164
  const confirm = await requestUiConfirm(this.state, {
1089
1165
  title: "OpenAI OAuth",
1090
- message: `Open your browser to authenticate.
1166
+ message: `You're almost done. Open your browser to continue sign in.
1091
1167
 
1092
1168
  ${session.authUrl}`,
1093
1169
  confirm_label: "Open browser",
@@ -1126,7 +1202,7 @@ ${session.authUrl}`,
1126
1202
  }
1127
1203
  const prompt = await requestUiPrompt(this.state, {
1128
1204
  title: label,
1129
- message: "Enter the API key.",
1205
+ message: "Paste your API key to continue. You can change it later with /logout.",
1130
1206
  secret: true
1131
1207
  });
1132
1208
  const value = prompt?.value?.trim() ?? "";
@@ -1327,7 +1403,8 @@ var createMcpOAuthSession = async (params) => {
1327
1403
  import { applyModelMetadata, DEFAULT_MODEL_REGISTRY } from "@codelia/core";
1328
1404
  import { ModelMetadataServiceImpl } from "@codelia/model-metadata";
1329
1405
  import { StoragePathServiceImpl as StoragePathServiceImpl2 } from "@codelia/storage";
1330
- var buildModelRegistry = async (llm) => {
1406
+ var buildModelRegistry = async (llm, options = {}) => {
1407
+ const strict = options.strict ?? true;
1331
1408
  const metadataService = new ModelMetadataServiceImpl({
1332
1409
  storagePathService: new StoragePathServiceImpl2()
1333
1410
  });
@@ -1342,9 +1419,11 @@ var buildModelRegistry = async (llm) => {
1342
1419
  fullIdEntry = providerEntries?.[`${llm.provider}/${llm.model}`];
1343
1420
  }
1344
1421
  if (!directEntry && !fullIdEntry) {
1345
- throw new Error(
1346
- `Model metadata not found for ${llm.provider}/${llm.model} after refresh`
1347
- );
1422
+ if (strict) {
1423
+ throw new Error(
1424
+ `Model metadata not found for ${llm.provider}/${llm.model} after refresh`
1425
+ );
1426
+ }
1348
1427
  }
1349
1428
  return applyModelMetadata(DEFAULT_MODEL_REGISTRY, { models: entries });
1350
1429
  };
@@ -1701,6 +1780,8 @@ var SYSTEM_TOOL_ALLOWLIST = [
1701
1780
  "agents_resolve",
1702
1781
  "skill_search",
1703
1782
  "skill_load",
1783
+ "lane_list",
1784
+ "lane_status",
1704
1785
  "done"
1705
1786
  ];
1706
1787
  var SYSTEM_BASH_ALLOWLIST = [
@@ -1792,7 +1873,27 @@ var PermissionService = class {
1792
1873
  message: `${skillName ? skillName : explicitPath || `skill_load ${rawArgs}`}${rememberPreview}`
1793
1874
  };
1794
1875
  }
1795
- return { title: "Run tool?", message: `${toolName} ${rawArgs}` };
1876
+ if (toolName === "write") {
1877
+ const parsed = parseRawArgsForPrompt(rawArgs);
1878
+ const filePath = parsed && typeof parsed.file_path === "string" ? parsed.file_path.trim() : "";
1879
+ const content = parsed && typeof parsed.content === "string" ? parsed.content : "";
1880
+ const target = filePath || "(unknown path)";
1881
+ return {
1882
+ title: "Run tool?",
1883
+ message: `write ${target} (${content.length} bytes)${rememberPreview}`
1884
+ };
1885
+ }
1886
+ if (toolName === "edit") {
1887
+ const parsed = parseRawArgsForPrompt(rawArgs);
1888
+ const filePath = parsed && typeof parsed.file_path === "string" ? parsed.file_path.trim() : "";
1889
+ const target = filePath || "(unknown path)";
1890
+ const mode = parsed && typeof parsed.match_mode === "string" ? parsed.match_mode : "auto";
1891
+ return {
1892
+ title: "Run tool?",
1893
+ message: `edit ${target} (match=${mode})${rememberPreview}`
1894
+ };
1895
+ }
1896
+ return { title: "Run tool?", message: toolName };
1796
1897
  }
1797
1898
  evaluate(toolName, rawArgs) {
1798
1899
  if (toolName === "bash") {
@@ -3288,151 +3389,877 @@ var createGrepTool = (sandboxKey) => defineTool6({
3288
3389
  }
3289
3390
  });
3290
3391
 
3291
- // src/tools/read.ts
3292
- import { promises as fs10 } from "fs";
3392
+ // src/tools/lane.ts
3293
3393
  import { defineTool as defineTool7 } from "@codelia/core";
3294
3394
  import { z as z8 } from "zod";
3295
- var DEFAULT_READ_LIMIT = 2e3;
3296
- var MAX_LINE_LENGTH = 2e3;
3297
- var MAX_BYTES = 50 * 1024;
3298
- var createReadTool = (sandboxKey) => defineTool7({
3299
- name: "read",
3300
- description: "Read a text file with optional 0-based line offset and line limit.",
3301
- input: z8.object({
3302
- file_path: z8.string().describe("File path under the sandbox root."),
3303
- offset: z8.number().int().nonnegative().optional().describe("0-based start line. Default 0."),
3304
- limit: z8.number().int().positive().optional().describe("Max lines to read. Default 2000.")
3305
- }),
3306
- execute: async (input, ctx) => {
3307
- let resolved;
3308
- try {
3309
- const sandbox = await getSandboxContext(ctx, sandboxKey);
3310
- resolved = sandbox.resolvePath(input.file_path);
3311
- } catch (error) {
3312
- return `Security error: ${String(error)}`;
3313
- }
3314
- try {
3315
- const stat = await fs10.stat(resolved);
3316
- if (stat.isDirectory()) {
3317
- return `Path is a directory: ${input.file_path}`;
3395
+
3396
+ // src/lanes/manager.ts
3397
+ import crypto4 from "crypto";
3398
+ import { promises as fs11 } from "fs";
3399
+ import os2 from "os";
3400
+ import path11 from "path";
3401
+
3402
+ // src/lanes/command.ts
3403
+ import { spawn as spawn3 } from "child_process";
3404
+ var DEFAULT_TIMEOUT_MS = 3e4;
3405
+ var MAX_CAPTURE_BYTES = 512 * 1024;
3406
+ var truncate2 = (value) => {
3407
+ if (value.length <= 8e3) return value;
3408
+ return `${value.slice(0, 8e3)}...[truncated]`;
3409
+ };
3410
+ var runCommand = async (command, args, options = {}) => {
3411
+ const timeoutMs = Math.max(1e3, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
3412
+ return new Promise((resolve, reject) => {
3413
+ const child = spawn3(command, args, {
3414
+ cwd: options.cwd,
3415
+ stdio: ["ignore", "pipe", "pipe"]
3416
+ });
3417
+ let stdout = "";
3418
+ let stderr = "";
3419
+ let bytes = 0;
3420
+ let done = false;
3421
+ const finish = (fn) => {
3422
+ if (done) return;
3423
+ done = true;
3424
+ clearTimeout(timer);
3425
+ fn();
3426
+ };
3427
+ const consume = (chunk, kind) => {
3428
+ const text = chunk.toString("utf8");
3429
+ bytes += Buffer.byteLength(text, "utf8");
3430
+ if (bytes > MAX_CAPTURE_BYTES) {
3431
+ finish(
3432
+ () => reject(
3433
+ new Error(
3434
+ `${command} output exceeded ${MAX_CAPTURE_BYTES} bytes while running ${args.join(" ")}`
3435
+ )
3436
+ )
3437
+ );
3438
+ try {
3439
+ child.kill("SIGTERM");
3440
+ } catch {
3441
+ }
3442
+ return;
3318
3443
  }
3319
- } catch {
3320
- return `File not found: ${input.file_path}`;
3321
- }
3322
- try {
3323
- const content = await fs10.readFile(resolved, "utf8");
3324
- const lines = content.split(/\r?\n/);
3325
- if (lines.length === 0) {
3326
- return "";
3444
+ if (kind === "stdout") {
3445
+ stdout += text;
3446
+ } else {
3447
+ stderr += text;
3327
3448
  }
3328
- const offset = input.offset ?? 0;
3329
- const limit = input.limit ?? DEFAULT_READ_LIMIT;
3330
- if (offset >= lines.length) {
3331
- return `Offset exceeds file length: ${input.file_path}`;
3449
+ };
3450
+ child.stdout?.on("data", (chunk) => consume(chunk, "stdout"));
3451
+ child.stderr?.on("data", (chunk) => consume(chunk, "stderr"));
3452
+ child.on("error", (error) => {
3453
+ finish(() => reject(error));
3454
+ });
3455
+ child.on("close", (code) => {
3456
+ if (code === 0) {
3457
+ finish(() => resolve({ stdout, stderr }));
3458
+ return;
3332
3459
  }
3333
- const raw = [];
3334
- let bytes = 0;
3335
- let truncatedByBytes = false;
3336
- for (let index = offset; index < Math.min(lines.length, offset + limit); index += 1) {
3337
- const line = lines[index].length > MAX_LINE_LENGTH ? `${lines[index].slice(0, MAX_LINE_LENGTH)}...` : lines[index];
3338
- const size = Buffer.byteLength(line, "utf8") + (raw.length > 0 ? 1 : 0);
3339
- if (bytes + size > MAX_BYTES) {
3340
- truncatedByBytes = true;
3341
- break;
3342
- }
3343
- raw.push(line);
3344
- bytes += size;
3460
+ const detail = `${stderr}
3461
+ ${stdout}`.trim();
3462
+ const message = detail ? `${command} ${args.join(" ")} failed (exit ${String(code)}): ${truncate2(detail)}` : `${command} ${args.join(" ")} failed (exit ${String(code)})`;
3463
+ finish(() => reject(new Error(message)));
3464
+ });
3465
+ const timer = setTimeout(() => {
3466
+ try {
3467
+ child.kill("SIGTERM");
3468
+ } catch {
3345
3469
  }
3346
- const numbered = raw.map(
3347
- (line, index) => `${String(index + offset + 1).padStart(5, " ")} ${line}`
3470
+ finish(
3471
+ () => reject(
3472
+ new Error(
3473
+ `${command} ${args.join(" ")} timed out after ${Math.floor(timeoutMs / 1e3)}s`
3474
+ )
3475
+ )
3348
3476
  );
3349
- const lastReadLine = offset + raw.length;
3350
- const hasMoreLines = lines.length > lastReadLine;
3351
- const truncated = truncatedByBytes || hasMoreLines;
3352
- let output = numbered.join("\n");
3353
- if (truncated) {
3354
- const reason = truncatedByBytes ? `Output truncated at ${MAX_BYTES} bytes.` : "File has more lines.";
3355
- output += `
3477
+ }, timeoutMs);
3478
+ });
3479
+ };
3356
3480
 
3357
- ${reason} Use offset to read beyond line ${lastReadLine}.`;
3358
- }
3359
- return output;
3360
- } catch (error) {
3361
- return `Error reading file: ${String(error)}`;
3481
+ // src/lanes/registry.ts
3482
+ import { promises as fs10 } from "fs";
3483
+ import path10 from "path";
3484
+ import { resolveStoragePaths as resolveStoragePaths2 } from "@codelia/storage";
3485
+ var REGISTRY_DIRNAME = "lanes";
3486
+ var REGISTRY_FILENAME = "registry.json";
3487
+ var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
3488
+ var sortByUpdatedDesc = (lanes) => [...lanes].sort((a, b) => b.updated_at.localeCompare(a.updated_at));
3489
+ var readJsonFile = async (filePath) => {
3490
+ try {
3491
+ const raw = await fs10.readFile(filePath, "utf8");
3492
+ const parsed = JSON.parse(raw);
3493
+ if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.lanes)) {
3494
+ return null;
3495
+ }
3496
+ return {
3497
+ version: 1,
3498
+ lanes: parsed.lanes
3499
+ };
3500
+ } catch {
3501
+ return null;
3502
+ }
3503
+ };
3504
+ var atomicWrite = async (filePath, payload) => {
3505
+ const dir = path10.dirname(filePath);
3506
+ const base = path10.basename(filePath);
3507
+ const tmp = path10.join(
3508
+ dir,
3509
+ `${base}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
3510
+ );
3511
+ await fs10.writeFile(tmp, payload, "utf8");
3512
+ await fs10.rename(tmp, filePath);
3513
+ };
3514
+ var LaneRegistryStore = class {
3515
+ registryPath;
3516
+ constructor(registryPath) {
3517
+ if (registryPath) {
3518
+ this.registryPath = registryPath;
3519
+ return;
3362
3520
  }
3521
+ const root = resolveStoragePaths2().root;
3522
+ this.registryPath = path10.join(root, REGISTRY_DIRNAME, REGISTRY_FILENAME);
3363
3523
  }
3364
- });
3524
+ async ensureDir() {
3525
+ await fs10.mkdir(path10.dirname(this.registryPath), { recursive: true });
3526
+ }
3527
+ async readAll() {
3528
+ await this.ensureDir();
3529
+ const data = await readJsonFile(this.registryPath);
3530
+ if (!data) return [];
3531
+ return sortByUpdatedDesc(data.lanes);
3532
+ }
3533
+ async writeAll(lanes) {
3534
+ await this.ensureDir();
3535
+ const payload = {
3536
+ version: 1,
3537
+ lanes: sortByUpdatedDesc(lanes)
3538
+ };
3539
+ await atomicWrite(
3540
+ this.registryPath,
3541
+ `${JSON.stringify(payload, null, 2)}
3542
+ `
3543
+ );
3544
+ }
3545
+ async list() {
3546
+ return this.readAll();
3547
+ }
3548
+ async get(laneId) {
3549
+ const lanes = await this.readAll();
3550
+ return lanes.find((lane) => lane.lane_id === laneId) ?? null;
3551
+ }
3552
+ async upsert(record) {
3553
+ const lanes = await this.readAll();
3554
+ const idx = lanes.findIndex((lane) => lane.lane_id === record.lane_id);
3555
+ const next = {
3556
+ ...record,
3557
+ updated_at: nowIso()
3558
+ };
3559
+ if (idx >= 0) {
3560
+ lanes[idx] = next;
3561
+ } else {
3562
+ lanes.push(next);
3563
+ }
3564
+ await this.writeAll(lanes);
3565
+ }
3566
+ async patch(laneId, patch) {
3567
+ const lanes = await this.readAll();
3568
+ const idx = lanes.findIndex((lane) => lane.lane_id === laneId);
3569
+ if (idx < 0) return null;
3570
+ const current = lanes[idx];
3571
+ const next = {
3572
+ ...current,
3573
+ ...patch,
3574
+ lane_id: current.lane_id,
3575
+ created_at: current.created_at,
3576
+ updated_at: nowIso()
3577
+ };
3578
+ lanes[idx] = next;
3579
+ await this.writeAll(lanes);
3580
+ return next;
3581
+ }
3582
+ };
3365
3583
 
3366
- // src/tools/skill-load.ts
3367
- import { defineTool as defineTool8 } from "@codelia/core";
3368
- import { z as z9 } from "zod";
3369
- var SkillLoadInputSchema = z9.object({
3370
- name: z9.string().min(1).optional().describe("Exact skill name to load."),
3371
- path: z9.string().min(1).optional().describe("Absolute or workspace-relative path to SKILL.md.")
3372
- }).refine((value) => !!value.name || !!value.path, {
3373
- message: "name or path is required",
3374
- path: ["name"]
3375
- });
3376
- var createSkillLoadTool = (skillsResolverKey) => defineTool8({
3377
- name: "skill_load",
3378
- description: "Load full SKILL.md content by exact name or path.",
3379
- input: SkillLoadInputSchema,
3380
- execute: async (input, ctx) => {
3584
+ // src/lanes/manager.ts
3585
+ var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
3586
+ var INITIAL_MESSAGE_FLAG = "--initial-message";
3587
+ var toSlug = (value) => {
3588
+ const normalized = value.trim().toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
3589
+ return normalized || "task";
3590
+ };
3591
+ var parseDate = (value) => {
3592
+ const ts = Date.parse(value);
3593
+ if (Number.isNaN(ts)) return 0;
3594
+ return ts;
3595
+ };
3596
+ var isNotFoundError = (error) => {
3597
+ const message = error instanceof Error ? error.message : String(error);
3598
+ return message.includes("command not found") || message.includes("ENOENT") || message.includes("not found");
3599
+ };
3600
+ var isExitFailure = (error) => {
3601
+ const message = error instanceof Error ? error.message : String(error);
3602
+ return message.includes("failed (exit");
3603
+ };
3604
+ var LaneManagerError = class extends Error {
3605
+ code;
3606
+ constructor(code, message) {
3607
+ super(message);
3608
+ this.code = code;
3609
+ this.name = "LaneManagerError";
3610
+ }
3611
+ };
3612
+ var LaneManager = class {
3613
+ runner;
3614
+ registry;
3615
+ now;
3616
+ randomId;
3617
+ launchCommand;
3618
+ defaultWorktreeRoot;
3619
+ constructor(options = {}) {
3620
+ this.runner = options.runner ?? runCommand;
3621
+ this.registry = options.registry ?? new LaneRegistryStore();
3622
+ this.now = options.now ?? nowIso2;
3623
+ this.randomId = options.randomId ?? (() => crypto4.randomUUID());
3624
+ this.launchCommand = options.launchCommand ?? process.env.CODELIA_LANE_LAUNCH_COMMAND?.trim() ?? "codelia";
3625
+ this.defaultWorktreeRoot = options.defaultWorktreeRoot ?? path11.join(os2.homedir(), ".codelia", "worktrees");
3626
+ }
3627
+ async cmd(command, args, options) {
3628
+ debugLog(`lane.cmd ${command} ${args.join(" ")}`);
3629
+ return this.runner(command, args, options);
3630
+ }
3631
+ async resolveRepoRoot(workingDir) {
3632
+ const { stdout } = await this.cmd(
3633
+ "git",
3634
+ ["-C", workingDir, "rev-parse", "--show-toplevel"],
3635
+ { cwd: workingDir }
3636
+ );
3637
+ const root = stdout.trim();
3638
+ if (!root) {
3639
+ throw new LaneManagerError(
3640
+ "backend_command_failed",
3641
+ "Could not resolve git repository root."
3642
+ );
3643
+ }
3644
+ return root;
3645
+ }
3646
+ async preflightBackend(backend) {
3647
+ if (backend === "tmux") {
3648
+ try {
3649
+ await this.cmd("tmux", ["-V"]);
3650
+ return;
3651
+ } catch (error) {
3652
+ if (isNotFoundError(error)) {
3653
+ throw new LaneManagerError(
3654
+ "backend_not_found",
3655
+ "tmux not found in PATH."
3656
+ );
3657
+ }
3658
+ throw new LaneManagerError(
3659
+ "backend_command_failed",
3660
+ `tmux preflight failed: ${String(error)}`
3661
+ );
3662
+ }
3663
+ }
3381
3664
  try {
3382
- const resolver = await getSkillsResolver(ctx, skillsResolverKey);
3383
- const result = await resolver.load(input);
3384
- if (!result.ok) {
3385
- return {
3386
- ok: false,
3387
- error: result.message,
3388
- ambiguous_paths: result.ambiguous_paths ?? [],
3389
- already_loaded: null,
3390
- skill: null,
3391
- content: null,
3392
- files: [],
3393
- files_truncated: false
3394
- };
3665
+ await this.cmd("zellij", ["--version"]);
3666
+ } catch (error) {
3667
+ if (isNotFoundError(error)) {
3668
+ throw new LaneManagerError(
3669
+ "backend_not_found",
3670
+ "zellij not found in PATH."
3671
+ );
3395
3672
  }
3396
- return {
3397
- ok: true,
3398
- error: null,
3399
- ambiguous_paths: [],
3400
- already_loaded: result.already_loaded,
3401
- skill: result.skill,
3402
- content: result.content,
3403
- files: result.files,
3404
- files_truncated: result.files_truncated
3405
- };
3673
+ throw new LaneManagerError(
3674
+ "backend_command_failed",
3675
+ `zellij preflight failed: ${String(error)}`
3676
+ );
3677
+ }
3678
+ throw new LaneManagerError(
3679
+ "backend_command_failed",
3680
+ "zellij backend is not supported yet in lane MVP. Use tmux."
3681
+ );
3682
+ }
3683
+ async startTmuxLane(target, worktreePath, seedContext) {
3684
+ const launchCommand = this.buildLaunchCommand(seedContext);
3685
+ await this.cmd("tmux", [
3686
+ "new-session",
3687
+ "-d",
3688
+ "-s",
3689
+ target,
3690
+ "-c",
3691
+ worktreePath
3692
+ ]);
3693
+ const paneTarget = `${target}:0.0`;
3694
+ await this.cmd("tmux", [
3695
+ "send-keys",
3696
+ "-t",
3697
+ paneTarget,
3698
+ "-l",
3699
+ launchCommand
3700
+ ]);
3701
+ await this.cmd("tmux", ["send-keys", "-t", paneTarget, "Enter"]);
3702
+ }
3703
+ shellQuote(value) {
3704
+ if (!value) return "''";
3705
+ return `'${value.replaceAll("'", "'\\''")}'`;
3706
+ }
3707
+ buildLaunchCommand(seedContext) {
3708
+ const seed = seedContext?.trim();
3709
+ if (!seed) {
3710
+ return this.launchCommand;
3711
+ }
3712
+ return `${this.launchCommand} ${INITIAL_MESSAGE_FLAG} ${this.shellQuote(seed)}`;
3713
+ }
3714
+ async isTmuxAlive(target) {
3715
+ try {
3716
+ await this.cmd("tmux", ["has-session", "-t", target], {
3717
+ timeoutMs: 5e3
3718
+ });
3719
+ return true;
3406
3720
  } catch (error) {
3407
- return {
3408
- ok: false,
3409
- error: `Error loading skill: ${String(error)}`,
3410
- ambiguous_paths: [],
3411
- already_loaded: null,
3412
- skill: null,
3413
- content: null,
3414
- files: [],
3415
- files_truncated: false
3416
- };
3721
+ if (isExitFailure(error)) return false;
3722
+ throw new LaneManagerError(
3723
+ "backend_command_failed",
3724
+ `tmux status failed: ${String(error)}`
3725
+ );
3417
3726
  }
3418
3727
  }
3419
- });
3420
-
3421
- // src/tools/skill-search.ts
3422
- import { defineTool as defineTool9 } from "@codelia/core";
3423
- import { z as z10 } from "zod";
3424
- var createSkillSearchTool = (skillsResolverKey) => defineTool9({
3425
- name: "skill_search",
3426
- description: "Search installed local skills by name, description, or path.",
3427
- input: z10.object({
3428
- query: z10.string().min(1).describe("Search query text."),
3429
- limit: z10.number().int().positive().optional().describe("Optional max result count (clamped by config)."),
3430
- scope: z10.enum(["repo", "user"]).optional().describe("Optional scope filter.")
3431
- }),
3432
- execute: async (input, ctx) => {
3728
+ async stopTmuxLane(target) {
3433
3729
  try {
3434
- const resolver = await getSkillsResolver(ctx, skillsResolverKey);
3435
- const result = await resolver.search(input);
3730
+ await this.cmd("tmux", ["kill-session", "-t", target], {
3731
+ timeoutMs: 1e4
3732
+ });
3733
+ } catch (error) {
3734
+ if (isExitFailure(error)) return;
3735
+ throw new LaneManagerError(
3736
+ "backend_command_failed",
3737
+ `tmux close failed: ${String(error)}`
3738
+ );
3739
+ }
3740
+ }
3741
+ async resolveWorktreeMainRoot(worktreePath) {
3742
+ const { stdout } = await this.cmd("git", [
3743
+ "-C",
3744
+ worktreePath,
3745
+ "rev-parse",
3746
+ "--git-common-dir"
3747
+ ]);
3748
+ const commonDir = path11.resolve(worktreePath, stdout.trim());
3749
+ if (path11.basename(commonDir) === ".git") {
3750
+ return path11.dirname(commonDir);
3751
+ }
3752
+ return await this.resolveRepoRoot(worktreePath);
3753
+ }
3754
+ async isWorktreeDirty(worktreePath) {
3755
+ try {
3756
+ await fs11.access(worktreePath);
3757
+ } catch {
3758
+ return false;
3759
+ }
3760
+ const { stdout } = await this.cmd("git", [
3761
+ "-C",
3762
+ worktreePath,
3763
+ "status",
3764
+ "--porcelain"
3765
+ ]);
3766
+ return stdout.trim().length > 0;
3767
+ }
3768
+ async removeWorktree(worktreePath, force) {
3769
+ try {
3770
+ await fs11.access(worktreePath);
3771
+ } catch {
3772
+ return;
3773
+ }
3774
+ const repoRoot = await this.resolveWorktreeMainRoot(worktreePath);
3775
+ const args = ["-C", repoRoot, "worktree", "remove"];
3776
+ if (force) args.push("--force");
3777
+ args.push(worktreePath);
3778
+ await this.cmd("git", args);
3779
+ }
3780
+ async create(input, options) {
3781
+ const taskId = input.task_id.trim();
3782
+ if (!taskId) {
3783
+ throw new LaneManagerError(
3784
+ "backend_command_failed",
3785
+ "task_id is required."
3786
+ );
3787
+ }
3788
+ const backend = input.mux_backend ?? "tmux";
3789
+ await this.preflightBackend(backend);
3790
+ const repoRoot = await this.resolveRepoRoot(options.workingDir);
3791
+ const laneId = this.randomId();
3792
+ const slug = toSlug(taskId);
3793
+ const shortId = laneId.slice(0, 8);
3794
+ const branchName = `lane/${slug}-${shortId}`;
3795
+ const worktreePath = input.worktree_path ? path11.resolve(options.workingDir, input.worktree_path) : path11.join(this.defaultWorktreeRoot, `${slug}-${shortId}`);
3796
+ const muxTarget = `codelia-lane-${shortId}`;
3797
+ const createdAt = this.now();
3798
+ const record = {
3799
+ lane_id: laneId,
3800
+ task_id: taskId,
3801
+ state: "creating",
3802
+ mux_backend: backend,
3803
+ mux_target: muxTarget,
3804
+ worktree_path: worktreePath,
3805
+ branch_name: branchName,
3806
+ session_id: crypto4.randomUUID(),
3807
+ created_at: createdAt,
3808
+ updated_at: createdAt,
3809
+ last_activity_at: createdAt
3810
+ };
3811
+ await this.registry.upsert(record);
3812
+ try {
3813
+ await fs11.mkdir(path11.dirname(worktreePath), { recursive: true });
3814
+ await this.cmd("git", [
3815
+ "-C",
3816
+ repoRoot,
3817
+ "worktree",
3818
+ "add",
3819
+ "-b",
3820
+ branchName,
3821
+ worktreePath,
3822
+ input.base_ref?.trim() || "HEAD"
3823
+ ]);
3824
+ if (backend === "tmux") {
3825
+ await this.startTmuxLane(muxTarget, worktreePath, input.seed_context);
3826
+ }
3827
+ const running = await this.registry.patch(laneId, {
3828
+ state: "running",
3829
+ last_activity_at: this.now(),
3830
+ last_error: void 0
3831
+ });
3832
+ if (!running) {
3833
+ throw new Error("lane record disappeared during create");
3834
+ }
3835
+ return running;
3836
+ } catch (error) {
3837
+ await this.registry.patch(laneId, {
3838
+ state: "error",
3839
+ last_error: String(error),
3840
+ last_activity_at: this.now()
3841
+ });
3842
+ throw new LaneManagerError(
3843
+ "backend_command_failed",
3844
+ `lane.create failed (${laneId}): ${String(error)}`
3845
+ );
3846
+ }
3847
+ }
3848
+ async list(input) {
3849
+ const all = await this.registry.list();
3850
+ if (input?.include_closed) return all;
3851
+ return all.filter((lane) => lane.state !== "closed");
3852
+ }
3853
+ async status(laneId) {
3854
+ const lane = await this.registry.get(laneId);
3855
+ if (!lane) {
3856
+ throw new LaneManagerError("lane_not_found", `Lane not found: ${laneId}`);
3857
+ }
3858
+ let alive = false;
3859
+ if (lane.state !== "closed") {
3860
+ if (lane.mux_backend === "tmux") {
3861
+ alive = await this.isTmuxAlive(lane.mux_target);
3862
+ }
3863
+ if (lane.state === "running" && !alive) {
3864
+ const patched = await this.registry.patch(lane.lane_id, {
3865
+ state: "finished",
3866
+ last_activity_at: this.now()
3867
+ });
3868
+ if (patched) {
3869
+ return { lane: patched, backend_alive: false };
3870
+ }
3871
+ }
3872
+ }
3873
+ return { lane, backend_alive: alive };
3874
+ }
3875
+ async close(input) {
3876
+ const lane = await this.registry.get(input.lane_id);
3877
+ if (!lane) {
3878
+ throw new LaneManagerError(
3879
+ "lane_not_found",
3880
+ `Lane not found: ${input.lane_id}`
3881
+ );
3882
+ }
3883
+ if (lane.state === "closed") {
3884
+ return lane;
3885
+ }
3886
+ const force = input.force === true;
3887
+ const removeWorktree = input.remove_worktree !== false;
3888
+ const status = await this.status(lane.lane_id);
3889
+ const refreshed = status.lane;
3890
+ if (status.backend_alive && !force) {
3891
+ throw new LaneManagerError(
3892
+ "lane_running",
3893
+ `Lane is still running: ${lane.lane_id}`
3894
+ );
3895
+ }
3896
+ if (status.backend_alive && refreshed.mux_backend === "tmux") {
3897
+ await this.stopTmuxLane(refreshed.mux_target);
3898
+ }
3899
+ if (removeWorktree) {
3900
+ const dirty = await this.isWorktreeDirty(refreshed.worktree_path);
3901
+ if (dirty && !force) {
3902
+ throw new LaneManagerError(
3903
+ "worktree_dirty",
3904
+ `Worktree has uncommitted changes: ${refreshed.worktree_path}`
3905
+ );
3906
+ }
3907
+ await this.removeWorktree(refreshed.worktree_path, force);
3908
+ }
3909
+ const closed = await this.registry.patch(refreshed.lane_id, {
3910
+ state: "closed",
3911
+ last_activity_at: this.now()
3912
+ });
3913
+ if (!closed) {
3914
+ throw new LaneManagerError(
3915
+ "backend_command_failed",
3916
+ "Failed to persist lane close state."
3917
+ );
3918
+ }
3919
+ return closed;
3920
+ }
3921
+ async gc(input) {
3922
+ const ttlMs = Math.max(1, input.idle_ttl_minutes) * 6e4;
3923
+ const now = Date.now();
3924
+ const lanes = await this.registry.list();
3925
+ let closed = 0;
3926
+ let skipped = 0;
3927
+ const errors = [];
3928
+ for (const lane of lanes) {
3929
+ try {
3930
+ const status = await this.status(lane.lane_id);
3931
+ const current = status.lane;
3932
+ if (current.state === "running" || current.state === "closed") {
3933
+ skipped += 1;
3934
+ continue;
3935
+ }
3936
+ if (current.state !== "finished" && current.state !== "error") {
3937
+ skipped += 1;
3938
+ continue;
3939
+ }
3940
+ const idleSince = parseDate(current.last_activity_at) || parseDate(current.updated_at);
3941
+ if (idleSince <= 0 || now - idleSince < ttlMs) {
3942
+ skipped += 1;
3943
+ continue;
3944
+ }
3945
+ await this.close({
3946
+ lane_id: current.lane_id,
3947
+ remove_worktree: input.remove_worktree,
3948
+ force: input.force
3949
+ });
3950
+ closed += 1;
3951
+ } catch (error) {
3952
+ errors.push(`${lane.lane_id}: ${String(error)}`);
3953
+ }
3954
+ }
3955
+ return {
3956
+ checked: lanes.length,
3957
+ closed,
3958
+ skipped,
3959
+ errors
3960
+ };
3961
+ }
3962
+ };
3963
+
3964
+ // src/tools/lane.ts
3965
+ var laneManager = new LaneManager();
3966
+ var backendSchema = z8.enum(["tmux", "zellij"]);
3967
+ var formatError = (error) => {
3968
+ if (error instanceof LaneManagerError) {
3969
+ return new Error(`${error.code}: ${error.message}`);
3970
+ }
3971
+ if (error instanceof Error) return error;
3972
+ return new Error(String(error));
3973
+ };
3974
+ var shQuote = (value) => {
3975
+ if (!value) return "''";
3976
+ return `'${value.replaceAll("'", "'\\''")}'`;
3977
+ };
3978
+ var buildLaneHints = (lane) => {
3979
+ const attachCommand = lane.mux_backend === "tmux" ? `tmux attach -t ${shQuote(lane.mux_target)}` : lane.mux_backend === "zellij" ? `zellij attach ${shQuote(lane.mux_target)}` : null;
3980
+ return {
3981
+ attach_command: attachCommand,
3982
+ enter_worktree_command: `cd ${shQuote(lane.worktree_path)}`,
3983
+ status_tool: {
3984
+ name: "lane_status",
3985
+ args: { lane_id: lane.lane_id }
3986
+ },
3987
+ close_tool: {
3988
+ name: "lane_close",
3989
+ args: { lane_id: lane.lane_id }
3990
+ }
3991
+ };
3992
+ };
3993
+ var createLaneCreateTool = (sandboxKey) => defineTool7({
3994
+ name: "lane_create",
3995
+ description: "Create a Task lane (worktree slot) and start autonomous execution in tmux/zellij.",
3996
+ input: z8.object({
3997
+ task_id: z8.string().describe("Task identifier."),
3998
+ base_ref: z8.string().optional().describe("Base git ref. Default: HEAD."),
3999
+ worktree_path: z8.string().optional().describe(
4000
+ "Optional worktree path override. Default: ~/.codelia/worktrees/<task-slug>-<lane-id8>."
4001
+ ),
4002
+ mux_backend: backendSchema.optional().describe("Multiplexer backend. Default: tmux."),
4003
+ seed_context: z8.string().optional().describe("Optional initial text context.")
4004
+ }),
4005
+ execute: async (input, ctx) => {
4006
+ const sandbox = await getSandboxContext(ctx, sandboxKey);
4007
+ try {
4008
+ const lane = await laneManager.create(
4009
+ {
4010
+ task_id: input.task_id,
4011
+ base_ref: input.base_ref,
4012
+ worktree_path: input.worktree_path,
4013
+ mux_backend: input.mux_backend,
4014
+ seed_context: input.seed_context
4015
+ },
4016
+ { workingDir: sandbox.workingDir }
4017
+ );
4018
+ return {
4019
+ ok: true,
4020
+ lane,
4021
+ hints: buildLaneHints(lane)
4022
+ };
4023
+ } catch (error) {
4024
+ throw formatError(error);
4025
+ }
4026
+ }
4027
+ });
4028
+ var createLaneListTool = (sandboxKey) => defineTool7({
4029
+ name: "lane_list",
4030
+ description: "List Task lanes (worktree slots) and current states.",
4031
+ input: z8.object({
4032
+ include_closed: z8.boolean().optional().describe("Include closed lanes. Default: false.")
4033
+ }),
4034
+ execute: async (input, ctx) => {
4035
+ await getSandboxContext(ctx, sandboxKey);
4036
+ try {
4037
+ const lanes = await laneManager.list({
4038
+ include_closed: input.include_closed
4039
+ });
4040
+ return {
4041
+ lanes,
4042
+ hints: lanes.map((lane) => ({
4043
+ lane_id: lane.lane_id,
4044
+ ...buildLaneHints(lane)
4045
+ }))
4046
+ };
4047
+ } catch (error) {
4048
+ throw formatError(error);
4049
+ }
4050
+ }
4051
+ });
4052
+ var createLaneStatusTool = (sandboxKey) => defineTool7({
4053
+ name: "lane_status",
4054
+ description: "Get Task lane status and backend liveness.",
4055
+ input: z8.object({
4056
+ lane_id: z8.string().describe("Lane id.")
4057
+ }),
4058
+ execute: async (input, ctx) => {
4059
+ await getSandboxContext(ctx, sandboxKey);
4060
+ try {
4061
+ const result = await laneManager.status(input.lane_id);
4062
+ return {
4063
+ ...result,
4064
+ hints: buildLaneHints(result.lane)
4065
+ };
4066
+ } catch (error) {
4067
+ throw formatError(error);
4068
+ }
4069
+ }
4070
+ });
4071
+ var createLaneCloseTool = (sandboxKey) => defineTool7({
4072
+ name: "lane_close",
4073
+ description: "Close a Task lane and optionally remove its worktree.",
4074
+ input: z8.object({
4075
+ lane_id: z8.string().describe("Lane id."),
4076
+ remove_worktree: z8.boolean().optional().describe("Remove worktree. Default: true."),
4077
+ force: z8.boolean().optional().describe("Force close/cleanup. Default: false.")
4078
+ }),
4079
+ execute: async (input, ctx) => {
4080
+ await getSandboxContext(ctx, sandboxKey);
4081
+ try {
4082
+ const lane = await laneManager.close({
4083
+ lane_id: input.lane_id,
4084
+ remove_worktree: input.remove_worktree,
4085
+ force: input.force
4086
+ });
4087
+ return {
4088
+ ok: true,
4089
+ lane
4090
+ };
4091
+ } catch (error) {
4092
+ throw formatError(error);
4093
+ }
4094
+ }
4095
+ });
4096
+ var createLaneGcTool = (sandboxKey) => defineTool7({
4097
+ name: "lane_gc",
4098
+ description: "Close stale finished/error lanes after idle TTL.",
4099
+ input: z8.object({
4100
+ idle_ttl_minutes: z8.number().int().positive().describe("Idle TTL in minutes."),
4101
+ remove_worktree: z8.boolean().optional().describe("Remove worktree on close. Default: false."),
4102
+ force: z8.boolean().optional().describe("Force cleanup for dirty worktrees. Default: false.")
4103
+ }),
4104
+ execute: async (input, ctx) => {
4105
+ await getSandboxContext(ctx, sandboxKey);
4106
+ try {
4107
+ return await laneManager.gc({
4108
+ idle_ttl_minutes: input.idle_ttl_minutes,
4109
+ remove_worktree: input.remove_worktree,
4110
+ force: input.force
4111
+ });
4112
+ } catch (error) {
4113
+ throw formatError(error);
4114
+ }
4115
+ }
4116
+ });
4117
+
4118
+ // src/tools/read.ts
4119
+ import { promises as fs12 } from "fs";
4120
+ import { defineTool as defineTool8 } from "@codelia/core";
4121
+ import { z as z9 } from "zod";
4122
+ var DEFAULT_READ_LIMIT = 2e3;
4123
+ var MAX_LINE_LENGTH = 2e3;
4124
+ var MAX_BYTES = 50 * 1024;
4125
+ var createReadTool = (sandboxKey) => defineTool8({
4126
+ name: "read",
4127
+ description: "Read a text file with optional 0-based line offset and line limit.",
4128
+ input: z9.object({
4129
+ file_path: z9.string().describe("File path under the sandbox root."),
4130
+ offset: z9.number().int().nonnegative().optional().describe("0-based start line. Default 0."),
4131
+ limit: z9.number().int().positive().optional().describe("Max lines to read. Default 2000.")
4132
+ }),
4133
+ execute: async (input, ctx) => {
4134
+ let resolved;
4135
+ try {
4136
+ const sandbox = await getSandboxContext(ctx, sandboxKey);
4137
+ resolved = sandbox.resolvePath(input.file_path);
4138
+ } catch (error) {
4139
+ return `Security error: ${String(error)}`;
4140
+ }
4141
+ try {
4142
+ const stat = await fs12.stat(resolved);
4143
+ if (stat.isDirectory()) {
4144
+ return `Path is a directory: ${input.file_path}`;
4145
+ }
4146
+ } catch {
4147
+ return `File not found: ${input.file_path}`;
4148
+ }
4149
+ try {
4150
+ const content = await fs12.readFile(resolved, "utf8");
4151
+ const lines = content.split(/\r?\n/);
4152
+ if (lines.length === 0) {
4153
+ return "";
4154
+ }
4155
+ const offset = input.offset ?? 0;
4156
+ const limit = input.limit ?? DEFAULT_READ_LIMIT;
4157
+ if (offset >= lines.length) {
4158
+ return `Offset exceeds file length: ${input.file_path}`;
4159
+ }
4160
+ const raw = [];
4161
+ let bytes = 0;
4162
+ let truncatedByBytes = false;
4163
+ for (let index = offset; index < Math.min(lines.length, offset + limit); index += 1) {
4164
+ const line = lines[index].length > MAX_LINE_LENGTH ? `${lines[index].slice(0, MAX_LINE_LENGTH)}...` : lines[index];
4165
+ const size = Buffer.byteLength(line, "utf8") + (raw.length > 0 ? 1 : 0);
4166
+ if (bytes + size > MAX_BYTES) {
4167
+ truncatedByBytes = true;
4168
+ break;
4169
+ }
4170
+ raw.push(line);
4171
+ bytes += size;
4172
+ }
4173
+ const numbered = raw.map(
4174
+ (line, index) => `${String(index + offset + 1).padStart(5, " ")} ${line}`
4175
+ );
4176
+ const lastReadLine = offset + raw.length;
4177
+ const hasMoreLines = lines.length > lastReadLine;
4178
+ const truncated = truncatedByBytes || hasMoreLines;
4179
+ let output = numbered.join("\n");
4180
+ if (truncated) {
4181
+ const reason = truncatedByBytes ? `Output truncated at ${MAX_BYTES} bytes.` : "File has more lines.";
4182
+ output += `
4183
+
4184
+ ${reason} Use offset to read beyond line ${lastReadLine}.`;
4185
+ }
4186
+ return output;
4187
+ } catch (error) {
4188
+ return `Error reading file: ${String(error)}`;
4189
+ }
4190
+ }
4191
+ });
4192
+
4193
+ // src/tools/skill-load.ts
4194
+ import { defineTool as defineTool9 } from "@codelia/core";
4195
+ import { z as z10 } from "zod";
4196
+ var SkillLoadInputSchema = z10.object({
4197
+ name: z10.string().min(1).optional().describe("Exact skill name to load."),
4198
+ path: z10.string().min(1).optional().describe("Absolute or workspace-relative path to SKILL.md.")
4199
+ }).refine((value) => !!value.name || !!value.path, {
4200
+ message: "name or path is required",
4201
+ path: ["name"]
4202
+ });
4203
+ var createSkillLoadTool = (skillsResolverKey) => defineTool9({
4204
+ name: "skill_load",
4205
+ description: "Load full SKILL.md content by exact name or path.",
4206
+ input: SkillLoadInputSchema,
4207
+ execute: async (input, ctx) => {
4208
+ try {
4209
+ const resolver = await getSkillsResolver(ctx, skillsResolverKey);
4210
+ const result = await resolver.load(input);
4211
+ if (!result.ok) {
4212
+ return {
4213
+ ok: false,
4214
+ error: result.message,
4215
+ ambiguous_paths: result.ambiguous_paths ?? [],
4216
+ already_loaded: null,
4217
+ skill: null,
4218
+ content: null,
4219
+ files: [],
4220
+ files_truncated: false
4221
+ };
4222
+ }
4223
+ return {
4224
+ ok: true,
4225
+ error: null,
4226
+ ambiguous_paths: [],
4227
+ already_loaded: result.already_loaded,
4228
+ skill: result.skill,
4229
+ content: result.content,
4230
+ files: result.files,
4231
+ files_truncated: result.files_truncated
4232
+ };
4233
+ } catch (error) {
4234
+ return {
4235
+ ok: false,
4236
+ error: `Error loading skill: ${String(error)}`,
4237
+ ambiguous_paths: [],
4238
+ already_loaded: null,
4239
+ skill: null,
4240
+ content: null,
4241
+ files: [],
4242
+ files_truncated: false
4243
+ };
4244
+ }
4245
+ }
4246
+ });
4247
+
4248
+ // src/tools/skill-search.ts
4249
+ import { defineTool as defineTool10 } from "@codelia/core";
4250
+ import { z as z11 } from "zod";
4251
+ var createSkillSearchTool = (skillsResolverKey) => defineTool10({
4252
+ name: "skill_search",
4253
+ description: "Search installed local skills by name, description, or path.",
4254
+ input: z11.object({
4255
+ query: z11.string().min(1).describe("Search query text."),
4256
+ limit: z11.number().int().positive().optional().describe("Optional max result count (clamped by config)."),
4257
+ scope: z11.enum(["repo", "user"]).optional().describe("Optional scope filter.")
4258
+ }),
4259
+ execute: async (input, ctx) => {
4260
+ try {
4261
+ const resolver = await getSkillsResolver(ctx, skillsResolverKey);
4262
+ const result = await resolver.search(input);
3436
4263
  return {
3437
4264
  query: input.query,
3438
4265
  count: result.results.length,
@@ -3450,17 +4277,17 @@ var createSkillSearchTool = (skillsResolverKey) => defineTool9({
3450
4277
  });
3451
4278
 
3452
4279
  // src/tools/todo-read.ts
3453
- import { defineTool as defineTool10 } from "@codelia/core";
3454
- import { z as z11 } from "zod";
4280
+ import { defineTool as defineTool11 } from "@codelia/core";
4281
+ import { z as z12 } from "zod";
3455
4282
 
3456
4283
  // src/tools/todo-store.ts
3457
4284
  var todoStore = /* @__PURE__ */ new Map();
3458
4285
 
3459
4286
  // src/tools/todo-read.ts
3460
- var createTodoReadTool = (sandboxKey) => defineTool10({
4287
+ var createTodoReadTool = (sandboxKey) => defineTool11({
3461
4288
  name: "todo_read",
3462
4289
  description: "Read the in-session todo list.",
3463
- input: z11.object({}),
4290
+ input: z12.object({}),
3464
4291
  execute: async (_input, ctx) => {
3465
4292
  const sandbox = await getSandboxContext(ctx, sandboxKey);
3466
4293
  const todos = todoStore.get(sandbox.sessionId) ?? [];
@@ -3473,17 +4300,17 @@ var createTodoReadTool = (sandboxKey) => defineTool10({
3473
4300
  });
3474
4301
 
3475
4302
  // src/tools/todo-write.ts
3476
- import { defineTool as defineTool11 } from "@codelia/core";
3477
- import { z as z12 } from "zod";
3478
- var createTodoWriteTool = (sandboxKey) => defineTool11({
4303
+ import { defineTool as defineTool12 } from "@codelia/core";
4304
+ import { z as z13 } from "zod";
4305
+ var createTodoWriteTool = (sandboxKey) => defineTool12({
3479
4306
  name: "todo_write",
3480
4307
  description: "Replace the in-session todo list.",
3481
- input: z12.object({
3482
- todos: z12.array(
3483
- z12.object({
3484
- content: z12.string().describe("Todo item text."),
3485
- status: z12.enum(["pending", "in_progress", "completed"]).describe("Todo status."),
3486
- activeForm: z12.string().optional().describe("Optional in-progress phrasing for UI display.")
4308
+ input: z13.object({
4309
+ todos: z13.array(
4310
+ z13.object({
4311
+ content: z13.string().describe("Todo item text."),
4312
+ status: z13.enum(["pending", "in_progress", "completed"]).describe("Todo status."),
4313
+ activeForm: z13.string().optional().describe("Optional in-progress phrasing for UI display.")
3487
4314
  })
3488
4315
  )
3489
4316
  }),
@@ -3500,15 +4327,15 @@ var createTodoWriteTool = (sandboxKey) => defineTool11({
3500
4327
  });
3501
4328
 
3502
4329
  // src/tools/tool-output-cache.ts
3503
- import { defineTool as defineTool12 } from "@codelia/core";
3504
- import { z as z13 } from "zod";
3505
- var createToolOutputCacheTool = (store) => defineTool12({
4330
+ import { defineTool as defineTool13 } from "@codelia/core";
4331
+ import { z as z14 } from "zod";
4332
+ var createToolOutputCacheTool = (store) => defineTool13({
3506
4333
  name: "tool_output_cache",
3507
4334
  description: "Read cached tool output by ref_id.",
3508
- input: z13.object({
3509
- ref_id: z13.string().describe("Tool output reference ID."),
3510
- offset: z13.number().int().nonnegative().optional().describe("Optional 0-based line offset."),
3511
- limit: z13.number().int().positive().optional().describe("Optional max number of lines to return.")
4335
+ input: z14.object({
4336
+ ref_id: z14.string().describe("Tool output reference ID."),
4337
+ offset: z14.number().int().nonnegative().optional().describe("Optional 0-based line offset."),
4338
+ limit: z14.number().int().positive().optional().describe("Optional max number of lines to return.")
3512
4339
  }),
3513
4340
  execute: async (input) => {
3514
4341
  if (!store.read) {
@@ -3524,16 +4351,16 @@ var createToolOutputCacheTool = (store) => defineTool12({
3524
4351
  }
3525
4352
  }
3526
4353
  });
3527
- var createToolOutputCacheGrepTool = (store) => defineTool12({
4354
+ var createToolOutputCacheGrepTool = (store) => defineTool13({
3528
4355
  name: "tool_output_cache_grep",
3529
4356
  description: "Search cached tool output by ref_id.",
3530
- input: z13.object({
3531
- ref_id: z13.string().describe("Tool output reference ID."),
3532
- pattern: z13.string().describe("Text or regex pattern to search for."),
3533
- regex: z13.boolean().optional().describe("Interpret pattern as regex when true. Default false."),
3534
- before: z13.number().int().nonnegative().optional().describe("Context lines before each match. Default 0."),
3535
- after: z13.number().int().nonnegative().optional().describe("Context lines after each match. Default 0."),
3536
- max_matches: z13.number().int().positive().optional().describe("Maximum number of matches to return.")
4357
+ input: z14.object({
4358
+ ref_id: z14.string().describe("Tool output reference ID."),
4359
+ pattern: z14.string().describe("Text or regex pattern to search for."),
4360
+ regex: z14.boolean().optional().describe("Interpret pattern as regex when true. Default false."),
4361
+ before: z14.number().int().nonnegative().optional().describe("Context lines before each match. Default 0."),
4362
+ after: z14.number().int().nonnegative().optional().describe("Context lines after each match. Default 0."),
4363
+ max_matches: z14.number().int().positive().optional().describe("Maximum number of matches to return.")
3537
4364
  }),
3538
4365
  execute: async (input) => {
3539
4366
  if (!store.grep) {
@@ -3554,16 +4381,16 @@ var createToolOutputCacheGrepTool = (store) => defineTool12({
3554
4381
  });
3555
4382
 
3556
4383
  // src/tools/write.ts
3557
- import { promises as fs11 } from "fs";
3558
- import path10 from "path";
3559
- import { defineTool as defineTool13 } from "@codelia/core";
3560
- import { z as z14 } from "zod";
3561
- var createWriteTool = (sandboxKey) => defineTool13({
4384
+ import { promises as fs13 } from "fs";
4385
+ import path12 from "path";
4386
+ import { defineTool as defineTool14 } from "@codelia/core";
4387
+ import { z as z15 } from "zod";
4388
+ var createWriteTool = (sandboxKey) => defineTool14({
3562
4389
  name: "write",
3563
4390
  description: "Write text to a file, creating parent directories if needed.",
3564
- input: z14.object({
3565
- file_path: z14.string().describe("File path under the sandbox root."),
3566
- content: z14.string().describe("UTF-8 text content to write.")
4391
+ input: z15.object({
4392
+ file_path: z15.string().describe("File path under the sandbox root."),
4393
+ content: z15.string().describe("UTF-8 text content to write.")
3567
4394
  }),
3568
4395
  execute: async (input, ctx) => {
3569
4396
  let resolved;
@@ -3574,8 +4401,8 @@ var createWriteTool = (sandboxKey) => defineTool13({
3574
4401
  return `Security error: ${String(error)}`;
3575
4402
  }
3576
4403
  try {
3577
- await fs11.mkdir(path10.dirname(resolved), { recursive: true });
3578
- await fs11.writeFile(resolved, input.content, "utf8");
4404
+ await fs13.mkdir(path12.dirname(resolved), { recursive: true });
4405
+ await fs13.writeFile(resolved, input.content, "utf8");
3579
4406
  return `Wrote ${input.content.length} bytes to ${input.file_path}`;
3580
4407
  } catch (error) {
3581
4408
  return `Error writing file: ${String(error)}`;
@@ -3600,9 +4427,106 @@ var createTools = (sandboxKey, agentsResolverKey, skillsResolverKey, options = {
3600
4427
  ] : [],
3601
4428
  createTodoReadTool(sandboxKey),
3602
4429
  createTodoWriteTool(sandboxKey),
4430
+ createLaneCreateTool(sandboxKey),
4431
+ createLaneListTool(sandboxKey),
4432
+ createLaneStatusTool(sandboxKey),
4433
+ createLaneCloseTool(sandboxKey),
4434
+ createLaneGcTool(sandboxKey),
3603
4435
  createDoneTool()
3604
4436
  ];
3605
4437
 
4438
+ // src/utils/language.ts
4439
+ var normalizeLanguage = (value) => {
4440
+ const token = value.trim().toLowerCase();
4441
+ if (!token) return "";
4442
+ switch (token) {
4443
+ case "yml":
4444
+ return "yaml";
4445
+ case "mjs":
4446
+ case "cjs":
4447
+ case "node":
4448
+ return "javascript";
4449
+ case "mts":
4450
+ case "cts":
4451
+ case "ts-node":
4452
+ case "deno":
4453
+ return "typescript";
4454
+ case "py":
4455
+ case "python3":
4456
+ return "python";
4457
+ case "rb":
4458
+ return "ruby";
4459
+ case "zsh":
4460
+ case "sh":
4461
+ return "bash";
4462
+ case "ps1":
4463
+ return "powershell";
4464
+ default:
4465
+ return token;
4466
+ }
4467
+ };
4468
+ var languageFromFilePath = (filePath) => {
4469
+ if (!filePath) return void 0;
4470
+ const path16 = filePath.trim().replace(/^["']|["']$/g, "");
4471
+ if (!path16 || path16 === "/dev/null") return void 0;
4472
+ const normalizedPath = path16.replace(/^a\//, "").replace(/^b\//, "");
4473
+ const lastSlash = Math.max(
4474
+ normalizedPath.lastIndexOf("/"),
4475
+ normalizedPath.lastIndexOf("\\")
4476
+ );
4477
+ const basename = lastSlash >= 0 ? normalizedPath.slice(lastSlash + 1) : normalizedPath;
4478
+ if (!basename) return void 0;
4479
+ const dotIndex = basename.lastIndexOf(".");
4480
+ if (dotIndex > 0 && dotIndex < basename.length - 1) {
4481
+ return normalizeLanguage(basename.slice(dotIndex + 1));
4482
+ }
4483
+ if (basename === "Dockerfile") return "dockerfile";
4484
+ if (/^Makefile(\..+)?$/i.test(basename)) return "makefile";
4485
+ return void 0;
4486
+ };
4487
+ var languageFromDiffHeaders = (diff) => {
4488
+ if (!diff) return void 0;
4489
+ for (const line of diff.split("\n")) {
4490
+ if (!line.startsWith("--- ") && !line.startsWith("+++ ")) continue;
4491
+ const rawPath = line.slice(4).trim().split(/\s+/)[0] ?? "";
4492
+ const language = languageFromFilePath(rawPath);
4493
+ if (language) return language;
4494
+ }
4495
+ return void 0;
4496
+ };
4497
+ var languageFromShebang = (content) => {
4498
+ if (!content) return void 0;
4499
+ const firstLine = content.split("\n", 1)[0]?.trim() ?? "";
4500
+ if (!firstLine.startsWith("#!")) return void 0;
4501
+ const body = firstLine.slice(2).trim();
4502
+ if (!body) return void 0;
4503
+ const parts = body.split(/\s+/).filter(Boolean);
4504
+ if (!parts.length) return void 0;
4505
+ let command = parts[0];
4506
+ if (command.endsWith("/env")) {
4507
+ let index = 1;
4508
+ if (parts[index] === "-S") index += 1;
4509
+ while (index < parts.length && parts[index].includes("=")) {
4510
+ index += 1;
4511
+ }
4512
+ command = parts[index] ?? "";
4513
+ }
4514
+ if (!command) return void 0;
4515
+ const last = command.split("/").pop() ?? command;
4516
+ const simplified = last.replace(/[0-9.]+$/g, "");
4517
+ const normalized = normalizeLanguage(simplified);
4518
+ return normalized || void 0;
4519
+ };
4520
+ var resolvePreviewLanguageHint = (input) => {
4521
+ const explicit = input.language ? normalizeLanguage(input.language) : "";
4522
+ if (explicit) return explicit;
4523
+ const shebang = languageFromShebang(input.content);
4524
+ if (shebang) return shebang;
4525
+ const fromDiff = languageFromDiffHeaders(input.diff);
4526
+ if (fromDiff) return fromDiff;
4527
+ return languageFromFilePath(input.filePath);
4528
+ };
4529
+
3606
4530
  // src/agent-factory.ts
3607
4531
  var requireApiKeyAuth = (provider, auth) => {
3608
4532
  if (auth.method !== "api_key") {
@@ -3615,6 +4539,7 @@ var envTruthy = (value) => {
3615
4539
  const normalized = value.trim().toLowerCase();
3616
4540
  return normalized === "1" || normalized === "true";
3617
4541
  };
4542
+ var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
3618
4543
  var buildOpenAiClientOptions = (authResolver, auth) => {
3619
4544
  if (auth.method === "api_key") {
3620
4545
  return { apiKey: auth.api_key };
@@ -3658,6 +4583,22 @@ var buildOpenAiClientOptions = (authResolver, auth) => {
3658
4583
  fetch: fetchWithAccount
3659
4584
  };
3660
4585
  };
4586
+ var buildOpenRouterClientOptions = (auth) => {
4587
+ const headers = {};
4588
+ const referer = readEnvValue("OPENROUTER_HTTP_REFERER");
4589
+ if (referer) {
4590
+ headers["HTTP-Referer"] = referer;
4591
+ }
4592
+ const title = readEnvValue("OPENROUTER_X_TITLE");
4593
+ if (title) {
4594
+ headers["X-Title"] = title;
4595
+ }
4596
+ return {
4597
+ apiKey: requireApiKeyAuth("OpenRouter", auth),
4598
+ baseURL: OPENROUTER_BASE_URL,
4599
+ ...Object.keys(headers).length ? { defaultHeaders: headers } : {}
4600
+ };
4601
+ };
3661
4602
  var sleep = (ms) => new Promise((resolve) => {
3662
4603
  setTimeout(resolve, ms);
3663
4604
  });
@@ -3671,6 +4612,41 @@ var waitForUiConfirmSupport = async (state, timeoutMs = 5e3) => {
3671
4612
  }
3672
4613
  return !!state.uiCapabilities?.supports_confirm;
3673
4614
  };
4615
+ var MAX_CONFIRM_PREVIEW_LINES = 120;
4616
+ var splitLines = (value) => value.split("\n").map((line) => line.replace(/\r$/, ""));
4617
+ var buildBoundedDiffPreview = (diff, maxLines = MAX_CONFIRM_PREVIEW_LINES) => {
4618
+ if (!diff.trim()) return { diff: null, truncated: false };
4619
+ const lines = splitLines(diff);
4620
+ if (!lines.length) return { diff: null, truncated: false };
4621
+ if (lines.length <= maxLines) {
4622
+ return { diff: lines.join("\n"), truncated: false };
4623
+ }
4624
+ return {
4625
+ diff: lines.slice(0, maxLines).join("\n"),
4626
+ truncated: true
4627
+ };
4628
+ };
4629
+ var parseToolArgsObject = (rawArgs) => {
4630
+ try {
4631
+ const parsed = JSON.parse(rawArgs);
4632
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4633
+ return null;
4634
+ }
4635
+ return parsed;
4636
+ } catch {
4637
+ return null;
4638
+ }
4639
+ };
4640
+ var unwrapToolJsonObject = (result) => {
4641
+ if (!result || typeof result !== "object") return null;
4642
+ const typed = result;
4643
+ if (typed.type !== "json") return null;
4644
+ const value = typed.value;
4645
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4646
+ return null;
4647
+ }
4648
+ return value;
4649
+ };
3674
4650
  var requestMcpOAuthTokens = async (state, serverId, oauth2, errorMessage) => {
3675
4651
  const canConfirm = state.uiCapabilities?.supports_confirm ? true : await waitForUiConfirmSupport(state);
3676
4652
  if (!canConfirm) {
@@ -3808,6 +4784,9 @@ var createAgentFactory = (state, options = {}) => {
3808
4784
  toolOutputCacheStore
3809
4785
  }
3810
4786
  );
4787
+ const editTool = localTools.find(
4788
+ (tool) => tool.definition.name === "edit"
4789
+ );
3811
4790
  let mcpTools = [];
3812
4791
  if (options.mcpManager) {
3813
4792
  try {
@@ -3829,6 +4808,7 @@ var createAgentFactory = (state, options = {}) => {
3829
4808
  }
3830
4809
  }
3831
4810
  const tools = [...localTools, ...mcpTools];
4811
+ state.tools = tools;
3832
4812
  state.toolDefinitions = tools.map((tool) => tool.definition);
3833
4813
  const baseSystemPrompt = await loadSystemPrompt(ctx.workingDir);
3834
4814
  const withAgentsContext = appendInitialAgentsContext(
@@ -3878,6 +4858,17 @@ var createAgentFactory = (state, options = {}) => {
3878
4858
  });
3879
4859
  break;
3880
4860
  }
4861
+ case "openrouter": {
4862
+ const reasoningEffort = resolveReasoningEffort(modelConfig.reasoning);
4863
+ const textVerbosity = resolveTextVerbosity(modelConfig.verbosity);
4864
+ llm = new ChatOpenAI({
4865
+ clientOptions: buildOpenRouterClientOptions(providerAuth),
4866
+ ...modelConfig.name ? { model: modelConfig.name } : {},
4867
+ ...reasoningEffort ? { reasoningEffort } : {},
4868
+ ...textVerbosity ? { textVerbosity } : {}
4869
+ });
4870
+ break;
4871
+ }
3881
4872
  case "anthropic": {
3882
4873
  llm = new ChatAnthropic({
3883
4874
  clientOptions: {
@@ -3890,14 +4881,16 @@ var createAgentFactory = (state, options = {}) => {
3890
4881
  default:
3891
4882
  throw new Error(`Unsupported model.provider: ${provider}`);
3892
4883
  }
3893
- const modelRegistry = await buildModelRegistry(llm);
4884
+ const modelRegistry = await buildModelRegistry(llm, {
4885
+ strict: provider !== "openrouter"
4886
+ });
3894
4887
  const agent = new Agent({
3895
4888
  llm,
3896
4889
  tools,
3897
4890
  systemPrompt,
3898
4891
  modelRegistry: modelRegistry ?? DEFAULT_MODEL_REGISTRY2,
3899
4892
  services: { toolOutputCacheStore },
3900
- canExecuteTool: async (call, rawArgs) => {
4893
+ canExecuteTool: async (call, rawArgs, toolCtx) => {
3901
4894
  const decision = permissionService.evaluate(
3902
4895
  call.function.name,
3903
4896
  rawArgs
@@ -3919,12 +4912,129 @@ var createAgentFactory = (state, options = {}) => {
3919
4912
  };
3920
4913
  }
3921
4914
  const runId = state.activeRunId ?? void 0;
4915
+ debugLog(
4916
+ `permission.request tool=${call.function.name} args=${rawArgs}`
4917
+ );
3922
4918
  const prompt = permissionService.getConfirmPrompt(
3923
4919
  call.function.name,
3924
4920
  rawArgs
3925
4921
  );
4922
+ let previewDiff = null;
4923
+ let previewSummary = null;
4924
+ let previewTruncated = false;
4925
+ let previewFilePath = null;
4926
+ let previewLanguage = null;
4927
+ if (call.function.name === "write") {
4928
+ const parsed = parseToolArgsObject(rawArgs);
4929
+ const filePath = typeof parsed?.file_path === "string" ? parsed.file_path : "";
4930
+ const content = typeof parsed?.content === "string" ? parsed.content : "";
4931
+ const language = typeof parsed?.language === "string" ? parsed.language : "";
4932
+ if (filePath) {
4933
+ previewFilePath = filePath;
4934
+ previewLanguage = resolvePreviewLanguageHint({
4935
+ language,
4936
+ filePath,
4937
+ content
4938
+ }) ?? previewLanguage;
4939
+ try {
4940
+ const sandbox = await getSandboxContext(toolCtx, sandboxKey);
4941
+ const resolved = sandbox.resolvePath(filePath);
4942
+ let before = "";
4943
+ try {
4944
+ const stat = await fs14.stat(resolved);
4945
+ if (!stat.isDirectory()) {
4946
+ before = await fs14.readFile(resolved, "utf8");
4947
+ }
4948
+ } catch {
4949
+ before = "";
4950
+ }
4951
+ const preview = buildBoundedDiffPreview(
4952
+ createUnifiedDiff(filePath, before, content)
4953
+ );
4954
+ previewDiff = preview.diff;
4955
+ previewTruncated = preview.truncated;
4956
+ } catch {
4957
+ previewDiff = null;
4958
+ previewSummary = null;
4959
+ previewTruncated = false;
4960
+ }
4961
+ }
4962
+ } else if (call.function.name === "edit" && editTool) {
4963
+ const parsed = parseToolArgsObject(rawArgs);
4964
+ if (parsed) {
4965
+ const filePath = typeof parsed.file_path === "string" ? parsed.file_path : "";
4966
+ const language = typeof parsed.language === "string" ? parsed.language : "";
4967
+ if (filePath) {
4968
+ previewFilePath = filePath;
4969
+ }
4970
+ previewLanguage = resolvePreviewLanguageHint({
4971
+ language,
4972
+ filePath
4973
+ }) ?? previewLanguage;
4974
+ const dryRunInput = { ...parsed, dry_run: true };
4975
+ try {
4976
+ const result = await editTool.executeRaw(
4977
+ JSON.stringify(dryRunInput),
4978
+ toolCtx
4979
+ );
4980
+ if (result && typeof result === "object") {
4981
+ const obj = unwrapToolJsonObject(result);
4982
+ if (!obj) {
4983
+ previewSummary = "Preview unavailable: unexpected dry-run output";
4984
+ } else {
4985
+ const resultFilePath = typeof obj.file_path === "string" ? obj.file_path : "";
4986
+ const resultLanguage = typeof obj.language === "string" ? obj.language : "";
4987
+ if (!previewFilePath && resultFilePath) {
4988
+ previewFilePath = resultFilePath;
4989
+ }
4990
+ const diff = typeof obj.diff === "string" ? obj.diff : "";
4991
+ const summary = typeof obj.summary === "string" ? obj.summary : "";
4992
+ previewLanguage = resolvePreviewLanguageHint({
4993
+ language: resultLanguage || previewLanguage,
4994
+ filePath: previewFilePath || resultFilePath,
4995
+ diff
4996
+ }) ?? previewLanguage;
4997
+ const preview = buildBoundedDiffPreview(diff);
4998
+ previewDiff = preview.diff;
4999
+ previewTruncated = preview.truncated;
5000
+ if (!previewDiff) {
5001
+ previewSummary = summary || "Preview: no diff content";
5002
+ }
5003
+ }
5004
+ }
5005
+ } catch {
5006
+ previewSummary = "Preview unavailable: dry-run failed";
5007
+ }
5008
+ }
5009
+ }
3926
5010
  if (runId) {
3927
- sendRunStatus(runId, "awaiting_ui", "waiting for confirmation");
5011
+ if (previewDiff || previewSummary) {
5012
+ previewLanguage = resolvePreviewLanguageHint({
5013
+ language: previewLanguage,
5014
+ filePath: previewFilePath,
5015
+ diff: previewDiff
5016
+ }) ?? previewLanguage;
5017
+ await sendAgentEventAsync(state, runId, {
5018
+ type: "permission.preview",
5019
+ tool: call.function.name,
5020
+ tool_call_id: call.id,
5021
+ ...previewFilePath ? { file_path: previewFilePath } : {},
5022
+ ...previewLanguage ? { language: previewLanguage } : {},
5023
+ ...previewDiff ? { diff: previewDiff } : {},
5024
+ ...previewSummary ? { summary: previewSummary } : {},
5025
+ ...previewTruncated ? { truncated: true } : {}
5026
+ });
5027
+ }
5028
+ await sendAgentEventAsync(state, runId, {
5029
+ type: "permission.ready",
5030
+ tool: call.function.name,
5031
+ tool_call_id: call.id
5032
+ });
5033
+ await sendRunStatusAsync(
5034
+ runId,
5035
+ "awaiting_ui",
5036
+ "waiting for confirmation"
5037
+ );
3928
5038
  }
3929
5039
  const confirmResult = await requestUiConfirm(state, {
3930
5040
  run_id: runId,
@@ -4270,7 +5380,7 @@ var HttpMcpClient = class {
4270
5380
  };
4271
5381
 
4272
5382
  // src/mcp/stdio-client.ts
4273
- import { spawn as spawn3 } from "child_process";
5383
+ import { spawn as spawn4 } from "child_process";
4274
5384
  import { createInterface } from "readline";
4275
5385
  var StdioMcpClient = class {
4276
5386
  constructor(options) {
@@ -4279,7 +5389,7 @@ var StdioMcpClient = class {
4279
5389
  ...process.env,
4280
5390
  ...options.env ?? {}
4281
5391
  };
4282
- this.child = spawn3(options.command, options.args, {
5392
+ this.child = spawn4(options.command, options.args, {
4283
5393
  cwd: options.cwd,
4284
5394
  env,
4285
5395
  stdio: ["pipe", "pipe", "pipe"]
@@ -4489,8 +5599,8 @@ var parseAuthorizationServerMetadata = (value) => {
4489
5599
  var buildAuthorizationServerMetadataUrl = (issuer) => {
4490
5600
  try {
4491
5601
  const parsed = new URL(issuer);
4492
- const path14 = parsed.pathname === "/" ? "" : parsed.pathname;
4493
- const basePath = path14.startsWith("/") ? path14 : `/${path14}`;
5602
+ const path16 = parsed.pathname === "/" ? "" : parsed.pathname;
5603
+ const basePath = path16.startsWith("/") ? path16 : `/${path16}`;
4494
5604
  const metadataPath = `/.well-known/oauth-authorization-server${basePath}`;
4495
5605
  return new URL(metadataPath, `${parsed.origin}/`).toString();
4496
5606
  } catch {
@@ -4587,24 +5697,24 @@ var fetchDiscoveredOAuthConfig = async (serverUrl) => {
4587
5697
  };
4588
5698
 
4589
5699
  // src/mcp/tooling.ts
4590
- import crypto4 from "crypto";
5700
+ import crypto5 from "crypto";
4591
5701
  var MAX_SCHEMA_SIZE_BYTES = 64 * 1024;
4592
5702
  var MAX_TOOL_OUTPUT_CHARS = 1e5;
4593
5703
  var MAX_TOOLS_LIST_PAGES = 100;
4594
5704
  var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4595
5705
  var isMcpToolDescriptor = (value) => isRecord4(value) && typeof value.name === "string";
4596
- var toSlug = (value) => {
5706
+ var toSlug2 = (value) => {
4597
5707
  const slug = value.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
4598
5708
  return slug || "tool";
4599
5709
  };
4600
- var hash8 = (value) => crypto4.createHash("sha256").update(value).digest("hex").slice(0, 8);
5710
+ var hash8 = (value) => crypto5.createHash("sha256").update(value).digest("hex").slice(0, 8);
4601
5711
  var clampToolName = (value) => {
4602
5712
  if (value.length <= 63) return value;
4603
5713
  return value.slice(0, 63);
4604
5714
  };
4605
5715
  var buildToolName = (serverId, toolName) => {
4606
- const serverSlug = toSlug(serverId);
4607
- const toolSlug = toSlug(toolName);
5716
+ const serverSlug = toSlug2(serverId);
5717
+ const toolSlug = toSlug2(toolName);
4608
5718
  const fingerprint = hash8(`${serverId}:${toolName}`);
4609
5719
  const maxPrefixLength = 63 - (fingerprint.length + 1);
4610
5720
  const prefix = `mcp_${serverSlug}_${toolSlug}`.slice(0, maxPrefixLength);
@@ -4762,7 +5872,7 @@ var createMcpToolAdapter = (params) => {
4762
5872
 
4763
5873
  // src/mcp/manager.ts
4764
5874
  var OAUTH_TOKEN_EXPIRY_SKEW_MS = 6e4;
4765
- var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
5875
+ var nowIso3 = () => (/* @__PURE__ */ new Date()).toISOString();
4766
5876
  var describeError2 = (error) => error instanceof Error ? error.message : String(error);
4767
5877
  var toListServer = (id, runtime) => ({
4768
5878
  id,
@@ -4894,7 +6004,7 @@ var McpManager = class {
4894
6004
  runtime.tools_count = runtime.toolAdapters.length;
4895
6005
  runtime.state = "ready";
4896
6006
  runtime.last_error = void 0;
4897
- runtime.last_connected_at = nowIso();
6007
+ runtime.last_connected_at = nowIso3();
4898
6008
  connectOptions?.onStatus?.(
4899
6009
  `MCP server ready: ${serverId} (${runtime.tools_count ?? 0} tools)`
4900
6010
  );
@@ -5147,6 +6257,10 @@ var McpManager = class {
5147
6257
  };
5148
6258
 
5149
6259
  // src/rpc/handlers.ts
6260
+ import { updateModelConfig as updateModelConfig2 } from "@codelia/config-loader";
6261
+ import {
6262
+ RPC_ERROR_CODE as RPC_ERROR_CODE7
6263
+ } from "@codelia/protocol";
5150
6264
  import {
5151
6265
  RunEventStoreFactoryImpl,
5152
6266
  SessionStateStoreImpl
@@ -5158,6 +6272,9 @@ var SERVER_VERSION = "0.1.0";
5158
6272
  var PROTOCOL_VERSION = "0";
5159
6273
 
5160
6274
  // src/rpc/context.ts
6275
+ import {
6276
+ RPC_ERROR_CODE
6277
+ } from "@codelia/protocol";
5161
6278
  var createContextHandlers = ({
5162
6279
  state,
5163
6280
  log: log2
@@ -5247,7 +6364,7 @@ var createContextHandlers = ({
5247
6364
  sendResult(id, result);
5248
6365
  } catch (error) {
5249
6366
  sendError(id, {
5250
- code: -32e3,
6367
+ code: RPC_ERROR_CODE.RUNTIME_INTERNAL,
5251
6368
  message: `context.inspect failed: ${String(error)}`
5252
6369
  });
5253
6370
  }
@@ -5256,12 +6373,50 @@ var createContextHandlers = ({
5256
6373
  };
5257
6374
 
5258
6375
  // src/rpc/history.ts
5259
- import { createReadStream, promises as fs12 } from "fs";
5260
- import path11 from "path";
6376
+ import { createReadStream, promises as fs15 } from "fs";
6377
+ import path13 from "path";
5261
6378
  import { createInterface as createInterface2 } from "readline";
5262
- import { resolveStoragePaths as resolveStoragePaths2 } from "@codelia/storage";
6379
+ import {
6380
+ RPC_ERROR_CODE as RPC_ERROR_CODE2
6381
+ } from "@codelia/protocol";
6382
+ import { resolveStoragePaths as resolveStoragePaths3 } from "@codelia/storage";
5263
6383
  var DEFAULT_HISTORY_RUNS = 20;
5264
6384
  var DEFAULT_HISTORY_EVENTS = 1500;
6385
+ var runStartInputToHiddenMessage = (input) => {
6386
+ if (!input) {
6387
+ return "";
6388
+ }
6389
+ if (input.type === "text") {
6390
+ return input.text;
6391
+ }
6392
+ if (!Array.isArray(input.parts)) {
6393
+ return "";
6394
+ }
6395
+ let message = "";
6396
+ for (const part of input.parts) {
6397
+ if (!part || typeof part !== "object") {
6398
+ continue;
6399
+ }
6400
+ if (part.type === "text") {
6401
+ message += part.text;
6402
+ continue;
6403
+ }
6404
+ if (part.type === "image_url") {
6405
+ message += "[image]";
6406
+ }
6407
+ }
6408
+ return message;
6409
+ };
6410
+ var parseRunHeader = (headerLine) => {
6411
+ try {
6412
+ const header = JSON.parse(headerLine);
6413
+ if (!header || typeof header !== "object") return null;
6414
+ if (header.type !== "header") return null;
6415
+ return header;
6416
+ } catch {
6417
+ return null;
6418
+ }
6419
+ };
5265
6420
  var readFirstLine = async (filePath) => {
5266
6421
  const stream = createReadStream(filePath, { encoding: "utf8" });
5267
6422
  const reader = createInterface2({
@@ -5278,12 +6433,12 @@ var readFirstLine = async (filePath) => {
5278
6433
  stream.destroy();
5279
6434
  }
5280
6435
  };
5281
- var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5282
- const paths = resolveStoragePaths2();
6436
+ var collectRunCandidates = async (log2) => {
6437
+ const paths = resolveStoragePaths3();
5283
6438
  const sessionsDir = paths.sessionsDir;
5284
6439
  let yearEntries;
5285
6440
  try {
5286
- yearEntries = await fs12.readdir(sessionsDir, {
6441
+ yearEntries = await fs15.readdir(sessionsDir, {
5287
6442
  withFileTypes: true,
5288
6443
  encoding: "utf8"
5289
6444
  });
@@ -5295,10 +6450,10 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5295
6450
  for (const yearEntry of yearEntries) {
5296
6451
  if (!yearEntry.isDirectory()) continue;
5297
6452
  if (!/^\d{4}$/.test(yearEntry.name)) continue;
5298
- const yearPath = path11.join(sessionsDir, yearEntry.name);
6453
+ const yearPath = path13.join(sessionsDir, yearEntry.name);
5299
6454
  let monthEntries;
5300
6455
  try {
5301
- monthEntries = await fs12.readdir(yearPath, {
6456
+ monthEntries = await fs15.readdir(yearPath, {
5302
6457
  withFileTypes: true,
5303
6458
  encoding: "utf8"
5304
6459
  });
@@ -5308,10 +6463,10 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5308
6463
  for (const monthEntry of monthEntries) {
5309
6464
  if (!monthEntry.isDirectory()) continue;
5310
6465
  if (!/^\d{2}$/.test(monthEntry.name)) continue;
5311
- const monthPath = path11.join(yearPath, monthEntry.name);
6466
+ const monthPath = path13.join(yearPath, monthEntry.name);
5312
6467
  let dayEntries;
5313
6468
  try {
5314
- dayEntries = await fs12.readdir(monthPath, {
6469
+ dayEntries = await fs15.readdir(monthPath, {
5315
6470
  withFileTypes: true,
5316
6471
  encoding: "utf8"
5317
6472
  });
@@ -5321,10 +6476,10 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5321
6476
  for (const dayEntry of dayEntries) {
5322
6477
  if (!dayEntry.isDirectory()) continue;
5323
6478
  if (!/^\d{2}$/.test(dayEntry.name)) continue;
5324
- const dayPath = path11.join(monthPath, dayEntry.name);
6479
+ const dayPath = path13.join(monthPath, dayEntry.name);
5325
6480
  let files;
5326
6481
  try {
5327
- files = await fs12.readdir(dayPath, {
6482
+ files = await fs15.readdir(dayPath, {
5328
6483
  withFileTypes: true,
5329
6484
  encoding: "utf8"
5330
6485
  });
@@ -5334,9 +6489,9 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5334
6489
  for (const file of files) {
5335
6490
  if (!file.isFile()) continue;
5336
6491
  if (!file.name.endsWith(".jsonl")) continue;
5337
- const filePath = path11.join(dayPath, file.name);
6492
+ const filePath = path13.join(dayPath, file.name);
5338
6493
  try {
5339
- const stat = await fs12.stat(filePath);
6494
+ const stat = await fs15.stat(filePath);
5340
6495
  candidates.push({ path: filePath, mtimeMs: stat.mtimeMs });
5341
6496
  } catch {
5342
6497
  }
@@ -5345,6 +6500,10 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5345
6500
  }
5346
6501
  }
5347
6502
  candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
6503
+ return candidates;
6504
+ };
6505
+ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
6506
+ const candidates = await collectRunCandidates(log2);
5348
6507
  const runs = [];
5349
6508
  for (const candidate of candidates) {
5350
6509
  if (runs.length >= maxRuns) break;
@@ -5355,19 +6514,16 @@ var collectSessionRuns = async (sessionId, maxRuns, log2) => {
5355
6514
  continue;
5356
6515
  }
5357
6516
  if (!headerLine) continue;
5358
- try {
5359
- const header = JSON.parse(headerLine);
5360
- if (header.type !== "header") continue;
5361
- if (header.session_id !== sessionId) continue;
5362
- const runId = header.run_id ?? path11.basename(candidate.path, ".jsonl");
5363
- const startedAt = header.started_at ?? new Date(candidate.mtimeMs).toISOString();
5364
- runs.push({
5365
- path: candidate.path,
5366
- run_id: runId,
5367
- started_at: startedAt
5368
- });
5369
- } catch {
5370
- }
6517
+ const header = parseRunHeader(headerLine);
6518
+ if (!header) continue;
6519
+ if (header.session_id !== sessionId) continue;
6520
+ const runId = header.run_id ?? path13.basename(candidate.path, ".jsonl");
6521
+ const startedAt = header.started_at ?? new Date(candidate.mtimeMs).toISOString();
6522
+ runs.push({
6523
+ path: candidate.path,
6524
+ run_id: runId,
6525
+ started_at: startedAt
6526
+ });
5371
6527
  }
5372
6528
  runs.sort((a, b) => a.started_at.localeCompare(b.started_at));
5373
6529
  return runs;
@@ -5415,7 +6571,7 @@ var createHistoryHandlers = ({
5415
6571
  sessions = await sessionStateStore.list();
5416
6572
  } catch (error) {
5417
6573
  sendError(id, {
5418
- code: -32006,
6574
+ code: RPC_ERROR_CODE2.SESSION_LIST_FAILED,
5419
6575
  message: `session list failed: ${String(error)}`
5420
6576
  });
5421
6577
  return;
@@ -5430,7 +6586,7 @@ var createHistoryHandlers = ({
5430
6586
  const handleSessionHistory = async (id, params) => {
5431
6587
  const sessionId = params?.session_id?.trim();
5432
6588
  if (!sessionId) {
5433
- sendError(id, { code: -32602, message: "session_id is required" });
6589
+ sendError(id, { code: RPC_ERROR_CODE2.INVALID_PARAMS, message: "session_id is required" });
5434
6590
  return;
5435
6591
  }
5436
6592
  const maxRuns = Math.max(0, params?.max_runs ?? DEFAULT_HISTORY_RUNS);
@@ -5460,7 +6616,7 @@ var createHistoryHandlers = ({
5460
6616
  }
5461
6617
  if (!record || typeof record !== "object") continue;
5462
6618
  if (record.type === "run.start") {
5463
- const input = record.input?.type === "text" ? record.input.text : "";
6619
+ const input = runStartInputToHiddenMessage(record.input);
5464
6620
  if (input) {
5465
6621
  sendHistoryEvent(record.run_id, -1, {
5466
6622
  type: "hidden_user_message",
@@ -5484,17 +6640,279 @@ var createHistoryHandlers = ({
5484
6640
  };
5485
6641
  sendResult(id, result);
5486
6642
  };
5487
- return { handleSessionList, handleSessionHistory };
6643
+ return { handleSessionList, handleSessionHistory };
6644
+ };
6645
+
6646
+ // src/rpc/model.ts
6647
+ import { updateModelConfig } from "@codelia/config-loader";
6648
+ import {
6649
+ DEFAULT_MODEL_REGISTRY as DEFAULT_MODEL_REGISTRY3,
6650
+ listModels,
6651
+ resolveModel
6652
+ } from "@codelia/core";
6653
+ import { ModelMetadataServiceImpl as ModelMetadataServiceImpl2 } from "@codelia/model-metadata";
6654
+ import {
6655
+ RPC_ERROR_CODE as RPC_ERROR_CODE3
6656
+ } from "@codelia/protocol";
6657
+ var isSupportedProvider = (provider) => provider === "openai" || provider === "anthropic" || provider === "openrouter";
6658
+ var resolveProviderModelEntry = (providerEntries, provider, model) => {
6659
+ if (!providerEntries) return null;
6660
+ return providerEntries[model] ?? providerEntries[`${provider}/${model}`] ?? null;
6661
+ };
6662
+ var parseReleaseTimestamp = (entry) => {
6663
+ const releaseDate = entry?.releaseDate?.trim();
6664
+ if (!releaseDate) return null;
6665
+ const timestamp = Date.parse(releaseDate);
6666
+ if (Number.isNaN(timestamp)) return null;
6667
+ return timestamp;
6668
+ };
6669
+ var normalizeUsdPer1M = (value) => {
6670
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
6671
+ return void 0;
6672
+ }
6673
+ return value;
6674
+ };
6675
+ var buildModelListDetail = (entry) => {
6676
+ if (!entry) return null;
6677
+ const detail = {};
6678
+ if (entry.releaseDate?.trim()) {
6679
+ detail.release_date = entry.releaseDate.trim();
6680
+ }
6681
+ const limits = entry.limits;
6682
+ if (typeof limits?.contextWindow === "number" && Number.isFinite(limits.contextWindow) && limits.contextWindow > 0) {
6683
+ detail.context_window = limits.contextWindow;
6684
+ }
6685
+ if (typeof limits?.inputTokens === "number" && Number.isFinite(limits.inputTokens) && limits.inputTokens > 0) {
6686
+ detail.max_input_tokens = limits.inputTokens;
6687
+ }
6688
+ if (typeof limits?.outputTokens === "number" && Number.isFinite(limits.outputTokens) && limits.outputTokens > 0) {
6689
+ detail.max_output_tokens = limits.outputTokens;
6690
+ }
6691
+ const inputCost = normalizeUsdPer1M(entry.cost?.input);
6692
+ if (inputCost !== void 0) {
6693
+ detail.cost_per_1m_input_tokens_usd = inputCost;
6694
+ }
6695
+ const outputCost = normalizeUsdPer1M(entry.cost?.output);
6696
+ if (outputCost !== void 0) {
6697
+ detail.cost_per_1m_output_tokens_usd = outputCost;
6698
+ }
6699
+ return Object.keys(detail).length ? detail : null;
6700
+ };
6701
+ var sortModelsByReleaseDate = (models, provider, providerEntries) => {
6702
+ const sortable = models.map((model, index) => ({
6703
+ model,
6704
+ index,
6705
+ releaseTimestamp: parseReleaseTimestamp(
6706
+ resolveProviderModelEntry(providerEntries, provider, model)
6707
+ )
6708
+ }));
6709
+ sortable.sort((left, right) => {
6710
+ if (left.releaseTimestamp !== null && right.releaseTimestamp !== null && left.releaseTimestamp !== right.releaseTimestamp) {
6711
+ return right.releaseTimestamp - left.releaseTimestamp;
6712
+ }
6713
+ if (left.releaseTimestamp !== null && right.releaseTimestamp === null) {
6714
+ return -1;
6715
+ }
6716
+ if (left.releaseTimestamp === null && right.releaseTimestamp !== null) {
6717
+ return 1;
6718
+ }
6719
+ return left.index - right.index;
6720
+ });
6721
+ return sortable.map((item) => item.model);
6722
+ };
6723
+ var loadProviderModelEntries = async (provider) => {
6724
+ const metadataService = new ModelMetadataServiceImpl2();
6725
+ const allEntries = await metadataService.getAllModelEntries();
6726
+ return allEntries[provider] ?? null;
6727
+ };
6728
+ var OPENROUTER_BASE_URL2 = "https://openrouter.ai/api/v1";
6729
+ var parseUnixSecondsToDate = (value) => {
6730
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
6731
+ return void 0;
6732
+ }
6733
+ const date = new Date(Math.floor(value) * 1e3);
6734
+ const timestamp = date.getTime();
6735
+ if (!Number.isFinite(timestamp)) {
6736
+ return void 0;
6737
+ }
6738
+ return date.toISOString().slice(0, 10);
6739
+ };
6740
+ var parsePositiveNumber = (value) => {
6741
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
6742
+ return void 0;
6743
+ }
6744
+ return value;
6745
+ };
6746
+ var buildOpenRouterHeaders = (apiKey) => {
6747
+ const headers = new Headers();
6748
+ headers.set("Authorization", `Bearer ${apiKey}`);
6749
+ const referer = readEnvValue("OPENROUTER_HTTP_REFERER");
6750
+ if (referer) {
6751
+ headers.set("HTTP-Referer", referer);
6752
+ }
6753
+ const title = readEnvValue("OPENROUTER_X_TITLE");
6754
+ if (title) {
6755
+ headers.set("X-Title", title);
6756
+ }
6757
+ return headers;
6758
+ };
6759
+ var resolveOpenRouterApiKey = async () => {
6760
+ const envKey = readEnvValue("OPENROUTER_API_KEY");
6761
+ if (envKey) {
6762
+ return envKey;
6763
+ }
6764
+ const store = new AuthStore();
6765
+ const auth = await store.load();
6766
+ const providerAuth = auth.providers.openrouter;
6767
+ if (providerAuth?.method !== "api_key") {
6768
+ return null;
6769
+ }
6770
+ const value = providerAuth.api_key.trim();
6771
+ return value ? value : null;
6772
+ };
6773
+ var resolveOpenRouterApiKeyWithPrompt = async ({
6774
+ state,
6775
+ log: log2
6776
+ }) => {
6777
+ const existing = await resolveOpenRouterApiKey();
6778
+ if (existing) {
6779
+ return existing;
6780
+ }
6781
+ if (!state) {
6782
+ throw new Error("OpenRouter API key is required");
6783
+ }
6784
+ const authResolver = await AuthResolver.create(state, log2);
6785
+ const auth = await authResolver.resolveProviderAuth("openrouter");
6786
+ if (auth.method !== "api_key") {
6787
+ throw new Error("OpenRouter API key is required");
6788
+ }
6789
+ const value = auth.api_key.trim();
6790
+ if (!value) {
6791
+ throw new Error("OpenRouter API key is required");
6792
+ }
6793
+ return value;
6794
+ };
6795
+ var parseOpenRouterModel = (value) => {
6796
+ if (!value || typeof value !== "object") {
6797
+ return null;
6798
+ }
6799
+ const entry = value;
6800
+ const id = typeof entry.id === "string" ? entry.id.trim() : "";
6801
+ if (!id) {
6802
+ return null;
6803
+ }
6804
+ const topProvider = entry.top_provider && typeof entry.top_provider === "object" ? entry.top_provider : null;
6805
+ return {
6806
+ id,
6807
+ created: typeof entry.created === "number" && Number.isFinite(entry.created) ? entry.created : void 0,
6808
+ context_length: parsePositiveNumber(entry.context_length),
6809
+ top_provider: topProvider ? {
6810
+ context_length: parsePositiveNumber(topProvider.context_length),
6811
+ max_completion_tokens: parsePositiveNumber(
6812
+ topProvider.max_completion_tokens
6813
+ )
6814
+ } : void 0
6815
+ };
6816
+ };
6817
+ var fetchOpenRouterModels = async (apiKey) => {
6818
+ const response = await fetch(`${OPENROUTER_BASE_URL2}/models`, {
6819
+ headers: buildOpenRouterHeaders(apiKey)
6820
+ });
6821
+ if (!response.ok) {
6822
+ const body = await response.text().catch(() => "");
6823
+ const snippet = body ? body.slice(0, 300) : "(empty)";
6824
+ throw new Error(
6825
+ `OpenRouter models request failed (${response.status}): ${snippet}`
6826
+ );
6827
+ }
6828
+ const payload = await response.json();
6829
+ if (!payload || typeof payload !== "object") {
6830
+ throw new Error("OpenRouter models response is not an object");
6831
+ }
6832
+ const data = payload.data;
6833
+ if (!Array.isArray(data)) {
6834
+ throw new Error("OpenRouter models response has no data array");
6835
+ }
6836
+ const models = data.map((entry) => parseOpenRouterModel(entry)).filter((entry) => !!entry);
6837
+ models.sort((left, right) => {
6838
+ const leftCreated = left.created ?? 0;
6839
+ const rightCreated = right.created ?? 0;
6840
+ if (leftCreated !== rightCreated) {
6841
+ return rightCreated - leftCreated;
6842
+ }
6843
+ return left.id.localeCompare(right.id);
6844
+ });
6845
+ return models;
6846
+ };
6847
+ var buildOpenRouterModelList = async ({
6848
+ includeDetails,
6849
+ state,
6850
+ log: log2
6851
+ }) => {
6852
+ const apiKey = await resolveOpenRouterApiKeyWithPrompt({ state, log: log2 });
6853
+ const models = await fetchOpenRouterModels(apiKey);
6854
+ const ids = models.map((model) => model.id);
6855
+ if (!includeDetails) {
6856
+ return { models: ids };
6857
+ }
6858
+ const details = {};
6859
+ for (const model of models) {
6860
+ const detail = {};
6861
+ const releaseDate = parseUnixSecondsToDate(model.created);
6862
+ if (releaseDate) {
6863
+ detail.release_date = releaseDate;
6864
+ }
6865
+ const contextWindow = model.context_length ?? model.top_provider?.context_length;
6866
+ if (contextWindow && contextWindow > 0) {
6867
+ detail.context_window = contextWindow;
6868
+ detail.max_input_tokens = contextWindow;
6869
+ }
6870
+ const maxOutputTokens = model.top_provider?.max_completion_tokens;
6871
+ if (maxOutputTokens && maxOutputTokens > 0) {
6872
+ detail.max_output_tokens = maxOutputTokens;
6873
+ }
6874
+ if (Object.keys(detail).length) {
6875
+ details[model.id] = detail;
6876
+ }
6877
+ }
6878
+ return Object.keys(details).length ? { models: ids, details } : { models: ids };
6879
+ };
6880
+ var buildProviderModelList = async ({
6881
+ provider,
6882
+ includeDetails,
6883
+ state,
6884
+ log: log2
6885
+ }) => {
6886
+ if (provider === "openrouter") {
6887
+ return buildOpenRouterModelList({ includeDetails, state, log: log2 });
6888
+ }
6889
+ let providerEntries = null;
6890
+ try {
6891
+ providerEntries = await loadProviderModelEntries(provider);
6892
+ } catch (error) {
6893
+ if (includeDetails) {
6894
+ log2(`model.list details error: ${error}`);
6895
+ }
6896
+ }
6897
+ const models = sortModelsByReleaseDate(
6898
+ listModels(DEFAULT_MODEL_REGISTRY3, provider).map((model) => model.id),
6899
+ provider,
6900
+ providerEntries
6901
+ );
6902
+ if (!includeDetails || !providerEntries) {
6903
+ return { models };
6904
+ }
6905
+ const details = {};
6906
+ for (const model of models) {
6907
+ const detail = buildModelListDetail(
6908
+ resolveProviderModelEntry(providerEntries, provider, model)
6909
+ );
6910
+ if (detail) {
6911
+ details[model] = detail;
6912
+ }
6913
+ }
6914
+ return Object.keys(details).length ? { models, details } : { models };
5488
6915
  };
5489
-
5490
- // src/rpc/model.ts
5491
- import { updateModelConfig } from "@codelia/config-loader";
5492
- import {
5493
- DEFAULT_MODEL_REGISTRY as DEFAULT_MODEL_REGISTRY3,
5494
- listModels,
5495
- resolveModel
5496
- } from "@codelia/core";
5497
- import { ModelMetadataServiceImpl as ModelMetadataServiceImpl2 } from "@codelia/model-metadata";
5498
6916
  var createModelHandlers = ({
5499
6917
  state,
5500
6918
  log: log2
@@ -5502,9 +6920,9 @@ var createModelHandlers = ({
5502
6920
  const handleModelList = async (id, params) => {
5503
6921
  const requestedProvider = params?.provider;
5504
6922
  const includeDetails = params?.include_details ?? false;
5505
- if (requestedProvider && requestedProvider !== "openai" && requestedProvider !== "anthropic") {
6923
+ if (requestedProvider && !isSupportedProvider(requestedProvider)) {
5506
6924
  sendError(id, {
5507
- code: -32602,
6925
+ code: RPC_ERROR_CODE3.INVALID_PARAMS,
5508
6926
  message: `unsupported provider: ${requestedProvider}`
5509
6927
  });
5510
6928
  return;
@@ -5518,54 +6936,35 @@ var createModelHandlers = ({
5518
6936
  current = config.name;
5519
6937
  }
5520
6938
  } catch (error) {
5521
- sendError(id, { code: -32e3, message: String(error) });
6939
+ sendError(id, { code: RPC_ERROR_CODE3.RUNTIME_INTERNAL, message: String(error) });
5522
6940
  return;
5523
6941
  }
5524
6942
  const provider = requestedProvider ?? configuredProvider ?? "openai";
5525
- if (provider !== "openai" && provider !== "anthropic") {
6943
+ if (!isSupportedProvider(provider)) {
5526
6944
  sendError(id, {
5527
- code: -32602,
6945
+ code: RPC_ERROR_CODE3.INVALID_PARAMS,
5528
6946
  message: `unsupported provider: ${provider}`
5529
6947
  });
5530
6948
  return;
5531
6949
  }
5532
- const models = listModels(DEFAULT_MODEL_REGISTRY3, provider).map((model) => model.id).sort();
6950
+ let models;
6951
+ let details;
6952
+ try {
6953
+ const result2 = await buildProviderModelList({
6954
+ provider,
6955
+ includeDetails,
6956
+ state,
6957
+ log: log2
6958
+ });
6959
+ models = result2.models;
6960
+ details = result2.details;
6961
+ } catch (error) {
6962
+ sendError(id, { code: RPC_ERROR_CODE3.RUNTIME_INTERNAL, message: String(error) });
6963
+ return;
6964
+ }
5533
6965
  if (current && !models.includes(current)) {
5534
6966
  current = void 0;
5535
6967
  }
5536
- let details;
5537
- if (includeDetails) {
5538
- try {
5539
- const metadataService = new ModelMetadataServiceImpl2();
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
6968
  const result = {
5570
6969
  provider,
5571
6970
  models,
@@ -5576,26 +6975,28 @@ var createModelHandlers = ({
5576
6975
  };
5577
6976
  const handleModelSet = async (id, params) => {
5578
6977
  if (state.activeRunId) {
5579
- sendError(id, { code: -32001, message: "runtime busy" });
6978
+ sendError(id, { code: RPC_ERROR_CODE3.RUNTIME_BUSY, message: "runtime busy" });
5580
6979
  return;
5581
6980
  }
5582
6981
  const provider = params?.provider ?? "openai";
5583
6982
  const name = params?.name?.trim();
5584
6983
  if (!name) {
5585
- sendError(id, { code: -32602, message: "model name is required" });
6984
+ sendError(id, { code: RPC_ERROR_CODE3.INVALID_PARAMS, message: "model name is required" });
5586
6985
  return;
5587
6986
  }
5588
- if (provider !== "openai" && provider !== "anthropic") {
6987
+ if (provider !== "openai" && provider !== "anthropic" && provider !== "openrouter") {
5589
6988
  sendError(id, {
5590
- code: -32602,
6989
+ code: RPC_ERROR_CODE3.INVALID_PARAMS,
5591
6990
  message: `unsupported provider: ${provider}`
5592
6991
  });
5593
6992
  return;
5594
6993
  }
5595
- const spec = resolveModel(DEFAULT_MODEL_REGISTRY3, name, provider);
5596
- if (!spec) {
5597
- sendError(id, { code: -32602, message: `unknown model: ${name}` });
5598
- return;
6994
+ if (provider !== "openrouter") {
6995
+ const spec = resolveModel(DEFAULT_MODEL_REGISTRY3, name, provider);
6996
+ if (!spec) {
6997
+ sendError(id, { code: RPC_ERROR_CODE3.INVALID_PARAMS, message: `unknown model: ${name}` });
6998
+ return;
6999
+ }
5599
7000
  }
5600
7001
  try {
5601
7002
  const configPath = resolveConfigPath();
@@ -5605,17 +7006,23 @@ var createModelHandlers = ({
5605
7006
  sendResult(id, result);
5606
7007
  log2(`model.set ${provider}/${name}`);
5607
7008
  } catch (error) {
5608
- sendError(id, { code: -32e3, message: String(error) });
7009
+ sendError(id, { code: RPC_ERROR_CODE3.RUNTIME_INTERNAL, message: String(error) });
5609
7010
  }
5610
7011
  };
5611
7012
  return { handleModelList, handleModelSet };
5612
7013
  };
5613
7014
 
7015
+ // src/rpc/run.ts
7016
+ import {
7017
+ RPC_ERROR_CODE as RPC_ERROR_CODE4
7018
+ } from "@codelia/protocol";
7019
+
5614
7020
  // src/rpc/run-debug.ts
5615
7021
  import { stringifyContent } from "@codelia/core";
5616
7022
  var DEBUG_MAX_CONTENT_CHARS = 2e3;
5617
7023
  var DEBUG_MAX_LOG_CHARS = 2e4;
5618
7024
  var DEBUG_MAX_EVENT_RESULT_CHARS = 500;
7025
+ var DEBUG_MAX_EVENT_ARGS_CHARS = 2e3;
5619
7026
  var truncateText2 = (value, maxChars) => {
5620
7027
  if (value.length <= maxChars) return value;
5621
7028
  return `${value.slice(0, maxChars)}...[truncated]`;
@@ -5687,7 +7094,9 @@ var summarizeRunEvent = (event) => {
5687
7094
  case "tool_call": {
5688
7095
  const tool = typeof event.tool === "string" ? event.tool : "unknown";
5689
7096
  const toolCallId = typeof event.tool_call_id === "string" ? event.tool_call_id : "unknown";
5690
- return `type=tool_call tool=${tool} tool_call_id=${toolCallId}`;
7097
+ const rawArgs = typeof event.raw_args === "string" ? event.raw_args : stringifyUnknown(event.args);
7098
+ const rawArgsSnippet = truncateText2(rawArgs, DEBUG_MAX_EVENT_ARGS_CHARS);
7099
+ return `type=tool_call tool=${tool} tool_call_id=${toolCallId} raw_args=${stringifyUnknown(rawArgsSnippet)}`;
5691
7100
  }
5692
7101
  case "tool_result": {
5693
7102
  const tool = typeof event.tool === "string" ? event.tool : "unknown";
@@ -5722,7 +7131,7 @@ var logRunDebug = (log2, runId, message) => {
5722
7131
  if (!isDebugEnabled()) return;
5723
7132
  log2(`run debug run_id=${runId} ${message}`);
5724
7133
  };
5725
- var normalizeCancelledHistory = (messages) => {
7134
+ var normalizeToolCallHistory = (messages) => {
5726
7135
  const assistantCallIds = /* @__PURE__ */ new Set();
5727
7136
  const toolOutputCallIds = /* @__PURE__ */ new Set();
5728
7137
  for (const message of messages) {
@@ -5806,8 +7215,92 @@ var prepareRunInputText = (inputText) => {
5806
7215
  return `${inputText}${buildSkillMentionHint(mentions)}`;
5807
7216
  };
5808
7217
 
7218
+ // src/rpc/run-input.ts
7219
+ var isRunInputImageMediaType = (value) => value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
7220
+ var isRunInputImageDetail = (value) => value === "auto" || value === "low" || value === "high";
7221
+ var normalizeRunInputTextPart = (part) => {
7222
+ if (typeof part.text !== "string") {
7223
+ throw new Error("run.start input.parts[text].text must be a string");
7224
+ }
7225
+ return {
7226
+ type: "text",
7227
+ text: prepareRunInputText(part.text)
7228
+ };
7229
+ };
7230
+ var normalizeRunInputImagePart = (part) => {
7231
+ const imageUrl = part.image_url;
7232
+ if (!imageUrl || typeof imageUrl !== "object") {
7233
+ throw new Error("run.start input.parts[image_url].image_url is required");
7234
+ }
7235
+ if (typeof imageUrl.url !== "string" || imageUrl.url.length === 0) {
7236
+ throw new Error(
7237
+ "run.start input.parts[image_url].image_url.url must be a non-empty string"
7238
+ );
7239
+ }
7240
+ if (imageUrl.media_type !== void 0 && !isRunInputImageMediaType(imageUrl.media_type)) {
7241
+ throw new Error(
7242
+ "run.start input.parts[image_url].image_url.media_type must be png/jpeg/webp/gif"
7243
+ );
7244
+ }
7245
+ if (imageUrl.detail !== void 0 && !isRunInputImageDetail(imageUrl.detail)) {
7246
+ throw new Error(
7247
+ "run.start input.parts[image_url].image_url.detail must be auto/low/high"
7248
+ );
7249
+ }
7250
+ return {
7251
+ type: "image_url",
7252
+ image_url: {
7253
+ url: imageUrl.url,
7254
+ ...imageUrl.media_type ? { media_type: imageUrl.media_type } : {},
7255
+ ...imageUrl.detail ? { detail: imageUrl.detail } : {}
7256
+ }
7257
+ };
7258
+ };
7259
+ var normalizeRunInput = (input) => {
7260
+ if (input.type === "text") {
7261
+ if (typeof input.text !== "string") {
7262
+ throw new Error("run.start input.text must be a string");
7263
+ }
7264
+ return prepareRunInputText(input.text);
7265
+ }
7266
+ if (!Array.isArray(input.parts)) {
7267
+ throw new Error("run.start input.parts must be an array");
7268
+ }
7269
+ return input.parts.map((part) => {
7270
+ if (!part || typeof part !== "object") {
7271
+ throw new Error("run.start input.parts entry must be an object");
7272
+ }
7273
+ if (part.type === "text") {
7274
+ return normalizeRunInputTextPart(part);
7275
+ }
7276
+ if (part.type === "image_url") {
7277
+ return normalizeRunInputImagePart(part);
7278
+ }
7279
+ throw new Error(
7280
+ `run.start input.parts type is not supported: ${String(part.type)}`
7281
+ );
7282
+ });
7283
+ };
7284
+ var runInputLengthForDebug = (input) => {
7285
+ if (typeof input === "string") {
7286
+ return input.length;
7287
+ }
7288
+ let total = 0;
7289
+ for (const part of input) {
7290
+ if (part.type === "text") {
7291
+ total += part.text.length;
7292
+ continue;
7293
+ }
7294
+ if (part.type === "image_url") {
7295
+ total += part.image_url.url.length;
7296
+ }
7297
+ }
7298
+ return total;
7299
+ };
7300
+
5809
7301
  // src/rpc/run.ts
5810
- var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
7302
+ var nowIso4 = () => (/* @__PURE__ */ new Date()).toISOString();
7303
+ var SESSION_STATE_SAVE_DEBOUNCE_MS = 1500;
5811
7304
  var createSessionAppender = (store, onError) => {
5812
7305
  let chain = Promise.resolve();
5813
7306
  return (record) => {
@@ -5819,7 +7312,7 @@ var createSessionAppender = (store, onError) => {
5819
7312
  var buildSessionState = (sessionId, runId, messages, invokeSeq) => ({
5820
7313
  schema_version: 1,
5821
7314
  session_id: sessionId,
5822
- updated_at: nowIso2(),
7315
+ updated_at: nowIso4(),
5823
7316
  run_id: runId,
5824
7317
  invoke_seq: invokeSeq,
5825
7318
  messages
@@ -5830,18 +7323,26 @@ var createRunHandlers = ({
5830
7323
  log: log2,
5831
7324
  runEventStoreFactory,
5832
7325
  sessionStateStore,
5833
- appendSession
7326
+ appendSession,
7327
+ beforeRunStart
5834
7328
  }) => {
5835
7329
  let activeRunAbort = null;
5836
7330
  let runStartQueue = Promise.resolve();
5837
7331
  const normalizeRunHistoryAfterCancel = (runId, runtimeAgent) => {
5838
7332
  const currentMessages = runtimeAgent.getHistoryMessages();
5839
- const normalizedMessages = normalizeCancelledHistory(currentMessages);
7333
+ const normalizedMessages = normalizeToolCallHistory(currentMessages);
5840
7334
  if (normalizedMessages !== currentMessages) {
5841
7335
  runtimeAgent.replaceHistoryMessages(normalizedMessages);
5842
7336
  log2(`run.cancel normalized history ${runId}`);
5843
7337
  }
5844
7338
  };
7339
+ const normalizeRestoredSessionMessages = (messages, sessionId) => {
7340
+ const normalizedMessages = normalizeToolCallHistory(messages);
7341
+ if (normalizedMessages !== messages) {
7342
+ log2(`session restore normalized history ${sessionId}`);
7343
+ }
7344
+ return normalizedMessages;
7345
+ };
5845
7346
  const emitRunStatus = (runId, status, message) => {
5846
7347
  sendRunStatus(runId, status, message);
5847
7348
  const suffix = message ? ` message=${message}` : "";
@@ -5849,7 +7350,7 @@ var createRunHandlers = ({
5849
7350
  appendSession({
5850
7351
  type: "run.status",
5851
7352
  run_id: runId,
5852
- ts: nowIso2(),
7353
+ ts: nowIso4(),
5853
7354
  status,
5854
7355
  ...message ? { message } : {}
5855
7356
  });
@@ -5858,22 +7359,43 @@ var createRunHandlers = ({
5858
7359
  appendSession({
5859
7360
  type: "run.end",
5860
7361
  run_id: runId,
5861
- ts: nowIso2(),
7362
+ ts: nowIso4(),
5862
7363
  outcome,
5863
7364
  ...final !== void 0 ? { final } : {}
5864
7365
  });
5865
7366
  };
5866
7367
  const handleRunStart = (id, params) => {
5867
7368
  const run = async () => {
7369
+ let normalizedInput;
7370
+ try {
7371
+ normalizedInput = normalizeRunInput(params.input);
7372
+ } catch (error) {
7373
+ sendError(id, {
7374
+ code: RPC_ERROR_CODE4.INVALID_PARAMS,
7375
+ message: String(error)
7376
+ });
7377
+ return;
7378
+ }
7379
+ if (beforeRunStart) {
7380
+ try {
7381
+ await beforeRunStart();
7382
+ } catch (error) {
7383
+ sendError(id, {
7384
+ code: RPC_ERROR_CODE4.RUNTIME_INTERNAL,
7385
+ message: `startup onboarding failed: ${String(error)}`
7386
+ });
7387
+ return;
7388
+ }
7389
+ }
5868
7390
  if (state.activeRunId) {
5869
- sendError(id, { code: -32001, message: "runtime busy" });
7391
+ sendError(id, { code: RPC_ERROR_CODE4.RUNTIME_BUSY, message: "runtime busy" });
5870
7392
  return;
5871
7393
  }
5872
7394
  let runtimeAgent;
5873
7395
  try {
5874
7396
  runtimeAgent = await getAgent();
5875
7397
  } catch (error) {
5876
- sendError(id, { code: -32e3, message: String(error) });
7398
+ sendError(id, { code: RPC_ERROR_CODE4.RUNTIME_INTERNAL, message: String(error) });
5877
7399
  return;
5878
7400
  }
5879
7401
  const requestedSessionId = params.session_id?.trim() || void 0;
@@ -5884,16 +7406,20 @@ var createRunHandlers = ({
5884
7406
  resumeState = await sessionStateStore.load(requestedSessionId);
5885
7407
  } catch (error) {
5886
7408
  sendError(id, {
5887
- code: -32005,
7409
+ code: RPC_ERROR_CODE4.SESSION_LOAD_FAILED,
5888
7410
  message: `session load failed: ${String(error)}`
5889
7411
  });
5890
7412
  return;
5891
7413
  }
5892
7414
  if (!resumeState) {
5893
- sendError(id, { code: -32004, message: "session not found" });
7415
+ sendError(id, { code: RPC_ERROR_CODE4.SESSION_NOT_FOUND, message: "session not found" });
5894
7416
  return;
5895
7417
  }
5896
- const messages = Array.isArray(resumeState.messages) ? resumeState.messages : [];
7418
+ const restoredMessages = Array.isArray(resumeState.messages) ? resumeState.messages : [];
7419
+ const messages = normalizeRestoredSessionMessages(
7420
+ restoredMessages,
7421
+ requestedSessionId
7422
+ );
5897
7423
  runtimeAgent.replaceHistoryMessages(messages);
5898
7424
  sessionId = requestedSessionId;
5899
7425
  state.sessionId = sessionId;
@@ -5901,17 +7427,25 @@ var createRunHandlers = ({
5901
7427
  try {
5902
7428
  resumeState = await sessionStateStore.load(state.sessionId);
5903
7429
  } catch (error) {
5904
- log2(`session state reload error: ${String(error)}`);
7430
+ sendError(id, {
7431
+ code: RPC_ERROR_CODE4.SESSION_LOAD_FAILED,
7432
+ message: `session reload failed: ${String(error)}`
7433
+ });
7434
+ return;
5905
7435
  }
5906
7436
  if (resumeState) {
5907
- const messages = Array.isArray(resumeState.messages) ? resumeState.messages : [];
7437
+ const restoredMessages = Array.isArray(resumeState.messages) ? resumeState.messages : [];
7438
+ const messages = normalizeRestoredSessionMessages(
7439
+ restoredMessages,
7440
+ state.sessionId
7441
+ );
5908
7442
  runtimeAgent.replaceHistoryMessages(messages);
5909
7443
  }
5910
7444
  } else if (!state.sessionId) {
5911
7445
  state.sessionId = sessionId;
5912
7446
  }
5913
7447
  const runId = state.nextRunId();
5914
- const startedAt = nowIso2();
7448
+ const startedAt = nowIso4();
5915
7449
  state.beginRun(runId, params.ui_context ?? state.lastUiContext);
5916
7450
  const runAbortController = new AbortController();
5917
7451
  activeRunAbort = { runId, controller: runAbortController };
@@ -5961,7 +7495,7 @@ var createRunHandlers = ({
5961
7495
  type: "run.start",
5962
7496
  run_id: runId,
5963
7497
  session_id: sessionId,
5964
- ts: nowIso2(),
7498
+ ts: nowIso4(),
5965
7499
  input: params.input,
5966
7500
  ui_context: params.ui_context,
5967
7501
  meta: params.meta
@@ -5972,25 +7506,58 @@ var createRunHandlers = ({
5972
7506
  emitRunStatus(runId, "running");
5973
7507
  void (async () => {
5974
7508
  let finalResponse;
5975
- let cancelledWhileStreaming = false;
5976
- const preparedInputText = prepareRunInputText(params.input.text);
7509
+ let sessionSaveChain = Promise.resolve();
7510
+ let lastSessionSaveAt = 0;
7511
+ const queueSessionSave = async (reason) => {
7512
+ sessionSaveChain = sessionSaveChain.then(async () => {
7513
+ if (!sessionId) return;
7514
+ const messages = runtimeAgent.getHistoryMessages();
7515
+ const snapshotMessages = normalizeToolCallHistory(messages);
7516
+ if (snapshotMessages !== messages) {
7517
+ logRunDebug(
7518
+ log2,
7519
+ runId,
7520
+ `session.save normalized reason=${reason}`
7521
+ );
7522
+ }
7523
+ const snapshot = buildSessionState(
7524
+ sessionId,
7525
+ runId,
7526
+ snapshotMessages,
7527
+ session.invoke_seq
7528
+ );
7529
+ await sessionStateStore.save(snapshot);
7530
+ logRunDebug(log2, runId, `session.save ${reason}`);
7531
+ }).catch((error) => {
7532
+ log2(`Error: session-state save failed: ${String(error)}`);
7533
+ });
7534
+ await sessionSaveChain;
7535
+ };
7536
+ const maybeDebouncedSessionSave = () => {
7537
+ const now = Date.now();
7538
+ if (now - lastSessionSaveAt < SESSION_STATE_SAVE_DEBOUNCE_MS) {
7539
+ return;
7540
+ }
7541
+ lastSessionSaveAt = now;
7542
+ void queueSessionSave("debounced");
7543
+ };
5977
7544
  try {
5978
7545
  logRunDebug(
5979
7546
  log2,
5980
7547
  runId,
5981
- `stream.start input_chars=${preparedInputText.length}`
7548
+ `stream.start input_chars=${runInputLengthForDebug(normalizedInput)}`
5982
7549
  );
5983
- for await (const event of runtimeAgent.runStream(preparedInputText, {
7550
+ for await (const event of runtimeAgent.runStream(normalizedInput, {
5984
7551
  session,
5985
7552
  signal: runAbortController.signal,
5986
7553
  forceCompaction: params.force_compaction
5987
7554
  })) {
5988
7555
  if (state.cancelRequested) {
5989
- cancelledWhileStreaming = true;
5990
7556
  break;
5991
7557
  }
5992
7558
  if (event.type === "final") {
5993
7559
  finalResponse = event.content;
7560
+ await queueSessionSave("final");
5994
7561
  }
5995
7562
  if (event.type === "compaction_complete") {
5996
7563
  logCompactionSnapshot(log2, runId, runtimeAgent, event.compacted);
@@ -6014,7 +7581,7 @@ var createRunHandlers = ({
6014
7581
  appendSession({
6015
7582
  type: "agent.event",
6016
7583
  run_id: runId,
6017
- ts: nowIso2(),
7584
+ ts: nowIso4(),
6018
7585
  seq,
6019
7586
  event
6020
7587
  });
@@ -6025,14 +7592,16 @@ var createRunHandlers = ({
6025
7592
  appendSession({
6026
7593
  type: "run.context",
6027
7594
  run_id: runId,
6028
- ts: nowIso2(),
7595
+ ts: nowIso4(),
6029
7596
  context_left_percent: contextLeftPercent
6030
7597
  });
6031
7598
  }
7599
+ maybeDebouncedSessionSave();
6032
7600
  }
6033
- if (cancelledWhileStreaming) {
7601
+ if (state.cancelRequested) {
6034
7602
  normalizeRunHistoryAfterCancel(runId, runtimeAgent);
6035
7603
  }
7604
+ await queueSessionSave("terminal");
6036
7605
  const status = state.cancelRequested ? "cancelled" : "completed";
6037
7606
  emitRunStatus(runId, status);
6038
7607
  emitRunEnd(
@@ -6044,15 +7613,17 @@ var createRunHandlers = ({
6044
7613
  const err = error instanceof Error ? error : new Error(String(error));
6045
7614
  if (state.cancelRequested || isAbortLikeError(err)) {
6046
7615
  normalizeRunHistoryAfterCancel(runId, runtimeAgent);
7616
+ await queueSessionSave("cancelled");
6047
7617
  emitRunStatus(runId, "cancelled", err.message || "cancelled");
6048
7618
  emitRunEnd(runId, "cancelled", finalResponse);
6049
7619
  return;
6050
7620
  }
7621
+ await queueSessionSave("error");
6051
7622
  emitRunStatus(runId, "error", err.message);
6052
7623
  appendSession({
6053
7624
  type: "run.error",
6054
7625
  run_id: runId,
6055
- ts: nowIso2(),
7626
+ ts: nowIso4(),
6056
7627
  error: {
6057
7628
  name: err.name,
6058
7629
  message: err.message,
@@ -6062,18 +7633,10 @@ var createRunHandlers = ({
6062
7633
  emitRunEnd(runId, "error");
6063
7634
  } finally {
6064
7635
  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
- });
7636
+ if (state.cancelRequested) {
7637
+ normalizeRunHistoryAfterCancel(runId, runtimeAgent);
6076
7638
  }
7639
+ await queueSessionSave("finally");
6077
7640
  if (activeRunAbort?.runId === runId) {
6078
7641
  activeRunAbort = null;
6079
7642
  }
@@ -6099,19 +7662,22 @@ var createRunHandlers = ({
6099
7662
  log2(`run.cancel ${params.run_id} (${params.reason ?? "no reason"})`);
6100
7663
  return;
6101
7664
  }
6102
- sendError(id, { code: -32002, message: "run not found" });
7665
+ sendError(id, { code: RPC_ERROR_CODE4.RUN_NOT_FOUND, message: "run not found" });
6103
7666
  };
6104
7667
  return { handleRunStart, handleRunCancel };
6105
7668
  };
6106
7669
 
6107
7670
  // src/rpc/skills.ts
6108
- import path12 from "path";
7671
+ import path14 from "path";
7672
+ import {
7673
+ RPC_ERROR_CODE as RPC_ERROR_CODE5
7674
+ } from "@codelia/protocol";
6109
7675
  var createSkillsHandlers = ({
6110
7676
  state,
6111
7677
  log: log2
6112
7678
  }) => {
6113
7679
  const handleSkillsList = async (id, params) => {
6114
- const requestedCwd = params?.cwd ? path12.resolve(params.cwd) : void 0;
7680
+ const requestedCwd = params?.cwd ? path14.resolve(params.cwd) : void 0;
6115
7681
  const workingDir = requestedCwd ?? state.runtimeWorkingDir ?? state.lastUiContext?.cwd ?? process.cwd();
6116
7682
  const forceReload = params?.force_reload ?? false;
6117
7683
  if (!forceReload) {
@@ -6135,7 +7701,7 @@ var createSkillsHandlers = ({
6135
7701
  });
6136
7702
  } catch (error) {
6137
7703
  sendError(id, {
6138
- code: -32e3,
7704
+ code: RPC_ERROR_CODE5.RUNTIME_INTERNAL,
6139
7705
  message: `skills resolver init failed: ${String(error)}`
6140
7706
  });
6141
7707
  return;
@@ -6157,7 +7723,7 @@ var createSkillsHandlers = ({
6157
7723
  } catch (error) {
6158
7724
  log2(`skills.list error: ${String(error)}`);
6159
7725
  sendError(id, {
6160
- code: -32e3,
7726
+ code: RPC_ERROR_CODE5.RUNTIME_INTERNAL,
6161
7727
  message: `skills list failed: ${String(error)}`
6162
7728
  });
6163
7729
  }
@@ -6165,6 +7731,77 @@ var createSkillsHandlers = ({
6165
7731
  return { handleSkillsList };
6166
7732
  };
6167
7733
 
7734
+ // src/rpc/tool.ts
7735
+ import {
7736
+ RPC_ERROR_CODE as RPC_ERROR_CODE6
7737
+ } from "@codelia/protocol";
7738
+ var normalizeToolResult = (result) => {
7739
+ switch (result.type) {
7740
+ case "json":
7741
+ return result.value;
7742
+ case "text":
7743
+ return result.text;
7744
+ case "parts":
7745
+ return result.parts;
7746
+ }
7747
+ };
7748
+ var createToolContext = () => {
7749
+ const deps = /* @__PURE__ */ Object.create(null);
7750
+ const cache = /* @__PURE__ */ new Map();
7751
+ const resolve = async (key) => {
7752
+ if (cache.has(key.id)) {
7753
+ return cache.get(key.id);
7754
+ }
7755
+ const value = await key.create();
7756
+ cache.set(key.id, value);
7757
+ return value;
7758
+ };
7759
+ return {
7760
+ deps,
7761
+ resolve,
7762
+ now: () => /* @__PURE__ */ new Date()
7763
+ };
7764
+ };
7765
+ var createToolHandlers = ({
7766
+ state,
7767
+ getAgent
7768
+ }) => {
7769
+ const handleToolCall = async (id, params) => {
7770
+ const toolName = params?.name;
7771
+ if (!toolName) {
7772
+ sendError(id, { code: RPC_ERROR_CODE6.INVALID_PARAMS, message: "tool name is required" });
7773
+ return;
7774
+ }
7775
+ if (!state.tools) {
7776
+ await getAgent();
7777
+ }
7778
+ const tool = state.tools?.find((entry) => entry.name === toolName);
7779
+ if (!tool) {
7780
+ sendError(id, { code: RPC_ERROR_CODE6.INVALID_PARAMS, message: `unknown tool: ${toolName}` });
7781
+ return;
7782
+ }
7783
+ try {
7784
+ const result = await tool.executeRaw(
7785
+ JSON.stringify(params?.arguments ?? {}),
7786
+ createToolContext()
7787
+ );
7788
+ const response = {
7789
+ ok: true,
7790
+ result: normalizeToolResult(result)
7791
+ };
7792
+ sendResult(id, response);
7793
+ } catch (error) {
7794
+ sendError(id, {
7795
+ code: RPC_ERROR_CODE6.RUNTIME_INTERNAL,
7796
+ message: `tool call failed: ${String(error)}`
7797
+ });
7798
+ }
7799
+ };
7800
+ return {
7801
+ handleToolCall
7802
+ };
7803
+ };
7804
+
6168
7805
  // src/rpc/handlers.ts
6169
7806
  var createRuntimeHandlers = ({
6170
7807
  state,
@@ -6172,7 +7809,8 @@ var createRuntimeHandlers = ({
6172
7809
  log: log2,
6173
7810
  mcpManager: injectedMcpManager,
6174
7811
  sessionStateStore: injectedSessionStateStore,
6175
- runEventStoreFactory: injectedRunEventStoreFactory
7812
+ runEventStoreFactory: injectedRunEventStoreFactory,
7813
+ buildProviderModelList: injectedBuildProviderModelList
6176
7814
  }) => {
6177
7815
  const sessionStateStore = injectedSessionStateStore ?? new SessionStateStoreImpl({
6178
7816
  onError: (error, context) => {
@@ -6186,17 +7824,117 @@ var createRuntimeHandlers = ({
6186
7824
  start: async () => void 0,
6187
7825
  list: () => ({ servers: [] })
6188
7826
  };
7827
+ const buildProviderModelList2 = injectedBuildProviderModelList ?? buildProviderModelList;
7828
+ let startupOnboardingPromise = null;
7829
+ let startupOnboardingStarted = false;
6189
7830
  const appendSession = (record) => {
6190
7831
  if (!state.sessionAppend) return;
6191
7832
  state.sessionAppend(record);
6192
7833
  };
7834
+ const formatUsdPer1M = (value) => {
7835
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
7836
+ return null;
7837
+ }
7838
+ const fixed = value.toFixed(4);
7839
+ return fixed.replace(/\.?0+$/, "");
7840
+ };
7841
+ const buildModelPickDetail = (details) => {
7842
+ const parts = [];
7843
+ if (details?.release_date) {
7844
+ parts.push(`released ${details.release_date}`);
7845
+ }
7846
+ const inputCost = formatUsdPer1M(details?.cost_per_1m_input_tokens_usd);
7847
+ const outputCost = formatUsdPer1M(details?.cost_per_1m_output_tokens_usd);
7848
+ if (inputCost !== null || outputCost !== null) {
7849
+ parts.push(
7850
+ `cost in/out ${inputCost ?? "-"}/${outputCost ?? "-"} USD per 1M`
7851
+ );
7852
+ }
7853
+ return parts.join(" \u2022 ");
7854
+ };
7855
+ const runStartupOnboarding = async () => {
7856
+ const supportsPick = !!state.uiCapabilities?.supports_pick;
7857
+ const supportsPrompt = !!state.uiCapabilities?.supports_prompt;
7858
+ if (!supportsPick || !supportsPrompt) {
7859
+ return;
7860
+ }
7861
+ const authResolver = await AuthResolver.create(state, log2);
7862
+ if (authResolver.hasAnyAvailableAuth()) {
7863
+ return;
7864
+ }
7865
+ const providerPick = await requestUiPick(state, {
7866
+ title: "Let's set up your provider to get started.",
7867
+ items: SUPPORTED_PROVIDERS.map((provider2) => ({
7868
+ id: provider2,
7869
+ label: provider2,
7870
+ detail: provider2 === "openai" ? "OAuth (ChatGPT Plus/Pro) or API key" : provider2 === "openrouter" ? "API key (OpenRouter)" : "API key"
7871
+ })),
7872
+ multi: false
7873
+ });
7874
+ const pickedProvider = providerPick?.ids?.[0];
7875
+ if (!pickedProvider || !SUPPORTED_PROVIDERS.includes(pickedProvider)) {
7876
+ log2("startup onboarding skipped (provider not selected)");
7877
+ return;
7878
+ }
7879
+ const provider = pickedProvider;
7880
+ try {
7881
+ await authResolver.resolveProviderAuth(provider);
7882
+ } catch (error) {
7883
+ log2(`startup onboarding auth failed: ${String(error)}`);
7884
+ return;
7885
+ }
7886
+ const { models, details } = await buildProviderModelList2({
7887
+ provider,
7888
+ includeDetails: true,
7889
+ state,
7890
+ log: log2
7891
+ });
7892
+ if (!models.length) {
7893
+ log2(`startup onboarding model list returned no models for ${provider}`);
7894
+ return;
7895
+ }
7896
+ const modelPick = await requestUiPick(state, {
7897
+ title: `Select model (${provider})`,
7898
+ items: models.map((model) => ({
7899
+ id: model,
7900
+ label: model,
7901
+ detail: buildModelPickDetail(details?.[model]) || void 0
7902
+ })),
7903
+ multi: false
7904
+ });
7905
+ const selectedModel = modelPick?.ids?.[0];
7906
+ if (!selectedModel || !models.includes(selectedModel)) {
7907
+ log2("startup onboarding skipped (model not selected)");
7908
+ return;
7909
+ }
7910
+ const configPath = resolveConfigPath();
7911
+ await updateModelConfig2(configPath, { provider, name: selectedModel });
7912
+ state.agent = null;
7913
+ log2(`startup onboarding completed: ${provider}/${selectedModel}`);
7914
+ };
7915
+ const launchStartupOnboarding = () => {
7916
+ if (startupOnboardingStarted) {
7917
+ return;
7918
+ }
7919
+ startupOnboardingStarted = true;
7920
+ startupOnboardingPromise = runStartupOnboarding().catch((error) => {
7921
+ log2(`startup onboarding failed: ${String(error)}`);
7922
+ }).finally(() => {
7923
+ startupOnboardingPromise = null;
7924
+ });
7925
+ };
7926
+ const waitForStartupOnboarding = async () => {
7927
+ if (!startupOnboardingPromise) return;
7928
+ await startupOnboardingPromise;
7929
+ };
6193
7930
  const { handleRunStart, handleRunCancel } = createRunHandlers({
6194
7931
  state,
6195
7932
  getAgent,
6196
7933
  log: log2,
6197
7934
  runEventStoreFactory,
6198
7935
  sessionStateStore,
6199
- appendSession
7936
+ appendSession,
7937
+ beforeRunStart: waitForStartupOnboarding
6200
7938
  });
6201
7939
  const { handleSessionList, handleSessionHistory } = createHistoryHandlers({
6202
7940
  sessionStateStore,
@@ -6214,6 +7952,10 @@ var createRuntimeHandlers = ({
6214
7952
  state,
6215
7953
  log: log2
6216
7954
  });
7955
+ const { handleToolCall } = createToolHandlers({
7956
+ state,
7957
+ getAgent
7958
+ });
6217
7959
  const handleInitialize = (id, params) => {
6218
7960
  const result = {
6219
7961
  protocol_version: PROTOCOL_VERSION,
@@ -6223,13 +7965,16 @@ var createRuntimeHandlers = ({
6223
7965
  supports_ui_requests: true,
6224
7966
  supports_mcp_list: true,
6225
7967
  supports_skills_list: true,
6226
- supports_context_inspect: true
7968
+ supports_context_inspect: true,
7969
+ supports_tool_call: true,
7970
+ supports_permission_preflight_events: true
6227
7971
  }
6228
7972
  };
6229
7973
  sendResult(id, result);
6230
7974
  log2(`initialize from ${params.client?.name ?? "unknown"}`);
6231
7975
  state.lastClientInfo = params.client ?? null;
6232
7976
  state.setUiCapabilities(params.ui_capabilities);
7977
+ launchStartupOnboarding();
6233
7978
  };
6234
7979
  const handleUiContextUpdate = (params) => {
6235
7980
  state.updateUiContext(params);
@@ -6239,7 +7984,7 @@ var createRuntimeHandlers = ({
6239
7984
  };
6240
7985
  const handleAuthLogout = async (id, params) => {
6241
7986
  if (state.activeRunId) {
6242
- sendError(id, { code: -32001, message: "runtime busy" });
7987
+ sendError(id, { code: RPC_ERROR_CODE7.RUNTIME_BUSY, message: "runtime busy" });
6243
7988
  return;
6244
7989
  }
6245
7990
  const clearSession = params?.clear_session ?? true;
@@ -6247,7 +7992,7 @@ var createRuntimeHandlers = ({
6247
7992
  const supportsConfirm = !!state.uiCapabilities?.supports_confirm;
6248
7993
  if (!supportsConfirm) {
6249
7994
  sendError(id, {
6250
- code: -32e3,
7995
+ code: RPC_ERROR_CODE7.RUNTIME_INTERNAL,
6251
7996
  message: "UI confirmation is required for logout"
6252
7997
  });
6253
7998
  return;
@@ -6289,7 +8034,7 @@ var createRuntimeHandlers = ({
6289
8034
  log2(`auth.logout session_cleared=${clearSession}`);
6290
8035
  } catch (error) {
6291
8036
  sendError(id, {
6292
- code: -32e3,
8037
+ code: RPC_ERROR_CODE7.RUNTIME_INTERNAL,
6293
8038
  message: `auth logout failed: ${String(error)}`
6294
8039
  });
6295
8040
  }
@@ -6312,6 +8057,8 @@ var createRuntimeHandlers = ({
6312
8057
  return handleModelList(req.id, req.params);
6313
8058
  case "model.set":
6314
8059
  return handleModelSet(req.id, req.params);
8060
+ case "tool.call":
8061
+ return handleToolCall(req.id, req.params);
6315
8062
  case "mcp.list":
6316
8063
  await mcpManager.start?.();
6317
8064
  return sendResult(
@@ -6323,7 +8070,7 @@ var createRuntimeHandlers = ({
6323
8070
  case "context.inspect":
6324
8071
  return handleContextInspect(req.id, req.params);
6325
8072
  default:
6326
- return sendError(req.id, { code: -32601, message: "method not found" });
8073
+ return sendError(req.id, { code: RPC_ERROR_CODE7.METHOD_NOT_FOUND, message: "method not found" });
6327
8074
  }
6328
8075
  };
6329
8076
  const handleNotification = (note) => {
@@ -6350,7 +8097,7 @@ var createRuntimeHandlers = ({
6350
8097
  };
6351
8098
 
6352
8099
  // src/runtime-state.ts
6353
- import crypto5 from "crypto";
8100
+ import crypto6 from "crypto";
6354
8101
  var RuntimeState = class {
6355
8102
  runSeq = /* @__PURE__ */ new Map();
6356
8103
  uiRequestCounter = 0;
@@ -6363,6 +8110,7 @@ var RuntimeState = class {
6363
8110
  uiCapabilities = null;
6364
8111
  systemPrompt = null;
6365
8112
  toolDefinitions = null;
8113
+ tools = null;
6366
8114
  sessionId = null;
6367
8115
  sessionAppend = null;
6368
8116
  agent = null;
@@ -6373,10 +8121,10 @@ var RuntimeState = class {
6373
8121
  runtimeWorkingDir = null;
6374
8122
  runtimeSandboxRoot = null;
6375
8123
  nextRunId() {
6376
- return crypto5.randomUUID();
8124
+ return crypto6.randomUUID();
6377
8125
  }
6378
8126
  nextSessionId() {
6379
- return crypto5.randomUUID();
8127
+ return crypto6.randomUUID();
6380
8128
  }
6381
8129
  beginRun(runId, uiContext) {
6382
8130
  this.activeRunId = runId;
@@ -6466,42 +8214,59 @@ var RuntimeState = class {
6466
8214
 
6467
8215
  // src/runtime.ts
6468
8216
  var startRuntime = () => {
6469
- const state = new RuntimeState();
6470
- const workingDir = process.env.CODELIA_SANDBOX_ROOT ? path13.resolve(process.env.CODELIA_SANDBOX_ROOT) : process.cwd();
6471
- state.runtimeWorkingDir = workingDir;
6472
- state.runtimeSandboxRoot = workingDir;
6473
- const mcpManager = new McpManager({ workingDir, log });
6474
- void mcpManager.start({
6475
- onStatus: (message) => 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,
6483
- mcpManager
6484
- });
6485
- 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
- log(`invalid json: ${String(error)}`);
6500
- }
8217
+ void (async () => {
8218
+ const state = new RuntimeState();
8219
+ const workingDir = process.env.CODELIA_SANDBOX_ROOT ? path15.resolve(process.env.CODELIA_SANDBOX_ROOT) : process.cwd();
8220
+ state.runtimeWorkingDir = workingDir;
8221
+ state.runtimeSandboxRoot = workingDir;
8222
+ const sessionStateStore = new SessionStateStoreImpl2({
8223
+ onError: (error, context) => {
8224
+ log(
8225
+ `Error: session-state ${context.action} error${context.detail ? ` (${context.detail})` : ""}: ${String(error)}`
8226
+ );
6501
8227
  }
6502
- index = buffer.indexOf("\n");
8228
+ });
8229
+ try {
8230
+ await sessionStateStore.list();
8231
+ } catch (error) {
8232
+ log(`Error: session index database is not available: ${String(error)}`);
8233
+ process.exit(1);
8234
+ return;
6503
8235
  }
6504
- });
8236
+ const mcpManager = new McpManager({ workingDir, log });
8237
+ void mcpManager.start({
8238
+ onStatus: (message) => log(`mcp: ${message}`),
8239
+ requestOAuthTokens: ({ server_id, oauth: oauth2, error }) => requestMcpOAuthTokensWithRunStatus(state, server_id, oauth2, error)
8240
+ });
8241
+ const getAgent = createAgentFactory(state, { mcpManager });
8242
+ const { processMessage } = createRuntimeHandlers({
8243
+ state,
8244
+ getAgent,
8245
+ log,
8246
+ mcpManager,
8247
+ sessionStateStore
8248
+ });
8249
+ log("runtime started");
8250
+ process.stdin.setEncoding("utf8");
8251
+ let buffer = "";
8252
+ process.stdin.on("data", (chunk) => {
8253
+ buffer += chunk;
8254
+ let index = buffer.indexOf("\n");
8255
+ while (index >= 0) {
8256
+ const line = buffer.slice(0, index).trim();
8257
+ buffer = buffer.slice(index + 1);
8258
+ if (line) {
8259
+ try {
8260
+ const msg = JSON.parse(line);
8261
+ processMessage(msg);
8262
+ } catch (error) {
8263
+ log(`invalid json: ${String(error)}`);
8264
+ }
8265
+ }
8266
+ index = buffer.indexOf("\n");
8267
+ }
8268
+ });
8269
+ })();
6505
8270
  };
6506
8271
 
6507
8272
  // src/index.ts