@chanlerdev/scorel 0.0.3 → 0.0.4

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, resolveDefaultShell, resolveRtkCommand, rtkRewriteResult, executableRewriteCommand, readRtkGain, rtkSavedTokenDelta, withRtkSavings, nonNegativeInteger, isRecord3, shellQuote, shellCommandArgs, userShell, truncate, 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");
@@ -1873,25 +1919,52 @@ String: ${input.old_string}`
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 bashResult({
1944
+ exitCode: 0,
1945
+ stdout: result.stdout,
1946
+ stderr: result.stderr,
1947
+ cwd: commandCwd,
1948
+ outputLimit,
1949
+ shell: defaultShell,
1950
+ command,
1951
+ rtk: withRtkSavings(rtkResult, rtkSavedTokens)
1952
+ });
1884
1953
  } catch (cause) {
1885
1954
  if (isTimeoutError(cause)) {
1886
1955
  throw new Error(`Bash command timed out after ${timeoutMs}ms`);
1887
1956
  }
1888
1957
  if (isExecError(cause)) {
1958
+ const rtkSavedTokens = rtk?.executable ? await rtkSavedTokenDelta(rtk.executable, commandCwd, rtkGainBefore) : void 0;
1889
1959
  return bashResult({
1890
1960
  exitCode: typeof cause.code === "number" ? cause.code : 1,
1891
1961
  stdout: String(cause.stdout ?? ""),
1892
1962
  stderr: String(cause.stderr ?? cause.message),
1893
1963
  cwd: commandCwd,
1894
- outputLimit
1964
+ outputLimit,
1965
+ shell: defaultShell,
1966
+ command,
1967
+ rtk: withRtkSavings(rtkResult, rtkSavedTokens)
1895
1968
  });
1896
1969
  }
1897
1970
  throw cause;
@@ -1905,7 +1978,7 @@ String: ${input.old_string}`
1905
1978
  const input = parseGlobArgs(args);
1906
1979
  const limit = input.head_limit ?? DEFAULT_SEARCH_LIMIT;
1907
1980
  const offset = input.offset ?? 0;
1908
- const all = await runRipgrep(["--files", "--hidden", "--glob", input.pattern, ...vcsExcludes()], workspaceTarget(input.path), root, signal);
1981
+ 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
1982
  const selected = paginate(all, limit, offset);
1910
1983
  const text = selected.items.map(toWorkspaceRelative(root)).join("\n");
