@chanlerdev/scorel 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -261,7 +261,7 @@ var init_src2 = __esm({
261
261
  this.#assertDaemonConnected();
262
262
  return this.#request("remove_model_provider", input);
263
263
  }
264
- async getMemorySettings(input) {
264
+ async getMemorySettings(input = {}) {
265
265
  this.#assertDaemonConnected();
266
266
  return (await this.#request("get_memory_settings", input)).memory;
267
267
  }
@@ -273,6 +273,14 @@ var init_src2 = __esm({
273
273
  this.#assertDaemonConnected();
274
274
  return (await this.#request("upsert_memory_settings", input)).memory;
275
275
  }
276
+ async getRuntimeSettings(input = {}) {
277
+ this.#assertDaemonConnected();
278
+ return (await this.#request("get_runtime_settings", input)).runtime;
279
+ }
280
+ async upsertRuntimeSettings(input) {
281
+ this.#assertDaemonConnected();
282
+ return (await this.#request("upsert_runtime_settings", input)).runtime;
283
+ }
276
284
  async getExtensionSettings(input) {
277
285
  this.#assertDaemonConnected();
278
286
  return (await this.#request("get_extension_settings", input)).extension;
@@ -807,7 +815,7 @@ var init_sessions = __esm({
807
815
  // packages/core/src/config/index.ts
808
816
  import { readFile as readFile3 } from "node:fs/promises";
809
817
  import { join as join4 } from "node:path";
810
- var SCOREL_CONFIG_SCHEMA, scorelUserRoot, scorelUserConfigPath, scorelSessionsDir, scorelProjectConfigPath, loadScorelConfig, loadScorelConfigProfile, listProviderConnections, listAvailableModels, listProviderModels, resolveModelSelection, renderModelProfileConfig, removeProvider, renderMemoryConfig, renderExtensionConfig, DEFAULT_MEMORY_CONFIG, loadMemory, loadExtensions, loadProviders, loadProviderProfiles, loadProviderModels, loadAvailableModels, loadRoles, readConfigText, parseToml, parseEditableConfig, renderRawConfig, emptyRawConfig, stripComment, requireString, normalizeProviderName, requireProviderCredential, resolveProviderApiKey, providerCredentialSummary, requireNumber, requireNonNegativeNumber, requireCompactThreshold, requireBoolean, requireCustomApi, requireProviderType, requireSection, ensureSection, setConfigValue, assertKnownKey, setValue, parseTomlValue, stripTrailingSlashes, requireIdentifier, tomlString, renderTomlValue, requireModelRole, modelRoles;
818
+ var SCOREL_CONFIG_SCHEMA, scorelUserRoot, scorelUserConfigPath, scorelSessionsDir, loadScorelConfig, loadScorelConfigProfile, listProviderConnections, listAvailableModels, listProviderModels, resolveModelSelection, renderModelProfileConfig, removeProvider, renderMemoryConfig, renderRuntimeConfig, renderExtensionConfig, DEFAULT_MEMORY_CONFIG, DEFAULT_RUNTIME_CONFIG, loadMemory, loadRuntime, loadExtensions, loadProviders, loadProviderProfiles, loadProviderModels, loadAvailableModels, loadRoles, readConfigText, configPathForDevice, parseToml, parseEditableConfig, renderRawConfig, emptyRawConfig, stripComment, requireString, normalizeProviderName, requireProviderCredential, resolveProviderApiKey, providerCredentialSummary, requireNumber, requireNonNegativeNumber, requireCompactThreshold, requireBoolean, requireCustomApi, requireProviderType, requireSection, ensureSection, setConfigValue, assertKnownKey, setValue, parseTomlValue, stripTrailingSlashes, requireIdentifier, tomlString, renderTomlValue, isNodeErrorCode, requireModelRole, modelRoles;
811
819
  var init_config = __esm({
812
820
  "packages/core/src/config/index.ts"() {
813
821
  "use strict";
@@ -815,8 +823,7 @@ var init_config = __esm({
815
823
  fixedPaths: {
816
824
  userRoot: "~/.scorel",
817
825
  userConfig: "~/.scorel/config.toml",
818
- sessionsDir: "~/.scorel/sessions",
819
- projectConfig: ".scorel/config.toml"
826
+ sessionsDir: "~/.scorel/sessions"
820
827
  },
821
828
  sections: {
822
829
  root: {
@@ -837,6 +844,9 @@ var init_config = __esm({
837
844
  memory: {
838
845
  keys: ["enabled", "daily", "sessionMemory", "autoDream", "promoteRoot", "dreamIdleMinutes", "autoCompactThreshold"]
839
846
  },
847
+ runtime: {
848
+ keys: ["tokenSavingRtk"]
849
+ },
840
850
  extension: {
841
851
  keys: ["enabled", "kind"]
842
852
  },
@@ -848,7 +858,6 @@ var init_config = __esm({
848
858
  scorelUserRoot = (homeDir) => join4(homeDir, ".scorel");
849
859
  scorelUserConfigPath = (homeDir) => join4(scorelUserRoot(homeDir), "config.toml");
850
860
  scorelSessionsDir = (homeDir) => join4(scorelUserRoot(homeDir), "sessions");
851
- scorelProjectConfigPath = (cwd) => join4(cwd, ".scorel", "config.toml");
852
861
  loadScorelConfig = async (options) => {
853
862
  const env = options.env ?? process.env;
854
863
  const raw = parseToml(await readConfigText(options));
@@ -862,6 +871,7 @@ var init_config = __esm({
862
871
  models,
863
872
  modelProfile: { roles },
864
873
  memory: loadMemory(raw),
874
+ runtime: loadRuntime(raw),
865
875
  extensions: loadExtensions(raw)
866
876
  };
867
877
  };
@@ -878,6 +888,7 @@ var init_config = __esm({
878
888
  models,
879
889
  modelProfile: { roles },
880
890
  memory: loadMemory(raw),
891
+ runtime: loadRuntime(raw),
881
892
  extensions: loadExtensions(raw)
882
893
  };
883
894
  };
@@ -1134,6 +1145,14 @@ var init_config = __esm({
1134
1145
  };
1135
1146
  return renderRawConfig(raw);
1136
1147
  };
1148
+ renderRuntimeConfig = (input) => {
1149
+ const raw = parseEditableConfig(input.existingConfigText);
1150
+ raw.runtime = {
1151
+ ...loadRuntime(raw),
1152
+ ...input.tokenSavingRtk !== void 0 ? { tokenSavingRtk: requireBoolean(input.tokenSavingRtk, "runtime.tokenSavingRtk") } : {}
1153
+ };
1154
+ return renderRawConfig(raw);
1155
+ };
1137
1156
  renderExtensionConfig = (input) => {
1138
1157
  const raw = parseEditableConfig(input.existingConfigText);
1139
1158
  const extensionId = requireIdentifier(input.extensionId, "extensionId");
@@ -1165,6 +1184,9 @@ var init_config = __esm({
1165
1184
  dreamIdleMinutes: 60,
1166
1185
  autoCompactThreshold: 0.8
1167
1186
  };
1187
+ DEFAULT_RUNTIME_CONFIG = {
1188
+ tokenSavingRtk: false
1189
+ };
1168
1190
  loadMemory = (raw) => ({
1169
1191
  enabled: raw.memory?.enabled ?? DEFAULT_MEMORY_CONFIG.enabled,
1170
1192
  daily: raw.memory?.daily ?? DEFAULT_MEMORY_CONFIG.daily,
@@ -1174,6 +1196,9 @@ var init_config = __esm({
1174
1196
  dreamIdleMinutes: requireNonNegativeNumber(raw.memory?.dreamIdleMinutes ?? DEFAULT_MEMORY_CONFIG.dreamIdleMinutes, "memory.dreamIdleMinutes"),
1175
1197
  autoCompactThreshold: requireCompactThreshold(raw.memory?.autoCompactThreshold ?? DEFAULT_MEMORY_CONFIG.autoCompactThreshold)
1176
1198
  });
1199
+ loadRuntime = (raw) => ({
1200
+ tokenSavingRtk: raw.runtime?.tokenSavingRtk ?? DEFAULT_RUNTIME_CONFIG.tokenSavingRtk
1201
+ });
1177
1202
  loadExtensions = (raw) => {
1178
1203
  const extensions = {};
1179
1204
  for (const [extensionId, extension] of Object.entries(raw.extensions)) {
@@ -1336,21 +1361,25 @@ var init_config = __esm({
1336
1361
  };
1337
1362
  };
1338
1363
  readConfigText = async (options) => {
1339
- const projectPath = scorelProjectConfigPath(options.cwd);
1364
+ const userPath = configPathForDevice(options);
1340
1365
  try {
1341
- return await readFile3(projectPath, "utf8");
1342
- } catch {
1343
- const home = options.homeDir ?? process.env.HOME;
1344
- if (!home) {
1345
- throw new Error(`Scorel config not found: ${projectPath}`);
1346
- }
1347
- const userPath = scorelUserConfigPath(home);
1348
- try {
1349
- return await readFile3(userPath, "utf8");
1350
- } catch {
1351
- throw new Error(`Scorel config not found: ${projectPath} or ${userPath}`);
1366
+ return await readFile3(userPath, "utf8");
1367
+ } catch (cause) {
1368
+ if (isNodeErrorCode(cause, "ENOENT")) {
1369
+ throw new Error(`Scorel config not found: ${userPath}`);
1352
1370
  }
1371
+ throw cause;
1372
+ }
1373
+ };
1374
+ configPathForDevice = (options) => {
1375
+ if (options.scorelHomeDir) {
1376
+ return join4(options.scorelHomeDir, "config.toml");
1377
+ }
1378
+ const home = options.homeDir ?? process.env.HOME;
1379
+ if (!home) {
1380
+ throw new Error("Scorel config not found: HOME is not set");
1353
1381
  }
1382
+ return scorelUserConfigPath(home);
1354
1383
  };
1355
1384
  parseToml = (text) => {
1356
1385
  const result = emptyRawConfig();
@@ -1453,6 +1482,12 @@ var init_config = __esm({
1453
1482
  lines.push(`autoCompactThreshold = ${memory.autoCompactThreshold}`);
1454
1483
  lines.push("");
1455
1484
  }
1485
+ if (raw.runtime) {
1486
+ const runtime = loadRuntime(raw);
1487
+ lines.push("[runtime]");
1488
+ lines.push(`tokenSavingRtk = ${runtime.tokenSavingRtk}`);
1489
+ lines.push("");
1490
+ }
1456
1491
  for (const [extensionId, extension] of Object.entries(raw.extensions).sort(([left], [right]) => left.localeCompare(right))) {
1457
1492
  lines.push(`[extensions.${extensionId}]`);
1458
1493
  lines.push(`enabled = ${extension.enabled === true}`);
@@ -1584,6 +1619,9 @@ var init_config = __esm({
1584
1619
  if (section2 === "memory") {
1585
1620
  return { kind: "memory" };
1586
1621
  }
1622
+ if (section2 === "runtime") {
1623
+ return { kind: "runtime" };
1624
+ }
1587
1625
  const extensionConfigMatch = /^extensions\.([A-Za-z0-9_-]+)\.config$/.exec(section2);
1588
1626
  if (extensionConfigMatch?.[1]) {
1589
1627
  return { kind: "extensionConfig", id: extensionConfigMatch[1] };
@@ -1606,6 +1644,8 @@ var init_config = __esm({
1606
1644
  config.modelProfile.roles ??= {};
1607
1645
  } else if (section2.kind === "memory") {
1608
1646
  config.memory ??= {};
1647
+ } else if (section2.kind === "runtime") {
1648
+ config.runtime ??= {};
1609
1649
  } else if (section2.kind === "extension") {
1610
1650
  config.extensions[section2.id] ??= {};
1611
1651
  } else if (section2.kind === "extensionConfig") {
@@ -1631,6 +1671,9 @@ var init_config = __esm({
1631
1671
  } else if (section2.kind === "memory") {
1632
1672
  config.memory ??= {};
1633
1673
  setValue(config.memory, key, value);
1674
+ } else if (section2.kind === "runtime") {
1675
+ config.runtime ??= {};
1676
+ setValue(config.runtime, key, value);
1634
1677
  } else if (section2.kind === "extension") {
1635
1678
  config.extensions[section2.id] ??= {};
1636
1679
  setValue(config.extensions[section2.id], key, value);
@@ -1684,6 +1727,7 @@ var init_config = __esm({
1684
1727
  };
1685
1728
  tomlString = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
1686
1729
  renderTomlValue = (value) => typeof value === "string" ? tomlString(value) : String(value);
1730
+ isNodeErrorCode = (cause, code) => typeof cause === "object" && cause !== null && "code" in cause && cause.code === code;
1687
1731
  requireModelRole = (value, role, models) => {
1688
1732
  const modelId = requireString(value, `model_profile.roles.${role}`);
1689
1733
  if (!models[modelId]) {
@@ -1699,9 +1743,10 @@ var init_config = __esm({
1699
1743
  import { createHash, randomUUID as randomUUID2 } from "node:crypto";
1700
1744
  import { execFile } from "node:child_process";
1701
1745
  import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
1702
- import { dirname as dirname3, extname, isAbsolute, relative, resolve } from "node:path";
1746
+ import { userInfo } from "node:os";
1747
+ import { basename as basename2, dirname as dirname3, extname, isAbsolute, relative, resolve } from "node:path";
1703
1748
  import { promisify } from "node:util";
1704
- var execFileAsync, DEFAULT_SEARCH_LIMIT, DEFAULT_GREP_LIMIT, DEFAULT_READ_LIMIT, DEFAULT_CONTEXT_WINDOW, READ_TOKEN_BUDGET_RATIO, FULL_READ_TOKEN_BUDGET_RATIO, createCodingTools, parseReadArgs, parseWriteArgs, parseEditArgs, parseBashArgs, parseGlobArgs, parseGrepArgs, parseTodoWriteArgs, parseTodoItem, expectRecord, expectPath, expectString, optionalString, optionalNumber, optionalBoolean, snapshotFile, sameSnapshot, exists, isWithin, linesOf, IMAGE_EXTENSIONS, DOCUMENT_EXTENSIONS, BINARY_EXTENSIONS, assertReadableFileKind, assertTextBuffer, selectCompleteLinesWithinBudget, estimateTokens, renderReadLines, readTokenBudget, completeRanges, hasCompleteCoverage, mergeRanges, countOccurrences, atomicWriteFile, bashResult, truncate, textResult, byteLength, isTimeoutError, isExecError, runRipgrep, splitOutput, vcsExcludes, grepArgs, splitGlobPatterns, paginate, toWorkspaceRelative, relativizeGrepLine, relativizeCountLine, sortPathsByMtime, formatPaginatedText, formatLimitSuffix, parseCountLines;
1749
+ var execFileAsync, DEFAULT_SEARCH_LIMIT, DEFAULT_GREP_LIMIT, DEFAULT_READ_LIMIT, DEFAULT_CONTEXT_WINDOW, READ_TOKEN_BUDGET_RATIO, FULL_READ_TOKEN_BUDGET_RATIO, createCodingTools, parseReadArgs, parseWriteArgs, parseEditArgs, parseBashArgs, parseGlobArgs, parseGrepArgs, parseTodoWriteArgs, parseTodoItem, expectRecord, expectPath, expectString, optionalString, optionalNumber, optionalBoolean, snapshotFile, sameSnapshot, exists, isWithin, linesOf, IMAGE_EXTENSIONS, DOCUMENT_EXTENSIONS, BINARY_EXTENSIONS, assertReadableFileKind, assertTextBuffer, selectCompleteLinesWithinBudget, estimateTokens, renderReadLines, readTokenBudget, completeRanges, hasCompleteCoverage, mergeRanges, countOccurrences, atomicWriteFile, bashResult, renderFullBashResult, writeBashArtifact, safeArtifactSegment, projectBashStreams, projectOutputStream, resolveDefaultShell, resolveRtkCommand, rtkRewriteResult, executableRewriteCommand, readRtkGain, rtkSavedTokenDelta, withRtkSavings, nonNegativeInteger, isRecord3, shellQuote, shellCommandArgs, userShell, truncate, sliceBytes, textResult, byteLength, isTimeoutError, isExecError, runRipgrep, splitOutput, vcsExcludes, grepArgs, splitGlobPatterns, paginate, toWorkspaceRelative, relativizeGrepLine, relativizeCountLine, sortPathsByMtime, formatPaginatedText, formatLimitSuffix, parseCountLines;
1705
1750
  var init_coding_tools = __esm({
1706
1751
  "packages/core/src/tools/coding-tools.ts"() {
1707
1752
  "use strict";
@@ -1721,6 +1766,7 @@ var init_coding_tools = __esm({
1721
1766
  const maxOutputBytes = options.maxOutputBytes ?? 16e3;
1722
1767
  const normalReadTokens = options.maxReadTokens ?? readTokenBudget(options.contextWindow, READ_TOKEN_BUDGET_RATIO);
1723
1768
  const fullReadTokens = options.maxReadTokens ?? readTokenBudget(options.contextWindow, FULL_READ_TOKEN_BUDGET_RATIO);
1769
+ const defaultShell = resolveDefaultShell(options.defaultShell);
1724
1770
  const resolveWorkspacePath = (input) => {
1725
1771
  if (input.length === 0) {
1726
1772
  throw new Error("path must not be empty");
@@ -1868,30 +1914,61 @@ String: ${input.old_string}`
1868
1914
  defineTool({
1869
1915
  name: "Bash",
1870
1916
  description: "Execute a shell command in the workspace with timeout and output truncation.",
1871
- execute: async (_toolCallId, args, signal) => {
1917
+ execute: async (toolCallId, args, signal) => {
1872
1918
  const input = parseBashArgs(args);
1873
1919
  const commandCwd = input.cwd ? resolveWorkspacePath(input.cwd) : root;
1874
1920
  const timeoutMs = Math.min(input.timeoutMs ?? defaultTimeoutMs, maxTimeoutMs);
1875
1921
  const outputLimit = input.maxOutputBytes ?? maxOutputBytes;
1922
+ const rtk = options.tokenSaving?.rtk;
1923
+ const rtkCommand = await resolveRtkCommand(rtk, input.command);
1924
+ const command = rtkCommand.rewrittenCommand ?? input.command;
1925
+ const executionCommand = rtkCommand.executionCommand ?? input.command;
1926
+ const executable = defaultShell;
1927
+ const argv = shellCommandArgs(defaultShell, executionCommand);
1928
+ const rtkGainBefore = rtkCommand.applied && rtk?.executable ? await readRtkGain(rtk.executable, commandCwd) : void 0;
1929
+ const rtkResult = {
1930
+ enabled: rtk?.enabled === true,
1931
+ applied: rtkCommand.applied,
1932
+ ...rtk?.executable ? { executable: rtk.executable } : {},
1933
+ ...rtkCommand.rewrittenCommand ? { rewrittenCommand: rtkCommand.rewrittenCommand } : {}
1934
+ };
1876
1935
  try {
1877
- const result = await execFileAsync("/bin/bash", ["-lc", input.command], {
1936
+ const result = await execFileAsync(executable, argv, {
1878
1937
  cwd: commandCwd,
1879
1938
  timeout: timeoutMs,
1880
1939
  signal,
1881
1940
  maxBuffer: Math.max(outputLimit * 4, 1024 * 1024)
1882
1941
  });
1883
- return bashResult({ exitCode: 0, stdout: result.stdout, stderr: result.stderr, cwd: commandCwd, outputLimit });
1942
+ const rtkSavedTokens = rtk?.executable ? await rtkSavedTokenDelta(rtk.executable, commandCwd, rtkGainBefore) : void 0;
1943
+ return await bashResult({
1944
+ exitCode: 0,
1945
+ stdout: result.stdout,
1946
+ stderr: result.stderr,
1947
+ cwd: commandCwd,
1948
+ outputLimit,
1949
+ artifactDir: options.toolResultArtifacts?.dir,
1950
+ toolCallId,
1951
+ shell: defaultShell,
1952
+ command,
1953
+ rtk: withRtkSavings(rtkResult, rtkSavedTokens)
1954
+ });
1884
1955
  } catch (cause) {
1885
1956
  if (isTimeoutError(cause)) {
1886
1957
  throw new Error(`Bash command timed out after ${timeoutMs}ms`);
1887
1958
  }
1888
1959
  if (isExecError(cause)) {
1889
- return bashResult({
1960
+ const rtkSavedTokens = rtk?.executable ? await rtkSavedTokenDelta(rtk.executable, commandCwd, rtkGainBefore) : void 0;
1961
+ return await bashResult({
1890
1962
  exitCode: typeof cause.code === "number" ? cause.code : 1,
1891
1963
  stdout: String(cause.stdout ?? ""),
1892
1964
  stderr: String(cause.stderr ?? cause.message),
1893
1965
  cwd: commandCwd,
1894
- outputLimit
1966
+ outputLimit,
1967
+ artifactDir: options.toolResultArtifacts?.dir,
1968
+ toolCallId,
1969
+ shell: defaultShell,
1970
+ command,
1971
+ rtk: withRtkSavings(rtkResult, rtkSavedTokens)
1895
1972
  });
1896
1973
  }
1897
1974
  throw cause;
@@ -1905,7 +1982,7 @@ String: ${input.old_string}`
1905
1982
  const input = parseGlobArgs(args);
1906
1983
  const limit = input.head_limit ?? DEFAULT_SEARCH_LIMIT;
1907
1984
  const offset = input.offset ?? 0;
1908
- const all = await runRipgrep(["--files", "--hidden", "--glob", input.pattern, ...vcsExcludes()], workspaceTarget(input.path), root, signal);
1985
+ const all = (await runRipgrep(["--files", "--hidden", "--glob", input.pattern, ...vcsExcludes()], workspaceTarget(input.path), root, signal)).sort((left, right) => toWorkspaceRelative(root)(left).localeCompare(toWorkspaceRelative(root)(right)));
1909
1986
  const selected = paginate(all, limit, offset);
1910
1987
  const text = selected.items.map(toWorkspaceRelative(root)).join("\n");
1911
1988
  return textResult(text || "No files found", {
@@ -2256,28 +2333,191 @@ ${filenames.join("\n")}`,
2256
2333
  throw cause;
2257
2334
  }
2258
2335
  };
2259
- bashResult = (input) => {
2260
- const stdout = truncate(input.stdout, input.outputLimit, "stdout");
2261
- const stderr = truncate(input.stderr, input.outputLimit, "stderr");
2262
- return textResult(`exitCode: ${input.exitCode}
2336
+ bashResult = async (input) => {
2337
+ const stdoutBytes = Buffer.byteLength(input.stdout);
2338
+ const stderrBytes = Buffer.byteLength(input.stderr);
2339
+ const fullResult = renderFullBashResult(input);
2340
+ const resultBytes = Buffer.byteLength(fullResult);
2341
+ const shouldArchive = Boolean(input.artifactDir) && resultBytes > input.outputLimit;
2342
+ const artifactPath = shouldArchive && input.artifactDir ? await writeBashArtifact(input.artifactDir, input.toolCallId, fullResult) : void 0;
2343
+ const projection = artifactPath ? projectBashStreams(input.stdout, input.stderr, input.outputLimit) : void 0;
2344
+ const stdout = projection?.stdout ?? truncate(input.stdout, input.outputLimit, "stdout");
2345
+ const stderr = projection?.stderr ?? truncate(input.stderr, input.outputLimit, "stderr");
2346
+ const text = artifactPath ? [
2347
+ `exitCode: ${input.exitCode}`,
2348
+ `cwd: ${input.cwd}`,
2349
+ `artifact: ${artifactPath}`,
2350
+ `resultBytes: ${resultBytes}`,
2351
+ `stdoutBytes: ${stdoutBytes}`,
2352
+ `stderrBytes: ${stderrBytes}`,
2353
+ ...projection?.lines ?? []
2354
+ ].join("\n") : `exitCode: ${input.exitCode}
2263
2355
  cwd: ${input.cwd}
2264
2356
  stdout:
2265
2357
  ${stdout}
2266
2358
  stderr:
2267
- ${stderr}`, {
2359
+ ${stderr}`;
2360
+ return textResult(text, {
2268
2361
  exitCode: input.exitCode,
2269
- cwd: input.cwd
2362
+ cwd: input.cwd,
2363
+ ...artifactPath ? {
2364
+ artifact: {
2365
+ path: artifactPath,
2366
+ resultBytes,
2367
+ stdoutBytes,
2368
+ stderrBytes
2369
+ }
2370
+ } : {},
2371
+ ...input.shell ? { shell: input.shell } : {},
2372
+ ...input.command ? { command: input.command } : {},
2373
+ ...input.rtk ? {
2374
+ rtk: {
2375
+ ...input.rtk,
2376
+ estimatedOutputTokens: estimateTokens(`${stdout}
2377
+ ${stderr}`)
2378
+ }
2379
+ } : {}
2270
2380
  });
2271
2381
  };
2382
+ renderFullBashResult = (input) => `exitCode: ${input.exitCode}
2383
+ cwd: ${input.cwd}
2384
+ stdout:
2385
+ ${input.stdout}
2386
+ stderr:
2387
+ ${input.stderr}`;
2388
+ writeBashArtifact = async (artifactDir, toolCallId, content) => {
2389
+ const directory = resolve(artifactDir, safeArtifactSegment(toolCallId));
2390
+ await mkdir2(directory, { recursive: true });
2391
+ const path = resolve(directory, "result.txt");
2392
+ await writeFile2(path, content, { encoding: "utf8", mode: 384 });
2393
+ return path;
2394
+ };
2395
+ safeArtifactSegment = (value) => value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "tool_call";
2396
+ projectBashStreams = (stdout, stderr, maxBytes) => {
2397
+ const streams = [
2398
+ { label: "stdout", value: stdout },
2399
+ { label: "stderr", value: stderr }
2400
+ ].filter((stream) => Buffer.byteLength(stream.value) > 0);
2401
+ if (streams.length === 0) {
2402
+ return { lines: ["stdout:", "", "stderr:", ""], stdout: "", stderr: "" };
2403
+ }
2404
+ const perStreamBudget = Math.max(1, Math.floor(maxBytes / streams.length));
2405
+ const projected = streams.map((stream) => projectOutputStream(stream.value, perStreamBudget, stream.label));
2406
+ const stdoutText = projected.find((stream) => stream.label === "stdout")?.text ?? "stdout:\n";
2407
+ const stderrText = projected.find((stream) => stream.label === "stderr")?.text ?? "stderr:\n";
2408
+ return {
2409
+ lines: projected.map((stream) => stream.text),
2410
+ stdout: stdoutText,
2411
+ stderr: stderrText
2412
+ };
2413
+ };
2414
+ projectOutputStream = (value, maxBytes, label) => {
2415
+ const bytes = Buffer.byteLength(value);
2416
+ if (bytes <= maxBytes) {
2417
+ return { label, text: `${label}:
2418
+ ${value}` };
2419
+ }
2420
+ const headBytes = Math.max(1, Math.floor(maxBytes / 2));
2421
+ const tailBytes = Math.max(1, maxBytes - headBytes);
2422
+ return {
2423
+ label,
2424
+ text: [
2425
+ `${label} head:`,
2426
+ sliceBytes(value, 0, headBytes),
2427
+ `${label} tail:`,
2428
+ sliceBytes(value, Math.max(0, bytes - tailBytes), bytes),
2429
+ `[${label} archived: ${bytes} bytes; projection budget ${maxBytes} bytes]`
2430
+ ].join("\n")
2431
+ };
2432
+ };
2433
+ resolveDefaultShell = (input) => {
2434
+ const shell = input || process.env.SHELL || userShell() || "/bin/sh";
2435
+ return shell.trim() || "/bin/sh";
2436
+ };
2437
+ resolveRtkCommand = async (rtk, command) => {
2438
+ if (rtk?.enabled !== true || typeof rtk.executable !== "string" || rtk.executable.length === 0) {
2439
+ return { applied: false };
2440
+ }
2441
+ try {
2442
+ const result = await execFileAsync(rtk.executable, ["rewrite", command], {
2443
+ timeout: 5e3,
2444
+ maxBuffer: 1024 * 1024
2445
+ });
2446
+ return rtkRewriteResult(result.stdout, rtk.executable);
2447
+ } catch (cause) {
2448
+ if (isExecError(cause) && typeof cause.stdout === "string") {
2449
+ return rtkRewriteResult(cause.stdout, rtk.executable);
2450
+ }
2451
+ return { applied: false };
2452
+ }
2453
+ };
2454
+ rtkRewriteResult = (stdout, executable) => {
2455
+ const rewrittenCommand = stdout.trim();
2456
+ return rewrittenCommand ? { applied: true, rewrittenCommand, executionCommand: executableRewriteCommand(rewrittenCommand, executable) } : { applied: false };
2457
+ };
2458
+ executableRewriteCommand = (command, executable) => command.replace(/^rtk(?=\s|$)/, shellQuote(executable));
2459
+ readRtkGain = async (rtkExecutable, cwd) => {
2460
+ try {
2461
+ const { stdout } = await execFileAsync(rtkExecutable, ["gain", "--project", "--format", "json"], {
2462
+ cwd,
2463
+ timeout: 5e3,
2464
+ maxBuffer: 5e6
2465
+ });
2466
+ const parsed = JSON.parse(stdout);
2467
+ if (!isRecord3(parsed) || !isRecord3(parsed.summary)) {
2468
+ return void 0;
2469
+ }
2470
+ return { savedTokens: nonNegativeInteger(parsed.summary.total_saved) };
2471
+ } catch {
2472
+ return void 0;
2473
+ }
2474
+ };
2475
+ rtkSavedTokenDelta = async (rtkExecutable, cwd, before) => {
2476
+ if (!before) {
2477
+ return void 0;
2478
+ }
2479
+ const after = await readRtkGain(rtkExecutable, cwd);
2480
+ if (!after) {
2481
+ return void 0;
2482
+ }
2483
+ return Math.max(0, after.savedTokens - before.savedTokens);
2484
+ };
2485
+ withRtkSavings = (rtk, savedTokens) => ({
2486
+ ...rtk,
2487
+ ...rtk.applied && savedTokens !== void 0 ? { estimatedSavedTokens: savedTokens } : {}
2488
+ });
2489
+ nonNegativeInteger = (value) => {
2490
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
2491
+ return 0;
2492
+ }
2493
+ return Math.floor(value);
2494
+ };
2495
+ isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2496
+ shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
2497
+ shellCommandArgs = (shell, command) => {
2498
+ const name = basename2(shell).toLowerCase();
2499
+ if (name === "csh" || name === "tcsh" || name === "fish") {
2500
+ return ["-c", command];
2501
+ }
2502
+ return ["-lc", command];
2503
+ };
2504
+ userShell = () => {
2505
+ try {
2506
+ return userInfo().shell ?? void 0;
2507
+ } catch {
2508
+ return void 0;
2509
+ }
2510
+ };
2272
2511
  truncate = (value, maxBytes, label) => {
2273
2512
  const bytes = Buffer.byteLength(value);
2274
2513
  if (bytes <= maxBytes) {
2275
2514
  return value;
2276
2515
  }
2277
- const truncated = Buffer.from(value).subarray(0, maxBytes).toString("utf8");
2516
+ const truncated = sliceBytes(value, 0, maxBytes);
2278
2517
  return `${truncated}
2279
2518
  [${label} truncated: ${bytes} bytes > ${maxBytes} bytes]`;
2280
2519
  };
2520
+ sliceBytes = (value, start, end) => Buffer.from(value).subarray(start, end).toString("utf8");
2281
2521
  textResult = (text, details) => ({
2282
2522
  content: [{ type: "text", text }],
2283
2523
  details
@@ -2452,7 +2692,7 @@ var init_tools = __esm({
2452
2692
  });
2453
2693
 
2454
2694
  // packages/core/src/channel/index.ts
2455
- var createSendChannelMessageTool, parseSendChannelMessageInput, parseAttachments, optionalString2, isRecord3;
2695
+ var createSendChannelMessageTool, parseSendChannelMessageInput, parseAttachments, optionalString2, isRecord4;
2456
2696
  var init_channel = __esm({
2457
2697
  "packages/core/src/channel/index.ts"() {
2458
2698
  "use strict";
@@ -2470,7 +2710,7 @@ var init_channel = __esm({
2470
2710
  }
2471
2711
  });
2472
2712
  parseSendChannelMessageInput = (value) => {
2473
- if (!isRecord3(value)) {
2713
+ if (!isRecord4(value)) {
2474
2714
  throw new Error("SendChannelMessage args must be an object");
2475
2715
  }
2476
2716
  const text = typeof value.text === "string" && value.text.trim().length > 0 ? value.text : void 0;
@@ -2499,7 +2739,7 @@ var init_channel = __esm({
2499
2739
  throw new Error("SendChannelMessage.attachments must be an array");
2500
2740
  }
2501
2741
  return value.map((item, index) => {
2502
- if (!isRecord3(item)) {
2742
+ if (!isRecord4(item)) {
2503
2743
  throw new Error(`SendChannelMessage.attachments.${index} must be an object`);
2504
2744
  }
2505
2745
  if (item.type !== "image" && item.type !== "file") {
@@ -2528,14 +2768,14 @@ var init_channel = __esm({
2528
2768
  }
2529
2769
  return value;
2530
2770
  };
2531
- isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2771
+ isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2532
2772
  }
2533
2773
  });
2534
2774
 
2535
2775
  // packages/core/src/extensions/index.ts
2536
2776
  import { readFile as readFile5 } from "node:fs/promises";
2537
2777
  import { dirname as dirname4, resolve as resolve2 } from "node:path";
2538
- var loadExtensionManifest, parseExtensionManifest, requireString2, requireIdentifier2, requireKind, requireRelativePath, optionalRelativePaths, isRecord4;
2778
+ var loadExtensionManifest, parseExtensionManifest, requireString2, requireIdentifier2, requireKind, requireRelativePath, optionalRelativePaths, isRecord5;
2539
2779
  var init_extensions = __esm({
2540
2780
  "packages/core/src/extensions/index.ts"() {
2541
2781
  "use strict";
@@ -2548,7 +2788,7 @@ var init_extensions = __esm({
2548
2788
  const message = cause instanceof Error ? cause.message : String(cause);
2549
2789
  throw new Error(`Invalid extension manifest JSON at ${manifestPath}: ${message}`);
2550
2790
  }
2551
- if (!isRecord4(value)) {
2791
+ if (!isRecord5(value)) {
2552
2792
  throw new Error(`Extension manifest at ${manifestPath} must be an object`);
2553
2793
  }
2554
2794
  const rootDir = dirname4(resolve2(manifestPath));
@@ -2604,7 +2844,7 @@ var init_extensions = __esm({
2604
2844
  }
2605
2845
  return value.map((item, index) => requireRelativePath(item, `${name}.${index}`, manifestPath));
2606
2846
  };
2607
- isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2847
+ isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2608
2848
  }
2609
2849
  });
2610
2850
 
@@ -2613,7 +2853,7 @@ import { existsSync } from "node:fs";
2613
2853
  import { readdir as readdir4, readFile as readFile6 } from "node:fs/promises";
2614
2854
  import { homedir as homedir2, platform, release } from "node:os";
2615
2855
  import { dirname as dirname5, join as join5, resolve as resolve3 } from "node:path";
2616
- var BASELINE_PROMPT, buildInstructionSnapshot, renderSystemPrompt, section, discoverAgentsSources, projectAgentsPaths, findGitRoot, renderAgentsBlock, renderWorkspaceBlock, renderEnvironmentBlock, renderTimeBlock, isNodeErrorCode;
2856
+ var BASELINE_PROMPT, buildInstructionSnapshot, renderSystemPrompt, section, discoverAgentsSources, projectAgentsPaths, findGitRoot, renderAgentsBlock, renderWorkspaceBlock, renderEnvironmentBlock, renderTimeBlock, isNodeErrorCode2;
2617
2857
  var init_instructions = __esm({
2618
2858
  "packages/core/src/instructions/index.ts"() {
2619
2859
  "use strict";
@@ -2677,7 +2917,7 @@ var init_instructions = __esm({
2677
2917
  content
2678
2918
  });
2679
2919
  } catch (cause) {
2680
- if (!isNodeErrorCode(cause, "ENOENT") && !isNodeErrorCode(cause, "ENOTDIR")) {
2920
+ if (!isNodeErrorCode2(cause, "ENOENT") && !isNodeErrorCode2(cause, "ENOTDIR")) {
2681
2921
  throw cause;
2682
2922
  }
2683
2923
  }
@@ -2740,7 +2980,7 @@ var init_instructions = __esm({
2740
2980
  };
2741
2981
  renderEnvironmentBlock = (env) => [`Platform: ${platform()} ${release()}`, `Shell: ${env.SHELL ?? "unknown"}`].join("\n");
2742
2982
  renderTimeBlock = (timestamp) => [`Session started at: ${new Date(timestamp).toISOString()}`, `Timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}`].join("\n");
2743
- isNodeErrorCode = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
2983
+ isNodeErrorCode2 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
2744
2984
  }
2745
2985
  });
2746
2986
 
@@ -2748,7 +2988,7 @@ var init_instructions = __esm({
2748
2988
  import { appendFile, mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "node:fs/promises";
2749
2989
  import { homedir as homedir3 } from "node:os";
2750
2990
  import { join as join6 } from "node:path";
2751
- var memoryDate, scorelMemoryPaths, scorelSessionMemoryPaths, buildMemoryContext, renderMemoryHarness, appendDailyEntry, createAppendDailyTool, renderDailyEntry, readMemoryDreamState, writeMemoryDreamState, readSessionMemory, writeSessionMemory, renderSessionMemory, ensureMemoryFiles, ensureFile, readOptional, trimForContext, compactLine, renderList, renderBullets, normalizeMarkdownFile, parseAppendDailyInput, validateAppendDailyInput, isLowSignalSummary, containsNormalizedDailyEntry, normalizeDailyText, requireString3, optionalStringArray, optionalNumber2, optionalString3, parseLastFailure, isRecord5, safeProjectId, isNodeErrorCode2;
2991
+ var memoryDate, scorelMemoryPaths, scorelSessionMemoryPaths, buildMemoryContext, renderMemoryHarness, appendDailyEntry, createAppendDailyTool, renderDailyEntry, readMemoryDreamState, writeMemoryDreamState, readSessionMemory, writeSessionMemory, renderSessionMemory, ensureMemoryFiles, ensureFile, readOptional, trimForContext, compactLine, renderList, renderBullets, normalizeMarkdownFile, parseAppendDailyInput, validateAppendDailyInput, isLowSignalSummary, containsNormalizedDailyEntry, normalizeDailyText, requireString3, optionalStringArray, optionalNumber2, optionalString3, parseLastFailure, isRecord6, safeProjectId, isNodeErrorCode3;
2752
2992
  var init_memory = __esm({
2753
2993
  "packages/core/src/memory/index.ts"() {
2754
2994
  "use strict";
@@ -2950,7 +3190,7 @@ var init_memory = __esm({
2950
3190
  try {
2951
3191
  await writeFile3(path, content, { encoding: "utf8", flag: "wx", mode: 384 });
2952
3192
  } catch (cause) {
2953
- if (!isNodeErrorCode2(cause, "EEXIST")) {
3193
+ if (!isNodeErrorCode3(cause, "EEXIST")) {
2954
3194
  throw cause;
2955
3195
  }
2956
3196
  }
@@ -2959,7 +3199,7 @@ var init_memory = __esm({
2959
3199
  try {
2960
3200
  return await readFile7(path, "utf8");
2961
3201
  } catch (cause) {
2962
- if (isNodeErrorCode2(cause, "ENOENT")) {
3202
+ if (isNodeErrorCode3(cause, "ENOENT")) {
2963
3203
  return "";
2964
3204
  }
2965
3205
  throw cause;
@@ -2980,7 +3220,7 @@ var init_memory = __esm({
2980
3220
  normalizeMarkdownFile = (value) => `${value.trimEnd()}
2981
3221
  `;
2982
3222
  parseAppendDailyInput = (value) => {
2983
- if (!isRecord5(value)) {
3223
+ if (!isRecord6(value)) {
2984
3224
  throw new Error("AppendDaily args must be an object");
2985
3225
  }
2986
3226
  const summary = requireString3(value.summary, "summary");
@@ -3046,19 +3286,19 @@ var init_memory = __esm({
3046
3286
  optionalNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
3047
3287
  optionalString3 = (value) => typeof value === "string" && value.trim() ? value : void 0;
3048
3288
  parseLastFailure = (value) => {
3049
- if (!isRecord5(value)) return void 0;
3289
+ if (!isRecord6(value)) return void 0;
3050
3290
  const at = optionalNumber2(value.at);
3051
3291
  const message = optionalString3(value.message);
3052
3292
  return at !== void 0 && message ? { at, message } : void 0;
3053
3293
  };
3054
- isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3294
+ isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3055
3295
  safeProjectId = (projectId) => {
3056
3296
  if (!/^[A-Za-z0-9_-]+$/.test(projectId)) {
3057
3297
  throw new Error("projectId must contain only letters, numbers, underscores, or hyphens");
3058
3298
  }
3059
3299
  return projectId;
3060
3300
  };
3061
- isNodeErrorCode2 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
3301
+ isNodeErrorCode3 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
3062
3302
  }
3063
3303
  });
3064
3304
 
@@ -3347,7 +3587,7 @@ var init_pi_ai = __esm({
3347
3587
  });
3348
3588
 
3349
3589
  // packages/core/src/runtime/index.ts
3350
- var ScorelRuntime, normalizeAssistantMessage, isAssistantMessage, partialAssistantMessage;
3590
+ var ScorelRuntime, toolResultForContext, normalizeAssistantMessage, isAssistantMessage, partialAssistantMessage;
3351
3591
  var init_runtime = __esm({
3352
3592
  "packages/core/src/runtime/index.ts"() {
3353
3593
  "use strict";
@@ -3496,7 +3736,7 @@ var init_runtime = __esm({
3496
3736
  type: "tool_result",
3497
3737
  toolCallId: toolCall.toolCallId,
3498
3738
  toolName: toolCall.toolName,
3499
- result,
3739
+ result: toolResultForContext(result),
3500
3740
  isError
3501
3741
  };
3502
3742
  return {
@@ -3505,6 +3745,9 @@ var init_runtime = __esm({
3505
3745
  };
3506
3746
  }
3507
3747
  };
3748
+ toolResultForContext = (result) => ({
3749
+ content: result.content
3750
+ });
3508
3751
  normalizeAssistantMessage = (value, streamed, fallbackStopReason) => {
3509
3752
  if (value) {
3510
3753
  if (!isAssistantMessage(value)) {
@@ -3536,7 +3779,7 @@ var init_runtime = __esm({
3536
3779
  import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
3537
3780
  import { dirname as dirname6, join as join7 } from "node:path";
3538
3781
  function assertTreeEvent(value) {
3539
- if (!isRecord6(value)) {
3782
+ if (!isRecord7(value)) {
3540
3783
  throw new SessionStoreError("invalid_event", "Event must be an object");
3541
3784
  }
3542
3785
  if (value.type === "session_header") {
@@ -3548,7 +3791,7 @@ function assertTreeEvent(value) {
3548
3791
  if (typeof value.id !== "string" || value.parentId !== null && typeof value.parentId !== "string" || typeof value.seq !== "number" || typeof value.clientId !== "string" || typeof value.ts !== "number") {
3549
3792
  throw new SessionStoreError("invalid_event", "Event is missing required base fields");
3550
3793
  }
3551
- if ((value.type === "user_message" || value.type === "assistant_message" || value.type === "tool_result") && !isRecord6(value.message)) {
3794
+ if ((value.type === "user_message" || value.type === "assistant_message" || value.type === "tool_result") && !isRecord7(value.message)) {
3552
3795
  throw new SessionStoreError("invalid_event", "Message event is missing message payload");
3553
3796
  }
3554
3797
  if (value.type === "session_title_updated" && !isSessionTitleUpdated(value)) {
@@ -3573,7 +3816,7 @@ function assertTreeEvent(value) {
3573
3816
  throw new SessionStoreError("invalid_event", "skill_index_delta is missing delta payload");
3574
3817
  }
3575
3818
  }
3576
- var SessionStoreError, SessionTree, JsonlSession, sessionFilePath, sessionLogFilePath, createSession, loadSession, buildContext, retainedMessagesBeforeCompact, isRetainedContextStart, parseJsonLine, parseHeader, parseSessionEvent, validateSessionMatch, isConversationEvent, isInstructionSnapshot, isHarnessItem, isCompactEvent, isQueueUpdate, isSessionTitleUpdated, isSkillIndexSnapshot, isSkillIndexDelta, isSkillIndexEntry, appendHarnessItemToContext, appendReminderToToolResult, isToolResultWithContent, renderSystemReminder, compactSummaryMessage, cloneMessage, isRecord6;
3819
+ var SessionStoreError, SessionTree, JsonlSession, sessionFilePath, sessionLogFilePath, sessionArtifactsDirPath, createSession, loadSession, buildContext, retainedMessagesBeforeCompact, isRetainedContextStart, parseJsonLine, parseHeader, parseSessionEvent, validateSessionMatch, isConversationEvent, isInstructionSnapshot, isHarnessItem, isCompactEvent, isQueueUpdate, isSessionTitleUpdated, isSkillIndexSnapshot, isSkillIndexDelta, isSkillIndexEntry, appendHarnessItemToContext, appendReminderToToolResult, isToolResultWithContent, renderSystemReminder, compactSummaryMessage, cloneMessage, isRecord7;
3577
3820
  var init_session = __esm({
3578
3821
  "packages/core/src/session/index.ts"() {
3579
3822
  "use strict";
@@ -3754,6 +3997,7 @@ var init_session = __esm({
3754
3997
  };
3755
3998
  sessionFilePath = (sessionsDir, sessionId) => join7(sessionsDir, `${sessionId}.jsonl`);
3756
3999
  sessionLogFilePath = (sessionsDir, sessionId) => join7(sessionsDir, `${sessionId}.log`);
4000
+ sessionArtifactsDirPath = (sessionsDir, sessionId) => join7(sessionsDir, `${sessionId}.artifacts`);
3757
4001
  createSession = async ({ sessionsDir, header }) => {
3758
4002
  const validHeader = parseHeader(header);
3759
4003
  await mkdir4(sessionsDir, { recursive: true });
@@ -3841,13 +4085,13 @@ var init_session = __esm({
3841
4085
  }
3842
4086
  };
3843
4087
  parseHeader = (value) => {
3844
- if (!isRecord6(value)) {
4088
+ if (!isRecord7(value)) {
3845
4089
  throw new SessionStoreError("invalid_header", "Session header must be an object");
3846
4090
  }
3847
4091
  if (value.version !== 1 || typeof value.sessionId !== "string" || typeof value.deviceId !== "string") {
3848
4092
  throw new SessionStoreError("invalid_header", "Session header is missing required identity fields");
3849
4093
  }
3850
- if (typeof value.createdAt !== "number" || !isRecord6(value.meta)) {
4094
+ if (typeof value.createdAt !== "number" || !isRecord7(value.meta)) {
3851
4095
  throw new SessionStoreError("invalid_header", "Session header is missing createdAt or meta");
3852
4096
  }
3853
4097
  if (typeof value.meta.projectId !== "string" || value.meta.projectId.length === 0) {
@@ -3861,7 +4105,7 @@ var init_session = __esm({
3861
4105
  return value;
3862
4106
  };
3863
4107
  validateSessionMatch = (header, value) => {
3864
- if (!isRecord6(value) || typeof value.sessionId !== "string") {
4108
+ if (!isRecord7(value) || typeof value.sessionId !== "string") {
3865
4109
  throw new SessionStoreError("invalid_header", "Event must be an object with a sessionId");
3866
4110
  }
3867
4111
  if (value.sessionId !== header.sessionId) {
@@ -3870,24 +4114,24 @@ var init_session = __esm({
3870
4114
  };
3871
4115
  isConversationEvent = (event) => event.type === "user_message" || event.type === "assistant_message" || event.type === "tool_result" || event.type === "harness_item" || event.type === "compact";
3872
4116
  isInstructionSnapshot = (value) => {
3873
- if (!isRecord6(value) || value.version !== 1 || typeof value.cwd !== "string" || !Array.isArray(value.sections)) {
4117
+ if (!isRecord7(value) || value.version !== 1 || typeof value.cwd !== "string" || !Array.isArray(value.sections)) {
3874
4118
  return false;
3875
4119
  }
3876
4120
  return value.sections.every(
3877
- (section2) => isRecord6(section2) && typeof section2.kind === "string" && typeof section2.frozenAt === "number" && typeof section2.renderedBlock === "string"
4121
+ (section2) => isRecord7(section2) && typeof section2.kind === "string" && typeof section2.frozenAt === "number" && typeof section2.renderedBlock === "string"
3878
4122
  );
3879
4123
  };
3880
- isHarnessItem = (value) => isRecord6(value) && typeof value.kind === "string" && typeof value.origin === "string" && typeof value.content === "string" && (value.visibility === "display" || value.visibility === "hidden" || value.visibility === "compact");
4124
+ isHarnessItem = (value) => isRecord7(value) && typeof value.kind === "string" && typeof value.origin === "string" && typeof value.content === "string" && (value.visibility === "display" || value.visibility === "hidden" || value.visibility === "compact");
3881
4125
  isCompactEvent = (value) => typeof value.summary === "string" && typeof value.compactedThrough === "string" && typeof value.tokensBefore === "number" && typeof value.tokensAfter === "number" && typeof value.retainedEventCount === "number";
3882
4126
  isQueueUpdate = (value) => (value.queue === "follow_up" || value.queue === "steer") && value.operation === "rewrite" && Array.isArray(value.items) && (value.anchorEventId === null || typeof value.anchorEventId === "string") && value.items.every(
3883
- (item) => isRecord6(item) && typeof item.id === "string" && Array.isArray(item.content) && typeof item.createdAt === "number" && typeof item.updatedAt === "number" && typeof item.clientId === "string"
4127
+ (item) => isRecord7(item) && typeof item.id === "string" && Array.isArray(item.content) && typeof item.createdAt === "number" && typeof item.updatedAt === "number" && typeof item.clientId === "string"
3884
4128
  );
3885
- isSessionTitleUpdated = (value) => typeof value.title === "string" && value.title.length > 0 && (value.source === "model" || value.source === "user") && (value.derivedFrom === void 0 || isRecord6(value.derivedFrom) && typeof value.derivedFrom.eventId === "string" && typeof value.derivedFrom.seq === "number");
4129
+ isSessionTitleUpdated = (value) => typeof value.title === "string" && value.title.length > 0 && (value.source === "model" || value.source === "user") && (value.derivedFrom === void 0 || isRecord7(value.derivedFrom) && typeof value.derivedFrom.eventId === "string" && typeof value.derivedFrom.seq === "number");
3886
4130
  isSkillIndexSnapshot = (value) => (value.anchorEventId === null || typeof value.anchorEventId === "string") && Array.isArray(value.entries) && value.entries.every(isSkillIndexEntry);
3887
4131
  isSkillIndexDelta = (value) => (value.anchorEventId === null || typeof value.anchorEventId === "string") && Array.isArray(value.added) && Array.isArray(value.changed) && Array.isArray(value.removed) && value.added.every(isSkillIndexEntry) && value.changed.every(isSkillIndexEntry) && value.removed.every(
3888
- (item) => isRecord6(item) && typeof item.name === "string" && typeof item.previousPath === "string"
4132
+ (item) => isRecord7(item) && typeof item.name === "string" && typeof item.previousPath === "string"
3889
4133
  );
3890
- isSkillIndexEntry = (value) => isRecord6(value) && typeof value.name === "string" && typeof value.path === "string" && (value.scope === "user" || value.scope === "project" || value.scope === "extension") && typeof value.description === "string" && typeof value.mtimeMs === "number" && typeof value.size === "number" && typeof value.contentHash === "string" && typeof value.priority === "number";
4134
+ isSkillIndexEntry = (value) => isRecord7(value) && typeof value.name === "string" && typeof value.path === "string" && (value.scope === "user" || value.scope === "project" || value.scope === "extension") && typeof value.description === "string" && typeof value.mtimeMs === "number" && typeof value.size === "number" && typeof value.contentHash === "string" && typeof value.priority === "number";
3891
4135
  appendHarnessItemToContext = (messages, event) => {
3892
4136
  const reminder = renderSystemReminder(event.item.content);
3893
4137
  const last = messages.at(-1);
@@ -3924,7 +4168,7 @@ ${reminder}` }]
3924
4168
  }
3925
4169
  return false;
3926
4170
  };
3927
- isToolResultWithContent = (value) => isRecord6(value) && Array.isArray(value.content);
4171
+ isToolResultWithContent = (value) => isRecord7(value) && Array.isArray(value.content);
3928
4172
  renderSystemReminder = (content) => `<system-reminder>
3929
4173
  ${content}
3930
4174
  </system-reminder>`;
@@ -3948,21 +4192,20 @@ ${content}
3948
4192
  cloneMessage = (message) => ({
3949
4193
  ...message,
3950
4194
  content: message.content.map((block) => {
3951
- if (block.type !== "tool_result" || !isRecord6(block.result)) {
4195
+ if (block.type !== "tool_result" || !isRecord7(block.result)) {
3952
4196
  return { ...block };
3953
4197
  }
3954
- const content = Array.isArray(block.result.content) ? { content: block.result.content.map((item) => isRecord6(item) ? { ...item } : item) } : {};
4198
+ const content = Array.isArray(block.result.content) ? { content: block.result.content.map((item) => isRecord7(item) ? { ...item } : item) } : {};
3955
4199
  return {
3956
4200
  ...block,
3957
4201
  result: {
3958
- ...block.result,
3959
- ...content
4202
+ content: content.content ?? []
3960
4203
  }
3961
4204
  };
3962
4205
  }),
3963
4206
  ...message.meta ? { meta: { ...message.meta } } : {}
3964
4207
  });
3965
- isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4208
+ isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3966
4209
  }
3967
4210
  });
3968
4211
 
@@ -3972,7 +4215,7 @@ import { existsSync as existsSync2 } from "node:fs";
3972
4215
  import { readdir as readdir5, readFile as readFile9, stat as stat4 } from "node:fs/promises";
3973
4216
  import { homedir as homedir4 } from "node:os";
3974
4217
  import { dirname as dirname7, join as join8, resolve as resolve4 } from "node:path";
3975
- var scanSkillIndex, diffSkillIndex, hasSkillIndexDelta, renderSkillListing, renderSkillDelta, createSkillTool, projectSkillRoots, readSkillEntry, parseSkillMetadata, firstParagraph, parseSkillArgs, findGitRoot2, isNodeErrorCode3;
4218
+ var scanSkillIndex, diffSkillIndex, hasSkillIndexDelta, renderSkillListing, renderSkillDelta, createSkillTool, projectSkillRoots, readSkillEntry, parseSkillMetadata, firstParagraph, parseSkillArgs, findGitRoot2, isNodeErrorCode4;
3976
4219
  var init_skills = __esm({
3977
4220
  "packages/core/src/skills/index.ts"() {
3978
4221
  "use strict";
@@ -3995,7 +4238,7 @@ var init_skills = __esm({
3995
4238
  try {
3996
4239
  children = await readdir5(root.path);
3997
4240
  } catch (cause) {
3998
- if (isNodeErrorCode3(cause, "ENOENT") || isNodeErrorCode3(cause, "ENOTDIR")) {
4241
+ if (isNodeErrorCode4(cause, "ENOENT") || isNodeErrorCode4(cause, "ENOTDIR")) {
3999
4242
  continue;
4000
4243
  }
4001
4244
  throw cause;
@@ -4108,7 +4351,7 @@ var init_skills = __esm({
4108
4351
  try {
4109
4352
  [fileStat, content] = await Promise.all([stat4(options.skillPath), readFile9(options.skillPath, "utf8")]);
4110
4353
  } catch (cause) {
4111
- if (isNodeErrorCode3(cause, "ENOENT") || isNodeErrorCode3(cause, "ENOTDIR")) {
4354
+ if (isNodeErrorCode4(cause, "ENOENT") || isNodeErrorCode4(cause, "ENOTDIR")) {
4112
4355
  return void 0;
4113
4356
  }
4114
4357
  throw cause;
@@ -4188,7 +4431,7 @@ var init_skills = __esm({
4188
4431
  current = next;
4189
4432
  }
4190
4433
  };
4191
- isNodeErrorCode3 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
4434
+ isNodeErrorCode4 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
4192
4435
  }
4193
4436
  });
4194
4437
 
@@ -4534,12 +4777,15 @@ var init_host_client = __esm({
4534
4777
  });
4535
4778
 
4536
4779
  // packages/daemon/src/index.ts
4780
+ import { execFile as execFile2 } from "node:child_process";
4537
4781
  import { existsSync as existsSync3 } from "node:fs";
4538
- import { appendFile as appendFile3, mkdir as mkdir6, readFile as readFile11, readdir as readdir6, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
4539
- import { dirname as dirname8, join as join10, resolve as resolve5 } from "node:path";
4782
+ import { appendFile as appendFile3, mkdir as mkdir6, readFile as readFile11, readdir as readdir6, rename as rename3, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
4783
+ import { userInfo as userInfo2 } from "node:os";
4784
+ import { basename as basename3, dirname as dirname8, join as join10, resolve as resolve5 } from "node:path";
4540
4785
  import { pathToFileURL } from "node:url";
4786
+ import { promisify as promisify2 } from "node:util";
4541
4787
  import { WebSocketServer } from "ws";
4542
- var daemonPackageName, SESSION_MEMORY_COMPACT_WAIT_MS, AUTO_COMPACT_RETAINED_EVENTS, localDaemonStateFile, createLocalDaemonState, readLocalDaemonState, removeLocalDaemonState, markDaemonStopped, daemonStateLiveness, defaultIsPidAlive, startRemoteDaemonWebSocketServer, startScorelHostWebSocketServer, closeWebSocketServer, createRealRuntime, ScorelHost, isMissingConfigError, createEmbeddedTransport, isNodeErrorCode4, wireErrorCode, hasContinuousCoverage, countContentBlocks, normalizeContent, inputText, assistantText, messageText, estimateScorelMessagesTokens, estimateTextTokens, compactLine2, parseSessionMemoryJson, stringArray, disabledMemorySettings, runtimeChannelContextFromWire, parseQueuedChannelContext, imBindingKey, defaultBuiltinExtensionsDir, runtimeModuleDir, findBuiltinExtensionsDir, isSteerMessage, stripImCommandPrefix, isRecord7, parseMemoryUpdate, normalizeMarkdownFile2, sanitizeSessionTitle, shortStack, formatDiagnosticLine, formatDiagnosticValue;
4788
+ var daemonPackageName, SESSION_MEMORY_COMPACT_WAIT_MS, AUTO_COMPACT_RETAINED_EVENTS, execFileAsync2, localDaemonStateFile, createLocalDaemonState, readLocalDaemonState, removeLocalDaemonState, markDaemonStopped, daemonStateLiveness, defaultIsPidAlive, startRemoteDaemonWebSocketServer, startScorelHostWebSocketServer, closeWebSocketServer, createRealRuntime, ScorelHost, isMissingConfigError, createEmbeddedTransport, isNodeErrorCode5, wireErrorCode, hasContinuousCoverage, countContentBlocks, normalizeContent, inputText, assistantText, messageText, estimateScorelMessagesTokens, estimateTextTokens, compactLine2, parseSessionMemoryJson, stringArray, disabledMemorySettings, detectRtk, ensureRtkAvailable, emptyRuntimeStats, readRuntimeStats, writeRuntimeStats, parseRuntimeStats, parseRuntimeStatsBuckets, addRtkSavings, addRuntimeStatsBucket, rtkSavingsFromToolResult, nonNegativeInteger2, resolveDefaultShell2, shellCommandArgs2, userShell2, runtimeChannelContextFromWire, parseQueuedChannelContext, parseQueuedModelSelection, imBindingKey, defaultBuiltinExtensionsDir, runtimeModuleDir, findBuiltinExtensionsDir, isSteerMessage, stripImCommandPrefix, isRecord8, parseMemoryUpdate, normalizeMarkdownFile2, sanitizeSessionTitle, shortStack, formatDiagnosticLine, formatDiagnosticValue;
4543
4789
  var init_src4 = __esm({
4544
4790
  "packages/daemon/src/index.ts"() {
4545
4791
  "use strict";
@@ -4554,6 +4800,7 @@ var init_src4 = __esm({
4554
4800
  daemonPackageName = "@scorel/daemon";
4555
4801
  SESSION_MEMORY_COMPACT_WAIT_MS = 5e3;
4556
4802
  AUTO_COMPACT_RETAINED_EVENTS = 8;
4803
+ execFileAsync2 = promisify2(execFile2);
4557
4804
  localDaemonStateFile = (stateDir) => join10(stateDir, "daemon.json");
4558
4805
  createLocalDaemonState = async (options) => {
4559
4806
  const state = {
@@ -4765,9 +5012,10 @@ var init_src4 = __esm({
4765
5012
  }
4766
5013
  server.close((error) => error ? reject(error) : resolve7());
4767
5014
  });
4768
- createRealRuntime = (options) => {
5015
+ createRealRuntime = async (options) => {
4769
5016
  const selection = resolveModelSelection(options.config, options.modelSelection);
4770
5017
  const model = resolvePiAiModel(selection.config);
5018
+ const rtkExecutable = options.rtkExecutable ?? (options.config.runtime.tokenSavingRtk ? (await detectRtk()).executable : void 0);
4771
5019
  const runtime = new ScorelRuntime({
4772
5020
  provider: createPiAiProvider({
4773
5021
  model,
@@ -4775,7 +5023,17 @@ var init_src4 = __esm({
4775
5023
  })
4776
5024
  });
4777
5025
  if (options.includeTools !== false) {
4778
- for (const tool of createCodingTools({ cwd: options.cwd, contextWindow: model.contextWindow })) {
5026
+ for (const tool of createCodingTools({
5027
+ cwd: options.cwd,
5028
+ contextWindow: model.contextWindow,
5029
+ ...options.sessionsDir && options.sessionId ? { toolResultArtifacts: { dir: sessionArtifactsDirPath(options.sessionsDir, options.sessionId) } } : {},
5030
+ tokenSaving: {
5031
+ rtk: {
5032
+ enabled: options.config.runtime.tokenSavingRtk,
5033
+ executable: rtkExecutable
5034
+ }
5035
+ }
5036
+ })) {
4779
5037
  runtime.registerTool(tool);
4780
5038
  }
4781
5039
  }
@@ -4794,6 +5052,8 @@ var init_src4 = __esm({
4794
5052
  #createRuntime;
4795
5053
  #memoryHomeDir;
4796
5054
  #onSessionListChanged;
5055
+ #idleShutdownMs;
5056
+ #onIdleShutdown;
4797
5057
  #now;
4798
5058
  #createId;
4799
5059
  #sessions = /* @__PURE__ */ new Map();
@@ -4805,6 +5065,9 @@ var init_src4 = __esm({
4805
5065
  #imExtensions = /* @__PURE__ */ new Map();
4806
5066
  #imBindings = /* @__PURE__ */ new Map();
4807
5067
  #registry;
5068
+ #runtimeStatsQueue = Promise.resolve();
5069
+ #idleShutdownTimer;
5070
+ #lastActiveWorkAt;
4808
5071
  #started = false;
4809
5072
  constructor(options) {
4810
5073
  this.#sessionsDir = options.sessionsDir;
@@ -4819,8 +5082,11 @@ var init_src4 = __esm({
4819
5082
  this.#createRuntime = options.createRuntime;
4820
5083
  this.#memoryHomeDir = options.memoryHomeDir;
4821
5084
  this.#onSessionListChanged = options.onSessionListChanged;
5085
+ this.#idleShutdownMs = options.idleShutdownMs;
5086
+ this.#onIdleShutdown = options.onIdleShutdown;
4822
5087
  this.#now = options.now ?? Date.now;
4823
5088
  this.#createId = options.createId ?? (() => crypto.randomUUID());
5089
+ this.#lastActiveWorkAt = this.#now();
4824
5090
  this.#registry = new ProjectRegistry({
4825
5091
  sessionsDir: this.#sessionsDir,
4826
5092
  projectsPath: options.projectsPath,
@@ -4833,8 +5099,10 @@ var init_src4 = __esm({
4833
5099
  await mkdir6(this.#scorelHomeDir, { recursive: true });
4834
5100
  await this.#loadImBindings();
4835
5101
  await this.#startEnabledImExtensions();
5102
+ this.#scheduleIdleShutdownCheck();
4836
5103
  }
4837
5104
  async shutdown() {
5105
+ this.#clearIdleShutdownTimer();
4838
5106
  for (const schedule of this.#memoryDreams.values()) {
4839
5107
  if (schedule.timer) {
4840
5108
  clearTimeout(schedule.timer);
@@ -4849,9 +5117,11 @@ var init_src4 = __esm({
4849
5117
  this.#assertStarted();
4850
5118
  await this.#stopImExtensions();
4851
5119
  await this.#startEnabledImExtensions();
5120
+ this.#scheduleIdleShutdownCheck();
4852
5121
  }
4853
5122
  connect(connection, sessionId) {
4854
5123
  this.#assertStarted();
5124
+ this.#clearIdleShutdownTimer();
4855
5125
  connection.sessionId = sessionId;
4856
5126
  this.#connections.add(connection);
4857
5127
  if (sessionId) {
@@ -4876,10 +5146,18 @@ var init_src4 = __esm({
4876
5146
  });
4877
5147
  }
4878
5148
  this.#connections.delete(connection);
5149
+ this.#scheduleIdleShutdownCheck();
4879
5150
  }
4880
5151
  releaseSessionEventBuffer(sessionId) {
4881
5152
  this.#events.delete(sessionId);
4882
5153
  }
5154
+ activityStatus() {
5155
+ const activeWork = this.#hasActiveWork();
5156
+ if (activeWork) {
5157
+ this.#lastActiveWorkAt = this.#now();
5158
+ }
5159
+ return { activeWork, lastActiveWorkAt: this.#lastActiveWorkAt };
5160
+ }
4883
5161
  async handleMessage(connection, message) {
4884
5162
  this.#assertStarted();
4885
5163
  try {
@@ -4896,6 +5174,8 @@ var init_src4 = __esm({
4896
5174
  return;
4897
5175
  }
4898
5176
  throw cause;
5177
+ } finally {
5178
+ this.#scheduleIdleShutdownCheck();
4899
5179
  }
4900
5180
  }
4901
5181
  async listDirectories(path) {
@@ -5004,7 +5284,7 @@ var init_src4 = __esm({
5004
5284
  break;
5005
5285
  }
5006
5286
  case "get_memory_settings": {
5007
- this.#respond(connection, message, { memory: await this.#memorySettingsForProject(message.projectId) });
5287
+ this.#respond(connection, message, { memory: await this.#memorySettings(message.projectId) });
5008
5288
  break;
5009
5289
  }
5010
5290
  case "get_memory_status": {
@@ -5015,6 +5295,14 @@ var init_src4 = __esm({
5015
5295
  this.#respond(connection, message, { memory: await this.#handleUpsertMemorySettings(message) });
5016
5296
  break;
5017
5297
  }
5298
+ case "get_runtime_settings": {
5299
+ this.#respond(connection, message, { runtime: await this.#runtimeSettings(message.projectId) });
5300
+ break;
5301
+ }
5302
+ case "upsert_runtime_settings": {
5303
+ this.#respond(connection, message, { runtime: await this.#handleUpsertRuntimeSettings(message) });
5304
+ break;
5305
+ }
5018
5306
  case "get_extension_settings": {
5019
5307
  this.#respond(connection, message, { extension: await this.#extensionSettings(message.extensionId) });
5020
5308
  break;
@@ -5043,6 +5331,39 @@ var init_src4 = __esm({
5043
5331
  break;
5044
5332
  }
5045
5333
  }
5334
+ #scheduleIdleShutdownCheck() {
5335
+ this.#clearIdleShutdownTimer();
5336
+ if (!this.#shouldIdleShutdown()) {
5337
+ return;
5338
+ }
5339
+ this.#idleShutdownTimer = setTimeout(() => {
5340
+ this.#idleShutdownTimer = void 0;
5341
+ if (this.#shouldIdleShutdown()) {
5342
+ this.#onIdleShutdown?.();
5343
+ }
5344
+ }, this.#idleShutdownMs);
5345
+ }
5346
+ #clearIdleShutdownTimer() {
5347
+ if (!this.#idleShutdownTimer) {
5348
+ return;
5349
+ }
5350
+ clearTimeout(this.#idleShutdownTimer);
5351
+ this.#idleShutdownTimer = void 0;
5352
+ }
5353
+ #shouldIdleShutdown() {
5354
+ return this.#started && this.#idleShutdownMs !== void 0 && this.#idleShutdownMs > 0 && this.#connections.size === 0 && this.#imExtensions.size === 0 && !this.#hasActiveWork();
5355
+ }
5356
+ #hasActiveWork() {
5357
+ for (const lane of this.#sessions.values()) {
5358
+ if (lane.runtime.running) {
5359
+ return true;
5360
+ }
5361
+ if (lane.session.tree.controlState.queues.follow_up.length > 0 || lane.session.tree.controlState.queues.steer.length > 0) {
5362
+ return true;
5363
+ }
5364
+ }
5365
+ return false;
5366
+ }
5046
5367
  async #handleCreateSession(connection, request) {
5047
5368
  const sessionId = request.sessionId ?? asSessionId(`ses_${this.#createId()}`);
5048
5369
  const project = await this.#resolveProject(sessionId, request.meta.projectId);
@@ -5059,7 +5380,7 @@ var init_src4 = __esm({
5059
5380
  try {
5060
5381
  lane = await this.#createLane(sessionId, request.meta, project);
5061
5382
  } catch (cause) {
5062
- if (!request.sessionId || !isNodeErrorCode4(cause, "EEXIST")) {
5383
+ if (!request.sessionId || !isNodeErrorCode5(cause, "EEXIST")) {
5063
5384
  throw cause;
5064
5385
  }
5065
5386
  lane = await this.#getLane(sessionId);
@@ -5125,6 +5446,7 @@ var init_src4 = __esm({
5125
5446
  content: normalizeContent(request.content),
5126
5447
  parentId: request.options?.parentId,
5127
5448
  source: "user",
5449
+ modelSelection: request.options?.modelSelection,
5128
5450
  channelContext: request.options?.channelContext ? runtimeChannelContextFromWire(request.options.channelContext) : void 0,
5129
5451
  onComplete: (result) => this.#respond(connection, request, { ...result, status: "completed" })
5130
5452
  });
@@ -5150,11 +5472,14 @@ var init_src4 = __esm({
5150
5472
  });
5151
5473
  }
5152
5474
  async #runUserTurn(lane, clientId, input) {
5475
+ this.#lastActiveWorkAt = this.#now();
5153
5476
  const sessionId = lane.session.header.sessionId;
5477
+ await this.#selectChatRuntime(lane, input.modelSelection);
5154
5478
  await this.#appendDiagnostic(sessionId, "send_message_started", {
5155
5479
  clientId,
5156
5480
  activeLeafId: lane.session.activeLeafId,
5157
- source: input.source
5481
+ source: input.source,
5482
+ selectedModelId: lane.selectedModel?.modelId
5158
5483
  });
5159
5484
  const instructionSnapshot = await this.#ensureInstructionSnapshot(lane, clientId);
5160
5485
  await this.#syncSkillIndex(lane, clientId);
@@ -5358,7 +5683,7 @@ var init_src4 = __esm({
5358
5683
  createdAt: now,
5359
5684
  updatedAt: now,
5360
5685
  clientId: connection.clientId,
5361
- ...request.options?.channelContext ? { data: { channelContext: request.options.channelContext } } : {}
5686
+ ...request.options?.channelContext || request.options?.modelSelection ? { data: { channelContext: request.options.channelContext, modelSelection: request.options.modelSelection } } : {}
5362
5687
  };
5363
5688
  lane.followUpWaiters.set(item.id, { connection, request });
5364
5689
  await this.#appendQueueRewrite(lane, "follow_up", [...lane.session.tree.controlState.queues.follow_up, item], {
@@ -5411,6 +5736,7 @@ var init_src4 = __esm({
5411
5736
  parentId: lane.session.activeLeafId,
5412
5737
  source: "follow_up",
5413
5738
  queueItemId: item.id,
5739
+ modelSelection: parseQueuedModelSelection(item.data?.modelSelection),
5414
5740
  channelContext: parseQueuedChannelContext(item.data?.channelContext),
5415
5741
  onComplete: waiter ? (result) => this.#respond(waiter.connection, waiter.request, { ...result, status: "completed" }) : void 0
5416
5742
  });
@@ -5578,6 +5904,18 @@ var init_src4 = __esm({
5578
5904
  ]
5579
5905
  }
5580
5906
  });
5907
+ const rtkSavings = rtkSavingsFromToolResult(rawEvent.result);
5908
+ if (rtkSavings) {
5909
+ await this.#recordRtkSavings({
5910
+ projectId: lane.project.projectId,
5911
+ sessionId: lane.session.header.sessionId,
5912
+ savings: rtkSavings
5913
+ }).catch(
5914
+ (cause) => this.#appendDiagnostic(lane.session.header.sessionId, "runtime_stats_update_failed", {
5915
+ message: cause instanceof Error ? cause.message : String(cause)
5916
+ })
5917
+ );
5918
+ }
5581
5919
  state.parentId = toolResultId;
5582
5920
  break;
5583
5921
  }
@@ -6381,6 +6719,7 @@ var init_src4 = __esm({
6381
6719
  session: loaded,
6382
6720
  project,
6383
6721
  runtime,
6722
+ ...selectedModel ? { selectedModel } : {},
6384
6723
  queue: Promise.resolve(),
6385
6724
  appendQueue: Promise.resolve(),
6386
6725
  followUpWaiters: /* @__PURE__ */ new Map()
@@ -6398,7 +6737,7 @@ var init_src4 = __esm({
6398
6737
  await this.#getLane(sessionId);
6399
6738
  return true;
6400
6739
  } catch (cause) {
6401
- if (isNodeErrorCode4(cause, "ENOENT")) {
6740
+ if (isNodeErrorCode5(cause, "ENOENT")) {
6402
6741
  return false;
6403
6742
  }
6404
6743
  throw cause;
@@ -6432,6 +6771,7 @@ var init_src4 = __esm({
6432
6771
  session,
6433
6772
  project,
6434
6773
  runtime,
6774
+ ...selectedModel ? { selectedModel } : {},
6435
6775
  queue: Promise.resolve(),
6436
6776
  appendQueue: Promise.resolve(),
6437
6777
  followUpWaiters: /* @__PURE__ */ new Map()
@@ -6447,6 +6787,32 @@ var init_src4 = __esm({
6447
6787
  })
6448
6788
  );
6449
6789
  }
6790
+ async #selectChatRuntime(lane, modelSelection) {
6791
+ if (!modelSelection) {
6792
+ return;
6793
+ }
6794
+ const selectedModel = await this.#selectedModelFromMeta(
6795
+ { projectId: lane.project.projectId, modelSelection },
6796
+ lane.project
6797
+ );
6798
+ if (!selectedModel || lane.selectedModel?.modelId === selectedModel.modelId) {
6799
+ return;
6800
+ }
6801
+ lane.runtime = await this.#createRuntime({
6802
+ sessionId: lane.session.header.sessionId,
6803
+ project: lane.project,
6804
+ selectedModel,
6805
+ purpose: "chat"
6806
+ });
6807
+ lane.selectedModel = selectedModel;
6808
+ this.#registerLaneTools(lane);
6809
+ await this.#appendDiagnostic(lane.session.header.sessionId, "chat_model_selected", {
6810
+ projectId: lane.project.projectId,
6811
+ workDir: lane.project.workDir,
6812
+ selectedModelId: selectedModel.modelId,
6813
+ role: selectedModel.role
6814
+ });
6815
+ }
6450
6816
  #syncChannelTool(lane, channelContext) {
6451
6817
  if (!channelContext) {
6452
6818
  lane.runtime.unregisterTool("SendChannelMessage");
@@ -6556,7 +6922,7 @@ var init_src4 = __esm({
6556
6922
  try {
6557
6923
  children = await readdir6(root);
6558
6924
  } catch (cause) {
6559
- if (isNodeErrorCode4(cause, "ENOENT") || isNodeErrorCode4(cause, "ENOTDIR")) {
6925
+ if (isNodeErrorCode5(cause, "ENOENT") || isNodeErrorCode5(cause, "ENOTDIR")) {
6560
6926
  continue;
6561
6927
  }
6562
6928
  throw cause;
@@ -6666,7 +7032,7 @@ var init_src4 = __esm({
6666
7032
  this.#imBindings.set(imBindingKey(binding.extensionId, binding.externalConversationId), binding);
6667
7033
  }
6668
7034
  } catch (cause) {
6669
- if (!isNodeErrorCode4(cause, "ENOENT")) {
7035
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
6670
7036
  throw cause;
6671
7037
  }
6672
7038
  }
@@ -6680,9 +7046,13 @@ var init_src4 = __esm({
6680
7046
  #imBindingsPath() {
6681
7047
  return join10(this.#scorelHomeDir, "channels", "im-bindings.json");
6682
7048
  }
6683
- async #loadUserConfigProfile() {
7049
+ async #loadUserConfigProfile(options = {}) {
6684
7050
  try {
6685
- return await loadScorelConfigProfile({ cwd: this.#userHomeDir, homeDir: this.#userHomeDir });
7051
+ return await loadScorelConfigProfile({
7052
+ cwd: this.#userHomeDir,
7053
+ scorelHomeDir: this.#scorelHomeDir,
7054
+ includeSecrets: options.includeSecrets ?? false
7055
+ });
6686
7056
  } catch (cause) {
6687
7057
  if (isMissingConfigError(cause)) {
6688
7058
  return void 0;
@@ -6690,16 +7060,24 @@ var init_src4 = __esm({
6690
7060
  throw cause;
6691
7061
  }
6692
7062
  }
7063
+ #configWriteTarget() {
7064
+ return {
7065
+ configDir: this.#scorelHomeDir,
7066
+ configPath: join10(this.#scorelHomeDir, "config.toml"),
7067
+ workDir: this.#userHomeDir
7068
+ };
7069
+ }
6693
7070
  async #listModels(projectId) {
6694
7071
  let config;
6695
7072
  try {
6696
- config = await this.#configProfileForProject(projectId);
7073
+ config = projectId ? await this.#configProfileForProject(projectId) : await this.#loadUserConfigProfile();
6697
7074
  } catch (cause) {
6698
7075
  if (!isMissingConfigError(cause)) {
6699
7076
  throw cause;
6700
7077
  }
6701
7078
  config = void 0;
6702
7079
  }
7080
+ config ??= projectId ? void 0 : this.#modelProfile;
6703
7081
  if (!config) {
6704
7082
  return {
6705
7083
  providers: [],
@@ -6722,19 +7100,18 @@ var init_src4 = __esm({
6722
7100
  };
6723
7101
  }
6724
7102
  async #handleUpsertModelProfile(request) {
6725
- const project = await this.#registry.require(request.projectId);
6726
- const configPath = join10(project.workDir, ".scorel", "config.toml");
7103
+ const target = this.#configWriteTarget();
6727
7104
  let existingConfigText;
6728
7105
  try {
6729
- existingConfigText = await readFile11(configPath, "utf8");
7106
+ existingConfigText = await readFile11(target.configPath, "utf8");
6730
7107
  } catch (cause) {
6731
- if (!isNodeErrorCode4(cause, "ENOENT")) {
7108
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
6732
7109
  throw cause;
6733
7110
  }
6734
7111
  }
6735
- await mkdir6(join10(project.workDir, ".scorel"), { recursive: true });
7112
+ await mkdir6(target.configDir, { recursive: true });
6736
7113
  await writeFile6(
6737
- configPath,
7114
+ target.configPath,
6738
7115
  renderModelProfileConfig({
6739
7116
  providerId: request.providerId,
6740
7117
  providerType: request.providerType,
@@ -6761,38 +7138,41 @@ var init_src4 = __esm({
6761
7138
  "utf8"
6762
7139
  );
6763
7140
  await this.#appendHostDiagnostic("model_profile_upserted", {
6764
- projectId: project.projectId,
6765
- workDir: project.workDir,
7141
+ ...request.projectId ? { ignoredProjectId: request.projectId } : {},
7142
+ scope: "device",
7143
+ workDir: target.workDir,
6766
7144
  providerId: request.providerId,
6767
7145
  modelId: request.modelId
6768
7146
  });
6769
- return this.#listModels(project.projectId);
7147
+ return this.#listModels();
6770
7148
  }
6771
7149
  async #handleRemoveModelProvider(request) {
6772
- const project = await this.#registry.require(request.projectId);
6773
- const configPath = join10(project.workDir, ".scorel", "config.toml");
7150
+ const target = this.#configWriteTarget();
6774
7151
  let existingConfigText;
6775
7152
  try {
6776
- existingConfigText = await readFile11(configPath, "utf8");
7153
+ existingConfigText = await readFile11(target.configPath, "utf8");
6777
7154
  } catch (cause) {
6778
- if (!isNodeErrorCode4(cause, "ENOENT")) {
7155
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
6779
7156
  throw cause;
6780
7157
  }
6781
7158
  }
6782
- await mkdir6(join10(project.workDir, ".scorel"), { recursive: true });
7159
+ await mkdir6(target.configDir, { recursive: true });
6783
7160
  await writeFile6(
6784
- configPath,
7161
+ target.configPath,
6785
7162
  renderModelProfileConfig({
6786
7163
  removeProviderId: request.providerId,
6787
7164
  existingConfigText
6788
7165
  }),
6789
7166
  "utf8"
6790
7167
  );
6791
- const profile = await this.#listModels(project.projectId);
7168
+ const profile = await this.#listModels();
6792
7169
  return { ...profile, removed: true };
6793
7170
  }
6794
7171
  async #memorySettingsForProject(projectId) {
6795
- const config = await this.#configProfileForProject(projectId).catch((cause) => {
7172
+ return this.#memorySettings(projectId);
7173
+ }
7174
+ async #memorySettings(projectId) {
7175
+ const config = await (projectId ? this.#configProfileForProject(projectId) : this.#loadUserConfigProfile()).catch((cause) => {
6796
7176
  if (isMissingConfigError(cause)) {
6797
7177
  return void 0;
6798
7178
  }
@@ -6814,19 +7194,18 @@ var init_src4 = __esm({
6814
7194
  }
6815
7195
  }
6816
7196
  async #handleUpsertMemorySettings(request) {
6817
- const project = await this.#registry.require(request.projectId);
6818
- const configPath = join10(project.workDir, ".scorel", "config.toml");
7197
+ const target = this.#configWriteTarget();
6819
7198
  let existingConfigText;
6820
7199
  try {
6821
- existingConfigText = await readFile11(configPath, "utf8");
7200
+ existingConfigText = await readFile11(target.configPath, "utf8");
6822
7201
  } catch (cause) {
6823
- if (!isNodeErrorCode4(cause, "ENOENT")) {
7202
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
6824
7203
  throw cause;
6825
7204
  }
6826
7205
  }
6827
- await mkdir6(join10(project.workDir, ".scorel"), { recursive: true });
7206
+ await mkdir6(target.configDir, { recursive: true });
6828
7207
  await writeFile6(
6829
- configPath,
7208
+ target.configPath,
6830
7209
  renderMemoryConfig({
6831
7210
  enabled: request.enabled,
6832
7211
  daily: request.daily,
@@ -6840,10 +7219,66 @@ var init_src4 = __esm({
6840
7219
  "utf8"
6841
7220
  );
6842
7221
  await this.#appendHostDiagnostic("memory_settings_upserted", {
6843
- projectId: project.projectId,
6844
- workDir: project.workDir
7222
+ ...request.projectId ? { ignoredProjectId: request.projectId } : {},
7223
+ scope: "device",
7224
+ workDir: target.workDir
7225
+ });
7226
+ return this.#memorySettings();
7227
+ }
7228
+ async #runtimeSettingsForProject(projectId, installStatus) {
7229
+ return this.#runtimeSettings(projectId, installStatus);
7230
+ }
7231
+ async #runtimeSettings(projectId, installStatus) {
7232
+ const config = await (projectId ? this.#configProfileForProject(projectId) : this.#loadUserConfigProfile()).catch((cause) => {
7233
+ if (isMissingConfigError(cause)) {
7234
+ return void 0;
7235
+ }
7236
+ throw cause;
7237
+ });
7238
+ const detected = await detectRtk();
7239
+ const savings = await readRuntimeStats(this.#runtimeStatsPath());
7240
+ return {
7241
+ tokenSavingRtk: config?.runtime.tokenSavingRtk ?? false,
7242
+ rtkAvailable: detected.available,
7243
+ ...detected.executable ? { rtkExecutable: detected.executable } : {},
7244
+ ...detected.version ? { rtkVersion: detected.version } : {},
7245
+ ...installStatus?.installStatus ? { installStatus: installStatus.installStatus } : {},
7246
+ ...installStatus?.installMessage ? { installMessage: installStatus.installMessage } : {},
7247
+ estimatedOutputTokens: savings.rtk.outputTokens,
7248
+ estimatedSavedTokens: savings.rtk.savedTokens
7249
+ };
7250
+ }
7251
+ async #handleUpsertRuntimeSettings(request) {
7252
+ const target = this.#configWriteTarget();
7253
+ let existingConfigText;
7254
+ try {
7255
+ existingConfigText = await readFile11(target.configPath, "utf8");
7256
+ } catch (cause) {
7257
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
7258
+ throw cause;
7259
+ }
7260
+ }
7261
+ await mkdir6(target.configDir, { recursive: true });
7262
+ await writeFile6(
7263
+ target.configPath,
7264
+ renderRuntimeConfig({
7265
+ tokenSavingRtk: request.tokenSavingRtk,
7266
+ existingConfigText
7267
+ }),
7268
+ "utf8"
7269
+ );
7270
+ const installResult = request.tokenSavingRtk === true ? await ensureRtkAvailable() : { status: "idle" };
7271
+ await this.#appendHostDiagnostic("runtime_settings_upserted", {
7272
+ ...request.projectId ? { ignoredProjectId: request.projectId } : {},
7273
+ scope: "device",
7274
+ workDir: target.workDir,
7275
+ tokenSavingRtk: request.tokenSavingRtk,
7276
+ installStatus: installResult.status
7277
+ });
7278
+ return this.#runtimeSettings(void 0, {
7279
+ installStatus: installResult.status,
7280
+ ...installResult.message ? { installMessage: installResult.message } : {}
6845
7281
  });
6846
- return this.#memorySettingsForProject(project.projectId);
6847
7282
  }
6848
7283
  async #extensionSettings(extensionId) {
6849
7284
  const config = await this.#loadUserConfigProfile().catch((cause) => {
@@ -6867,7 +7302,7 @@ var init_src4 = __esm({
6867
7302
  try {
6868
7303
  existingConfigText = await readFile11(configPath, "utf8");
6869
7304
  } catch (cause) {
6870
- if (!isNodeErrorCode4(cause, "ENOENT")) {
7305
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
6871
7306
  throw cause;
6872
7307
  }
6873
7308
  }
@@ -6891,8 +7326,11 @@ var init_src4 = __esm({
6891
7326
  return this.#extensionSettings(request.extensionId);
6892
7327
  }
6893
7328
  async #fetchProviderModels(projectId, providerId) {
6894
- const project = await this.#registry.require(projectId);
6895
- const config = await loadScorelConfigProfile({ cwd: project.workDir, includeSecrets: true });
7329
+ const config = projectId ? await loadScorelConfigProfile({
7330
+ cwd: (await this.#registry.require(projectId)).workDir,
7331
+ scorelHomeDir: this.#scorelHomeDir,
7332
+ includeSecrets: true
7333
+ }) : await this.#loadUserConfigProfile({ includeSecrets: true });
6896
7334
  if (!config) {
6897
7335
  throw new Error("Model profile config is not configured");
6898
7336
  }
@@ -6935,9 +7373,10 @@ var init_src4 = __esm({
6935
7373
  }
6936
7374
  const persistedSelection = "selectedModel" in meta ? meta.selectedModel : void 0;
6937
7375
  const requestedSelection = "modelSelection" in meta ? meta.modelSelection : void 0;
7376
+ const selectionInput = persistedSelection ? config.models[persistedSelection.modelId] ? { modelId: persistedSelection.modelId, role: persistedSelection.role } : persistedSelection.role ? { role: persistedSelection.role } : void 0 : requestedSelection;
6938
7377
  const selection = resolveModelSelection(
6939
7378
  config,
6940
- persistedSelection ? { modelId: persistedSelection.modelId, role: persistedSelection.role } : requestedSelection
7379
+ selectionInput
6941
7380
  );
6942
7381
  const model = resolvePiAiModel(selection.config);
6943
7382
  return {
@@ -6977,7 +7416,7 @@ var init_src4 = __esm({
6977
7416
  }
6978
7417
  const project = await this.#registry.require(projectId);
6979
7418
  try {
6980
- return await loadScorelConfigProfile({ cwd: project.workDir });
7419
+ return await loadScorelConfigProfile({ cwd: project.workDir, scorelHomeDir: this.#scorelHomeDir });
6981
7420
  } catch (cause) {
6982
7421
  if (!isMissingConfigError(cause)) {
6983
7422
  throw cause;
@@ -7018,6 +7457,20 @@ var init_src4 = __esm({
7018
7457
  await appendFile3(join10(this.#sessionsDir, "host.log"), `${line}
7019
7458
  `, "utf8");
7020
7459
  }
7460
+ #runtimeStatsPath() {
7461
+ return join10(this.#scorelHomeDir, "runtime-stats.json");
7462
+ }
7463
+ async #recordRtkSavings(input) {
7464
+ const updateTask = this.#runtimeStatsQueue.then(async () => {
7465
+ const path = this.#runtimeStatsPath();
7466
+ const stats = await readRuntimeStats(path);
7467
+ addRtkSavings(stats, String(input.projectId), String(input.sessionId), input.savings);
7468
+ await writeRuntimeStats(path, stats);
7469
+ });
7470
+ this.#runtimeStatsQueue = updateTask.catch(() => {
7471
+ });
7472
+ await updateTask;
7473
+ }
7021
7474
  async #resolveProject(sessionId, projectId) {
7022
7475
  const project = await this.#registry.require(projectId);
7023
7476
  await this.#appendDiagnostic(sessionId, "project_resolved", {
@@ -7073,7 +7526,7 @@ var init_src4 = __esm({
7073
7526
  }
7074
7527
  };
7075
7528
  };
7076
- isNodeErrorCode4 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
7529
+ isNodeErrorCode5 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
7077
7530
  wireErrorCode = (cause) => {
7078
7531
  if (!(cause instanceof ProjectRegistryError)) {
7079
7532
  return "internal_error";
@@ -7124,7 +7577,7 @@ var init_src4 = __esm({
7124
7577
  return void 0;
7125
7578
  }
7126
7579
  const parsed = JSON.parse(text);
7127
- if (!isRecord7(parsed)) {
7580
+ if (!isRecord8(parsed)) {
7128
7581
  return void 0;
7129
7582
  }
7130
7583
  return {
@@ -7144,6 +7597,147 @@ var init_src4 = __esm({
7144
7597
  dreamIdleMinutes: 60,
7145
7598
  autoCompactThreshold: 0.8
7146
7599
  });
7600
+ detectRtk = async () => {
7601
+ try {
7602
+ const shell = resolveDefaultShell2();
7603
+ const path = (await execFileAsync2(shell, shellCommandArgs2(shell, "command -v rtk"), { timeout: 5e3 })).stdout.trim();
7604
+ if (!path) {
7605
+ return { available: false };
7606
+ }
7607
+ const version = await execFileAsync2(path, ["--version"], { timeout: 5e3 }).then((result) => result.stdout.trim() || result.stderr.trim()).catch(() => void 0);
7608
+ return {
7609
+ available: true,
7610
+ executable: path,
7611
+ ...version ? { version } : {}
7612
+ };
7613
+ } catch {
7614
+ return { available: false };
7615
+ }
7616
+ };
7617
+ ensureRtkAvailable = async () => {
7618
+ const existing = await detectRtk();
7619
+ if (existing.available) {
7620
+ return { status: "installed", message: existing.version ?? existing.executable };
7621
+ }
7622
+ const shell = resolveDefaultShell2();
7623
+ const brew = await execFileAsync2(shell, shellCommandArgs2(shell, "command -v brew"), { timeout: 5e3 }).then((result) => result.stdout.trim()).catch(() => "");
7624
+ if (!brew) {
7625
+ return { status: "failed", message: "Homebrew is not available; install RTK manually with `brew install rtk`." };
7626
+ }
7627
+ try {
7628
+ await execFileAsync2(brew, ["install", "rtk"], { timeout: 12e4, maxBuffer: 2e7 });
7629
+ const installed = await detectRtk();
7630
+ return installed.available ? { status: "installed", message: installed.version ?? installed.executable } : { status: "failed", message: "RTK install finished but `rtk` is still not on PATH." };
7631
+ } catch (cause) {
7632
+ const message = cause instanceof Error ? cause.message : String(cause);
7633
+ return { status: "failed", message };
7634
+ }
7635
+ };
7636
+ emptyRuntimeStats = () => ({
7637
+ version: 1,
7638
+ rtk: {
7639
+ outputTokens: 0,
7640
+ savedTokens: 0,
7641
+ byProject: {},
7642
+ bySession: {}
7643
+ }
7644
+ });
7645
+ readRuntimeStats = async (path) => {
7646
+ try {
7647
+ return parseRuntimeStats(JSON.parse(await readFile11(path, "utf8")));
7648
+ } catch (cause) {
7649
+ if (isNodeErrorCode5(cause, "ENOENT")) {
7650
+ return emptyRuntimeStats();
7651
+ }
7652
+ return emptyRuntimeStats();
7653
+ }
7654
+ };
7655
+ writeRuntimeStats = async (path, stats) => {
7656
+ await mkdir6(dirname8(path), { recursive: true });
7657
+ const tempPath = join10(dirname8(path), `.runtime-stats-${process.pid}-${Date.now()}.tmp`);
7658
+ try {
7659
+ await writeFile6(tempPath, `${JSON.stringify(stats, null, 2)}
7660
+ `, "utf8");
7661
+ await rename3(tempPath, path);
7662
+ } catch (cause) {
7663
+ await rm2(tempPath, { force: true }).catch(() => void 0);
7664
+ throw cause;
7665
+ }
7666
+ };
7667
+ parseRuntimeStats = (value) => {
7668
+ if (!isRecord8(value) || !isRecord8(value.rtk)) {
7669
+ return emptyRuntimeStats();
7670
+ }
7671
+ return {
7672
+ version: 1,
7673
+ rtk: {
7674
+ outputTokens: nonNegativeInteger2(value.rtk.outputTokens),
7675
+ savedTokens: nonNegativeInteger2(value.rtk.savedTokens),
7676
+ byProject: parseRuntimeStatsBuckets(value.rtk.byProject),
7677
+ bySession: parseRuntimeStatsBuckets(value.rtk.bySession)
7678
+ }
7679
+ };
7680
+ };
7681
+ parseRuntimeStatsBuckets = (value) => {
7682
+ if (!isRecord8(value)) {
7683
+ return {};
7684
+ }
7685
+ return Object.fromEntries(
7686
+ Object.entries(value).map(([key, bucket]) => [
7687
+ key,
7688
+ isRecord8(bucket) ? {
7689
+ outputTokens: nonNegativeInteger2(bucket.outputTokens),
7690
+ savedTokens: nonNegativeInteger2(bucket.savedTokens)
7691
+ } : { outputTokens: 0, savedTokens: 0 }
7692
+ ])
7693
+ );
7694
+ };
7695
+ addRtkSavings = (stats, projectId, sessionId, savings) => {
7696
+ addRuntimeStatsBucket(stats.rtk, savings);
7697
+ stats.rtk.byProject[projectId] = addRuntimeStatsBucket(stats.rtk.byProject[projectId] ?? { outputTokens: 0, savedTokens: 0 }, savings);
7698
+ stats.rtk.bySession[sessionId] = addRuntimeStatsBucket(stats.rtk.bySession[sessionId] ?? { outputTokens: 0, savedTokens: 0 }, savings);
7699
+ };
7700
+ addRuntimeStatsBucket = (bucket, savings) => {
7701
+ bucket.outputTokens += savings.outputTokens;
7702
+ bucket.savedTokens += savings.savedTokens;
7703
+ return bucket;
7704
+ };
7705
+ rtkSavingsFromToolResult = (result) => {
7706
+ if (!isRecord8(result) || !isRecord8(result.details)) {
7707
+ return void 0;
7708
+ }
7709
+ const rtk = result.details.rtk;
7710
+ if (!isRecord8(rtk) || rtk.applied !== true) {
7711
+ return void 0;
7712
+ }
7713
+ const outputTokens = nonNegativeInteger2(rtk.estimatedOutputTokens);
7714
+ const savedTokens = nonNegativeInteger2(rtk.estimatedSavedTokens);
7715
+ return outputTokens > 0 || savedTokens > 0 ? { outputTokens, savedTokens } : void 0;
7716
+ };
7717
+ nonNegativeInteger2 = (value) => {
7718
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
7719
+ return 0;
7720
+ }
7721
+ return Math.floor(value);
7722
+ };
7723
+ resolveDefaultShell2 = () => {
7724
+ const shell = process.env.SHELL || userShell2() || "/bin/sh";
7725
+ return shell.trim() || "/bin/sh";
7726
+ };
7727
+ shellCommandArgs2 = (shell, command) => {
7728
+ const name = basename3(shell).toLowerCase();
7729
+ if (name === "csh" || name === "tcsh" || name === "fish") {
7730
+ return ["-c", command];
7731
+ }
7732
+ return ["-lc", command];
7733
+ };
7734
+ userShell2 = () => {
7735
+ try {
7736
+ return userInfo2().shell ?? void 0;
7737
+ } catch {
7738
+ return void 0;
7739
+ }
7740
+ };
7147
7741
  runtimeChannelContextFromWire = (context) => ({
7148
7742
  extensionId: context.channel,
7149
7743
  channel: context.channel,
@@ -7158,7 +7752,7 @@ var init_src4 = __esm({
7158
7752
  ...context.data ? { data: context.data } : {}
7159
7753
  });
7160
7754
  parseQueuedChannelContext = (value) => {
7161
- if (!isRecord7(value)) {
7755
+ if (!isRecord8(value)) {
7162
7756
  return void 0;
7163
7757
  }
7164
7758
  if (typeof value.channel !== "string" || typeof value.externalConversationId !== "string") {
@@ -7170,9 +7764,22 @@ var init_src4 = __esm({
7170
7764
  ...typeof value.conversationType === "string" ? { conversationType: value.conversationType } : {},
7171
7765
  ...typeof value.senderDisplayName === "string" ? { senderDisplayName: value.senderDisplayName } : {},
7172
7766
  ...typeof value.mentionedBot === "boolean" ? { mentionedBot: value.mentionedBot } : {},
7173
- ...isRecord7(value.data) ? { data: value.data } : {}
7767
+ ...isRecord8(value.data) ? { data: value.data } : {}
7174
7768
  });
7175
7769
  };
7770
+ parseQueuedModelSelection = (value) => {
7771
+ if (!isRecord8(value)) {
7772
+ return void 0;
7773
+ }
7774
+ const selection = {};
7775
+ if (typeof value.modelId === "string") {
7776
+ selection.modelId = value.modelId;
7777
+ }
7778
+ if (value.role === "primary" || value.role === "standard" || value.role === "auxiliary") {
7779
+ selection.role = value.role;
7780
+ }
7781
+ return selection.modelId || selection.role ? selection : void 0;
7782
+ };
7176
7783
  imBindingKey = (extensionId, externalConversationId) => `${extensionId}:${externalConversationId}`;
7177
7784
  defaultBuiltinExtensionsDir = () => findBuiltinExtensionsDir([
7178
7785
  runtimeModuleDir(),
@@ -7203,7 +7810,7 @@ var init_src4 = __esm({
7203
7810
  };
7204
7811
  isSteerMessage = (text) => /^\/(?:steer|interrupt)\b/i.test(text.trim());
7205
7812
  stripImCommandPrefix = (text) => text.trim().replace(/^\/(?:steer|interrupt)\s*/i, "").trim() || text;
7206
- isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7813
+ isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7207
7814
  parseMemoryUpdate = (raw) => {
7208
7815
  const text = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim();
7209
7816
  if (!text) {
@@ -7308,27 +7915,139 @@ var init_relay_cli = __esm({
7308
7915
  }
7309
7916
  });
7310
7917
 
7918
+ // apps/cli/src/update-cli.ts
7919
+ import { execFile as execFileCallback } from "node:child_process";
7920
+ import { readFile as readFile12 } from "node:fs/promises";
7921
+ import { dirname as dirname9, join as join12 } from "node:path";
7922
+ import { fileURLToPath } from "node:url";
7923
+ import { promisify as promisify3 } from "node:util";
7924
+ var SCOREL_PACKAGE_NAME, AUTO_UPDATE_INTERVAL_MS, ACTIVE_WORK_STALE_MS, execFileAsync3, compareSemver, shouldRunAutoUpdate, createNpmPackageUpdater, readInstalledScorelVersion, runCliUpdate, writeUpdateUsage, parseSemver;
7925
+ var init_update_cli = __esm({
7926
+ "apps/cli/src/update-cli.ts"() {
7927
+ "use strict";
7928
+ SCOREL_PACKAGE_NAME = "@chanlerdev/scorel";
7929
+ AUTO_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
7930
+ ACTIVE_WORK_STALE_MS = 3 * 60 * 60 * 1e3;
7931
+ execFileAsync3 = promisify3(execFileCallback);
7932
+ compareSemver = (a, b) => {
7933
+ const left = parseSemver(a);
7934
+ const right = parseSemver(b);
7935
+ for (let index = 0; index < 3; index += 1) {
7936
+ const delta = left[index] - right[index];
7937
+ if (delta !== 0) return delta;
7938
+ }
7939
+ return 0;
7940
+ };
7941
+ shouldRunAutoUpdate = (activity) => !activity.activeWork || activity.now - activity.lastActiveWorkAt >= ACTIVE_WORK_STALE_MS;
7942
+ createNpmPackageUpdater = (options) => {
7943
+ const packageName = options.packageName ?? SCOREL_PACKAGE_NAME;
7944
+ const execFile3 = options.execFile ?? ((command, argv) => execFileAsync3(command, argv));
7945
+ return {
7946
+ async checkLatest() {
7947
+ const result = await execFile3("npm", ["view", packageName, "version"]);
7948
+ const latest = result.stdout.trim();
7949
+ if (!latest) {
7950
+ throw new Error(`npm did not return a latest version for ${packageName}`);
7951
+ }
7952
+ parseSemver(latest);
7953
+ return latest;
7954
+ },
7955
+ async update() {
7956
+ const latestVersion = await this.checkLatest();
7957
+ if (compareSemver(options.currentVersion, latestVersion) >= 0) {
7958
+ return { status: "current", currentVersion: options.currentVersion, latestVersion };
7959
+ }
7960
+ await execFile3("npm", ["install", "-g", `${packageName}@${latestVersion}`]);
7961
+ return { status: "updated", currentVersion: options.currentVersion, latestVersion };
7962
+ }
7963
+ };
7964
+ };
7965
+ readInstalledScorelVersion = async () => {
7966
+ const here = dirname9(fileURLToPath(import.meta.url));
7967
+ for (const candidate of [
7968
+ join12(here, "..", "package.json"),
7969
+ join12(here, "..", "..", "package.json"),
7970
+ join12(process.cwd(), "package.json")
7971
+ ]) {
7972
+ try {
7973
+ const parsed = JSON.parse(await readFile12(candidate, "utf8"));
7974
+ if (typeof parsed.version === "string" && (parsed.name === SCOREL_PACKAGE_NAME || parsed.name === "@scorel/app-cli")) {
7975
+ return parsed.version;
7976
+ }
7977
+ } catch {
7978
+ }
7979
+ }
7980
+ return "0.0.0";
7981
+ };
7982
+ runCliUpdate = async (argv, io, options = {}) => {
7983
+ if (argv.includes("--help") || argv.includes("-h")) {
7984
+ writeUpdateUsage(io.output);
7985
+ return 0;
7986
+ }
7987
+ if (argv.length > 0) {
7988
+ writeUpdateUsage(io.error);
7989
+ return 1;
7990
+ }
7991
+ const currentVersion = options.currentVersion ?? await readInstalledScorelVersion();
7992
+ const updater = options.updater ?? createNpmPackageUpdater({ currentVersion });
7993
+ try {
7994
+ const result = await updater.update();
7995
+ if (result.status === "current") {
7996
+ io.output.write(`scorel is current (${result.currentVersion})
7997
+ `);
7998
+ } else {
7999
+ io.output.write(`updated scorel ${result.currentVersion} -> ${result.latestVersion}
8000
+ `);
8001
+ }
8002
+ return 0;
8003
+ } catch (cause) {
8004
+ io.error.write(`scorel update error: ${cause instanceof Error ? cause.message : String(cause)}
8005
+ `);
8006
+ return 1;
8007
+ }
8008
+ };
8009
+ writeUpdateUsage = (output) => {
8010
+ output.write("Usage: scorel update\n scorel upgrade\n");
8011
+ };
8012
+ parseSemver = (version) => {
8013
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
8014
+ if (!match) {
8015
+ throw new Error(`Invalid semver version: ${version}`);
8016
+ }
8017
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
8018
+ };
8019
+ }
8020
+ });
8021
+
7311
8022
  // apps/cli/src/daemon-cli.ts
7312
8023
  import { randomUUID as randomUUID4 } from "node:crypto";
8024
+ import { spawn } from "node:child_process";
7313
8025
  import { homedir as homedir6 } from "node:os";
7314
- import { join as join12 } from "node:path";
7315
- var DEFAULT_HOST, DEFAULT_PORT, STOP_POLL_INTERVAL_MS, STOP_GRACE_MS, defaultStateDir2, isLoopbackHost, formatTimestamp, runCliDaemon, runServeCommand, stopRunningDaemon, runStatusCommand, runStopCommand, runResetCommand, formatStatusLine, parseServeFlags, parseStatusFlags, requireValue2, sleep, writeDaemonUsage;
8026
+ import { dirname as dirname10, join as join13 } from "node:path";
8027
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
8028
+ var DEFAULT_HOST, DEFAULT_PORT, STOP_POLL_INTERVAL_MS, STOP_GRACE_MS, START_READY_TIMEOUT_MS, AUTO_STARTED_IDLE_SHUTDOWN_MS, FOREGROUND_IDLE_SHUTDOWN_MS, defaultStateDir2, isLoopbackHost, formatTimestamp, runCliDaemon, runStartCommand, runServeCommand, startAutoUpdateLoop, stopRunningDaemon, runStatusCommand, runStopCommand, runResetCommand, formatStatusLine, parseServeFlags, parseStatusFlags, requireValue2, sleep, waitForDaemonReady, detachBackgroundDaemon, nodeEntrypointArgs, writeDaemonUsage;
7316
8029
  var init_daemon_cli = __esm({
7317
8030
  "apps/cli/src/daemon-cli.ts"() {
7318
8031
  "use strict";
7319
8032
  init_src4();
7320
8033
  init_relay_cli();
8034
+ init_update_cli();
7321
8035
  DEFAULT_HOST = "127.0.0.1";
7322
8036
  DEFAULT_PORT = 7777;
7323
8037
  STOP_POLL_INTERVAL_MS = 200;
7324
8038
  STOP_GRACE_MS = 5e3;
7325
- defaultStateDir2 = () => join12(homedir6(), ".scorel");
8039
+ START_READY_TIMEOUT_MS = 1e4;
8040
+ AUTO_STARTED_IDLE_SHUTDOWN_MS = 15 * 60 * 1e3;
8041
+ FOREGROUND_IDLE_SHUTDOWN_MS = 0;
8042
+ defaultStateDir2 = () => join13(homedir6(), ".scorel");
7326
8043
  isLoopbackHost = (host) => host === "127.0.0.1" || host === "::1" || host === "localhost";
7327
8044
  formatTimestamp = (epochMs) => new Date(epochMs).toISOString();
7328
8045
  runCliDaemon = async (argv, options) => {
7329
8046
  const [command, ...rest] = argv;
7330
8047
  const stateDir = options.stateDir ?? defaultStateDir2();
7331
8048
  switch (command) {
8049
+ case "start":
8050
+ return runStartCommand(rest, { ...options, stateDir });
7332
8051
  case "serve":
7333
8052
  return runServeCommand(rest, { ...options, stateDir });
7334
8053
  case "status":
@@ -7346,10 +8065,67 @@ var init_daemon_cli = __esm({
7346
8065
  return 1;
7347
8066
  }
7348
8067
  };
8068
+ runStartCommand = async (argv, options) => {
8069
+ let flags;
8070
+ try {
8071
+ flags = parseServeFlags(argv, options.cwd ?? process.cwd(), options.env ?? process.env, FOREGROUND_IDLE_SHUTDOWN_MS);
8072
+ } catch (cause) {
8073
+ options.error.write(`scorel daemon start error: ${cause.message}
8074
+ `);
8075
+ return 1;
8076
+ }
8077
+ const readState = options.readState ?? ((stateDir) => readLocalDaemonState({ stateDir }));
8078
+ const existing = await readState(options.stateDir);
8079
+ if (existing && daemonStateLiveness(existing) === "running") {
8080
+ options.output.write(`scorel host already running url=${existing.wsUrl} pid=${existing.pid}
8081
+ `);
8082
+ return 0;
8083
+ }
8084
+ const cliEntrypoint = options.cliEntrypoint ?? fileURLToPath2(import.meta.url).replace(/daemon-cli\.ts$/, "index.ts");
8085
+ const child = (options.spawn ?? spawn)(process.execPath, [
8086
+ ...nodeEntrypointArgs(cliEntrypoint),
8087
+ "host",
8088
+ "serve",
8089
+ "--host",
8090
+ flags.host,
8091
+ "--port",
8092
+ String(flags.port),
8093
+ "--cwd",
8094
+ flags.cwd,
8095
+ "--idle-timeout-ms",
8096
+ String(flags.idleShutdownMs),
8097
+ ...flags.token ? ["--token", flags.token] : [],
8098
+ ...flags.relayUrl ? ["--relay", flags.relayUrl] : ["--no-relay"],
8099
+ ...flags.replace ? ["--replace"] : []
8100
+ ], {
8101
+ cwd: dirname10(cliEntrypoint),
8102
+ env: { ...process.env, ...options.env ?? {} },
8103
+ detached: true,
8104
+ stdio: ["ignore", "pipe", "pipe"]
8105
+ });
8106
+ try {
8107
+ await waitForDaemonReady(child, options.daemonReadyTimeoutMs ?? START_READY_TIMEOUT_MS);
8108
+ } catch (cause) {
8109
+ options.error.write(`scorel daemon start error: ${cause.message}
8110
+ `);
8111
+ child.kill("SIGTERM");
8112
+ return 1;
8113
+ }
8114
+ const state = await readState(options.stateDir);
8115
+ if (!state || daemonStateLiveness(state) !== "running") {
8116
+ options.error.write("scorel daemon start error: daemon state missing after start\n");
8117
+ child.kill("SIGTERM");
8118
+ return 1;
8119
+ }
8120
+ detachBackgroundDaemon(child);
8121
+ options.output.write(`scorel host started url=${state.wsUrl} pid=${state.pid}
8122
+ `);
8123
+ return 0;
8124
+ };
7349
8125
  runServeCommand = async (argv, options) => {
7350
8126
  let flags;
7351
8127
  try {
7352
- flags = parseServeFlags(argv, options.cwd ?? process.cwd(), options.env ?? process.env);
8128
+ flags = parseServeFlags(argv, options.cwd ?? process.cwd(), options.env ?? process.env, FOREGROUND_IDLE_SHUTDOWN_MS);
7353
8129
  } catch (cause) {
7354
8130
  options.error.write(`scorel daemon serve error: ${cause.message}
7355
8131
  `);
@@ -7373,22 +8149,45 @@ Use --replace to stop it and start a new one.
7373
8149
  }
7374
8150
  const token = flags.token ?? existing?.token ?? randomUUID4();
7375
8151
  const identity = await loadOrCreateHostDeviceIdentity({ stateDir: options.stateDir });
8152
+ const configScope = { scorelHomeDir: options.stateDir };
8153
+ let signalReason = "natural";
8154
+ let resolveStopWaiter;
8155
+ let stopRequested = false;
8156
+ const requestStop = (reason) => {
8157
+ signalReason = reason;
8158
+ stopRequested = true;
8159
+ resolveStopWaiter?.();
8160
+ };
8161
+ const sessionsDir = options.sessionsDir ?? scorelSessionsDir(homedir6());
7376
8162
  const daemon = new ScorelHost({
7377
- sessionsDir: options.sessionsDir ?? scorelSessionsDir(homedir6()),
7378
- projectsPath: join12(options.stateDir, "projects.json"),
8163
+ sessionsDir,
8164
+ projectsPath: join13(options.stateDir, "projects.json"),
7379
8165
  deviceId: identity.deviceId,
7380
8166
  deviceDisplayName: identity.displayName,
7381
- loadConfig: async ({ project }) => loadScorelConfig({ cwd: project.workDir }),
7382
- loadConfigProfile: async ({ project }) => loadScorelConfigProfile({ cwd: project.workDir }),
7383
- createRuntime: async ({ project, selectedModel, purpose }) => createRealRuntime({
8167
+ idleShutdownMs: flags.idleShutdownMs,
8168
+ onIdleShutdown: () => requestStop("idle"),
8169
+ scorelHomeDir: options.stateDir,
8170
+ loadConfig: async ({ project }) => loadScorelConfig({ cwd: project.workDir, ...configScope }),
8171
+ loadConfigProfile: async ({ project }) => loadScorelConfigProfile({ cwd: project.workDir, ...configScope }),
8172
+ createRuntime: async ({ sessionId, project, selectedModel, purpose }) => createRealRuntime({
7384
8173
  cwd: project.workDir,
7385
- config: await loadScorelConfig({ cwd: project.workDir }),
8174
+ config: await loadScorelConfig({ cwd: project.workDir, ...configScope }),
8175
+ sessionsDir,
8176
+ sessionId,
7386
8177
  modelSelection: selectedModel ? { modelId: selectedModel.modelId, role: selectedModel.role } : void 0,
7387
8178
  includeTools: purpose === "chat"
7388
8179
  })
7389
8180
  });
7390
8181
  await daemon.start();
7391
8182
  await daemon.registerProject(flags.cwd);
8183
+ const autoUpdater = await startAutoUpdateLoop({
8184
+ host: daemon,
8185
+ requestStop,
8186
+ output: options.output,
8187
+ error: options.error,
8188
+ updater: options.packageUpdater,
8189
+ intervalMs: options.autoUpdateIntervalMs ?? AUTO_UPDATE_INTERVAL_MS
8190
+ });
7392
8191
  const server = await startScorelHostWebSocketServer({
7393
8192
  hostService: daemon,
7394
8193
  host: flags.host,
@@ -7434,6 +8233,7 @@ Use --replace to stop it and start a new one.
7434
8233
  }
7435
8234
  const shutdown = async () => {
7436
8235
  try {
8236
+ autoUpdater.stop();
7437
8237
  relayClient?.close();
7438
8238
  await server.close();
7439
8239
  } finally {
@@ -7441,20 +8241,22 @@ Use --replace to stop it and start a new one.
7441
8241
  await markDaemonStopped({ stateDir: options.stateDir, stoppedAt: Date.now() });
7442
8242
  }
7443
8243
  };
7444
- let signalReason = "natural";
7445
8244
  const signalHandlers = /* @__PURE__ */ new Map();
7446
8245
  const stopWaiter = new Promise((resolve7) => {
8246
+ resolveStopWaiter = resolve7;
8247
+ if (stopRequested) {
8248
+ resolve7();
8249
+ return;
8250
+ }
7447
8251
  if (options.serveSignal) {
7448
8252
  if (options.serveSignal.aborted) {
7449
- signalReason = "abort";
7450
- resolve7();
8253
+ requestStop("abort");
7451
8254
  return;
7452
8255
  }
7453
8256
  options.serveSignal.addEventListener(
7454
8257
  "abort",
7455
8258
  () => {
7456
- signalReason = "abort";
7457
- resolve7();
8259
+ requestStop("abort");
7458
8260
  },
7459
8261
  { once: true }
7460
8262
  );
@@ -7462,8 +8264,7 @@ Use --replace to stop it and start a new one.
7462
8264
  }
7463
8265
  const installSignal = (signal) => {
7464
8266
  const handler = () => {
7465
- signalReason = signal;
7466
- resolve7();
8267
+ requestStop(signal);
7467
8268
  };
7468
8269
  signalHandlers.set(signal, handler);
7469
8270
  process.once(signal, handler);
@@ -7483,6 +8284,42 @@ Use --replace to stop it and start a new one.
7483
8284
  `);
7484
8285
  return 0;
7485
8286
  };
8287
+ startAutoUpdateLoop = async (options) => {
8288
+ const updater = options.updater ?? createNpmPackageUpdater({ currentVersion: await readInstalledScorelVersion() });
8289
+ let timer;
8290
+ let running = false;
8291
+ const tick = async () => {
8292
+ if (running) return;
8293
+ running = true;
8294
+ try {
8295
+ const activity = options.host.activityStatus();
8296
+ if (!shouldRunAutoUpdate({ ...activity, now: Date.now() })) {
8297
+ return;
8298
+ }
8299
+ const result = await updater.update();
8300
+ if (result.status === "updated") {
8301
+ options.output.write(`scorel auto-updated ${result.currentVersion} -> ${result.latestVersion}; restarting host
8302
+ `);
8303
+ options.requestStop("auto-update");
8304
+ }
8305
+ } catch (cause) {
8306
+ options.error.write(`scorel auto-update error: ${cause instanceof Error ? cause.message : String(cause)}
8307
+ `);
8308
+ } finally {
8309
+ running = false;
8310
+ }
8311
+ };
8312
+ timer = setInterval(() => void tick(), options.intervalMs);
8313
+ timer.unref?.();
8314
+ return {
8315
+ stop() {
8316
+ if (timer) {
8317
+ clearInterval(timer);
8318
+ timer = void 0;
8319
+ }
8320
+ }
8321
+ };
8322
+ };
7486
8323
  stopRunningDaemon = async (state, options) => {
7487
8324
  try {
7488
8325
  process.kill(state.pid, "SIGTERM");
@@ -7586,13 +8423,14 @@ Use --replace to stop it and start a new one.
7586
8423
  const stoppedAt = state.stoppedAt !== null ? formatTimestamp(state.stoppedAt) : "unknown";
7587
8424
  return `stopped url=${state.wsUrl} last-pid=${state.pid} stoppedAt=${stoppedAt} liveness=${liveness}`;
7588
8425
  };
7589
- parseServeFlags = (argv, defaultCwd, env) => {
8426
+ parseServeFlags = (argv, defaultCwd, env, defaultIdleShutdownMs) => {
7590
8427
  let host = DEFAULT_HOST;
7591
8428
  let port = DEFAULT_PORT;
7592
8429
  let cwd = defaultCwd;
7593
8430
  let token;
7594
8431
  let relayUrl = resolveDefaultRelayUrl(env);
7595
8432
  let replace = false;
8433
+ let idleShutdownMs = defaultIdleShutdownMs;
7596
8434
  for (let index = 0; index < argv.length; index += 1) {
7597
8435
  const arg = argv[index];
7598
8436
  if (arg === "--host") {
@@ -7636,9 +8474,17 @@ Use --replace to stop it and start a new one.
7636
8474
  replace = true;
7637
8475
  continue;
7638
8476
  }
8477
+ if (arg === "--idle-timeout-ms") {
8478
+ idleShutdownMs = Number(requireValue2(argv, index, "--idle-timeout-ms"));
8479
+ if (!Number.isInteger(idleShutdownMs) || idleShutdownMs < 0) {
8480
+ throw new Error("--idle-timeout-ms must be a non-negative integer");
8481
+ }
8482
+ index += 1;
8483
+ continue;
8484
+ }
7639
8485
  throw new Error(`Unknown serve option: ${arg}`);
7640
8486
  }
7641
- return { host, port, token, cwd, relayUrl, replace };
8487
+ return { host, port, token, cwd, relayUrl, replace, idleShutdownMs };
7642
8488
  };
7643
8489
  parseStatusFlags = (argv) => {
7644
8490
  let showToken = false;
@@ -7661,11 +8507,66 @@ Use --replace to stop it and start a new one.
7661
8507
  sleep = (ms) => new Promise((resolve7) => {
7662
8508
  setTimeout(resolve7, ms);
7663
8509
  });
8510
+ waitForDaemonReady = (child, timeoutMs) => new Promise((resolveReady, rejectReady) => {
8511
+ if (!child.stdout) {
8512
+ rejectReady(new Error("daemon child has no stdout stream"));
8513
+ return;
8514
+ }
8515
+ let buffer = "";
8516
+ let stderrBuffer = "";
8517
+ let settled = false;
8518
+ const timer = setTimeout(() => {
8519
+ if (settled) return;
8520
+ settled = true;
8521
+ cleanup();
8522
+ rejectReady(new Error("timed out waiting for daemon ready line"));
8523
+ }, timeoutMs);
8524
+ const onData = (chunk) => {
8525
+ buffer += chunk.toString();
8526
+ if (!buffer.includes("\n")) return;
8527
+ if (buffer.includes("scorel daemon serving url=") || buffer.includes("scorel host serving url=")) {
8528
+ if (settled) return;
8529
+ settled = true;
8530
+ cleanup();
8531
+ resolveReady();
8532
+ }
8533
+ const newlineIndex = buffer.lastIndexOf("\n");
8534
+ buffer = newlineIndex >= 0 ? buffer.slice(newlineIndex + 1) : buffer;
8535
+ };
8536
+ const onStderr = (chunk) => {
8537
+ stderrBuffer += chunk.toString();
8538
+ };
8539
+ const onExit = (code) => {
8540
+ if (settled) return;
8541
+ settled = true;
8542
+ cleanup();
8543
+ const trimmed = stderrBuffer.trim();
8544
+ const detail = trimmed ? `: ${trimmed}` : "";
8545
+ rejectReady(new Error(`daemon exited before ready code=${code}${detail}`));
8546
+ };
8547
+ const cleanup = () => {
8548
+ clearTimeout(timer);
8549
+ child.stdout?.off("data", onData);
8550
+ child.stderr?.off("data", onStderr);
8551
+ child.off("exit", onExit);
8552
+ };
8553
+ child.stdout.on("data", onData);
8554
+ child.stderr?.on("data", onStderr);
8555
+ child.once("exit", onExit);
8556
+ });
8557
+ detachBackgroundDaemon = (child) => {
8558
+ child.stdout?.destroy();
8559
+ child.stderr?.destroy();
8560
+ child.unref();
8561
+ };
8562
+ nodeEntrypointArgs = (entrypoint) => entrypoint.endsWith(".ts") ? ["--import", "tsx", entrypoint] : [entrypoint];
7664
8563
  writeDaemonUsage = (output) => {
7665
8564
  output.write(
7666
8565
  [
7667
8566
  "Usage: scorel host serve [--host <h>] [--port <p>] [--token <t>] [--project <dir>]",
7668
- " [--relay <relay-url> | --no-relay] [--replace]",
8567
+ " [--relay <relay-url> | --no-relay] [--replace] [--idle-timeout-ms <ms>]",
8568
+ " scorel host start [--host <h>] [--port <p>] [--token <t>] [--project <dir>]",
8569
+ " [--relay <relay-url> | --no-relay] [--replace] [--idle-timeout-ms <ms>]",
7669
8570
  " scorel host status [--show-token]",
7670
8571
  " scorel host stop",
7671
8572
  " scorel host reset",
@@ -7866,8 +8767,8 @@ var init_routing = __esm({
7866
8767
  });
7867
8768
 
7868
8769
  // apps/relay/src/store.ts
7869
- import { mkdir as mkdir7, readFile as readFile12, writeFile as writeFile7 } from "node:fs/promises";
7870
- import { join as join13 } from "node:path";
8770
+ import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile7 } from "node:fs/promises";
8771
+ import { join as join14 } from "node:path";
7871
8772
  var FileRelayStore, emptyStoreFile;
7872
8773
  var init_store = __esm({
7873
8774
  "apps/relay/src/store.ts"() {
@@ -7877,7 +8778,7 @@ var init_store = __esm({
7877
8778
  #now;
7878
8779
  #queue = Promise.resolve();
7879
8780
  constructor(options) {
7880
- this.#filePath = join13(options.dataDir, "relay-store.json");
8781
+ this.#filePath = join14(options.dataDir, "relay-store.json");
7881
8782
  this.#now = options.now ?? Date.now;
7882
8783
  }
7883
8784
  async upsertDevice(record) {
@@ -7920,7 +8821,7 @@ var init_store = __esm({
7920
8821
  this.#queue = this.#queue.then(async () => {
7921
8822
  const file = await this.#read();
7922
8823
  mutator(file);
7923
- await mkdir7(join13(this.#filePath, ".."), { recursive: true });
8824
+ await mkdir7(join14(this.#filePath, ".."), { recursive: true });
7924
8825
  await writeFile7(this.#filePath, `${JSON.stringify(file, null, 2)}
7925
8826
  `);
7926
8827
  });
@@ -7928,7 +8829,7 @@ var init_store = __esm({
7928
8829
  }
7929
8830
  async #read() {
7930
8831
  try {
7931
- const raw = JSON.parse(await readFile12(this.#filePath, "utf8"));
8832
+ const raw = JSON.parse(await readFile13(this.#filePath, "utf8"));
7932
8833
  if (raw.version !== 1 || !Array.isArray(raw.devices) || !Array.isArray(raw.clients) || !Array.isArray(raw.bindings)) {
7933
8834
  return emptyStoreFile();
7934
8835
  }
@@ -8181,7 +9082,7 @@ var init_library = __esm({
8181
9082
 
8182
9083
  // apps/cli/src/relay-server-cli.ts
8183
9084
  import { homedir as homedir7 } from "node:os";
8184
- import { join as join14 } from "node:path";
9085
+ import { join as join15 } from "node:path";
8185
9086
  var DEFAULT_HOST2, DEFAULT_PORT2, runCliRelay, runRelayServe, parseRelayServeFlags, waitForStop, requireValue3, writeRelayUsage;
8186
9087
  var init_relay_server_cli = __esm({
8187
9088
  "apps/cli/src/relay-server-cli.ts"() {
@@ -8233,7 +9134,7 @@ var init_relay_server_cli = __esm({
8233
9134
  parseRelayServeFlags = (argv) => {
8234
9135
  let host = DEFAULT_HOST2;
8235
9136
  let port = DEFAULT_PORT2;
8236
- let dataDir = join14(homedir7(), ".scorel", "relay");
9137
+ let dataDir = join15(homedir7(), ".scorel", "relay");
8237
9138
  for (let index = 0; index < argv.length; index += 1) {
8238
9139
  const arg = argv[index];
8239
9140
  if (arg === "--host") {
@@ -8289,19 +9190,20 @@ var init_relay_server_cli = __esm({
8289
9190
  });
8290
9191
 
8291
9192
  // apps/cli/src/up-cli.ts
8292
- import { spawn } from "node:child_process";
9193
+ import { spawn as spawn2 } from "node:child_process";
8293
9194
  import { homedir as homedir8 } from "node:os";
8294
- import { join as join15 } from "node:path";
8295
- import { fileURLToPath } from "node:url";
8296
- var DEFAULT_DAEMON_PORT, DEFAULT_WEBUI_PORT, DEFAULT_DAEMON_READY_TIMEOUT_MS, defaultStateDir3, defaultAttachSigint, runCliUp, parseUpFlags, requireValue4, waitForDaemonReady, pipeWithPrefix, pipeStreamLines, once;
9195
+ import { dirname as dirname11, join as join16 } from "node:path";
9196
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
9197
+ var DEFAULT_DAEMON_PORT, DEFAULT_WEBUI_PORT, DEFAULT_DAEMON_READY_TIMEOUT_MS, defaultStateDir3, defaultAttachSigint, runCliUp, parseUpFlags, requireValue4, waitForDaemonReady2, pipeWithPrefix, detachBackgroundDaemon2, nodeEntrypointArgs2, pipeStreamLines, once;
8297
9198
  var init_up_cli = __esm({
8298
9199
  "apps/cli/src/up-cli.ts"() {
8299
9200
  "use strict";
8300
9201
  init_src4();
9202
+ init_daemon_cli();
8301
9203
  DEFAULT_DAEMON_PORT = 7777;
8302
9204
  DEFAULT_WEBUI_PORT = 3e3;
8303
9205
  DEFAULT_DAEMON_READY_TIMEOUT_MS = 1e4;
8304
- defaultStateDir3 = () => join15(homedir8(), ".scorel");
9206
+ defaultStateDir3 = () => join16(homedir8(), ".scorel");
8305
9207
  defaultAttachSigint = (listener) => {
8306
9208
  process.on("SIGINT", listener);
8307
9209
  return () => process.off("SIGINT", listener);
@@ -8316,8 +9218,8 @@ var init_up_cli = __esm({
8316
9218
  return 1;
8317
9219
  }
8318
9220
  const stateDir = options.stateDir ?? defaultStateDir3();
8319
- const cliEntrypoint = options.cliEntrypoint ?? fileURLToPath(import.meta.url).replace(/up-cli\.ts$/, "index.ts");
8320
- const spawnFn = options.spawn ?? spawn;
9221
+ const cliEntrypoint = options.cliEntrypoint ?? fileURLToPath3(import.meta.url).replace(/up-cli\.ts$/, "index.ts");
9222
+ const spawnFn = options.spawn ?? spawn2;
8321
9223
  const readState = options.readState ?? ((dir) => readLocalDaemonState({ stateDir: dir }));
8322
9224
  const attachSigint = options.attachSigint ?? defaultAttachSigint;
8323
9225
  const readyTimeout = options.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
@@ -8328,31 +9230,31 @@ var init_up_cli = __esm({
8328
9230
  let daemonState = existingState;
8329
9231
  if (!reuseDaemon) {
8330
9232
  const daemonArgs = [
8331
- "--import",
8332
- "tsx",
8333
- cliEntrypoint,
9233
+ ...nodeEntrypointArgs2(cliEntrypoint),
8334
9234
  "daemon",
8335
9235
  "serve",
8336
9236
  "--port",
8337
9237
  String(flags.daemonPort),
8338
9238
  "--cwd",
8339
9239
  flags.cwd,
9240
+ "--idle-timeout-ms",
9241
+ String(AUTO_STARTED_IDLE_SHUTDOWN_MS),
8340
9242
  "--no-relay"
8341
9243
  ];
8342
9244
  daemonChild = spawnFn(process.execPath, daemonArgs, {
8343
- cwd: flags.cwd,
9245
+ cwd: dirname11(cliEntrypoint),
8344
9246
  env: { ...process.env },
9247
+ detached: true,
8345
9248
  stdio: ["ignore", "pipe", "pipe"]
8346
9249
  });
8347
9250
  try {
8348
- await waitForDaemonReady(daemonChild, readyTimeout);
9251
+ await waitForDaemonReady2(daemonChild, readyTimeout);
8349
9252
  } catch (cause) {
8350
9253
  options.error.write(`scorel up error: ${cause.message}
8351
9254
  `);
8352
9255
  daemonChild.kill("SIGTERM");
8353
9256
  return 1;
8354
9257
  }
8355
- pipeWithPrefix(daemonChild, "[daemon]", options.output, options.error);
8356
9258
  daemonState = await readState(stateDir);
8357
9259
  }
8358
9260
  if (!daemonState) {
@@ -8360,16 +9262,17 @@ var init_up_cli = __esm({
8360
9262
  daemonChild?.kill("SIGTERM");
8361
9263
  return 1;
8362
9264
  }
9265
+ if (daemonChild) {
9266
+ detachBackgroundDaemon2(daemonChild);
9267
+ }
8363
9268
  const webuiArgs = [
8364
- "--import",
8365
- "tsx",
8366
- cliEntrypoint,
9269
+ ...nodeEntrypointArgs2(cliEntrypoint),
8367
9270
  "webui",
8368
9271
  "--port",
8369
9272
  String(flags.webuiPort)
8370
9273
  ];
8371
9274
  const webuiChild = spawnFn(process.execPath, webuiArgs, {
8372
- cwd: flags.cwd,
9275
+ cwd: dirname11(cliEntrypoint),
8373
9276
  env: { ...process.env },
8374
9277
  stdio: ["ignore", "pipe", "pipe"]
8375
9278
  });
@@ -8386,33 +9289,21 @@ var init_up_cli = __esm({
8386
9289
  return;
8387
9290
  }
8388
9291
  shuttingDown = true;
8389
- daemonChild?.kill("SIGTERM");
8390
9292
  webuiChild.kill("SIGTERM");
8391
9293
  });
8392
- const daemonExit = daemonChild ? once(daemonChild) : Promise.resolve(0);
8393
9294
  const webuiExit = once(webuiChild);
8394
- const daemonDeathWatcher = daemonChild ? daemonExit.then((code) => {
8395
- if (!shuttingDown) {
8396
- shuttingDown = true;
8397
- options.error.write(`scorel up daemon exited code=${code}
8398
- `);
8399
- webuiChild.kill("SIGTERM");
8400
- }
8401
- return code;
8402
- }) : Promise.resolve(0);
8403
9295
  const webuiDeathWatcher = webuiExit.then((code) => {
8404
9296
  if (!shuttingDown) {
8405
9297
  shuttingDown = true;
8406
9298
  options.error.write(`scorel up webui exited code=${code}
8407
9299
  `);
8408
- daemonChild?.kill("SIGTERM");
8409
9300
  }
8410
9301
  return code;
8411
9302
  });
8412
- const [daemonCode, webuiCode] = await Promise.all([daemonDeathWatcher, webuiDeathWatcher]);
9303
+ const webuiCode = await webuiDeathWatcher;
8413
9304
  detachSigint();
8414
9305
  options.output.write("scorel up stopped\n");
8415
- return daemonCode === 0 && webuiCode === 0 ? 0 : 1;
9306
+ return webuiCode === 0 ? 0 : 1;
8416
9307
  };
8417
9308
  parseUpFlags = (argv, defaultCwd) => {
8418
9309
  let daemonPort = DEFAULT_DAEMON_PORT;
@@ -8452,7 +9343,7 @@ var init_up_cli = __esm({
8452
9343
  }
8453
9344
  return value;
8454
9345
  };
8455
- waitForDaemonReady = (child, timeoutMs) => new Promise((resolveReady, rejectReady) => {
9346
+ waitForDaemonReady2 = (child, timeoutMs) => new Promise((resolveReady, rejectReady) => {
8456
9347
  if (!child.stdout) {
8457
9348
  rejectReady(new Error("daemon child has no stdout stream"));
8458
9349
  return;
@@ -8509,6 +9400,12 @@ var init_up_cli = __esm({
8509
9400
  pipeStreamLines(child.stderr, prefix, error);
8510
9401
  }
8511
9402
  };
9403
+ detachBackgroundDaemon2 = (child) => {
9404
+ child.stdout?.destroy();
9405
+ child.stderr?.destroy();
9406
+ child.unref();
9407
+ };
9408
+ nodeEntrypointArgs2 = (entrypoint) => entrypoint.endsWith(".ts") ? ["--import", "tsx", entrypoint] : [entrypoint];
8512
9409
  pipeStreamLines = (stream, prefix, destination) => {
8513
9410
  let buffer = "";
8514
9411
  stream.setEncoding?.("utf8");
@@ -8538,10 +9435,10 @@ var init_up_cli = __esm({
8538
9435
  });
8539
9436
 
8540
9437
  // apps/cli/src/webui-cli.ts
8541
- import { spawn as spawn2 } from "node:child_process";
9438
+ import { spawn as spawn3 } from "node:child_process";
8542
9439
  import { existsSync as existsSync4 } from "node:fs";
8543
- import { dirname as dirname9, resolve as resolve6 } from "node:path";
8544
- import { fileURLToPath as fileURLToPath2 } from "node:url";
9440
+ import { dirname as dirname12, resolve as resolve6 } from "node:path";
9441
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
8545
9442
  var DEFAULT_PORT3, DEFAULT_HOST3, runCliWebUi, findWebuiAppDir, buildWebUiSpawnPlan, parseWebUiFlags, requireValue5, waitForChildExit;
8546
9443
  var init_webui_cli = __esm({
8547
9444
  "apps/cli/src/webui-cli.ts"() {
@@ -8563,7 +9460,7 @@ var init_webui_cli = __esm({
8563
9460
  return 1;
8564
9461
  }
8565
9462
  const plan = buildWebUiSpawnPlan(flags, webuiAppDir);
8566
- const spawnFn = options.spawn ?? spawn2;
9463
+ const spawnFn = options.spawn ?? spawn3;
8567
9464
  const child = spawnFn(plan.command, plan.argv, {
8568
9465
  cwd: plan.cwd,
8569
9466
  env: plan.env,
@@ -8572,7 +9469,7 @@ var init_webui_cli = __esm({
8572
9469
  return await waitForChildExit(child, options);
8573
9470
  };
8574
9471
  findWebuiAppDir = () => {
8575
- let cursor = dirname9(fileURLToPath2(import.meta.url));
9472
+ let cursor = dirname12(fileURLToPath4(import.meta.url));
8576
9473
  for (let depth = 0; depth < 8; depth += 1) {
8577
9474
  const candidate = resolve6(cursor, "apps/webui/package.json");
8578
9475
  if (existsSync4(candidate)) {
@@ -8661,11 +9558,11 @@ __export(index_exports, {
8661
9558
  runCli: () => runCli
8662
9559
  });
8663
9560
  import { createHash as createHash3 } from "node:crypto";
8664
- import { appendFile as appendFile4, mkdir as mkdir8, readFile as readFile13, realpath as realpath3, readdir as readdir7, writeFile as writeFile8 } from "node:fs/promises";
9561
+ import { appendFile as appendFile4, mkdir as mkdir8, readFile as readFile14, realpath as realpath3, readdir as readdir7, writeFile as writeFile8 } from "node:fs/promises";
8665
9562
  import { createInterface } from "node:readline/promises";
8666
9563
  import { homedir as homedir9 } from "node:os";
8667
- import { fileURLToPath as fileURLToPath3 } from "node:url";
8668
- import { basename as basename2, dirname as dirname10, join as join16 } from "node:path";
9564
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
9565
+ import { basename as basename4, dirname as dirname13, join as join17 } from "node:path";
8669
9566
  var cliAppName, cliClientDependency, cliDaemonDependency, defaultSessionsDir, defaultStateDir4, runCli, runProject, runLogs, runAttach, attachCacheScope, attachCacheFilePath, attachDiagnosticsFilePath, findAttachDiagnosticsFilePath, stateDirFromSessionsDir, AttachDiagnostics, readAttachCache, writeAttachCache, emptyAttachCacheSnapshot, mergePersistentEvents, highestSeq, highestCachedStreamSeq, updateAttachCacheSnapshot, removeCompletedTransients, isCachedTransientMessage, AsyncInputQueue, parseAttachOptions, parseLogsOptions, runChat, createSigintHandler, loadOrCreateSession, parseChatOptions, requireValue6, promptIfInteractive, writeUsage, writeProjectUsage, writeEventError, writeToolResult, redactDiagnosticFields, formatDiagnosticLine2, formatDiagnosticValue2, AttachEventRenderer, blocksToText, isCliEntrypoint;
8670
9567
  var init_index = __esm({
8671
9568
  async "apps/cli/src/index.ts"() {
@@ -8678,13 +9575,19 @@ var init_index = __esm({
8678
9575
  init_relay_server_cli();
8679
9576
  init_up_cli();
8680
9577
  init_webui_cli();
9578
+ init_update_cli();
8681
9579
  cliAppName = "@scorel/app-cli";
8682
9580
  cliClientDependency = clientPackageName;
8683
9581
  cliDaemonDependency = daemonPackageName;
8684
9582
  defaultSessionsDir = () => scorelSessionsDir(homedir9());
8685
- defaultStateDir4 = () => join16(homedir9(), ".scorel");
9583
+ defaultStateDir4 = () => join17(homedir9(), ".scorel");
8686
9584
  runCli = async (argv, io = { input: process.stdin, output: process.stdout, error: process.stderr }, runOptions = {}) => {
8687
9585
  const [command, ...rest] = argv;
9586
+ if (command === "--version" || command === "-v" || command === "version") {
9587
+ io.output.write(`${await readInstalledScorelVersion()}
9588
+ `);
9589
+ return 0;
9590
+ }
8688
9591
  if (!command || command === "chat") {
8689
9592
  if (rest.includes("--help") || rest.includes("-h")) {
8690
9593
  writeUsage(io.output);
@@ -8730,6 +9633,9 @@ var init_index = __esm({
8730
9633
  error: io.error
8731
9634
  });
8732
9635
  }
9636
+ if (command === "update" || command === "upgrade") {
9637
+ return runCliUpdate(rest, { output: io.output, error: io.error });
9638
+ }
8733
9639
  if (command === "attach") {
8734
9640
  try {
8735
9641
  return runAttach(parseAttachOptions(rest), {
@@ -8817,10 +9723,10 @@ var init_index = __esm({
8817
9723
  }
8818
9724
  };
8819
9725
  runLogs = async (options, io) => {
8820
- const filePath = options.attach ? await findAttachDiagnosticsFilePath(io.stateDir, options.sessionId, options.remoteUrl) : join16(io.sessionsDir, `${options.sessionId}.log`);
9726
+ const filePath = options.attach ? await findAttachDiagnosticsFilePath(io.stateDir, options.sessionId, options.remoteUrl) : join17(io.sessionsDir, `${options.sessionId}.log`);
8821
9727
  let content;
8822
9728
  try {
8823
- content = await readFile13(filePath, "utf8");
9729
+ content = await readFile14(filePath, "utf8");
8824
9730
  } catch (cause) {
8825
9731
  io.error.write(`scorel logs error: ${cause instanceof Error ? cause.message : String(cause)}
8826
9732
  `);
@@ -8974,31 +9880,31 @@ var init_index = __esm({
8974
9880
  };
8975
9881
  attachCacheFilePath = (stateDir, scope, sessionId) => {
8976
9882
  const scopeKey = createHash3("sha256").update(`${scope.kind}\0${scope.locator}`).digest("hex").slice(0, 24);
8977
- return join16(stateDir, "attach-cache", scopeKey, `${sessionId}.json`);
9883
+ return join17(stateDir, "attach-cache", scopeKey, `${sessionId}.json`);
8978
9884
  };
8979
9885
  attachDiagnosticsFilePath = (stateDir, scope, sessionId) => {
8980
9886
  const scopeKey = createHash3("sha256").update(`${scope.kind}\0${scope.locator}`).digest("hex").slice(0, 24);
8981
- return join16(stateDir, "attach-cache", scopeKey, `${sessionId}.log`);
9887
+ return join17(stateDir, "attach-cache", scopeKey, `${sessionId}.log`);
8982
9888
  };
8983
9889
  findAttachDiagnosticsFilePath = async (stateDir, sessionId, _remoteUrl) => {
8984
- const root = join16(stateDir, "attach-cache");
9890
+ const root = join17(stateDir, "attach-cache");
8985
9891
  const scopes = await readdir7(root).catch(() => []);
8986
9892
  for (const scope of scopes) {
8987
- const candidate = join16(root, scope, `${sessionId}.log`);
9893
+ const candidate = join17(root, scope, `${sessionId}.log`);
8988
9894
  try {
8989
- await readFile13(candidate, "utf8");
9895
+ await readFile14(candidate, "utf8");
8990
9896
  return candidate;
8991
9897
  } catch {
8992
9898
  continue;
8993
9899
  }
8994
9900
  }
8995
- return join16(root, "__missing__", `${sessionId}.log`);
9901
+ return join17(root, "__missing__", `${sessionId}.log`);
8996
9902
  };
8997
9903
  stateDirFromSessionsDir = (sessionsDir) => {
8998
9904
  if (!sessionsDir) {
8999
9905
  return defaultStateDir4();
9000
9906
  }
9001
- return basename2(sessionsDir) === "sessions" ? dirname10(sessionsDir) : sessionsDir;
9907
+ return basename4(sessionsDir) === "sessions" ? dirname13(sessionsDir) : sessionsDir;
9002
9908
  };
9003
9909
  AttachDiagnostics = class {
9004
9910
  #stateDir;
@@ -9050,14 +9956,14 @@ var init_index = __esm({
9050
9956
  }
9051
9957
  const filePath = attachDiagnosticsFilePath(this.#stateDir, this.#scope, this.#sessionId);
9052
9958
  this.#writes.push(
9053
- mkdir8(dirname10(filePath), { recursive: true }).then(() => appendFile4(filePath, `${line}
9959
+ mkdir8(dirname13(filePath), { recursive: true }).then(() => appendFile4(filePath, `${line}
9054
9960
  `, "utf8"))
9055
9961
  );
9056
9962
  }
9057
9963
  };
9058
9964
  readAttachCache = async (stateDir, scope, sessionId) => {
9059
9965
  try {
9060
- const raw = JSON.parse(await readFile13(attachCacheFilePath(stateDir, scope, sessionId), "utf8"));
9966
+ const raw = JSON.parse(await readFile14(attachCacheFilePath(stateDir, scope, sessionId), "utf8"));
9061
9967
  if (raw.version !== 1 || raw.sessionId !== String(sessionId) || raw.scope.kind !== scope.kind || raw.scope.locator !== scope.locator || !Array.isArray(raw.events)) {
9062
9968
  return emptyAttachCacheSnapshot();
9063
9969
  }
@@ -9080,7 +9986,7 @@ var init_index = __esm({
9080
9986
  const filePath = attachCacheFilePath(stateDir, scope, sessionId);
9081
9987
  const uniqueEvents = mergePersistentEvents(snapshot.events);
9082
9988
  const transients = removeCompletedTransients(snapshot.transients, uniqueEvents);
9083
- await mkdir8(dirname10(filePath), { recursive: true });
9989
+ await mkdir8(dirname13(filePath), { recursive: true });
9084
9990
  await writeFile8(
9085
9991
  filePath,
9086
9992
  `${JSON.stringify({ version: 1, scope, sessionId: String(sessionId), events: uniqueEvents, transients }, null, 2)}
@@ -9217,17 +10123,21 @@ var init_index = __esm({
9217
10123
  return { sessionId, tail, attach, remoteUrl };
9218
10124
  };
9219
10125
  runChat = async (options, io) => {
9220
- const loadProjectConfig = async (project2) => options.config ?? await loadScorelConfig({ cwd: project2.workDir });
9221
- const loadProjectConfigProfile = async (project2) => options.config ?? await loadScorelConfigProfile({ cwd: project2.workDir });
10126
+ const configScope = { scorelHomeDir: options.stateDir };
10127
+ const loadProjectConfig = async (project2) => options.config ?? await loadScorelConfig({ cwd: project2.workDir, ...configScope });
10128
+ const loadProjectConfigProfile = async (project2) => options.config ?? await loadScorelConfigProfile({ cwd: project2.workDir, ...configScope });
9222
10129
  const daemon = new ScorelHost({
9223
10130
  sessionsDir: options.sessionsDir,
9224
- projectsPath: join16(options.stateDir, "projects.json"),
10131
+ projectsPath: join17(options.stateDir, "projects.json"),
9225
10132
  deviceId: asDeviceId("device_local"),
10133
+ scorelHomeDir: options.stateDir,
9226
10134
  loadConfig: async ({ project: project2 }) => loadProjectConfig(project2),
9227
10135
  loadConfigProfile: async ({ project: project2 }) => loadProjectConfigProfile(project2),
9228
- createRuntime: async ({ project: project2, selectedModel, purpose }) => createRealRuntime({
10136
+ createRuntime: async ({ sessionId, project: project2, selectedModel, purpose }) => createRealRuntime({
9229
10137
  cwd: project2.workDir,
9230
10138
  config: await loadProjectConfig(project2),
10139
+ sessionsDir: options.sessionsDir,
10140
+ sessionId,
9231
10141
  modelSelection: selectedModel ? { modelId: selectedModel.modelId, role: selectedModel.role } : void 0,
9232
10142
  includeTools: purpose === "chat"
9233
10143
  })
@@ -9356,8 +10266,10 @@ var init_index = __esm({
9356
10266
  "Usage: scorel chat [--session <id>] [--cwd <dir>]",
9357
10267
  " scorel [--session <id>] [--cwd <dir>]",
9358
10268
  " scorel attach --session <id> --remote <ws-url> --token <token>",
10269
+ " scorel host start [--host <h>] [--port <p>] [--token <t>] [--project <dir>]",
10270
+ " [--relay <relay-url> | --no-relay] [--replace] [--idle-timeout-ms <ms>]",
9359
10271
  " scorel host serve [--host <h>] [--port <p>] [--token <t>] [--project <dir>]",
9360
- " [--relay <relay-url> | --no-relay] [--replace]",
10272
+ " [--relay <relay-url> | --no-relay] [--replace] [--idle-timeout-ms <ms>]",
9361
10273
  " scorel host status [--show-token]",
9362
10274
  " scorel host stop",
9363
10275
  " scorel host reset",
@@ -9365,6 +10277,9 @@ var init_index = __esm({
9365
10277
  " scorel relay serve [--host <h>] [--port <p>] [--data-dir <dir>]",
9366
10278
  " scorel webui [--port <p>] [--host <h>]",
9367
10279
  " scorel up [--daemon-port <p>] [--webui-port <p>] [--cwd <d>]",
10280
+ " scorel update",
10281
+ " scorel upgrade",
10282
+ " scorel version",
9368
10283
  " scorel logs [--attach] --session <id> [--remote <ws-url>] [--tail <n>]",
9369
10284
  " scorel project list",
9370
10285
  " scorel project add <dir>",
@@ -9496,7 +10411,7 @@ ${text}
9496
10411
  if (!process.argv[1]) return false;
9497
10412
  const [argvPath, modulePath] = await Promise.all([
9498
10413
  realpath3(process.argv[1]).catch(() => process.argv[1]),
9499
- realpath3(fileURLToPath3(import.meta.url)).catch(() => fileURLToPath3(import.meta.url))
10414
+ realpath3(fileURLToPath5(import.meta.url)).catch(() => fileURLToPath5(import.meta.url))
9500
10415
  ]);
9501
10416
  return argvPath === modulePath;
9502
10417
  };