1911
1984
  return textResult(text || "No files found", {
@@ -2266,9 +2339,96 @@ ${stdout}
2266
2339
  stderr:
2267
2340
  ${stderr}`, {
2268
2341
  exitCode: input.exitCode,
2269
- cwd: input.cwd
2342
+ cwd: input.cwd,
2343
+ ...input.shell ? { shell: input.shell } : {},
2344
+ ...input.command ? { command: input.command } : {},
2345
+ ...input.rtk ? {
2346
+ rtk: {
2347
+ ...input.rtk,
2348
+ estimatedOutputTokens: estimateTokens(`${stdout}
2349
+ ${stderr}`)
2350
+ }
2351
+ } : {}
2270
2352
  });
2271
2353
  };
2354
+ resolveDefaultShell = (input) => {
2355
+ const shell = input || process.env.SHELL || userShell() || "/bin/sh";
2356
+ return shell.trim() || "/bin/sh";
2357
+ };
2358
+ resolveRtkCommand = async (rtk, command) => {
2359
+ if (rtk?.enabled !== true || typeof rtk.executable !== "string" || rtk.executable.length === 0) {
2360
+ return { applied: false };
2361
+ }
2362
+ try {
2363
+ const result = await execFileAsync(rtk.executable, ["rewrite", command], {
2364
+ timeout: 5e3,
2365
+ maxBuffer: 1024 * 1024
2366
+ });
2367
+ return rtkRewriteResult(result.stdout, rtk.executable);
2368
+ } catch (cause) {
2369
+ if (isExecError(cause) && typeof cause.stdout === "string") {
2370
+ return rtkRewriteResult(cause.stdout, rtk.executable);
2371
+ }
2372
+ return { applied: false };
2373
+ }
2374
+ };
2375
+ rtkRewriteResult = (stdout, executable) => {
2376
+ const rewrittenCommand = stdout.trim();
2377
+ return rewrittenCommand ? { applied: true, rewrittenCommand, executionCommand: executableRewriteCommand(rewrittenCommand, executable) } : { applied: false };
2378
+ };
2379
+ executableRewriteCommand = (command, executable) => command.replace(/^rtk(?=\s|$)/, shellQuote(executable));
2380
+ readRtkGain = async (rtkExecutable, cwd) => {
2381
+ try {
2382
+ const { stdout } = await execFileAsync(rtkExecutable, ["gain", "--project", "--format", "json"], {
2383
+ cwd,
2384
+ timeout: 5e3,
2385
+ maxBuffer: 5e6
2386
+ });
2387
+ const parsed = JSON.parse(stdout);
2388
+ if (!isRecord3(parsed) || !isRecord3(parsed.summary)) {
2389
+ return void 0;
2390
+ }
2391
+ return { savedTokens: nonNegativeInteger(parsed.summary.total_saved) };
2392
+ } catch {
2393
+ return void 0;
2394
+ }
2395
+ };
2396
+ rtkSavedTokenDelta = async (rtkExecutable, cwd, before) => {
2397
+ if (!before) {
2398
+ return void 0;
2399
+ }
2400
+ const after = await readRtkGain(rtkExecutable, cwd);
2401
+ if (!after) {
2402
+ return void 0;
2403
+ }
2404
+ return Math.max(0, after.savedTokens - before.savedTokens);
2405
+ };
2406
+ withRtkSavings = (rtk, savedTokens) => ({
2407
+ ...rtk,
2408
+ ...rtk.applied && savedTokens !== void 0 ? { estimatedSavedTokens: savedTokens } : {}
2409
+ });
2410
+ nonNegativeInteger = (value) => {
2411
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
2412
+ return 0;
2413
+ }
2414
+ return Math.floor(value);
2415
+ };
2416
+ isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2417
+ shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
2418
+ shellCommandArgs = (shell, command) => {
2419
+ const name = basename2(shell).toLowerCase();
2420
+ if (name === "csh" || name === "tcsh" || name === "fish") {
2421
+ return ["-c", command];
2422
+ }
2423
+ return ["-lc", command];
2424
+ };
2425
+ userShell = () => {
2426
+ try {
2427
+ return userInfo().shell ?? void 0;
2428
+ } catch {
2429
+ return void 0;
2430
+ }
2431
+ };
2272
2432
  truncate = (value, maxBytes, label) => {
2273
2433
  const bytes = Buffer.byteLength(value);
2274
2434
  if (bytes <= maxBytes) {
@@ -2452,7 +2612,7 @@ var init_tools = __esm({
2452
2612
  });
2453
2613
 
2454
2614
  // packages/core/src/channel/index.ts
2455
- var createSendChannelMessageTool, parseSendChannelMessageInput, parseAttachments, optionalString2, isRecord3;
2615
+ var createSendChannelMessageTool, parseSendChannelMessageInput, parseAttachments, optionalString2, isRecord4;
2456
2616
  var init_channel = __esm({
2457
2617
  "packages/core/src/channel/index.ts"() {
2458
2618
  "use strict";
@@ -2470,7 +2630,7 @@ var init_channel = __esm({
2470
2630
  }
2471
2631
  });
2472
2632
  parseSendChannelMessageInput = (value) => {
2473
- if (!isRecord3(value)) {
2633
+ if (!isRecord4(value)) {
2474
2634
  throw new Error("SendChannelMessage args must be an object");
2475
2635
  }
2476
2636
  const text = typeof value.text === "string" && value.text.trim().length > 0 ? value.text : void 0;
@@ -2499,7 +2659,7 @@ var init_channel = __esm({
2499
2659
  throw new Error("SendChannelMessage.attachments must be an array");
2500
2660
  }
2501
2661
  return value.map((item, index) => {
2502
- if (!isRecord3(item)) {
2662
+ if (!isRecord4(item)) {
2503
2663
  throw new Error(`SendChannelMessage.attachments.${index} must be an object`);
2504
2664
  }
2505
2665
  if (item.type !== "image" && item.type !== "file") {
@@ -2528,14 +2688,14 @@ var init_channel = __esm({
2528
2688
  }
2529
2689
  return value;
2530
2690
  };
2531
- isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2691
+ isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2532
2692
  }
2533
2693
  });
2534
2694
 
2535
2695
  // packages/core/src/extensions/index.ts
2536
2696
  import { readFile as readFile5 } from "node:fs/promises";
2537
2697
  import { dirname as dirname4, resolve as resolve2 } from "node:path";
2538
- var loadExtensionManifest, parseExtensionManifest, requireString2, requireIdentifier2, requireKind, requireRelativePath, optionalRelativePaths, isRecord4;
2698
+ var loadExtensionManifest, parseExtensionManifest, requireString2, requireIdentifier2, requireKind, requireRelativePath, optionalRelativePaths, isRecord5;
2539
2699
  var init_extensions = __esm({
2540
2700
  "packages/core/src/extensions/index.ts"() {
2541
2701
  "use strict";
@@ -2548,7 +2708,7 @@ var init_extensions = __esm({
2548
2708
  const message = cause instanceof Error ? cause.message : String(cause);
2549
2709
  throw new Error(`Invalid extension manifest JSON at ${manifestPath}: ${message}`);
2550
2710
  }
2551
- if (!isRecord4(value)) {
2711
+ if (!isRecord5(value)) {
2552
2712
  throw new Error(`Extension manifest at ${manifestPath} must be an object`);
2553
2713
  }
2554
2714
  const rootDir = dirname4(resolve2(manifestPath));
@@ -2604,7 +2764,7 @@ var init_extensions = __esm({
2604
2764
  }
2605
2765
  return value.map((item, index) => requireRelativePath(item, `${name}.${index}`, manifestPath));
2606
2766
  };
2607
- isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2767
+ isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2608
2768
  }
2609
2769
  });
2610
2770
 
@@ -2613,7 +2773,7 @@ import { existsSync } from "node:fs";
2613
2773
  import { readdir as readdir4, readFile as readFile6 } from "node:fs/promises";
2614
2774
  import { homedir as homedir2, platform, release } from "node:os";
2615
2775
  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;
2776
+ var BASELINE_PROMPT, buildInstructionSnapshot, renderSystemPrompt, section, discoverAgentsSources, projectAgentsPaths, findGitRoot, renderAgentsBlock, renderWorkspaceBlock, renderEnvironmentBlock, renderTimeBlock, isNodeErrorCode2;
2617
2777
  var init_instructions = __esm({
2618
2778
  "packages/core/src/instructions/index.ts"() {
2619
2779
  "use strict";
@@ -2677,7 +2837,7 @@ var init_instructions = __esm({
2677
2837
  content
2678
2838
  });
2679
2839
  } catch (cause) {
2680
- if (!isNodeErrorCode(cause, "ENOENT") && !isNodeErrorCode(cause, "ENOTDIR")) {
2840
+ if (!isNodeErrorCode2(cause, "ENOENT") && !isNodeErrorCode2(cause, "ENOTDIR")) {
2681
2841
  throw cause;
2682
2842
  }
2683
2843
  }
@@ -2740,7 +2900,7 @@ var init_instructions = __esm({
2740
2900
  };
2741
2901
  renderEnvironmentBlock = (env) => [`Platform: ${platform()} ${release()}`, `Shell: ${env.SHELL ?? "unknown"}`].join("\n");
2742
2902
  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;
2903
+ isNodeErrorCode2 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
2744
2904
  }
2745
2905
  });
2746
2906
 
@@ -2748,7 +2908,7 @@ var init_instructions = __esm({
2748
2908
  import { appendFile, mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "node:fs/promises";
2749
2909
  import { homedir as homedir3 } from "node:os";
2750
2910
  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;
2911
+ 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
2912
  var init_memory = __esm({
2753
2913
  "packages/core/src/memory/index.ts"() {
2754
2914
  "use strict";
@@ -2950,7 +3110,7 @@ var init_memory = __esm({
2950
3110
  try {
2951
3111
  await writeFile3(path, content, { encoding: "utf8", flag: "wx", mode: 384 });
2952
3112
  } catch (cause) {
2953
- if (!isNodeErrorCode2(cause, "EEXIST")) {
3113
+ if (!isNodeErrorCode3(cause, "EEXIST")) {
2954
3114
  throw cause;
2955
3115
  }
2956
3116
  }
@@ -2959,7 +3119,7 @@ var init_memory = __esm({
2959
3119
  try {
2960
3120
  return await readFile7(path, "utf8");
2961
3121
  } catch (cause) {
2962
- if (isNodeErrorCode2(cause, "ENOENT")) {
3122
+ if (isNodeErrorCode3(cause, "ENOENT")) {
2963
3123
  return "";
2964
3124
  }
2965
3125
  throw cause;
@@ -2980,7 +3140,7 @@ var init_memory = __esm({
2980
3140
  normalizeMarkdownFile = (value) => `${value.trimEnd()}
2981
3141
  `;
2982
3142
  parseAppendDailyInput = (value) => {
2983
- if (!isRecord5(value)) {
3143
+ if (!isRecord6(value)) {
2984
3144
  throw new Error("AppendDaily args must be an object");
2985
3145
  }
2986
3146
  const summary = requireString3(value.summary, "summary");
@@ -3046,19 +3206,19 @@ var init_memory = __esm({
3046
3206
  optionalNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
3047
3207
  optionalString3 = (value) => typeof value === "string" && value.trim() ? value : void 0;
3048
3208
  parseLastFailure = (value) => {
3049
- if (!isRecord5(value)) return void 0;
3209
+ if (!isRecord6(value)) return void 0;
3050
3210
  const at = optionalNumber2(value.at);
3051
3211
  const message = optionalString3(value.message);
3052
3212
  return at !== void 0 && message ? { at, message } : void 0;
3053
3213
  };
3054
- isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3214
+ isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3055
3215
  safeProjectId = (projectId) => {
3056
3216
  if (!/^[A-Za-z0-9_-]+$/.test(projectId)) {
3057
3217
  throw new Error("projectId must contain only letters, numbers, underscores, or hyphens");
3058
3218
  }
3059
3219
  return projectId;
3060
3220
  };
3061
- isNodeErrorCode2 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
3221
+ isNodeErrorCode3 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
3062
3222
  }
3063
3223
  });
3064
3224
 
@@ -3347,7 +3507,7 @@ var init_pi_ai = __esm({
3347
3507
  });
3348
3508
 
3349
3509
  // packages/core/src/runtime/index.ts
3350
- var ScorelRuntime, normalizeAssistantMessage, isAssistantMessage, partialAssistantMessage;
3510
+ var ScorelRuntime, toolResultForContext, normalizeAssistantMessage, isAssistantMessage, partialAssistantMessage;
3351
3511
  var init_runtime = __esm({
3352
3512
  "packages/core/src/runtime/index.ts"() {
3353
3513
  "use strict";
@@ -3496,7 +3656,7 @@ var init_runtime = __esm({
3496
3656
  type: "tool_result",
3497
3657
  toolCallId: toolCall.toolCallId,
3498
3658
  toolName: toolCall.toolName,
3499
- result,
3659
+ result: toolResultForContext(result),
3500
3660
  isError
3501
3661
  };
3502
3662
  return {
@@ -3505,6 +3665,9 @@ var init_runtime = __esm({
3505
3665
  };
3506
3666
  }
3507
3667
  };
3668
+ toolResultForContext = (result) => ({
3669
+ content: result.content
3670
+ });
3508
3671
  normalizeAssistantMessage = (value, streamed, fallbackStopReason) => {
3509
3672
  if (value) {
3510
3673
  if (!isAssistantMessage(value)) {
@@ -3536,7 +3699,7 @@ var init_runtime = __esm({
3536
3699
  import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
3537
3700
  import { dirname as dirname6, join as join7 } from "node:path";
3538
3701
  function assertTreeEvent(value) {
3539
- if (!isRecord6(value)) {
3702
+ if (!isRecord7(value)) {
3540
3703
  throw new SessionStoreError("invalid_event", "Event must be an object");
3541
3704
  }
3542
3705
  if (value.type === "session_header") {
@@ -3548,7 +3711,7 @@ function assertTreeEvent(value) {
3548
3711
  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
3712
  throw new SessionStoreError("invalid_event", "Event is missing required base fields");
3550
3713
  }
3551
- if ((value.type === "user_message" || value.type === "assistant_message" || value.type === "tool_result") && !isRecord6(value.message)) {
3714
+ if ((value.type === "user_message" || value.type === "assistant_message" || value.type === "tool_result") && !isRecord7(value.message)) {
3552
3715
  throw new SessionStoreError("invalid_event", "Message event is missing message payload");
3553
3716
  }
3554
3717
  if (value.type === "session_title_updated" && !isSessionTitleUpdated(value)) {
@@ -3573,7 +3736,7 @@ function assertTreeEvent(value) {
3573
3736
  throw new SessionStoreError("invalid_event", "skill_index_delta is missing delta payload");
3574
3737
  }
3575
3738
  }
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;
3739
+ 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, isRecord7;
3577
3740
  var init_session = __esm({
3578
3741
  "packages/core/src/session/index.ts"() {
3579
3742
  "use strict";
@@ -3841,13 +4004,13 @@ var init_session = __esm({
3841
4004
  }
3842
4005
  };
3843
4006
  parseHeader = (value) => {
3844
- if (!isRecord6(value)) {
4007
+ if (!isRecord7(value)) {
3845
4008
  throw new SessionStoreError("invalid_header", "Session header must be an object");
3846
4009
  }
3847
4010
  if (value.version !== 1 || typeof value.sessionId !== "string" || typeof value.deviceId !== "string") {
3848
4011
  throw new SessionStoreError("invalid_header", "Session header is missing required identity fields");
3849
4012
  }
3850
- if (typeof value.createdAt !== "number" || !isRecord6(value.meta)) {
4013
+ if (typeof value.createdAt !== "number" || !isRecord7(value.meta)) {
3851
4014
  throw new SessionStoreError("invalid_header", "Session header is missing createdAt or meta");
3852
4015
  }
3853
4016
  if (typeof value.meta.projectId !== "string" || value.meta.projectId.length === 0) {
@@ -3861,7 +4024,7 @@ var init_session = __esm({
3861
4024
  return value;
3862
4025
  };
3863
4026
  validateSessionMatch = (header, value) => {
3864
- if (!isRecord6(value) || typeof value.sessionId !== "string") {
4027
+ if (!isRecord7(value) || typeof value.sessionId !== "string") {
3865
4028
  throw new SessionStoreError("invalid_header", "Event must be an object with a sessionId");
3866
4029
  }
3867
4030
  if (value.sessionId !== header.sessionId) {
@@ -3870,24 +4033,24 @@ var init_session = __esm({
3870
4033
  };
3871
4034
  isConversationEvent = (event) => event.type === "user_message" || event.type === "assistant_message" || event.type === "tool_result" || event.type === "harness_item" || event.type === "compact";
3872
4035
  isInstructionSnapshot = (value) => {
3873
- if (!isRecord6(value) || value.version !== 1 || typeof value.cwd !== "string" || !Array.isArray(value.sections)) {
4036
+ if (!isRecord7(value) || value.version !== 1 || typeof value.cwd !== "string" || !Array.isArray(value.sections)) {
3874
4037
  return false;
3875
4038
  }
3876
4039
  return value.sections.every(
3877
- (section2) => isRecord6(section2) && typeof section2.kind === "string" && typeof section2.frozenAt === "number" && typeof section2.renderedBlock === "string"
4040
+ (section2) => isRecord7(section2) && typeof section2.kind === "string" && typeof section2.frozenAt === "number" && typeof section2.renderedBlock === "string"
3878
4041
  );
3879
4042
  };
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");
4043
+ 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
4044
  isCompactEvent = (value) => typeof value.summary === "string" && typeof value.compactedThrough === "string" && typeof value.tokensBefore === "number" && typeof value.tokensAfter === "number" && typeof value.retainedEventCount === "number";
3882
4045
  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"
4046
+ (item) => isRecord7(item) && typeof item.id === "string" && Array.isArray(item.content) && typeof item.createdAt === "number" && typeof item.updatedAt === "number" && typeof item.clientId === "string"
3884
4047
  );
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");
4048
+ 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
4049
  isSkillIndexSnapshot = (value) => (value.anchorEventId === null || typeof value.anchorEventId === "string") && Array.isArray(value.entries) && value.entries.every(isSkillIndexEntry);
3887
4050
  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"
4051
+ (item) => isRecord7(item) && typeof item.name === "string" && typeof item.previousPath === "string"
3889
4052
  );
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";
4053
+ 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
4054
  appendHarnessItemToContext = (messages, event) => {
3892
4055
  const reminder = renderSystemReminder(event.item.content);
3893
4056
  const last = messages.at(-1);
@@ -3924,7 +4087,7 @@ ${reminder}` }]
3924
4087
  }
3925
4088
  return false;
3926
4089
  };
3927
- isToolResultWithContent = (value) => isRecord6(value) && Array.isArray(value.content);
4090
+ isToolResultWithContent = (value) => isRecord7(value) && Array.isArray(value.content);
3928
4091
  renderSystemReminder = (content) => `<system-reminder>
3929
4092
  ${content}
3930
4093
  </system-reminder>`;
@@ -3948,21 +4111,20 @@ ${content}
3948
4111
  cloneMessage = (message) => ({
3949
4112
  ...message,
3950
4113
  content: message.content.map((block) => {
3951
- if (block.type !== "tool_result" || !isRecord6(block.result)) {
4114
+ if (block.type !== "tool_result" || !isRecord7(block.result)) {
3952
4115
  return { ...block };
3953
4116
  }
3954
- const content = Array.isArray(block.result.content) ? { content: block.result.content.map((item) => isRecord6(item) ? { ...item } : item) } : {};
4117
+ const content = Array.isArray(block.result.content) ? { content: block.result.content.map((item) => isRecord7(item) ? { ...item } : item) } : {};
3955
4118
  return {
3956
4119
  ...block,
3957
4120
  result: {
3958
- ...block.result,
3959
- ...content
4121
+ content: content.content ?? []
3960
4122
  }
3961
4123
  };
3962
4124
  }),
3963
4125
  ...message.meta ? { meta: { ...message.meta } } : {}
3964
4126
  });
3965
- isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4127
+ isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3966
4128
  }
3967
4129
  });
3968
4130
 
@@ -3972,7 +4134,7 @@ import { existsSync as existsSync2 } from "node:fs";
3972
4134
  import { readdir as readdir5, readFile as readFile9, stat as stat4 } from "node:fs/promises";
3973
4135
  import { homedir as homedir4 } from "node:os";
3974
4136
  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;
4137
+ var scanSkillIndex, diffSkillIndex, hasSkillIndexDelta, renderSkillListing, renderSkillDelta, createSkillTool, projectSkillRoots, readSkillEntry, parseSkillMetadata, firstParagraph, parseSkillArgs, findGitRoot2, isNodeErrorCode4;
3976
4138
  var init_skills = __esm({
3977
4139
  "packages/core/src/skills/index.ts"() {
3978
4140
  "use strict";
@@ -3995,7 +4157,7 @@ var init_skills = __esm({
3995
4157
  try {
3996
4158
  children = await readdir5(root.path);
3997
4159
  } catch (cause) {
3998
- if (isNodeErrorCode3(cause, "ENOENT") || isNodeErrorCode3(cause, "ENOTDIR")) {
4160
+ if (isNodeErrorCode4(cause, "ENOENT") || isNodeErrorCode4(cause, "ENOTDIR")) {
3999
4161
  continue;
4000
4162
  }
4001
4163
  throw cause;
@@ -4108,7 +4270,7 @@ var init_skills = __esm({
4108
4270
  try {
4109
4271
  [fileStat, content] = await Promise.all([stat4(options.skillPath), readFile9(options.skillPath, "utf8")]);
4110
4272
  } catch (cause) {
4111
- if (isNodeErrorCode3(cause, "ENOENT") || isNodeErrorCode3(cause, "ENOTDIR")) {
4273
+ if (isNodeErrorCode4(cause, "ENOENT") || isNodeErrorCode4(cause, "ENOTDIR")) {
4112
4274
  return void 0;
4113
4275
  }
4114
4276
  throw cause;
@@ -4188,7 +4350,7 @@ var init_skills = __esm({
4188
4350
  current = next;
4189
4351
  }
4190
4352
  };
4191
- isNodeErrorCode3 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
4353
+ isNodeErrorCode4 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
4192
4354
  }
4193
4355
  });
4194
4356
 
@@ -4534,12 +4696,15 @@ var init_host_client = __esm({
4534
4696
  });
4535
4697
 
4536
4698
  // packages/daemon/src/index.ts
4699
+ import { execFile as execFile2 } from "node:child_process";
4537
4700
  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";
4701
+ 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";
4702
+ import { userInfo as userInfo2 } from "node:os";
4703
+ import { basename as basename3, dirname as dirname8, join as join10, resolve as resolve5 } from "node:path";
4540
4704
  import { pathToFileURL } from "node:url";
4705
+ import { promisify as promisify2 } from "node:util";
4541
4706
  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;
4707
+ 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, imBindingKey, defaultBuiltinExtensionsDir, runtimeModuleDir, findBuiltinExtensionsDir, isSteerMessage, stripImCommandPrefix, isRecord8, parseMemoryUpdate, normalizeMarkdownFile2, sanitizeSessionTitle, shortStack, formatDiagnosticLine, formatDiagnosticValue;
4543
4708
  var init_src4 = __esm({
4544
4709
  "packages/daemon/src/index.ts"() {
4545
4710
  "use strict";
@@ -4554,6 +4719,7 @@ var init_src4 = __esm({
4554
4719
  daemonPackageName = "@scorel/daemon";
4555
4720
  SESSION_MEMORY_COMPACT_WAIT_MS = 5e3;
4556
4721
  AUTO_COMPACT_RETAINED_EVENTS = 8;
4722
+ execFileAsync2 = promisify2(execFile2);
4557
4723
  localDaemonStateFile = (stateDir) => join10(stateDir, "daemon.json");
4558
4724
  createLocalDaemonState = async (options) => {
4559
4725
  const state = {
@@ -4765,9 +4931,10 @@ var init_src4 = __esm({
4765
4931
  }
4766
4932
  server.close((error) => error ? reject(error) : resolve7());
4767
4933
  });
4768
- createRealRuntime = (options) => {
4934
+ createRealRuntime = async (options) => {
4769
4935
  const selection = resolveModelSelection(options.config, options.modelSelection);
4770
4936
  const model = resolvePiAiModel(selection.config);
4937
+ const rtkExecutable = options.rtkExecutable ?? (options.config.runtime.tokenSavingRtk ? (await detectRtk()).executable : void 0);
4771
4938
  const runtime = new ScorelRuntime({
4772
4939
  provider: createPiAiProvider({
4773
4940
  model,
@@ -4775,7 +4942,16 @@ var init_src4 = __esm({
4775
4942
  })
4776
4943
  });
4777
4944
  if (options.includeTools !== false) {
4778
- for (const tool of createCodingTools({ cwd: options.cwd, contextWindow: model.contextWindow })) {
4945
+ for (const tool of createCodingTools({
4946
+ cwd: options.cwd,
4947
+ contextWindow: model.contextWindow,
4948
+ tokenSaving: {
4949
+ rtk: {
4950
+ enabled: options.config.runtime.tokenSavingRtk,
4951
+ executable: rtkExecutable
4952
+ }
4953
+ }
4954
+ })) {
4779
4955
  runtime.registerTool(tool);
4780
4956
  }
4781
4957
  }
@@ -4794,6 +4970,8 @@ var init_src4 = __esm({
4794
4970
  #createRuntime;
4795
4971
  #memoryHomeDir;
4796
4972
  #onSessionListChanged;
4973
+ #idleShutdownMs;
4974
+ #onIdleShutdown;
4797
4975
  #now;
4798
4976
  #createId;
4799
4977
  #sessions = /* @__PURE__ */ new Map();
@@ -4805,6 +4983,8 @@ var init_src4 = __esm({
4805
4983
  #imExtensions = /* @__PURE__ */ new Map();
4806
4984
  #imBindings = /* @__PURE__ */ new Map();
4807
4985
  #registry;
4986
+ #runtimeStatsQueue = Promise.resolve();
4987
+ #idleShutdownTimer;
4808
4988
  #started = false;
4809
4989
  constructor(options) {
4810
4990
  this.#sessionsDir = options.sessionsDir;
@@ -4819,6 +4999,8 @@ var init_src4 = __esm({
4819
4999
  this.#createRuntime = options.createRuntime;
4820
5000
  this.#memoryHomeDir = options.memoryHomeDir;
4821
5001
  this.#onSessionListChanged = options.onSessionListChanged;
5002
+ this.#idleShutdownMs = options.idleShutdownMs;
5003
+ this.#onIdleShutdown = options.onIdleShutdown;
4822
5004
  this.#now = options.now ?? Date.now;
4823
5005
  this.#createId = options.createId ?? (() => crypto.randomUUID());
4824
5006
  this.#registry = new ProjectRegistry({
@@ -4833,8 +5015,10 @@ var init_src4 = __esm({
4833
5015
  await mkdir6(this.#scorelHomeDir, { recursive: true });
4834
5016
  await this.#loadImBindings();
4835
5017
  await this.#startEnabledImExtensions();
5018
+ this.#scheduleIdleShutdownCheck();
4836
5019
  }
4837
5020
  async shutdown() {
5021
+ this.#clearIdleShutdownTimer();
4838
5022
  for (const schedule of this.#memoryDreams.values()) {
4839
5023
  if (schedule.timer) {
4840
5024
  clearTimeout(schedule.timer);
@@ -4849,9 +5033,11 @@ var init_src4 = __esm({
4849
5033
  this.#assertStarted();
4850
5034
  await this.#stopImExtensions();
4851
5035
  await this.#startEnabledImExtensions();
5036
+ this.#scheduleIdleShutdownCheck();
4852
5037
  }
4853
5038
  connect(connection, sessionId) {
4854
5039
  this.#assertStarted();
5040
+ this.#clearIdleShutdownTimer();
4855
5041
  connection.sessionId = sessionId;
4856
5042
  this.#connections.add(connection);
4857
5043
  if (sessionId) {
@@ -4876,6 +5062,7 @@ var init_src4 = __esm({
4876
5062
  });
4877
5063
  }
4878
5064
  this.#connections.delete(connection);
5065
+ this.#scheduleIdleShutdownCheck();
4879
5066
  }
4880
5067
  releaseSessionEventBuffer(sessionId) {
4881
5068
  this.#events.delete(sessionId);
@@ -4896,6 +5083,8 @@ var init_src4 = __esm({
4896
5083
  return;
4897
5084
  }
4898
5085
  throw cause;
5086
+ } finally {
5087
+ this.#scheduleIdleShutdownCheck();
4899
5088
  }
4900
5089
  }
4901
5090
  async listDirectories(path) {
@@ -5004,7 +5193,7 @@ var init_src4 = __esm({
5004
5193
  break;
5005
5194
  }
5006
5195
  case "get_memory_settings": {
5007
- this.#respond(connection, message, { memory: await this.#memorySettingsForProject(message.projectId) });
5196
+ this.#respond(connection, message, { memory: await this.#memorySettings(message.projectId) });
5008
5197
  break;
5009
5198
  }
5010
5199
  case "get_memory_status": {
@@ -5015,6 +5204,14 @@ var init_src4 = __esm({
5015
5204
  this.#respond(connection, message, { memory: await this.#handleUpsertMemorySettings(message) });
5016
5205
  break;
5017
5206
  }
5207
+ case "get_runtime_settings": {
5208
+ this.#respond(connection, message, { runtime: await this.#runtimeSettings(message.projectId) });
5209
+ break;
5210
+ }
5211
+ case "upsert_runtime_settings": {
5212
+ this.#respond(connection, message, { runtime: await this.#handleUpsertRuntimeSettings(message) });
5213
+ break;
5214
+ }
5018
5215
  case "get_extension_settings": {
5019
5216
  this.#respond(connection, message, { extension: await this.#extensionSettings(message.extensionId) });
5020
5217
  break;
@@ -5043,6 +5240,39 @@ var init_src4 = __esm({
5043
5240
  break;
5044
5241
  }
5045
5242
  }
5243
+ #scheduleIdleShutdownCheck() {
5244
+ this.#clearIdleShutdownTimer();
5245
+ if (!this.#shouldIdleShutdown()) {
5246
+ return;
5247
+ }
5248
+ this.#idleShutdownTimer = setTimeout(() => {
5249
+ this.#idleShutdownTimer = void 0;
5250
+ if (this.#shouldIdleShutdown()) {
5251
+ this.#onIdleShutdown?.();
5252
+ }
5253
+ }, this.#idleShutdownMs);
5254
+ }
5255
+ #clearIdleShutdownTimer() {
5256
+ if (!this.#idleShutdownTimer) {
5257
+ return;
5258
+ }
5259
+ clearTimeout(this.#idleShutdownTimer);
5260
+ this.#idleShutdownTimer = void 0;
5261
+ }
5262
+ #shouldIdleShutdown() {
5263
+ return this.#started && this.#idleShutdownMs !== void 0 && this.#idleShutdownMs > 0 && this.#connections.size === 0 && this.#imExtensions.size === 0 && !this.#hasActiveWork();
5264
+ }
5265
+ #hasActiveWork() {
5266
+ for (const lane of this.#sessions.values()) {
5267
+ if (lane.runtime.running) {
5268
+ return true;
5269
+ }
5270
+ if (lane.session.tree.controlState.queues.follow_up.length > 0 || lane.session.tree.controlState.queues.steer.length > 0) {
5271
+ return true;
5272
+ }
5273
+ }
5274
+ return false;
5275
+ }
5046
5276
  async #handleCreateSession(connection, request) {
5047
5277
  const sessionId = request.sessionId ?? asSessionId(`ses_${this.#createId()}`);
5048
5278
  const project = await this.#resolveProject(sessionId, request.meta.projectId);
@@ -5059,7 +5289,7 @@ var init_src4 = __esm({
5059
5289
  try {
5060
5290
  lane = await this.#createLane(sessionId, request.meta, project);
5061
5291
  } catch (cause) {
5062
- if (!request.sessionId || !isNodeErrorCode4(cause, "EEXIST")) {
5292
+ if (!request.sessionId || !isNodeErrorCode5(cause, "EEXIST")) {
5063
5293
  throw cause;
5064
5294
  }
5065
5295
  lane = await this.#getLane(sessionId);
@@ -5578,6 +5808,18 @@ var init_src4 = __esm({
5578
5808
  ]
5579
5809
  }
5580
5810
  });
5811
+ const rtkSavings = rtkSavingsFromToolResult(rawEvent.result);
5812
+ if (rtkSavings) {
5813
+ await this.#recordRtkSavings({
5814
+ projectId: lane.project.projectId,
5815
+ sessionId: lane.session.header.sessionId,
5816
+ savings: rtkSavings
5817
+ }).catch(
5818
+ (cause) => this.#appendDiagnostic(lane.session.header.sessionId, "runtime_stats_update_failed", {
5819
+ message: cause instanceof Error ? cause.message : String(cause)
5820
+ })
5821
+ );
5822
+ }
5581
5823
  state.parentId = toolResultId;
5582
5824
  break;
5583
5825
  }
@@ -6398,7 +6640,7 @@ var init_src4 = __esm({
6398
6640
  await this.#getLane(sessionId);
6399
6641
  return true;
6400
6642
  } catch (cause) {
6401
- if (isNodeErrorCode4(cause, "ENOENT")) {
6643
+ if (isNodeErrorCode5(cause, "ENOENT")) {
6402
6644
  return false;
6403
6645
  }
6404
6646
  throw cause;
@@ -6556,7 +6798,7 @@ var init_src4 = __esm({
6556
6798
  try {
6557
6799
  children = await readdir6(root);
6558
6800
  } catch (cause) {
6559
- if (isNodeErrorCode4(cause, "ENOENT") || isNodeErrorCode4(cause, "ENOTDIR")) {
6801
+ if (isNodeErrorCode5(cause, "ENOENT") || isNodeErrorCode5(cause, "ENOTDIR")) {
6560
6802
  continue;
6561
6803
  }
6562
6804
  throw cause;
@@ -6666,7 +6908,7 @@ var init_src4 = __esm({
6666
6908
  this.#imBindings.set(imBindingKey(binding.extensionId, binding.externalConversationId), binding);
6667
6909
  }
6668
6910
  } catch (cause) {
6669
- if (!isNodeErrorCode4(cause, "ENOENT")) {
6911
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
6670
6912
  throw cause;
6671
6913
  }
6672
6914
  }
@@ -6680,9 +6922,13 @@ var init_src4 = __esm({
6680
6922
  #imBindingsPath() {
6681
6923
  return join10(this.#scorelHomeDir, "channels", "im-bindings.json");
6682
6924
  }
6683
- async #loadUserConfigProfile() {
6925
+ async #loadUserConfigProfile(options = {}) {
6684
6926
  try {
6685
- return await loadScorelConfigProfile({ cwd: this.#userHomeDir, homeDir: this.#userHomeDir });
6927
+ return await loadScorelConfigProfile({
6928
+ cwd: this.#userHomeDir,
6929
+ scorelHomeDir: this.#scorelHomeDir,
6930
+ includeSecrets: options.includeSecrets ?? false
6931
+ });
6686
6932
  } catch (cause) {
6687
6933
  if (isMissingConfigError(cause)) {
6688
6934
  return void 0;
@@ -6690,16 +6936,24 @@ var init_src4 = __esm({
6690
6936
  throw cause;
6691
6937
  }
6692
6938
  }
6939
+ #configWriteTarget() {
6940
+ return {
6941
+ configDir: this.#scorelHomeDir,
6942
+ configPath: join10(this.#scorelHomeDir, "config.toml"),
6943
+ workDir: this.#userHomeDir
6944
+ };
6945
+ }
6693
6946
  async #listModels(projectId) {
6694
6947
  let config;
6695
6948
  try {
6696
- config = await this.#configProfileForProject(projectId);
6949
+ config = projectId ? await this.#configProfileForProject(projectId) : await this.#loadUserConfigProfile();
6697
6950
  } catch (cause) {
6698
6951
  if (!isMissingConfigError(cause)) {
6699
6952
  throw cause;
6700
6953
  }
6701
6954
  config = void 0;
6702
6955
  }
6956
+ config ??= projectId ? void 0 : this.#modelProfile;
6703
6957
  if (!config) {
6704
6958
  return {
6705
6959
  providers: [],
@@ -6722,19 +6976,18 @@ var init_src4 = __esm({
6722
6976
  };
6723
6977
  }
6724
6978
  async #handleUpsertModelProfile(request) {
6725
- const project = await this.#registry.require(request.projectId);
6726
- const configPath = join10(project.workDir, ".scorel", "config.toml");
6979
+ const target = this.#configWriteTarget();
6727
6980
  let existingConfigText;
6728
6981
  try {
6729
- existingConfigText = await readFile11(configPath, "utf8");
6982
+ existingConfigText = await readFile11(target.configPath, "utf8");
6730
6983
  } catch (cause) {
6731
- if (!isNodeErrorCode4(cause, "ENOENT")) {
6984
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
6732
6985
  throw cause;
6733
6986
  }
6734
6987
  }
6735
- await mkdir6(join10(project.workDir, ".scorel"), { recursive: true });
6988
+ await mkdir6(target.configDir, { recursive: true });
6736
6989
  await writeFile6(
6737
- configPath,
6990
+ target.configPath,
6738
6991
  renderModelProfileConfig({
6739
6992
  providerId: request.providerId,
6740
6993
  providerType: request.providerType,
@@ -6761,38 +7014,41 @@ var init_src4 = __esm({
6761
7014
  "utf8"
6762
7015
  );
6763
7016
  await this.#appendHostDiagnostic("model_profile_upserted", {
6764
- projectId: project.projectId,
6765
- workDir: project.workDir,
7017
+ ...request.projectId ? { ignoredProjectId: request.projectId } : {},
7018
+ scope: "device",
7019
+ workDir: target.workDir,
6766
7020
  providerId: request.providerId,
6767
7021
  modelId: request.modelId
6768
7022
  });
6769
- return this.#listModels(project.projectId);
7023
+ return this.#listModels();
6770
7024
  }
6771
7025
  async #handleRemoveModelProvider(request) {
6772
- const project = await this.#registry.require(request.projectId);
6773
- const configPath = join10(project.workDir, ".scorel", "config.toml");
7026
+ const target = this.#configWriteTarget();
6774
7027
  let existingConfigText;
6775
7028
  try {
6776
- existingConfigText = await readFile11(configPath, "utf8");
7029
+ existingConfigText = await readFile11(target.configPath, "utf8");
6777
7030
  } catch (cause) {
6778
- if (!isNodeErrorCode4(cause, "ENOENT")) {
7031
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
6779
7032
  throw cause;
6780
7033
  }
6781
7034
  }
6782
- await mkdir6(join10(project.workDir, ".scorel"), { recursive: true });
7035
+ await mkdir6(target.configDir, { recursive: true });
6783
7036
  await writeFile6(
6784
- configPath,
7037
+ target.configPath,
6785
7038
  renderModelProfileConfig({
6786
7039
  removeProviderId: request.providerId,
6787
7040
  existingConfigText
6788
7041
  }),
6789
7042
  "utf8"
6790
7043
  );
6791
- const profile = await this.#listModels(project.projectId);
7044
+ const profile = await this.#listModels();
6792
7045
  return { ...profile, removed: true };
6793
7046
  }
6794
7047
  async #memorySettingsForProject(projectId) {
6795
- const config = await this.#configProfileForProject(projectId).catch((cause) => {
7048
+ return this.#memorySettings(projectId);
7049
+ }
7050
+ async #memorySettings(projectId) {
7051
+ const config = await (projectId ? this.#configProfileForProject(projectId) : this.#loadUserConfigProfile()).catch((cause) => {
6796
7052
  if (isMissingConfigError(cause)) {
6797
7053
  return void 0;
6798
7054
  }
@@ -6814,19 +7070,18 @@ var init_src4 = __esm({
6814
7070
  }
6815
7071
  }
6816
7072
  async #handleUpsertMemorySettings(request) {
6817
- const project = await this.#registry.require(request.projectId);
6818
- const configPath = join10(project.workDir, ".scorel", "config.toml");
7073
+ const target = this.#configWriteTarget();
6819
7074
  let existingConfigText;
6820
7075
  try {
6821
- existingConfigText = await readFile11(configPath, "utf8");
7076
+ existingConfigText = await readFile11(target.configPath, "utf8");
6822
7077
  } catch (cause) {
6823
- if (!isNodeErrorCode4(cause, "ENOENT")) {
7078
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
6824
7079
  throw cause;
6825
7080
  }
6826
7081
  }
6827
- await mkdir6(join10(project.workDir, ".scorel"), { recursive: true });
7082
+ await mkdir6(target.configDir, { recursive: true });
6828
7083
  await writeFile6(
6829
- configPath,
7084
+ target.configPath,
6830
7085
  renderMemoryConfig({
6831
7086
  enabled: request.enabled,
6832
7087
  daily: request.daily,
@@ -6840,10 +7095,66 @@ var init_src4 = __esm({
6840
7095
  "utf8"
6841
7096
  );
6842
7097
  await this.#appendHostDiagnostic("memory_settings_upserted", {
6843
- projectId: project.projectId,
6844
- workDir: project.workDir
7098
+ ...request.projectId ? { ignoredProjectId: request.projectId } : {},
7099
+ scope: "device",
7100
+ workDir: target.workDir
7101
+ });
7102
+ return this.#memorySettings();
7103
+ }
7104
+ async #runtimeSettingsForProject(projectId, installStatus) {
7105
+ return this.#runtimeSettings(projectId, installStatus);
7106
+ }
7107
+ async #runtimeSettings(projectId, installStatus) {
7108
+ const config = await (projectId ? this.#configProfileForProject(projectId) : this.#loadUserConfigProfile()).catch((cause) => {
7109
+ if (isMissingConfigError(cause)) {
7110
+ return void 0;
7111
+ }
7112
+ throw cause;
7113
+ });
7114
+ const detected = await detectRtk();
7115
+ const savings = await readRuntimeStats(this.#runtimeStatsPath());
7116
+ return {
7117
+ tokenSavingRtk: config?.runtime.tokenSavingRtk ?? false,
7118
+ rtkAvailable: detected.available,
7119
+ ...detected.executable ? { rtkExecutable: detected.executable } : {},
7120
+ ...detected.version ? { rtkVersion: detected.version } : {},
7121
+ ...installStatus?.installStatus ? { installStatus: installStatus.installStatus } : {},
7122
+ ...installStatus?.installMessage ? { installMessage: installStatus.installMessage } : {},
7123
+ estimatedOutputTokens: savings.rtk.outputTokens,
7124
+ estimatedSavedTokens: savings.rtk.savedTokens
7125
+ };
7126
+ }
7127
+ async #handleUpsertRuntimeSettings(request) {
7128
+ const target = this.#configWriteTarget();
7129
+ let existingConfigText;
7130
+ try {
7131
+ existingConfigText = await readFile11(target.configPath, "utf8");
7132
+ } catch (cause) {
7133
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
7134
+ throw cause;
7135
+ }
7136
+ }
7137
+ await mkdir6(target.configDir, { recursive: true });
7138
+ await writeFile6(
7139
+ target.configPath,
7140
+ renderRuntimeConfig({
7141
+ tokenSavingRtk: request.tokenSavingRtk,
7142
+ existingConfigText
7143
+ }),
7144
+ "utf8"
7145
+ );
7146
+ const installResult = request.tokenSavingRtk === true ? await ensureRtkAvailable() : { status: "idle" };
7147
+ await this.#appendHostDiagnostic("runtime_settings_upserted", {
7148
+ ...request.projectId ? { ignoredProjectId: request.projectId } : {},
7149
+ scope: "device",
7150
+ workDir: target.workDir,
7151
+ tokenSavingRtk: request.tokenSavingRtk,
7152
+ installStatus: installResult.status
7153
+ });
7154
+ return this.#runtimeSettings(void 0, {
7155
+ installStatus: installResult.status,
7156
+ ...installResult.message ? { installMessage: installResult.message } : {}
6845
7157
  });
6846
- return this.#memorySettingsForProject(project.projectId);
6847
7158
  }
6848
7159
  async #extensionSettings(extensionId) {
6849
7160
  const config = await this.#loadUserConfigProfile().catch((cause) => {
@@ -6867,7 +7178,7 @@ var init_src4 = __esm({
6867
7178
  try {
6868
7179
  existingConfigText = await readFile11(configPath, "utf8");
6869
7180
  } catch (cause) {
6870
- if (!isNodeErrorCode4(cause, "ENOENT")) {
7181
+ if (!isNodeErrorCode5(cause, "ENOENT")) {
6871
7182
  throw cause;
6872
7183
  }
6873
7184
  }
@@ -6891,8 +7202,11 @@ var init_src4 = __esm({
6891
7202
  return this.#extensionSettings(request.extensionId);
6892
7203
  }
6893
7204
  async #fetchProviderModels(projectId, providerId) {
6894
- const project = await this.#registry.require(projectId);
6895
- const config = await loadScorelConfigProfile({ cwd: project.workDir, includeSecrets: true });
7205
+ const config = projectId ? await loadScorelConfigProfile({
7206
+ cwd: (await this.#registry.require(projectId)).workDir,
7207
+ scorelHomeDir: this.#scorelHomeDir,
7208
+ includeSecrets: true
7209
+ }) : await this.#loadUserConfigProfile({ includeSecrets: true });
6896
7210
  if (!config) {
6897
7211
  throw new Error("Model profile config is not configured");
6898
7212
  }
@@ -6977,7 +7291,7 @@ var init_src4 = __esm({
6977
7291
  }
6978
7292
  const project = await this.#registry.require(projectId);
6979
7293
  try {
6980
- return await loadScorelConfigProfile({ cwd: project.workDir });
7294
+ return await loadScorelConfigProfile({ cwd: project.workDir, scorelHomeDir: this.#scorelHomeDir });
6981
7295
  } catch (cause) {
6982
7296
  if (!isMissingConfigError(cause)) {
6983
7297
  throw cause;
@@ -7018,6 +7332,20 @@ var init_src4 = __esm({
7018
7332
  await appendFile3(join10(this.#sessionsDir, "host.log"), `${line}
7019
7333
  `, "utf8");
7020
7334
  }
7335
+ #runtimeStatsPath() {
7336
+ return join10(this.#scorelHomeDir, "runtime-stats.json");
7337
+ }
7338
+ async #recordRtkSavings(input) {
7339
+ const updateTask = this.#runtimeStatsQueue.then(async () => {
7340
+ const path = this.#runtimeStatsPath();
7341
+ const stats = await readRuntimeStats(path);
7342
+ addRtkSavings(stats, String(input.projectId), String(input.sessionId), input.savings);
7343
+ await writeRuntimeStats(path, stats);
7344
+ });
7345
+ this.#runtimeStatsQueue = updateTask.catch(() => {
7346
+ });
7347
+ await updateTask;
7348
+ }
7021
7349
  async #resolveProject(sessionId, projectId) {
7022
7350
  const project = await this.#registry.require(projectId);
7023
7351
  await this.#appendDiagnostic(sessionId, "project_resolved", {
@@ -7073,7 +7401,7 @@ var init_src4 = __esm({
7073
7401
  }
7074
7402
  };
7075
7403
  };
7076
- isNodeErrorCode4 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
7404
+ isNodeErrorCode5 = (cause, code) => cause instanceof Error && "code" in cause && cause.code === code;
7077
7405
  wireErrorCode = (cause) => {
7078
7406
  if (!(cause instanceof ProjectRegistryError)) {
7079
7407
  return "internal_error";
@@ -7124,7 +7452,7 @@ var init_src4 = __esm({
7124
7452
  return void 0;
7125
7453
  }
7126
7454
  const parsed = JSON.parse(text);
7127
- if (!isRecord7(parsed)) {
7455
+ if (!isRecord8(parsed)) {
7128
7456
  return void 0;
7129
7457
  }
7130
7458
  return {
@@ -7144,6 +7472,147 @@ var init_src4 = __esm({
7144
7472
  dreamIdleMinutes: 60,
7145
7473
  autoCompactThreshold: 0.8
7146
7474
  });
7475
+ detectRtk = async () => {
7476
+ try {
7477
+ const shell = resolveDefaultShell2();
7478
+ const path = (await execFileAsync2(shell, shellCommandArgs2(shell, "command -v rtk"), { timeout: 5e3 })).stdout.trim();
7479
+ if (!path) {
7480
+ return { available: false };
7481
+ }
7482
+ const version = await execFileAsync2(path, ["--version"], { timeout: 5e3 }).then((result) => result.stdout.trim() || result.stderr.trim()).catch(() => void 0);
7483
+ return {
7484
+ available: true,
7485
+ executable: path,
7486
+ ...version ? { version } : {}
7487
+ };
7488
+ } catch {
7489
+ return { available: false };
7490
+ }
7491
+ };
7492
+ ensureRtkAvailable = async () => {
7493
+ const existing = await detectRtk();
7494
+ if (existing.available) {
7495
+ return { status: "installed", message: existing.version ?? existing.executable };
7496
+ }
7497
+ const shell = resolveDefaultShell2();
7498
+ const brew = await execFileAsync2(shell, shellCommandArgs2(shell, "command -v brew"), { timeout: 5e3 }).then((result) => result.stdout.trim()).catch(() => "");
7499
+ if (!brew) {
7500
+ return { status: "failed", message: "Homebrew is not available; install RTK manually with `brew install rtk`." };
7501
+ }
7502
+ try {
7503
+ await execFileAsync2(brew, ["install", "rtk"], { timeout: 12e4, maxBuffer: 2e7 });
7504
+ const installed = await detectRtk();
7505
+ return installed.available ? { status: "installed", message: installed.version ?? installed.executable } : { status: "failed", message: "RTK install finished but `rtk` is still not on PATH." };
7506
+ } catch (cause) {
7507
+ const message = cause instanceof Error ? cause.message : String(cause);
7508
+ return { status: "failed", message };
7509
+ }
7510
+ };
7511
+ emptyRuntimeStats = () => ({
7512
+ version: 1,
7513
+ rtk: {
7514
+ outputTokens: 0,
7515
+ savedTokens: 0,
7516
+ byProject: {},
7517
+ bySession: {}
7518
+ }
7519
+ });
7520
+ readRuntimeStats = async (path) => {
7521
+ try {
7522
+ return parseRuntimeStats(JSON.parse(await readFile11(path, "utf8")));
7523
+ } catch (cause) {
7524
+ if (isNodeErrorCode5(cause, "ENOENT")) {
7525
+ return emptyRuntimeStats();
7526
+ }
7527
+ return emptyRuntimeStats();
7528
+ }
7529
+ };
7530
+ writeRuntimeStats = async (path, stats) => {
7531
+ await mkdir6(dirname8(path), { recursive: true });
7532
+ const tempPath = join10(dirname8(path), `.runtime-stats-${process.pid}-${Date.now()}.tmp`);
7533
+ try {
7534
+ await writeFile6(tempPath, `${JSON.stringify(stats, null, 2)}
7535
+ `, "utf8");
7536
+ await rename3(tempPath, path);
7537
+ } catch (cause) {
7538
+ await rm2(tempPath, { force: true }).catch(() => void 0);
7539
+ throw cause;
7540
+ }
7541
+ };
7542
+ parseRuntimeStats = (value) => {
7543
+ if (!isRecord8(value) || !isRecord8(value.rtk)) {
7544
+ return emptyRuntimeStats();
7545
+ }
7546
+ return {
7547
+ version: 1,
7548
+ rtk: {
7549
+ outputTokens: nonNegativeInteger2(value.rtk.outputTokens),
7550
+ savedTokens: nonNegativeInteger2(value.rtk.savedTokens),
7551
+ byProject: parseRuntimeStatsBuckets(value.rtk.byProject),
7552
+ bySession: parseRuntimeStatsBuckets(value.rtk.bySession)
7553
+ }
7554
+ };
7555
+ };
7556
+ parseRuntimeStatsBuckets = (value) => {
7557
+ if (!isRecord8(value)) {
7558
+ return {};
7559
+ }
7560
+ return Object.fromEntries(
7561
+ Object.entries(value).map(([key, bucket]) => [
7562
+ key,
7563
+ isRecord8(bucket) ? {
7564
+ outputTokens: nonNegativeInteger2(bucket.outputTokens),
7565
+ savedTokens: nonNegativeInteger2(bucket.savedTokens)
7566
+ } : { outputTokens: 0, savedTokens: 0 }
7567
+ ])
7568
+ );
7569
+ };
7570
+ addRtkSavings = (stats, projectId, sessionId, savings) => {
7571
+ addRuntimeStatsBucket(stats.rtk, savings);
7572
+ stats.rtk.byProject[projectId] = addRuntimeStatsBucket(stats.rtk.byProject[projectId] ?? { outputTokens: 0, savedTokens: 0 }, savings);
7573
+ stats.rtk.bySession[sessionId] = addRuntimeStatsBucket(stats.rtk.bySession[sessionId] ?? { outputTokens: 0, savedTokens: 0 }, savings);
7574
+ };
7575
+ addRuntimeStatsBucket = (bucket, savings) => {
7576
+ bucket.outputTokens += savings.outputTokens;
7577
+ bucket.savedTokens += savings.savedTokens;
7578
+ return bucket;
7579
+ };
7580
+ rtkSavingsFromToolResult = (result) => {
7581
+ if (!isRecord8(result) || !isRecord8(result.details)) {
7582
+ return void 0;
7583
+ }
7584
+ const rtk = result.details.rtk;
7585
+ if (!isRecord8(rtk) || rtk.applied !== true) {
7586
+ return void 0;
7587
+ }
7588
+ const outputTokens = nonNegativeInteger2(rtk.estimatedOutputTokens);
7589
+ const savedTokens = nonNegativeInteger2(rtk.estimatedSavedTokens);
7590
+ return outputTokens > 0 || savedTokens > 0 ? { outputTokens, savedTokens } : void 0;
7591
+ };
7592
+ nonNegativeInteger2 = (value) => {
7593
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
7594
+ return 0;
7595
+ }
7596
+ return Math.floor(value);
7597
+ };
7598
+ resolveDefaultShell2 = () => {
7599
+ const shell = process.env.SHELL || userShell2() || "/bin/sh";
7600
+ return shell.trim() || "/bin/sh";
7601
+ };
7602
+ shellCommandArgs2 = (shell, command) => {
7603
+ const name = basename3(shell).toLowerCase();
7604
+ if (name === "csh" || name === "tcsh" || name === "fish") {
7605
+ return ["-c", command];
7606
+ }
7607
+ return ["-lc", command];
7608
+ };
7609
+ userShell2 = () => {
7610
+ try {
7611
+ return userInfo2().shell ?? void 0;
7612
+ } catch {
7613
+ return void 0;
7614
+ }
7615
+ };
7147
7616
  runtimeChannelContextFromWire = (context) => ({
7148
7617
  extensionId: context.channel,
7149
7618
  channel: context.channel,
@@ -7158,7 +7627,7 @@ var init_src4 = __esm({
7158
7627
  ...context.data ? { data: context.data } : {}
7159
7628
  });
7160
7629
  parseQueuedChannelContext = (value) => {
7161
- if (!isRecord7(value)) {
7630
+ if (!isRecord8(value)) {
7162
7631
  return void 0;
7163
7632
  }
7164
7633
  if (typeof value.channel !== "string" || typeof value.externalConversationId !== "string") {
@@ -7170,7 +7639,7 @@ var init_src4 = __esm({
7170
7639
  ...typeof value.conversationType === "string" ? { conversationType: value.conversationType } : {},
7171
7640
  ...typeof value.senderDisplayName === "string" ? { senderDisplayName: value.senderDisplayName } : {},
7172
7641
  ...typeof value.mentionedBot === "boolean" ? { mentionedBot: value.mentionedBot } : {},
7173
- ...isRecord7(value.data) ? { data: value.data } : {}
7642
+ ...isRecord8(value.data) ? { data: value.data } : {}
7174
7643
  });
7175
7644
  };
7176
7645
  imBindingKey = (extensionId, externalConversationId) => `${extensionId}:${externalConversationId}`;
@@ -7203,7 +7672,7 @@ var init_src4 = __esm({
7203
7672
  };
7204
7673
  isSteerMessage = (text) => /^\/(?:steer|interrupt)\b/i.test(text.trim());
7205
7674
  stripImCommandPrefix = (text) => text.trim().replace(/^\/(?:steer|interrupt)\s*/i, "").trim() || text;
7206
- isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7675
+ isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7207
7676
  parseMemoryUpdate = (raw) => {
7208
7677
  const text = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim();
7209
7678
  if (!text) {
@@ -7310,9 +7779,11 @@ var init_relay_cli = __esm({
7310
7779
 
7311
7780
  // apps/cli/src/daemon-cli.ts
7312
7781
  import { randomUUID as randomUUID4 } from "node:crypto";
7782
+ import { spawn } from "node:child_process";
7313
7783
  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;
7784
+ import { dirname as dirname9, join as join12 } from "node:path";
7785
+ import { fileURLToPath } from "node:url";
7786
+ var DEFAULT_HOST, DEFAULT_PORT, STOP_POLL_INTERVAL_MS, STOP_GRACE_MS, START_READY_TIMEOUT_MS, DEFAULT_IDLE_SHUTDOWN_MS, defaultStateDir2, isLoopbackHost, formatTimestamp, runCliDaemon, runStartCommand, runServeCommand, stopRunningDaemon, runStatusCommand, runStopCommand, runResetCommand, formatStatusLine, parseServeFlags, parseStatusFlags, requireValue2, sleep, waitForDaemonReady, detachBackgroundDaemon, nodeEntrypointArgs, writeDaemonUsage;
7316
7787
  var init_daemon_cli = __esm({
7317
7788
  "apps/cli/src/daemon-cli.ts"() {
7318
7789
  "use strict";
@@ -7322,6 +7793,8 @@ var init_daemon_cli = __esm({
7322
7793
  DEFAULT_PORT = 7777;
7323
7794
  STOP_POLL_INTERVAL_MS = 200;
7324
7795
  STOP_GRACE_MS = 5e3;
7796
+ START_READY_TIMEOUT_MS = 1e4;
7797
+ DEFAULT_IDLE_SHUTDOWN_MS = 15 * 60 * 1e3;
7325
7798
  defaultStateDir2 = () => join12(homedir6(), ".scorel");
7326
7799
  isLoopbackHost = (host) => host === "127.0.0.1" || host === "::1" || host === "localhost";
7327
7800
  formatTimestamp = (epochMs) => new Date(epochMs).toISOString();
@@ -7329,6 +7802,8 @@ var init_daemon_cli = __esm({
7329
7802
  const [command, ...rest] = argv;
7330
7803
  const stateDir = options.stateDir ?? defaultStateDir2();
7331
7804
  switch (command) {
7805
+ case "start":
7806
+ return runStartCommand(rest, { ...options, stateDir });
7332
7807
  case "serve":
7333
7808
  return runServeCommand(rest, { ...options, stateDir });
7334
7809
  case "status":
@@ -7346,6 +7821,63 @@ var init_daemon_cli = __esm({
7346
7821
  return 1;
7347
7822
  }
7348
7823
  };
7824
+ runStartCommand = async (argv, options) => {
7825
+ let flags;
7826
+ try {
7827
+ flags = parseServeFlags(argv, options.cwd ?? process.cwd(), options.env ?? process.env);
7828
+ } catch (cause) {
7829
+ options.error.write(`scorel daemon start error: ${cause.message}
7830
+ `);
7831
+ return 1;
7832
+ }
7833
+ const readState = options.readState ?? ((stateDir) => readLocalDaemonState({ stateDir }));
7834
+ const existing = await readState(options.stateDir);
7835
+ if (existing && daemonStateLiveness(existing) === "running") {
7836
+ options.output.write(`scorel host already running url=${existing.wsUrl} pid=${existing.pid}
7837
+ `);
7838
+ return 0;
7839
+ }
7840
+ const cliEntrypoint = options.cliEntrypoint ?? fileURLToPath(import.meta.url).replace(/daemon-cli\.ts$/, "index.ts");
7841
+ const child = (options.spawn ?? spawn)(process.execPath, [
7842
+ ...nodeEntrypointArgs(cliEntrypoint),
7843
+ "host",
7844
+ "serve",
7845
+ "--host",
7846
+ flags.host,
7847
+ "--port",
7848
+ String(flags.port),
7849
+ "--cwd",
7850
+ flags.cwd,
7851
+ "--idle-timeout-ms",
7852
+ String(flags.idleShutdownMs),
7853
+ ...flags.token ? ["--token", flags.token] : [],
7854
+ ...flags.relayUrl ? ["--relay", flags.relayUrl] : ["--no-relay"],
7855
+ ...flags.replace ? ["--replace"] : []
7856
+ ], {
7857
+ cwd: dirname9(cliEntrypoint),
7858
+ env: { ...process.env, ...options.env ?? {} },
7859
+ detached: true,
7860
+ stdio: ["ignore", "pipe", "pipe"]
7861
+ });
7862
+ try {
7863
+ await waitForDaemonReady(child, options.daemonReadyTimeoutMs ?? START_READY_TIMEOUT_MS);
7864
+ } catch (cause) {
7865
+ options.error.write(`scorel daemon start error: ${cause.message}
7866
+ `);
7867
+ child.kill("SIGTERM");
7868
+ return 1;
7869
+ }
7870
+ const state = await readState(options.stateDir);
7871
+ if (!state || daemonStateLiveness(state) !== "running") {
7872
+ options.error.write("scorel daemon start error: daemon state missing after start\n");
7873
+ child.kill("SIGTERM");
7874
+ return 1;
7875
+ }
7876
+ detachBackgroundDaemon(child);
7877
+ options.output.write(`scorel host started url=${state.wsUrl} pid=${state.pid}
7878
+ `);
7879
+ return 0;
7880
+ };
7349
7881
  runServeCommand = async (argv, options) => {
7350
7882
  let flags;
7351
7883
  try {
@@ -7373,16 +7905,28 @@ Use --replace to stop it and start a new one.
7373
7905
  }
7374
7906
  const token = flags.token ?? existing?.token ?? randomUUID4();
7375
7907
  const identity = await loadOrCreateHostDeviceIdentity({ stateDir: options.stateDir });
7908
+ const configScope = { scorelHomeDir: options.stateDir };
7909
+ let signalReason = "natural";
7910
+ let resolveStopWaiter;
7911
+ let stopRequested = false;
7912
+ const requestStop = (reason) => {
7913
+ signalReason = reason;
7914
+ stopRequested = true;
7915
+ resolveStopWaiter?.();
7916
+ };
7376
7917
  const daemon = new ScorelHost({
7377
7918
  sessionsDir: options.sessionsDir ?? scorelSessionsDir(homedir6()),
7378
7919
  projectsPath: join12(options.stateDir, "projects.json"),
7379
7920
  deviceId: identity.deviceId,
7380
7921
  deviceDisplayName: identity.displayName,
7381
- loadConfig: async ({ project }) => loadScorelConfig({ cwd: project.workDir }),
7382
- loadConfigProfile: async ({ project }) => loadScorelConfigProfile({ cwd: project.workDir }),
7922
+ idleShutdownMs: flags.idleShutdownMs,
7923
+ onIdleShutdown: () => requestStop("idle"),
7924
+ scorelHomeDir: options.stateDir,
7925
+ loadConfig: async ({ project }) => loadScorelConfig({ cwd: project.workDir, ...configScope }),
7926
+ loadConfigProfile: async ({ project }) => loadScorelConfigProfile({ cwd: project.workDir, ...configScope }),
7383
7927
  createRuntime: async ({ project, selectedModel, purpose }) => createRealRuntime({
7384
7928
  cwd: project.workDir,
7385
- config: await loadScorelConfig({ cwd: project.workDir }),
7929
+ config: await loadScorelConfig({ cwd: project.workDir, ...configScope }),
7386
7930
  modelSelection: selectedModel ? { modelId: selectedModel.modelId, role: selectedModel.role } : void 0,
7387
7931
  includeTools: purpose === "chat"
7388
7932
  })
@@ -7441,20 +7985,22 @@ Use --replace to stop it and start a new one.
7441
7985
  await markDaemonStopped({ stateDir: options.stateDir, stoppedAt: Date.now() });
7442
7986
  }
7443
7987
  };
7444
- let signalReason = "natural";
7445
7988
  const signalHandlers = /* @__PURE__ */ new Map();
7446
7989
  const stopWaiter = new Promise((resolve7) => {
7990
+ resolveStopWaiter = resolve7;
7991
+ if (stopRequested) {
7992
+ resolve7();
7993
+ return;
7994
+ }
7447
7995
  if (options.serveSignal) {
7448
7996
  if (options.serveSignal.aborted) {
7449
- signalReason = "abort";
7450
- resolve7();
7997
+ requestStop("abort");
7451
7998
  return;
7452
7999
  }
7453
8000
  options.serveSignal.addEventListener(
7454
8001
  "abort",
7455
8002
  () => {
7456
- signalReason = "abort";
7457
- resolve7();
8003
+ requestStop("abort");
7458
8004
  },
7459
8005
  { once: true }
7460
8006
  );
@@ -7462,8 +8008,7 @@ Use --replace to stop it and start a new one.
7462
8008
  }
7463
8009
  const installSignal = (signal) => {
7464
8010
  const handler = () => {
7465
- signalReason = signal;
7466
- resolve7();
8011
+ requestStop(signal);
7467
8012
  };
7468
8013
  signalHandlers.set(signal, handler);
7469
8014
  process.once(signal, handler);
@@ -7593,6 +8138,7 @@ Use --replace to stop it and start a new one.
7593
8138
  let token;
7594
8139
  let relayUrl = resolveDefaultRelayUrl(env);
7595
8140
  let replace = false;
8141
+ let idleShutdownMs = DEFAULT_IDLE_SHUTDOWN_MS;
7596
8142
  for (let index = 0; index < argv.length; index += 1) {
7597
8143
  const arg = argv[index];
7598
8144
  if (arg === "--host") {
@@ -7636,9 +8182,17 @@ Use --replace to stop it and start a new one.
7636
8182
  replace = true;
7637
8183
  continue;
7638
8184
  }
8185
+ if (arg === "--idle-timeout-ms") {
8186
+ idleShutdownMs = Number(requireValue2(argv, index, "--idle-timeout-ms"));
8187
+ if (!Number.isInteger(idleShutdownMs) || idleShutdownMs < 0) {
8188
+ throw new Error("--idle-timeout-ms must be a non-negative integer");
8189
+ }
8190
+ index += 1;
8191
+ continue;
8192
+ }
7639
8193
  throw new Error(`Unknown serve option: ${arg}`);
7640
8194
  }
7641
- return { host, port, token, cwd, relayUrl, replace };
8195
+ return { host, port, token, cwd, relayUrl, replace, idleShutdownMs };
7642
8196
  };
7643
8197
  parseStatusFlags = (argv) => {
7644
8198
  let showToken = false;
@@ -7661,11 +8215,66 @@ Use --replace to stop it and start a new one.
7661
8215
  sleep = (ms) => new Promise((resolve7) => {
7662
8216
  setTimeout(resolve7, ms);
7663
8217
  });
8218
+ waitForDaemonReady = (child, timeoutMs) => new Promise((resolveReady, rejectReady) => {
8219
+ if (!child.stdout) {
8220
+ rejectReady(new Error("daemon child has no stdout stream"));
8221
+ return;
8222
+ }
8223
+ let buffer = "";
8224
+ let stderrBuffer = "";
8225
+ let settled = false;
8226
+ const timer = setTimeout(() => {
8227
+ if (settled) return;
8228
+ settled = true;
8229
+ cleanup();
8230
+ rejectReady(new Error("timed out waiting for daemon ready line"));
8231
+ }, timeoutMs);
8232
+ const onData = (chunk) => {
8233
+ buffer += chunk.toString();
8234
+ if (!buffer.includes("\n")) return;
8235
+ if (buffer.includes("scorel daemon serving url=") || buffer.includes("scorel host serving url=")) {
8236
+ if (settled) return;
8237
+ settled = true;
8238
+ cleanup();
8239
+ resolveReady();
8240
+ }
8241
+ const newlineIndex = buffer.lastIndexOf("\n");
8242
+ buffer = newlineIndex >= 0 ? buffer.slice(newlineIndex + 1) : buffer;
8243
+ };
8244
+ const onStderr = (chunk) => {
8245
+ stderrBuffer += chunk.toString();
8246
+ };
8247
+ const onExit = (code) => {
8248
+ if (settled) return;
8249
+ settled = true;
8250
+ cleanup();
8251
+ const trimmed = stderrBuffer.trim();
8252
+ const detail = trimmed ? `: ${trimmed}` : "";
8253
+ rejectReady(new Error(`daemon exited before ready code=${code}${detail}`));
8254
+ };
8255
+ const cleanup = () => {
8256
+ clearTimeout(timer);
8257
+ child.stdout?.off("data", onData);
8258
+ child.stderr?.off("data", onStderr);
8259
+ child.off("exit", onExit);
8260
+ };
8261
+ child.stdout.on("data", onData);
8262
+ child.stderr?.on("data", onStderr);
8263
+ child.once("exit", onExit);
8264
+ });
8265
+ detachBackgroundDaemon = (child) => {
8266
+ child.stdout?.destroy();
8267
+ child.stderr?.destroy();
8268
+ child.unref();
8269
+ };
8270
+ nodeEntrypointArgs = (entrypoint) => entrypoint.endsWith(".ts") ? ["--import", "tsx", entrypoint] : [entrypoint];
7664
8271
  writeDaemonUsage = (output) => {
7665
8272
  output.write(
7666
8273
  [
7667
8274
  "Usage: scorel host serve [--host <h>] [--port <p>] [--token <t>] [--project <dir>]",
7668
- " [--relay <relay-url> | --no-relay] [--replace]",
8275
+ " [--relay <relay-url> | --no-relay] [--replace] [--idle-timeout-ms <ms>]",
8276
+ " scorel host start [--host <h>] [--port <p>] [--token <t>] [--project <dir>]",
8277
+ " [--relay <relay-url> | --no-relay] [--replace] [--idle-timeout-ms <ms>]",
7669
8278
  " scorel host status [--show-token]",
7670
8279
  " scorel host stop",
7671
8280
  " scorel host reset",
@@ -8289,11 +8898,11 @@ var init_relay_server_cli = __esm({
8289
8898
  });
8290
8899
 
8291
8900
  // apps/cli/src/up-cli.ts
8292
- import { spawn } from "node:child_process";
8901
+ import { spawn as spawn2 } from "node:child_process";
8293
8902
  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;
8903
+ import { dirname as dirname10, join as join15 } from "node:path";
8904
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
8905
+ var DEFAULT_DAEMON_PORT, DEFAULT_WEBUI_PORT, DEFAULT_DAEMON_READY_TIMEOUT_MS, defaultStateDir3, defaultAttachSigint, runCliUp, parseUpFlags, requireValue4, waitForDaemonReady2, pipeWithPrefix, detachBackgroundDaemon2, nodeEntrypointArgs2, pipeStreamLines, once;
8297
8906
  var init_up_cli = __esm({
8298
8907
  "apps/cli/src/up-cli.ts"() {
8299
8908
  "use strict";
@@ -8316,8 +8925,8 @@ var init_up_cli = __esm({
8316
8925
  return 1;
8317
8926
  }
8318
8927
  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;
8928
+ const cliEntrypoint = options.cliEntrypoint ?? fileURLToPath2(import.meta.url).replace(/up-cli\.ts$/, "index.ts");
8929
+ const spawnFn = options.spawn ?? spawn2;
8321
8930
  const readState = options.readState ?? ((dir) => readLocalDaemonState({ stateDir: dir }));
8322
8931
  const attachSigint = options.attachSigint ?? defaultAttachSigint;
8323
8932
  const readyTimeout = options.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
@@ -8328,9 +8937,7 @@ var init_up_cli = __esm({
8328
8937
  let daemonState = existingState;
8329
8938
  if (!reuseDaemon) {
8330
8939
  const daemonArgs = [
8331
- "--import",
8332
- "tsx",
8333
- cliEntrypoint,
8940
+ ...nodeEntrypointArgs2(cliEntrypoint),
8334
8941
  "daemon",
8335
8942
  "serve",
8336
8943
  "--port",
@@ -8340,19 +8947,19 @@ var init_up_cli = __esm({
8340
8947
  "--no-relay"
8341
8948
  ];
8342
8949
  daemonChild = spawnFn(process.execPath, daemonArgs, {
8343
- cwd: flags.cwd,
8950
+ cwd: dirname10(cliEntrypoint),
8344
8951
  env: { ...process.env },
8952
+ detached: true,
8345
8953
  stdio: ["ignore", "pipe", "pipe"]
8346
8954
  });
8347
8955
  try {
8348
- await waitForDaemonReady(daemonChild, readyTimeout);
8956
+ await waitForDaemonReady2(daemonChild, readyTimeout);
8349
8957
  } catch (cause) {
8350
8958
  options.error.write(`scorel up error: ${cause.message}
8351
8959
  `);
8352
8960
  daemonChild.kill("SIGTERM");
8353
8961
  return 1;
8354
8962
  }
8355
- pipeWithPrefix(daemonChild, "[daemon]", options.output, options.error);
8356
8963
  daemonState = await readState(stateDir);
8357
8964
  }
8358
8965
  if (!daemonState) {
@@ -8360,16 +8967,17 @@ var init_up_cli = __esm({
8360
8967
  daemonChild?.kill("SIGTERM");
8361
8968
  return 1;
8362
8969
  }
8970
+ if (daemonChild) {
8971
+ detachBackgroundDaemon2(daemonChild);
8972
+ }
8363
8973
  const webuiArgs = [
8364
- "--import",
8365
- "tsx",
8366
- cliEntrypoint,
8974
+ ...nodeEntrypointArgs2(cliEntrypoint),
8367
8975
  "webui",
8368
8976
  "--port",
8369
8977
  String(flags.webuiPort)
8370
8978
  ];
8371
8979
  const webuiChild = spawnFn(process.execPath, webuiArgs, {
8372
- cwd: flags.cwd,
8980
+ cwd: dirname10(cliEntrypoint),
8373
8981
  env: { ...process.env },
8374
8982
  stdio: ["ignore", "pipe", "pipe"]
8375
8983
  });
@@ -8386,33 +8994,21 @@ var init_up_cli = __esm({
8386
8994
  return;
8387
8995
  }
8388
8996
  shuttingDown = true;
8389
- daemonChild?.kill("SIGTERM");
8390
8997
  webuiChild.kill("SIGTERM");
8391
8998
  });
8392
- const daemonExit = daemonChild ? once(daemonChild) : Promise.resolve(0);
8393
8999
  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
9000
  const webuiDeathWatcher = webuiExit.then((code) => {
8404
9001
  if (!shuttingDown) {
8405
9002
  shuttingDown = true;
8406
9003
  options.error.write(`scorel up webui exited code=${code}
8407
9004
  `);
8408
- daemonChild?.kill("SIGTERM");
8409
9005
  }
8410
9006
  return code;
8411
9007
  });
8412
- const [daemonCode, webuiCode] = await Promise.all([daemonDeathWatcher, webuiDeathWatcher]);
9008
+ const webuiCode = await webuiDeathWatcher;
8413
9009
  detachSigint();
8414
9010
  options.output.write("scorel up stopped\n");
8415
- return daemonCode === 0 && webuiCode === 0 ? 0 : 1;
9011
+ return webuiCode === 0 ? 0 : 1;
8416
9012
  };
8417
9013
  parseUpFlags = (argv, defaultCwd) => {
8418
9014
  let daemonPort = DEFAULT_DAEMON_PORT;
@@ -8452,7 +9048,7 @@ var init_up_cli = __esm({
8452
9048
  }
8453
9049
  return value;
8454
9050
  };
8455
- waitForDaemonReady = (child, timeoutMs) => new Promise((resolveReady, rejectReady) => {
9051
+ waitForDaemonReady2 = (child, timeoutMs) => new Promise((resolveReady, rejectReady) => {
8456
9052
  if (!child.stdout) {
8457
9053
  rejectReady(new Error("daemon child has no stdout stream"));
8458
9054
  return;
@@ -8509,6 +9105,12 @@ var init_up_cli = __esm({
8509
9105
  pipeStreamLines(child.stderr, prefix, error);
8510
9106
  }
8511
9107
  };
9108
+ detachBackgroundDaemon2 = (child) => {
9109
+ child.stdout?.destroy();
9110
+ child.stderr?.destroy();
9111
+ child.unref();
9112
+ };
9113
+ nodeEntrypointArgs2 = (entrypoint) => entrypoint.endsWith(".ts") ? ["--import", "tsx", entrypoint] : [entrypoint];
8512
9114
  pipeStreamLines = (stream, prefix, destination) => {
8513
9115
  let buffer = "";
8514
9116
  stream.setEncoding?.("utf8");
@@ -8538,10 +9140,10 @@ var init_up_cli = __esm({
8538
9140
  });
8539
9141
 
8540
9142
  // apps/cli/src/webui-cli.ts
8541
- import { spawn as spawn2 } from "node:child_process";
9143
+ import { spawn as spawn3 } from "node:child_process";
8542
9144
  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";
9145
+ import { dirname as dirname11, resolve as resolve6 } from "node:path";
9146
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
8545
9147
  var DEFAULT_PORT3, DEFAULT_HOST3, runCliWebUi, findWebuiAppDir, buildWebUiSpawnPlan, parseWebUiFlags, requireValue5, waitForChildExit;
8546
9148
  var init_webui_cli = __esm({
8547
9149
  "apps/cli/src/webui-cli.ts"() {
@@ -8563,7 +9165,7 @@ var init_webui_cli = __esm({
8563
9165
  return 1;
8564
9166
  }
8565
9167
  const plan = buildWebUiSpawnPlan(flags, webuiAppDir);
8566
- const spawnFn = options.spawn ?? spawn2;
9168
+ const spawnFn = options.spawn ?? spawn3;
8567
9169
  const child = spawnFn(plan.command, plan.argv, {
8568
9170
  cwd: plan.cwd,
8569
9171
  env: plan.env,
@@ -8572,7 +9174,7 @@ var init_webui_cli = __esm({
8572
9174
  return await waitForChildExit(child, options);
8573
9175
  };
8574
9176
  findWebuiAppDir = () => {
8575
- let cursor = dirname9(fileURLToPath2(import.meta.url));
9177
+ let cursor = dirname11(fileURLToPath3(import.meta.url));
8576
9178
  for (let depth = 0; depth < 8; depth += 1) {
8577
9179
  const candidate = resolve6(cursor, "apps/webui/package.json");
8578
9180
  if (existsSync4(candidate)) {
@@ -8664,8 +9266,8 @@ import { createHash as createHash3 } from "node:crypto";
8664
9266
  import { appendFile as appendFile4, mkdir as mkdir8, readFile as readFile13, realpath as realpath3, readdir as readdir7, writeFile as writeFile8 } from "node:fs/promises";
8665
9267
  import { createInterface } from "node:readline/promises";
8666
9268
  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";
9269
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
9270
+ import { basename as basename4, dirname as dirname12, join as join16 } from "node:path";
8669
9271
  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
9272
  var init_index = __esm({
8671
9273
  async "apps/cli/src/index.ts"() {
@@ -8998,7 +9600,7 @@ var init_index = __esm({
8998
9600
  if (!sessionsDir) {
8999
9601
  return defaultStateDir4();
9000
9602
  }
9001
- return basename2(sessionsDir) === "sessions" ? dirname10(sessionsDir) : sessionsDir;
9603
+ return basename4(sessionsDir) === "sessions" ? dirname12(sessionsDir) : sessionsDir;
9002
9604
  };
9003
9605
  AttachDiagnostics = class {
9004
9606
  #stateDir;
@@ -9050,7 +9652,7 @@ var init_index = __esm({
9050
9652
  }
9051
9653
  const filePath = attachDiagnosticsFilePath(this.#stateDir, this.#scope, this.#sessionId);
9052
9654
  this.#writes.push(
9053
- mkdir8(dirname10(filePath), { recursive: true }).then(() => appendFile4(filePath, `${line}
9655
+ mkdir8(dirname12(filePath), { recursive: true }).then(() => appendFile4(filePath, `${line}
9054
9656
  `, "utf8"))
9055
9657
  );
9056
9658
  }
@@ -9080,7 +9682,7 @@ var init_index = __esm({
9080
9682
  const filePath = attachCacheFilePath(stateDir, scope, sessionId);
9081
9683
  const uniqueEvents = mergePersistentEvents(snapshot.events);
9082
9684
  const transients = removeCompletedTransients(snapshot.transients, uniqueEvents);
9083
- await mkdir8(dirname10(filePath), { recursive: true });
9685
+ await mkdir8(dirname12(filePath), { recursive: true });
9084
9686
  await writeFile8(
9085
9687
  filePath,
9086
9688
  `${JSON.stringify({ version: 1, scope, sessionId: String(sessionId), events: uniqueEvents, transients }, null, 2)}
@@ -9217,12 +9819,14 @@ var init_index = __esm({
9217
9819
  return { sessionId, tail, attach, remoteUrl };
9218
9820
  };
9219
9821
  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 });
9822
+ const configScope = { scorelHomeDir: options.stateDir };
9823
+ const loadProjectConfig = async (project2) => options.config ?? await loadScorelConfig({ cwd: project2.workDir, ...configScope });
9824
+ const loadProjectConfigProfile = async (project2) => options.config ?? await loadScorelConfigProfile({ cwd: project2.workDir, ...configScope });
9222
9825
  const daemon = new ScorelHost({
9223
9826
  sessionsDir: options.sessionsDir,
9224
9827
  projectsPath: join16(options.stateDir, "projects.json"),
9225
9828
  deviceId: asDeviceId("device_local"),
9829
+ scorelHomeDir: options.stateDir,
9226
9830
  loadConfig: async ({ project: project2 }) => loadProjectConfig(project2),
9227
9831
  loadConfigProfile: async ({ project: project2 }) => loadProjectConfigProfile(project2),
9228
9832
  createRuntime: async ({ project: project2, selectedModel, purpose }) => createRealRuntime({
@@ -9356,8 +9960,10 @@ var init_index = __esm({
9356
9960
  "Usage: scorel chat [--session <id>] [--cwd <dir>]",
9357
9961
  " scorel [--session <id>] [--cwd <dir>]",
9358
9962
  " scorel attach --session <id> --remote <ws-url> --token <token>",
9963
+ " scorel host start [--host <h>] [--port <p>] [--token <t>] [--project <dir>]",
9964
+ " [--relay <relay-url> | --no-relay] [--replace] [--idle-timeout-ms <ms>]",
9359
9965
  " scorel host serve [--host <h>] [--port <p>] [--token <t>] [--project <dir>]",
9360
- " [--relay <relay-url> | --no-relay] [--replace]",
9966
+ " [--relay <relay-url> | --no-relay] [--replace] [--idle-timeout-ms <ms>]",
9361
9967
  " scorel host status [--show-token]",
9362
9968
  " scorel host stop",
9363
9969
  " scorel host reset",
@@ -9496,7 +10102,7 @@ ${text}
9496
10102
  if (!process.argv[1]) return false;
9497
10103
  const [argvPath, modulePath] = await Promise.all([
9498
10104
  realpath3(process.argv[1]).catch(() => process.argv[1]),
9499
- realpath3(fileURLToPath3(import.meta.url)).catch(() => fileURLToPath3(import.meta.url))
10105
+ realpath3(fileURLToPath4(import.meta.url)).catch(() => fileURLToPath4(import.meta.url))
9500
10106
  ]);
9501
10107
  return argvPath === modulePath;
9502
10108
  };