@chanlerdev/scorel 0.0.4 → 0.0.6

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
@@ -1746,7 +1746,8 @@ import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm, stat as
1746
1746
  import { userInfo } from "node:os";
1747
1747
  import { basename as basename2, dirname as dirname3, extname, isAbsolute, relative, resolve } from "node:path";
1748
1748
  import { promisify } from "node:util";
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;
1749
+ import { Type } from "@mariozechner/pi-ai";
1750
+ var execFileAsync, DEFAULT_SEARCH_LIMIT, DEFAULT_GREP_LIMIT, DEFAULT_READ_LIMIT, DEFAULT_CONTEXT_WINDOW, READ_TOKEN_BUDGET_RATIO, FULL_READ_TOKEN_BUDGET_RATIO, createCodingTools, parseReadArgs, parseWriteArgs, parseEditArgs, parseBashArgs, parseGlobArgs, parseGrepArgs, parseTodoWriteArgs, parseTodoItem, expectRecord, expectPath, expectString, optionalString, optionalNumber, optionalBoolean, snapshotFile, sameSnapshot, exists, isWithin, linesOf, IMAGE_EXTENSIONS, DOCUMENT_EXTENSIONS, BINARY_EXTENSIONS, assertReadableFileKind, assertTextBuffer, selectCompleteLinesWithinBudget, estimateTokens, renderReadLines, readTokenBudget, completeRanges, hasCompleteCoverage, mergeRanges, countOccurrences, atomicWriteFile, bashResult, renderFullBashResult, writeBashArtifact, safeArtifactSegment, projectBashStreams, projectOutputStream, resolveDefaultShell, resolveRtkCommand, rtkRewriteResult, executableRewriteCommand, readRtkGain, rtkSavedTokenDelta, withRtkSavings, nonNegativeInteger, isRecord3, shellQuote, shellCommandArgs, userShell, truncate, sliceBytes, textResult, byteLength, isTimeoutError, isExecError, runRipgrep, splitOutput, vcsExcludes, grepArgs, splitGlobPatterns, paginate, toWorkspaceRelative, relativizeGrepLine, relativizeCountLine, sortPathsByMtime, formatPaginatedText, formatLimitSuffix, parseCountLines;
1750
1751
  var init_coding_tools = __esm({
1751
1752
  "packages/core/src/tools/coding-tools.ts"() {
1752
1753
  "use strict";
@@ -1799,6 +1800,12 @@ var init_coding_tools = __esm({
1799
1800
  defineTool({
1800
1801
  name: "Read",
1801
1802
  description: "Read a text file from the workspace. Long reads are truncated by complete lines; accumulated coverage unlocks Write/Edit.",
1803
+ parameters: Type.Object({
1804
+ file_path: Type.String(),
1805
+ offset: Type.Optional(Type.Number()),
1806
+ limit: Type.Optional(Type.Number()),
1807
+ full: Type.Optional(Type.Boolean())
1808
+ }),
1802
1809
  execute: async (_toolCallId, args) => {
1803
1810
  const input = parseReadArgs(args);
1804
1811
  if (input.full && (input.offset !== void 0 || input.limit !== void 0)) {
@@ -1858,6 +1865,10 @@ var init_coding_tools = __esm({
1858
1865
  defineTool({
1859
1866
  name: "Write",
1860
1867
  description: "Create a new file or fully overwrite an existing file. Existing files require complete read coverage of the current file.",
1868
+ parameters: Type.Object({
1869
+ file_path: Type.String(),
1870
+ content: Type.String()
1871
+ }),
1861
1872
  execute: async (_toolCallId, args) => {
1862
1873
  const input = parseWriteArgs(args);
1863
1874
  const path = resolveWorkspacePath(input.path);
@@ -1879,6 +1890,12 @@ var init_coding_tools = __esm({
1879
1890
  defineTool({
1880
1891
  name: "Edit",
1881
1892
  description: "Perform an exact string replacement in an existing file. Requires complete read coverage and a unique old_string unless replace_all is true.",
1893
+ parameters: Type.Object({
1894
+ file_path: Type.String(),
1895
+ old_string: Type.String(),
1896
+ new_string: Type.String(),
1897
+ replace_all: Type.Optional(Type.Boolean())
1898
+ }),
1882
1899
  execute: async (_toolCallId, args) => {
1883
1900
  const input = parseEditArgs(args);
1884
1901
  const path = resolveWorkspacePath(input.path);
@@ -1914,7 +1931,14 @@ String: ${input.old_string}`
1914
1931
  defineTool({
1915
1932
  name: "Bash",
1916
1933
  description: "Execute a shell command in the workspace with timeout and output truncation.",
1917
- execute: async (_toolCallId, args, signal) => {
1934
+ parameters: Type.Object({
1935
+ command: Type.String(),
1936
+ cwd: Type.Optional(Type.String()),
1937
+ timeout: Type.Optional(Type.Number()),
1938
+ description: Type.Optional(Type.String()),
1939
+ maxOutputBytes: Type.Optional(Type.Number())
1940
+ }),
1941
+ execute: async (toolCallId, args, signal) => {
1918
1942
  const input = parseBashArgs(args);
1919
1943
  const commandCwd = input.cwd ? resolveWorkspacePath(input.cwd) : root;
1920
1944
  const timeoutMs = Math.min(input.timeoutMs ?? defaultTimeoutMs, maxTimeoutMs);
@@ -1940,12 +1964,14 @@ String: ${input.old_string}`
1940
1964
  maxBuffer: Math.max(outputLimit * 4, 1024 * 1024)
1941
1965
  });
1942
1966
  const rtkSavedTokens = rtk?.executable ? await rtkSavedTokenDelta(rtk.executable, commandCwd, rtkGainBefore) : void 0;
1943
- return bashResult({
1967
+ return await bashResult({
1944
1968
  exitCode: 0,
1945
1969
  stdout: result.stdout,
1946
1970
  stderr: result.stderr,
1947
1971
  cwd: commandCwd,
1948
1972
  outputLimit,
1973
+ artifactDir: options.toolResultArtifacts?.dir,
1974
+ toolCallId,
1949
1975
  shell: defaultShell,
1950
1976
  command,
1951
1977
  rtk: withRtkSavings(rtkResult, rtkSavedTokens)
@@ -1956,12 +1982,14 @@ String: ${input.old_string}`
1956
1982
  }
1957
1983
  if (isExecError(cause)) {
1958
1984
  const rtkSavedTokens = rtk?.executable ? await rtkSavedTokenDelta(rtk.executable, commandCwd, rtkGainBefore) : void 0;
1959
- return bashResult({
1985
+ return await bashResult({
1960
1986
  exitCode: typeof cause.code === "number" ? cause.code : 1,
1961
1987
  stdout: String(cause.stdout ?? ""),
1962
1988
  stderr: String(cause.stderr ?? cause.message),
1963
1989
  cwd: commandCwd,
1964
1990
  outputLimit,
1991
+ artifactDir: options.toolResultArtifacts?.dir,
1992
+ toolCallId,
1965
1993
  shell: defaultShell,
1966
1994
  command,
1967
1995
  rtk: withRtkSavings(rtkResult, rtkSavedTokens)
@@ -1974,6 +2002,12 @@ String: ${input.old_string}`
1974
2002
  defineTool({
1975
2003
  name: "Glob",
1976
2004
  description: "Find files by glob pattern using ripgrep file discovery.",
2005
+ parameters: Type.Object({
2006
+ pattern: Type.String(),
2007
+ path: Type.Optional(Type.String()),
2008
+ head_limit: Type.Optional(Type.Number()),
2009
+ offset: Type.Optional(Type.Number())
2010
+ }),
1977
2011
  execute: async (_toolCallId, args, signal) => {
1978
2012
  const input = parseGlobArgs(args);
1979
2013
  const limit = input.head_limit ?? DEFAULT_SEARCH_LIMIT;
@@ -1994,6 +2028,22 @@ String: ${input.old_string}`
1994
2028
  defineTool({
1995
2029
  name: "Grep",
1996
2030
  description: 'Search file contents with ripgrep. Default output_mode is "files" for matching paths; use "content" for matching lines or "count" for match counts.',
2031
+ parameters: Type.Object({
2032
+ pattern: Type.String(),
2033
+ path: Type.Optional(Type.String()),
2034
+ glob: Type.Optional(Type.String()),
2035
+ output_mode: Type.Optional(Type.Union([Type.Literal("files"), Type.Literal("content"), Type.Literal("count")])),
2036
+ "-B": Type.Optional(Type.Number()),
2037
+ "-A": Type.Optional(Type.Number()),
2038
+ "-C": Type.Optional(Type.Number()),
2039
+ context: Type.Optional(Type.Number()),
2040
+ "-n": Type.Optional(Type.Boolean()),
2041
+ "-i": Type.Optional(Type.Boolean()),
2042
+ type: Type.Optional(Type.String()),
2043
+ head_limit: Type.Optional(Type.Number()),
2044
+ offset: Type.Optional(Type.Number()),
2045
+ multiline: Type.Optional(Type.Boolean())
2046
+ }),
1997
2047
  execute: async (_toolCallId, args, signal) => {
1998
2048
  const input = parseGrepArgs(args);
1999
2049
  const mode = input.output_mode ?? "files";
@@ -2047,6 +2097,15 @@ ${filenames.join("\n")}`,
2047
2097
  defineTool({
2048
2098
  name: "TodoWrite",
2049
2099
  description: "Replace the current session todo list with a complete updated list.",
2100
+ parameters: Type.Object({
2101
+ todos: Type.Array(
2102
+ Type.Object({
2103
+ content: Type.String(),
2104
+ status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")]),
2105
+ activeForm: Type.Optional(Type.String())
2106
+ })
2107
+ )
2108
+ }),
2050
2109
  execute: async (_toolCallId, args) => {
2051
2110
  const input = parseTodoWriteArgs(args);
2052
2111
  const oldTodos = state.todos;
@@ -2329,17 +2388,41 @@ ${filenames.join("\n")}`,
2329
2388
  throw cause;
2330
2389
  }
2331
2390
  };
2332
- bashResult = (input) => {
2333
- const stdout = truncate(input.stdout, input.outputLimit, "stdout");
2334
- const stderr = truncate(input.stderr, input.outputLimit, "stderr");
2335
- return textResult(`exitCode: ${input.exitCode}
2391
+ bashResult = async (input) => {
2392
+ const stdoutBytes = Buffer.byteLength(input.stdout);
2393
+ const stderrBytes = Buffer.byteLength(input.stderr);
2394
+ const fullResult = renderFullBashResult(input);
2395
+ const resultBytes = Buffer.byteLength(fullResult);
2396
+ const shouldArchive = Boolean(input.artifactDir) && resultBytes > input.outputLimit;
2397
+ const artifactPath = shouldArchive && input.artifactDir ? await writeBashArtifact(input.artifactDir, input.toolCallId, fullResult) : void 0;
2398
+ const projection = artifactPath ? projectBashStreams(input.stdout, input.stderr, input.outputLimit) : void 0;
2399
+ const stdout = projection?.stdout ?? truncate(input.stdout, input.outputLimit, "stdout");
2400
+ const stderr = projection?.stderr ?? truncate(input.stderr, input.outputLimit, "stderr");
2401
+ const text = artifactPath ? [
2402
+ `exitCode: ${input.exitCode}`,
2403
+ `cwd: ${input.cwd}`,
2404
+ `artifact: ${artifactPath}`,
2405
+ `resultBytes: ${resultBytes}`,
2406
+ `stdoutBytes: ${stdoutBytes}`,
2407
+ `stderrBytes: ${stderrBytes}`,
2408
+ ...projection?.lines ?? []
2409
+ ].join("\n") : `exitCode: ${input.exitCode}
2336
2410
  cwd: ${input.cwd}
2337
2411
  stdout:
2338
2412
  ${stdout}
2339
2413
  stderr:
2340
- ${stderr}`, {
2414
+ ${stderr}`;
2415
+ return textResult(text, {
2341
2416
  exitCode: input.exitCode,
2342
2417
  cwd: input.cwd,
2418
+ ...artifactPath ? {
2419
+ artifact: {
2420
+ path: artifactPath,
2421
+ resultBytes,
2422
+ stdoutBytes,
2423
+ stderrBytes
2424
+ }
2425
+ } : {},
2343
2426
  ...input.shell ? { shell: input.shell } : {},
2344
2427
  ...input.command ? { command: input.command } : {},
2345
2428
  ...input.rtk ? {
@@ -2351,6 +2434,57 @@ ${stderr}`)
2351
2434
  } : {}
2352
2435
  });
2353
2436
  };
2437
+ renderFullBashResult = (input) => `exitCode: ${input.exitCode}
2438
+ cwd: ${input.cwd}
2439
+ stdout:
2440
+ ${input.stdout}
2441
+ stderr:
2442
+ ${input.stderr}`;
2443
+ writeBashArtifact = async (artifactDir, toolCallId, content) => {
2444
+ const directory = resolve(artifactDir, safeArtifactSegment(toolCallId));
2445
+ await mkdir2(directory, { recursive: true });
2446
+ const path = resolve(directory, "result.txt");
2447
+ await writeFile2(path, content, { encoding: "utf8", mode: 384 });
2448
+ return path;
2449
+ };
2450
+ safeArtifactSegment = (value) => value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "tool_call";
2451
+ projectBashStreams = (stdout, stderr, maxBytes) => {
2452
+ const streams = [
2453
+ { label: "stdout", value: stdout },
2454
+ { label: "stderr", value: stderr }
2455
+ ].filter((stream) => Buffer.byteLength(stream.value) > 0);
2456
+ if (streams.length === 0) {
2457
+ return { lines: ["stdout:", "", "stderr:", ""], stdout: "", stderr: "" };
2458
+ }
2459
+ const perStreamBudget = Math.max(1, Math.floor(maxBytes / streams.length));
2460
+ const projected = streams.map((stream) => projectOutputStream(stream.value, perStreamBudget, stream.label));
2461
+ const stdoutText = projected.find((stream) => stream.label === "stdout")?.text ?? "stdout:\n";
2462
+ const stderrText = projected.find((stream) => stream.label === "stderr")?.text ?? "stderr:\n";
2463
+ return {
2464
+ lines: projected.map((stream) => stream.text),
2465
+ stdout: stdoutText,
2466
+ stderr: stderrText
2467
+ };
2468
+ };
2469
+ projectOutputStream = (value, maxBytes, label) => {
2470
+ const bytes = Buffer.byteLength(value);
2471
+ if (bytes <= maxBytes) {
2472
+ return { label, text: `${label}:
2473
+ ${value}` };
2474
+ }
2475
+ const headBytes = Math.max(1, Math.floor(maxBytes / 2));
2476
+ const tailBytes = Math.max(1, maxBytes - headBytes);
2477
+ return {
2478
+ label,
2479
+ text: [
2480
+ `${label} head:`,
2481
+ sliceBytes(value, 0, headBytes),
2482
+ `${label} tail:`,
2483
+ sliceBytes(value, Math.max(0, bytes - tailBytes), bytes),
2484
+ `[${label} archived: ${bytes} bytes; projection budget ${maxBytes} bytes]`
2485
+ ].join("\n")
2486
+ };
2487
+ };
2354
2488
  resolveDefaultShell = (input) => {
2355
2489
  const shell = input || process.env.SHELL || userShell() || "/bin/sh";
2356
2490
  return shell.trim() || "/bin/sh";
@@ -2434,10 +2568,11 @@ ${stderr}`)
2434
2568
  if (bytes <= maxBytes) {
2435
2569
  return value;
2436
2570
  }
2437
- const truncated = Buffer.from(value).subarray(0, maxBytes).toString("utf8");
2571
+ const truncated = sliceBytes(value, 0, maxBytes);
2438
2572
  return `${truncated}
2439
2573
  [${label} truncated: ${bytes} bytes > ${maxBytes} bytes]`;
2440
2574
  };
2575
+ sliceBytes = (value, start, end) => Buffer.from(value).subarray(start, end).toString("utf8");
2441
2576
  textResult = (text, details) => ({
2442
2577
  content: [{ type: "text", text }],
2443
2578
  details
@@ -2602,17 +2737,58 @@ ${stderr}`)
2602
2737
  });
2603
2738
 
2604
2739
  // packages/core/src/tools/index.ts
2605
- var defineTool;
2740
+ import { Type as Type2 } from "@mariozechner/pi-ai";
2741
+ var defineTool, createSnipTool, parseSnipToolInput, isRecord4;
2606
2742
  var init_tools = __esm({
2607
2743
  "packages/core/src/tools/index.ts"() {
2608
2744
  "use strict";
2609
2745
  init_coding_tools();
2610
2746
  defineTool = (tool) => tool;
2747
+ createSnipTool = (options) => defineTool({
2748
+ name: "snip",
2749
+ description: [
2750
+ "Hide a completed user turn from future model context.",
2751
+ "Use only when an earlier user turn is obsolete or noisy.",
2752
+ "The session JSONL evidence is preserved; the hidden turn disappears from the next context build.",
2753
+ "Input: { userMessageId: string, reason?: string }."
2754
+ ].join(" "),
2755
+ parameters: Type2.Object({
2756
+ userMessageId: Type2.String(),
2757
+ reason: Type2.Optional(Type2.String())
2758
+ }),
2759
+ execute: async (_toolCallId, args) => {
2760
+ const input = parseSnipToolInput(args);
2761
+ const result = await options.snip(input);
2762
+ return {
2763
+ content: [{
2764
+ type: "text",
2765
+ text: [
2766
+ `Snipped user turn ${result.anchorUserEventId}.`,
2767
+ `Hidden through ${result.throughEventId}.`,
2768
+ `${result.hiddenEventCount} event(s) will be omitted from future model context.`,
2769
+ "Original session JSONL remains unchanged."
2770
+ ].join(" ")
2771
+ }],
2772
+ details: result
2773
+ };
2774
+ }
2775
+ });
2776
+ parseSnipToolInput = (args) => {
2777
+ if (!isRecord4(args) || typeof args.userMessageId !== "string") {
2778
+ throw new Error("snip requires { userMessageId: string }");
2779
+ }
2780
+ return {
2781
+ userMessageId: args.userMessageId,
2782
+ ...typeof args.reason === "string" && args.reason.trim() ? { reason: args.reason.trim() } : {}
2783
+ };
2784
+ };
2785
+ isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2611
2786
  }
2612
2787
  });
2613
2788
 
2614
2789
  // packages/core/src/channel/index.ts
2615
- var createSendChannelMessageTool, parseSendChannelMessageInput, parseAttachments, optionalString2, isRecord4;
2790
+ import { Type as Type3 } from "@mariozechner/pi-ai";
2791
+ var createSendChannelMessageTool, parseSendChannelMessageInput, parseAttachments, optionalString2, isRecord5;
2616
2792
  var init_channel = __esm({
2617
2793
  "packages/core/src/channel/index.ts"() {
2618
2794
  "use strict";
@@ -2620,6 +2796,18 @@ var init_channel = __esm({
2620
2796
  createSendChannelMessageTool = (options) => defineTool({
2621
2797
  name: "SendChannelMessage",
2622
2798
  description: "Send a text reply to the current IM channel conversation. Do not provide raw platform user ids or group ids.",
2799
+ parameters: Type3.Object({
2800
+ text: Type3.Optional(Type3.String()),
2801
+ attachments: Type3.Optional(Type3.Array(Type3.Object({
2802
+ type: Type3.Union([Type3.Literal("image"), Type3.Literal("file")]),
2803
+ path: Type3.Optional(Type3.String()),
2804
+ url: Type3.Optional(Type3.String()),
2805
+ mimeType: Type3.Optional(Type3.String()),
2806
+ caption: Type3.Optional(Type3.String())
2807
+ }))),
2808
+ channel: Type3.Optional(Type3.String()),
2809
+ target: Type3.Optional(Type3.Literal("current"))
2810
+ }),
2623
2811
  execute: async (_toolCallId, args) => {
2624
2812
  const input = parseSendChannelMessageInput(args);
2625
2813
  const result = await options.sendCurrent(input);
@@ -2630,7 +2818,7 @@ var init_channel = __esm({
2630
2818
  }
2631
2819
  });
2632
2820
  parseSendChannelMessageInput = (value) => {
2633
- if (!isRecord4(value)) {
2821
+ if (!isRecord5(value)) {
2634
2822
  throw new Error("SendChannelMessage args must be an object");
2635
2823
  }
2636
2824
  const text = typeof value.text === "string" && value.text.trim().length > 0 ? value.text : void 0;
@@ -2659,7 +2847,7 @@ var init_channel = __esm({
2659
2847
  throw new Error("SendChannelMessage.attachments must be an array");
2660
2848
  }
2661
2849
  return value.map((item, index) => {
2662
- if (!isRecord4(item)) {
2850
+ if (!isRecord5(item)) {
2663
2851
  throw new Error(`SendChannelMessage.attachments.${index} must be an object`);
2664
2852
  }
2665
2853
  if (item.type !== "image" && item.type !== "file") {
@@ -2688,14 +2876,14 @@ var init_channel = __esm({
2688
2876
  }
2689
2877
  return value;
2690
2878
  };
2691
- isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2879
+ isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2692
2880
  }
2693
2881
  });
2694
2882
 
2695
2883
  // packages/core/src/extensions/index.ts
2696
2884
  import { readFile as readFile5 } from "node:fs/promises";
2697
2885
  import { dirname as dirname4, resolve as resolve2 } from "node:path";
2698
- var loadExtensionManifest, parseExtensionManifest, requireString2, requireIdentifier2, requireKind, requireRelativePath, optionalRelativePaths, isRecord5;
2886
+ var loadExtensionManifest, parseExtensionManifest, requireString2, requireIdentifier2, requireKind, requireRelativePath, optionalRelativePaths, isRecord6;
2699
2887
  var init_extensions = __esm({
2700
2888
  "packages/core/src/extensions/index.ts"() {
2701
2889
  "use strict";
@@ -2708,7 +2896,7 @@ var init_extensions = __esm({
2708
2896
  const message = cause instanceof Error ? cause.message : String(cause);
2709
2897
  throw new Error(`Invalid extension manifest JSON at ${manifestPath}: ${message}`);
2710
2898
  }
2711
- if (!isRecord5(value)) {
2899
+ if (!isRecord6(value)) {
2712
2900
  throw new Error(`Extension manifest at ${manifestPath} must be an object`);
2713
2901
  }
2714
2902
  const rootDir = dirname4(resolve2(manifestPath));
@@ -2764,7 +2952,7 @@ var init_extensions = __esm({
2764
2952
  }
2765
2953
  return value.map((item, index) => requireRelativePath(item, `${name}.${index}`, manifestPath));
2766
2954
  };
2767
- isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2955
+ isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2768
2956
  }
2769
2957
  });
2770
2958
 
@@ -2908,7 +3096,8 @@ var init_instructions = __esm({
2908
3096
  import { appendFile, mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "node:fs/promises";
2909
3097
  import { homedir as homedir3 } from "node:os";
2910
3098
  import { join as join6 } from "node:path";
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;
3099
+ import { Type as Type4 } from "@mariozechner/pi-ai";
3100
+ 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, isRecord7, safeProjectId, isNodeErrorCode3;
2912
3101
  var init_memory = __esm({
2913
3102
  "packages/core/src/memory/index.ts"() {
2914
3103
  "use strict";
@@ -2997,6 +3186,14 @@ var init_memory = __esm({
2997
3186
  "Use this once near the end of a completed user turn when there is progress, a decision, or a follow-up worth preserving.",
2998
3187
  "Do not include secrets, raw logs, speculation, or facts that should be re-read from the repository."
2999
3188
  ].join(" "),
3189
+ parameters: Type4.Object({
3190
+ summary: Type4.String(),
3191
+ completed: Type4.Optional(Type4.Array(Type4.String())),
3192
+ decisions: Type4.Optional(Type4.Array(Type4.String())),
3193
+ followUps: Type4.Optional(Type4.Array(Type4.String())),
3194
+ memoryCandidates: Type4.Optional(Type4.Array(Type4.String())),
3195
+ evidence: Type4.Optional(Type4.Array(Type4.String()))
3196
+ }),
3000
3197
  execute: async (_toolCallId, args) => {
3001
3198
  const input = parseAppendDailyInput(args);
3002
3199
  validateAppendDailyInput(input);
@@ -3140,7 +3337,7 @@ var init_memory = __esm({
3140
3337
  normalizeMarkdownFile = (value) => `${value.trimEnd()}
3141
3338
  `;
3142
3339
  parseAppendDailyInput = (value) => {
3143
- if (!isRecord6(value)) {
3340
+ if (!isRecord7(value)) {
3144
3341
  throw new Error("AppendDaily args must be an object");
3145
3342
  }
3146
3343
  const summary = requireString3(value.summary, "summary");
@@ -3206,12 +3403,12 @@ var init_memory = __esm({
3206
3403
  optionalNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
3207
3404
  optionalString3 = (value) => typeof value === "string" && value.trim() ? value : void 0;
3208
3405
  parseLastFailure = (value) => {
3209
- if (!isRecord6(value)) return void 0;
3406
+ if (!isRecord7(value)) return void 0;
3210
3407
  const at = optionalNumber2(value.at);
3211
3408
  const message = optionalString3(value.message);
3212
3409
  return at !== void 0 && message ? { at, message } : void 0;
3213
3410
  };
3214
- isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3411
+ isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3215
3412
  safeProjectId = (projectId) => {
3216
3413
  if (!/^[A-Za-z0-9_-]+$/.test(projectId)) {
3217
3414
  throw new Error("projectId must contain only letters, numbers, underscores, or hyphens");
@@ -3224,11 +3421,10 @@ var init_memory = __esm({
3224
3421
 
3225
3422
  // packages/core/src/provider/pi-ai.ts
3226
3423
  import {
3227
- Type,
3228
3424
  getModels,
3229
3425
  streamSimple
3230
3426
  } from "@mariozechner/pi-ai";
3231
- var DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW, DEFAULT_CUSTOM_MODEL_MAX_TOKENS, createPiAiProvider, resolvePiAiModel, toPiContext, toPiMessage, toPiAssistantBlock, fromPiAssistant, fromPiContentBlock, toPiTool, toolParameters, textContent, toolResultText, stringMeta, toPiStopReason, fromPiStopReason, fromPiUsage;
3427
+ var DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW, DEFAULT_CUSTOM_MODEL_MAX_TOKENS, createPiAiProvider, resolvePiAiModel, toPiContext, toPiMessage, toPiAssistantBlock, fromPiAssistant, fromPiContentBlock, toPiTool, textContent, toolResultText, stringMeta, toPiStopReason, fromPiStopReason, fromPiUsage;
3232
3428
  var init_pi_ai = __esm({
3233
3429
  "packages/core/src/provider/pi-ai.ts"() {
3234
3430
  "use strict";
@@ -3366,89 +3562,8 @@ var init_pi_ai = __esm({
3366
3562
  toPiTool = (tool) => ({
3367
3563
  name: tool.name,
3368
3564
  description: tool.description,
3369
- parameters: toolParameters(tool.name)
3565
+ parameters: tool.parameters
3370
3566
  });
3371
- toolParameters = (name) => {
3372
- switch (name) {
3373
- case "Read":
3374
- return Type.Object({
3375
- file_path: Type.String(),
3376
- offset: Type.Optional(Type.Number()),
3377
- limit: Type.Optional(Type.Number()),
3378
- full: Type.Optional(Type.Boolean())
3379
- });
3380
- case "Write":
3381
- return Type.Object({
3382
- file_path: Type.String(),
3383
- content: Type.String()
3384
- });
3385
- case "Edit":
3386
- return Type.Object({
3387
- file_path: Type.String(),
3388
- old_string: Type.String(),
3389
- new_string: Type.String(),
3390
- replace_all: Type.Optional(Type.Boolean())
3391
- });
3392
- case "Bash":
3393
- return Type.Object({
3394
- command: Type.String(),
3395
- cwd: Type.Optional(Type.String()),
3396
- timeout: Type.Optional(Type.Number()),
3397
- description: Type.Optional(Type.String()),
3398
- maxOutputBytes: Type.Optional(Type.Number())
3399
- });
3400
- case "Glob":
3401
- return Type.Object({
3402
- pattern: Type.String(),
3403
- path: Type.Optional(Type.String()),
3404
- head_limit: Type.Optional(Type.Number()),
3405
- offset: Type.Optional(Type.Number())
3406
- });
3407
- case "Grep":
3408
- return Type.Object({
3409
- pattern: Type.String(),
3410
- path: Type.Optional(Type.String()),
3411
- glob: Type.Optional(Type.String()),
3412
- output_mode: Type.Optional(Type.Union([Type.Literal("files"), Type.Literal("content"), Type.Literal("count")])),
3413
- "-B": Type.Optional(Type.Number()),
3414
- "-A": Type.Optional(Type.Number()),
3415
- "-C": Type.Optional(Type.Number()),
3416
- context: Type.Optional(Type.Number()),
3417
- "-n": Type.Optional(Type.Boolean()),
3418
- "-i": Type.Optional(Type.Boolean()),
3419
- type: Type.Optional(Type.String()),
3420
- head_limit: Type.Optional(Type.Number()),
3421
- offset: Type.Optional(Type.Number()),
3422
- multiline: Type.Optional(Type.Boolean())
3423
- });
3424
- case "TodoWrite":
3425
- return Type.Object({
3426
- todos: Type.Array(
3427
- Type.Object({
3428
- content: Type.String(),
3429
- status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")]),
3430
- activeForm: Type.Optional(Type.String())
3431
- })
3432
- )
3433
- });
3434
- case "AppendDaily":
3435
- return Type.Object({
3436
- summary: Type.String(),
3437
- completed: Type.Optional(Type.Array(Type.String())),
3438
- decisions: Type.Optional(Type.Array(Type.String())),
3439
- followUps: Type.Optional(Type.Array(Type.String())),
3440
- memoryCandidates: Type.Optional(Type.Array(Type.String())),
3441
- evidence: Type.Optional(Type.Array(Type.String()))
3442
- });
3443
- case "Skill":
3444
- return Type.Object({
3445
- name: Type.String(),
3446
- args: Type.Optional(Type.String())
3447
- });
3448
- default:
3449
- return Type.Object({});
3450
- }
3451
- };
3452
3567
  textContent = (message) => message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
3453
3568
  toolResultText = (result) => {
3454
3569
  if (typeof result === "object" && result !== null && "content" in result) {
@@ -3696,22 +3811,23 @@ var init_runtime = __esm({
3696
3811
  });
3697
3812
 
3698
3813
  // packages/core/src/session/index.ts
3814
+ import { createHash as createHash2 } from "node:crypto";
3699
3815
  import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
3700
3816
  import { dirname as dirname6, join as join7 } from "node:path";
3701
3817
  function assertTreeEvent(value) {
3702
- if (!isRecord7(value)) {
3818
+ if (!isRecord8(value)) {
3703
3819
  throw new SessionStoreError("invalid_event", "Event must be an object");
3704
3820
  }
3705
3821
  if (value.type === "session_header") {
3706
3822
  throw new SessionStoreError("invalid_event", "Session header must be stored as the JSONL header line");
3707
3823
  }
3708
- if (value.type !== "user_message" && value.type !== "assistant_message" && value.type !== "tool_result" && value.type !== "session_title_updated" && value.type !== "instruction_snapshot" && value.type !== "harness_item" && value.type !== "compact" && value.type !== "queue_update" && value.type !== "skill_index_snapshot" && value.type !== "skill_index_delta") {
3824
+ if (value.type !== "user_message" && value.type !== "assistant_message" && value.type !== "tool_result" && value.type !== "session_title_updated" && value.type !== "instruction_snapshot" && value.type !== "harness_item" && value.type !== "compact" && value.type !== "context_control" && value.type !== "queue_update" && value.type !== "skill_index_snapshot" && value.type !== "skill_index_delta") {
3709
3825
  throw new SessionStoreError("invalid_event", "Unsupported session event type");
3710
3826
  }
3711
3827
  if (typeof value.id !== "string" || value.parentId !== null && typeof value.parentId !== "string" || typeof value.seq !== "number" || typeof value.clientId !== "string" || typeof value.ts !== "number") {
3712
3828
  throw new SessionStoreError("invalid_event", "Event is missing required base fields");
3713
3829
  }
3714
- if ((value.type === "user_message" || value.type === "assistant_message" || value.type === "tool_result") && !isRecord7(value.message)) {
3830
+ if ((value.type === "user_message" || value.type === "assistant_message" || value.type === "tool_result") && !isRecord8(value.message)) {
3715
3831
  throw new SessionStoreError("invalid_event", "Message event is missing message payload");
3716
3832
  }
3717
3833
  if (value.type === "session_title_updated" && !isSessionTitleUpdated(value)) {
@@ -3726,6 +3842,9 @@ function assertTreeEvent(value) {
3726
3842
  if (value.type === "compact" && !isCompactEvent(value)) {
3727
3843
  throw new SessionStoreError("invalid_event", "compact is missing summary payload");
3728
3844
  }
3845
+ if (value.type === "context_control" && !isContextControlEvent(value)) {
3846
+ throw new SessionStoreError("invalid_event", "context_control is missing hide_user_turn payload");
3847
+ }
3729
3848
  if (value.type === "queue_update" && !isQueueUpdate(value)) {
3730
3849
  throw new SessionStoreError("invalid_event", "queue_update is missing queue payload");
3731
3850
  }
@@ -3736,11 +3855,12 @@ function assertTreeEvent(value) {
3736
3855
  throw new SessionStoreError("invalid_event", "skill_index_delta is missing delta payload");
3737
3856
  }
3738
3857
  }
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;
3858
+ var snipUserMessageAlias, SessionStoreError, SessionTree, JsonlSession, sessionFilePath, sessionLogFilePath, sessionArtifactsDirPath, createSession, loadSession, buildContext, hiddenContextEventIds, retainedMessagesBeforeCompact, isRetainedContextStart, parseJsonLine, parseHeader, parseSessionEvent, validateSessionMatch, isConversationEvent, isInstructionSnapshot, isHarnessItem, isCompactEvent, isContextControlEvent, isQueueUpdate, isSessionTitleUpdated, isSkillIndexSnapshot, isSkillIndexDelta, isSkillIndexEntry, appendHarnessItemToContext, appendReminderToToolResult, isToolResultWithContent, renderSystemReminder, compactSummaryMessage, cloneMessage, isRecord8;
3740
3859
  var init_session = __esm({
3741
3860
  "packages/core/src/session/index.ts"() {
3742
3861
  "use strict";
3743
3862
  init_src();
3863
+ snipUserMessageAlias = (eventId) => `u_${createHash2("sha256").update(eventId).digest("hex").slice(0, 8)}`;
3744
3864
  SessionStoreError = class extends Error {
3745
3865
  code;
3746
3866
  line;
@@ -3764,7 +3884,8 @@ var init_session = __esm({
3764
3884
  steer: []
3765
3885
  },
3766
3886
  skillIndexInitialized: false,
3767
- skillIndex: {}
3887
+ skillIndex: {},
3888
+ hiddenUserTurnSpans: []
3768
3889
  };
3769
3890
  get rootId() {
3770
3891
  return this.#rootId;
@@ -3884,6 +4005,16 @@ var init_session = __esm({
3884
4005
  delete next[removed.name];
3885
4006
  }
3886
4007
  this.controlState.skillIndex = next;
4008
+ } else if (event.type === "context_control") {
4009
+ this.controlState.hiddenUserTurnSpans = [
4010
+ ...this.controlState.hiddenUserTurnSpans.filter(
4011
+ (span) => span.anchorUserEventId !== event.anchorUserEventId
4012
+ ),
4013
+ {
4014
+ anchorUserEventId: event.anchorUserEventId,
4015
+ throughEventId: event.throughEventId
4016
+ }
4017
+ ];
3887
4018
  }
3888
4019
  }
3889
4020
  };
@@ -3917,6 +4048,7 @@ var init_session = __esm({
3917
4048
  };
3918
4049
  sessionFilePath = (sessionsDir, sessionId) => join7(sessionsDir, `${sessionId}.jsonl`);
3919
4050
  sessionLogFilePath = (sessionsDir, sessionId) => join7(sessionsDir, `${sessionId}.log`);
4051
+ sessionArtifactsDirPath = (sessionsDir, sessionId) => join7(sessionsDir, `${sessionId}.artifacts`);
3920
4052
  createSession = async ({ sessionsDir, header }) => {
3921
4053
  const validHeader = parseHeader(header);
3922
4054
  await mkdir4(sessionsDir, { recursive: true });
@@ -3944,7 +4076,11 @@ var init_session = __esm({
3944
4076
  };
3945
4077
  buildContext = (tree, leafId) => {
3946
4078
  const path = tree.getPath(leafId);
4079
+ const hiddenIds = hiddenContextEventIds(tree, path, leafId);
3947
4080
  return path.reduce((messages, id, index) => {
4081
+ if (hiddenIds.has(id)) {
4082
+ return messages;
4083
+ }
3948
4084
  const event = tree.get(id)?.event;
3949
4085
  if (!event) {
3950
4086
  return messages;
@@ -3957,7 +4093,7 @@ var init_session = __esm({
3957
4093
  appendHarnessItemToContext(messages, event);
3958
4094
  }
3959
4095
  if (event.type === "compact") {
3960
- const retained = retainedMessagesBeforeCompact(tree, path.slice(0, index), event.retainedEventCount);
4096
+ const retained = retainedMessagesBeforeCompact(tree, path.slice(0, index), event.retainedEventCount, hiddenIds);
3961
4097
  messages.length = 0;
3962
4098
  messages.push(compactSummaryMessage(event));
3963
4099
  messages.push(...retained);
@@ -3965,7 +4101,29 @@ var init_session = __esm({
3965
4101
  return messages;
3966
4102
  }, []);
3967
4103
  };
3968
- retainedMessagesBeforeCompact = (tree, pathBeforeCompact, retainedEventCount) => {
4104
+ hiddenContextEventIds = (tree, path, leafId) => {
4105
+ const leaf = tree.get(leafId)?.event;
4106
+ if (!leaf) {
4107
+ return /* @__PURE__ */ new Set();
4108
+ }
4109
+ const pathIndexes = new Map(path.map((id, index) => [id, index]));
4110
+ const hidden = /* @__PURE__ */ new Set();
4111
+ for (const event of tree) {
4112
+ if (event.type !== "context_control" || Number(event.seq) > Number(leaf.seq)) {
4113
+ continue;
4114
+ }
4115
+ const start = pathIndexes.get(event.anchorUserEventId);
4116
+ const end = pathIndexes.get(event.throughEventId);
4117
+ if (start === void 0 || end === void 0 || end < start) {
4118
+ continue;
4119
+ }
4120
+ for (const id of path.slice(start, end + 1)) {
4121
+ hidden.add(id);
4122
+ }
4123
+ }
4124
+ return hidden;
4125
+ };
4126
+ retainedMessagesBeforeCompact = (tree, pathBeforeCompact, retainedEventCount, hiddenIds = /* @__PURE__ */ new Set()) => {
3969
4127
  if (retainedEventCount <= 0) {
3970
4128
  return [];
3971
4129
  }
@@ -3980,6 +4138,9 @@ var init_session = __esm({
3980
4138
  }
3981
4139
  const retained = [];
3982
4140
  for (const id of pathBeforeCompact.slice(start)) {
4141
+ if (hiddenIds.has(id)) {
4142
+ continue;
4143
+ }
3983
4144
  const event = tree.get(id)?.event;
3984
4145
  if (!event) {
3985
4146
  continue;
@@ -4004,13 +4165,13 @@ var init_session = __esm({
4004
4165
  }
4005
4166
  };
4006
4167
  parseHeader = (value) => {
4007
- if (!isRecord7(value)) {
4168
+ if (!isRecord8(value)) {
4008
4169
  throw new SessionStoreError("invalid_header", "Session header must be an object");
4009
4170
  }
4010
4171
  if (value.version !== 1 || typeof value.sessionId !== "string" || typeof value.deviceId !== "string") {
4011
4172
  throw new SessionStoreError("invalid_header", "Session header is missing required identity fields");
4012
4173
  }
4013
- if (typeof value.createdAt !== "number" || !isRecord7(value.meta)) {
4174
+ if (typeof value.createdAt !== "number" || !isRecord8(value.meta)) {
4014
4175
  throw new SessionStoreError("invalid_header", "Session header is missing createdAt or meta");
4015
4176
  }
4016
4177
  if (typeof value.meta.projectId !== "string" || value.meta.projectId.length === 0) {
@@ -4024,7 +4185,7 @@ var init_session = __esm({
4024
4185
  return value;
4025
4186
  };
4026
4187
  validateSessionMatch = (header, value) => {
4027
- if (!isRecord7(value) || typeof value.sessionId !== "string") {
4188
+ if (!isRecord8(value) || typeof value.sessionId !== "string") {
4028
4189
  throw new SessionStoreError("invalid_header", "Event must be an object with a sessionId");
4029
4190
  }
4030
4191
  if (value.sessionId !== header.sessionId) {
@@ -4033,24 +4194,25 @@ var init_session = __esm({
4033
4194
  };
4034
4195
  isConversationEvent = (event) => event.type === "user_message" || event.type === "assistant_message" || event.type === "tool_result" || event.type === "harness_item" || event.type === "compact";
4035
4196
  isInstructionSnapshot = (value) => {
4036
- if (!isRecord7(value) || value.version !== 1 || typeof value.cwd !== "string" || !Array.isArray(value.sections)) {
4197
+ if (!isRecord8(value) || value.version !== 1 || typeof value.cwd !== "string" || !Array.isArray(value.sections)) {
4037
4198
  return false;
4038
4199
  }
4039
4200
  return value.sections.every(
4040
- (section2) => isRecord7(section2) && typeof section2.kind === "string" && typeof section2.frozenAt === "number" && typeof section2.renderedBlock === "string"
4201
+ (section2) => isRecord8(section2) && typeof section2.kind === "string" && typeof section2.frozenAt === "number" && typeof section2.renderedBlock === "string"
4041
4202
  );
4042
4203
  };
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");
4204
+ isHarnessItem = (value) => isRecord8(value) && typeof value.kind === "string" && typeof value.origin === "string" && typeof value.content === "string" && (value.visibility === "display" || value.visibility === "hidden" || value.visibility === "compact");
4044
4205
  isCompactEvent = (value) => typeof value.summary === "string" && typeof value.compactedThrough === "string" && typeof value.tokensBefore === "number" && typeof value.tokensAfter === "number" && typeof value.retainedEventCount === "number";
4206
+ isContextControlEvent = (value) => value.operation === "hide_user_turn" && typeof value.anchorUserEventId === "string" && typeof value.throughEventId === "string" && (value.actor === "agent" || value.actor === "user" || value.actor === "system") && (value.reason === void 0 || typeof value.reason === "string");
4045
4207
  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(
4046
- (item) => isRecord7(item) && typeof item.id === "string" && Array.isArray(item.content) && typeof item.createdAt === "number" && typeof item.updatedAt === "number" && typeof item.clientId === "string"
4208
+ (item) => isRecord8(item) && typeof item.id === "string" && Array.isArray(item.content) && typeof item.createdAt === "number" && typeof item.updatedAt === "number" && typeof item.clientId === "string"
4047
4209
  );
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");
4210
+ isSessionTitleUpdated = (value) => typeof value.title === "string" && value.title.length > 0 && (value.source === "model" || value.source === "user") && (value.derivedFrom === void 0 || isRecord8(value.derivedFrom) && typeof value.derivedFrom.eventId === "string" && typeof value.derivedFrom.seq === "number");
4049
4211
  isSkillIndexSnapshot = (value) => (value.anchorEventId === null || typeof value.anchorEventId === "string") && Array.isArray(value.entries) && value.entries.every(isSkillIndexEntry);
4050
4212
  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(
4051
- (item) => isRecord7(item) && typeof item.name === "string" && typeof item.previousPath === "string"
4213
+ (item) => isRecord8(item) && typeof item.name === "string" && typeof item.previousPath === "string"
4052
4214
  );
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";
4215
+ isSkillIndexEntry = (value) => isRecord8(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";
4054
4216
  appendHarnessItemToContext = (messages, event) => {
4055
4217
  const reminder = renderSystemReminder(event.item.content);
4056
4218
  const last = messages.at(-1);
@@ -4087,7 +4249,7 @@ ${reminder}` }]
4087
4249
  }
4088
4250
  return false;
4089
4251
  };
4090
- isToolResultWithContent = (value) => isRecord7(value) && Array.isArray(value.content);
4252
+ isToolResultWithContent = (value) => isRecord8(value) && Array.isArray(value.content);
4091
4253
  renderSystemReminder = (content) => `<system-reminder>
4092
4254
  ${content}
4093
4255
  </system-reminder>`;
@@ -4111,10 +4273,10 @@ ${content}
4111
4273
  cloneMessage = (message) => ({
4112
4274
  ...message,
4113
4275
  content: message.content.map((block) => {
4114
- if (block.type !== "tool_result" || !isRecord7(block.result)) {
4276
+ if (block.type !== "tool_result" || !isRecord8(block.result)) {
4115
4277
  return { ...block };
4116
4278
  }
4117
- const content = Array.isArray(block.result.content) ? { content: block.result.content.map((item) => isRecord7(item) ? { ...item } : item) } : {};
4279
+ const content = Array.isArray(block.result.content) ? { content: block.result.content.map((item) => isRecord8(item) ? { ...item } : item) } : {};
4118
4280
  return {
4119
4281
  ...block,
4120
4282
  result: {
@@ -4124,16 +4286,17 @@ ${content}
4124
4286
  }),
4125
4287
  ...message.meta ? { meta: { ...message.meta } } : {}
4126
4288
  });
4127
- isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4289
+ isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4128
4290
  }
4129
4291
  });
4130
4292
 
4131
4293
  // packages/core/src/skills/index.ts
4132
- import { createHash as createHash2 } from "node:crypto";
4294
+ import { createHash as createHash3 } from "node:crypto";
4133
4295
  import { existsSync as existsSync2 } from "node:fs";
4134
4296
  import { readdir as readdir5, readFile as readFile9, stat as stat4 } from "node:fs/promises";
4135
4297
  import { homedir as homedir4 } from "node:os";
4136
4298
  import { dirname as dirname7, join as join8, resolve as resolve4 } from "node:path";
4299
+ import { Type as Type5 } from "@mariozechner/pi-ai";
4137
4300
  var scanSkillIndex, diffSkillIndex, hasSkillIndexDelta, renderSkillListing, renderSkillDelta, createSkillTool, projectSkillRoots, readSkillEntry, parseSkillMetadata, firstParagraph, parseSkillArgs, findGitRoot2, isNodeErrorCode4;
4138
4301
  var init_skills = __esm({
4139
4302
  "packages/core/src/skills/index.ts"() {
@@ -4224,6 +4387,10 @@ var init_skills = __esm({
4224
4387
  createSkillTool = (options) => defineTool({
4225
4388
  name: "Skill",
4226
4389
  description: "Load the full SKILL.md instructions for an available session-indexed skill by name.",
4390
+ parameters: Type5.Object({
4391
+ name: Type5.String(),
4392
+ args: Type5.Optional(Type5.String())
4393
+ }),
4227
4394
  execute: async (_toolCallId, args) => {
4228
4395
  const input = parseSkillArgs(args);
4229
4396
  const entry = options.getEntry(input.name);
@@ -4288,7 +4455,7 @@ var init_skills = __esm({
4288
4455
  ...parsed.displayName ? { displayName: parsed.displayName } : {},
4289
4456
  mtimeMs: fileStat.mtimeMs,
4290
4457
  size: fileStat.size,
4291
- contentHash: createHash2("sha256").update(content).digest("hex"),
4458
+ contentHash: createHash3("sha256").update(content).digest("hex"),
4292
4459
  priority: options.priority
4293
4460
  };
4294
4461
  };
@@ -4704,7 +4871,7 @@ import { basename as basename3, dirname as dirname8, join as join10, resolve as
4704
4871
  import { pathToFileURL } from "node:url";
4705
4872
  import { promisify as promisify2 } from "node:util";
4706
4873
  import { WebSocketServer } from "ws";
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;
4874
+ 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, snipUserMessageIdBlock, inputText, assistantText, messageText, estimateScorelMessagesTokens, estimateTextTokens, compactLine2, parseSessionMemoryJson, stringArray, disabledMemorySettings, detectRtk, ensureRtkAvailable, emptyRuntimeStats, readRuntimeStats, writeRuntimeStats, parseRuntimeStats, parseRuntimeStatsBuckets, addRtkSavings, addRuntimeStatsBucket, rtkSavingsFromToolResult, nonNegativeInteger2, resolveDefaultShell2, shellCommandArgs2, userShell2, runtimeChannelContextFromWire, parseQueuedChannelContext, parseQueuedModelSelection, imBindingKey, defaultBuiltinExtensionsDir, runtimeModuleDir, findBuiltinExtensionsDir, isSteerMessage, stripImCommandPrefix, isRecord9, parseMemoryUpdate, normalizeMarkdownFile2, sanitizeSessionTitle, shortStack, formatDiagnosticLine, formatDiagnosticValue;
4708
4875
  var init_src4 = __esm({
4709
4876
  "packages/daemon/src/index.ts"() {
4710
4877
  "use strict";
@@ -4945,6 +5112,7 @@ var init_src4 = __esm({
4945
5112
  for (const tool of createCodingTools({
4946
5113
  cwd: options.cwd,
4947
5114
  contextWindow: model.contextWindow,
5115
+ ...options.sessionsDir && options.sessionId ? { toolResultArtifacts: { dir: sessionArtifactsDirPath(options.sessionsDir, options.sessionId) } } : {},
4948
5116
  tokenSaving: {
4949
5117
  rtk: {
4950
5118
  enabled: options.config.runtime.tokenSavingRtk,
@@ -4985,6 +5153,7 @@ var init_src4 = __esm({
4985
5153
  #registry;
4986
5154
  #runtimeStatsQueue = Promise.resolve();
4987
5155
  #idleShutdownTimer;
5156
+ #lastActiveWorkAt;
4988
5157
  #started = false;
4989
5158
  constructor(options) {
4990
5159
  this.#sessionsDir = options.sessionsDir;
@@ -5003,6 +5172,7 @@ var init_src4 = __esm({
5003
5172
  this.#onIdleShutdown = options.onIdleShutdown;
5004
5173
  this.#now = options.now ?? Date.now;
5005
5174
  this.#createId = options.createId ?? (() => crypto.randomUUID());
5175
+ this.#lastActiveWorkAt = this.#now();
5006
5176
  this.#registry = new ProjectRegistry({
5007
5177
  sessionsDir: this.#sessionsDir,
5008
5178
  projectsPath: options.projectsPath,
@@ -5067,6 +5237,13 @@ var init_src4 = __esm({
5067
5237
  releaseSessionEventBuffer(sessionId) {
5068
5238
  this.#events.delete(sessionId);
5069
5239
  }
5240
+ activityStatus() {
5241
+ const activeWork = this.#hasActiveWork();
5242
+ if (activeWork) {
5243
+ this.#lastActiveWorkAt = this.#now();
5244
+ }
5245
+ return { activeWork, lastActiveWorkAt: this.#lastActiveWorkAt };
5246
+ }
5070
5247
  async handleMessage(connection, message) {
5071
5248
  this.#assertStarted();
5072
5249
  try {
@@ -5355,6 +5532,7 @@ var init_src4 = __esm({
5355
5532
  content: normalizeContent(request.content),
5356
5533
  parentId: request.options?.parentId,
5357
5534
  source: "user",
5535
+ modelSelection: request.options?.modelSelection,
5358
5536
  channelContext: request.options?.channelContext ? runtimeChannelContextFromWire(request.options.channelContext) : void 0,
5359
5537
  onComplete: (result) => this.#respond(connection, request, { ...result, status: "completed" })
5360
5538
  });
@@ -5380,11 +5558,14 @@ var init_src4 = __esm({
5380
5558
  });
5381
5559
  }
5382
5560
  async #runUserTurn(lane, clientId, input) {
5561
+ this.#lastActiveWorkAt = this.#now();
5383
5562
  const sessionId = lane.session.header.sessionId;
5563
+ await this.#selectChatRuntime(lane, input.modelSelection);
5384
5564
  await this.#appendDiagnostic(sessionId, "send_message_started", {
5385
5565
  clientId,
5386
5566
  activeLeafId: lane.session.activeLeafId,
5387
- source: input.source
5567
+ source: input.source,
5568
+ selectedModelId: lane.selectedModel?.modelId
5388
5569
  });
5389
5570
  const instructionSnapshot = await this.#ensureInstructionSnapshot(lane, clientId);
5390
5571
  await this.#syncSkillIndex(lane, clientId);
@@ -5407,7 +5588,7 @@ var init_src4 = __esm({
5407
5588
  ts: this.#now(),
5408
5589
  message: {
5409
5590
  role: "user",
5410
- content: input.content,
5591
+ content: [...input.content, snipUserMessageIdBlock(userEventId)],
5411
5592
  ...input.source === "follow_up" ? { meta: { source: "follow_up", queueItemId: input.queueItemId } } : {}
5412
5593
  }
5413
5594
  });
@@ -5427,6 +5608,7 @@ var init_src4 = __esm({
5427
5608
  finalAssistantEventId: firstAssistantEventId
5428
5609
  };
5429
5610
  lane.channelContext = input.channelContext;
5611
+ lane.snipClientId = clientId;
5430
5612
  try {
5431
5613
  for await (const rawEvent of lane.runtime.executeTurn(
5432
5614
  buildContext(lane.session.tree, userEvent.id),
@@ -5442,6 +5624,7 @@ var init_src4 = __esm({
5442
5624
  }
5443
5625
  } finally {
5444
5626
  lane.channelContext = void 0;
5627
+ lane.snipClientId = void 0;
5445
5628
  lane.runtime.unregisterTool("SendChannelMessage");
5446
5629
  }
5447
5630
  const result = { userEventId, assistantEventId: state.finalAssistantEventId };
@@ -5588,7 +5771,7 @@ var init_src4 = __esm({
5588
5771
  createdAt: now,
5589
5772
  updatedAt: now,
5590
5773
  clientId: connection.clientId,
5591
- ...request.options?.channelContext ? { data: { channelContext: request.options.channelContext } } : {}
5774
+ ...request.options?.channelContext || request.options?.modelSelection ? { data: { channelContext: request.options.channelContext, modelSelection: request.options.modelSelection } } : {}
5592
5775
  };
5593
5776
  lane.followUpWaiters.set(item.id, { connection, request });
5594
5777
  await this.#appendQueueRewrite(lane, "follow_up", [...lane.session.tree.controlState.queues.follow_up, item], {
@@ -5641,6 +5824,7 @@ var init_src4 = __esm({
5641
5824
  parentId: lane.session.activeLeafId,
5642
5825
  source: "follow_up",
5643
5826
  queueItemId: item.id,
5827
+ modelSelection: parseQueuedModelSelection(item.data?.modelSelection),
5644
5828
  channelContext: parseQueuedChannelContext(item.data?.channelContext),
5645
5829
  onComplete: waiter ? (result) => this.#respond(waiter.connection, waiter.request, { ...result, status: "completed" }) : void 0
5646
5830
  });
@@ -6623,6 +6807,7 @@ var init_src4 = __esm({
6623
6807
  session: loaded,
6624
6808
  project,
6625
6809
  runtime,
6810
+ ...selectedModel ? { selectedModel } : {},
6626
6811
  queue: Promise.resolve(),
6627
6812
  appendQueue: Promise.resolve(),
6628
6813
  followUpWaiters: /* @__PURE__ */ new Map()
@@ -6674,6 +6859,7 @@ var init_src4 = __esm({
6674
6859
  session,
6675
6860
  project,
6676
6861
  runtime,
6862
+ ...selectedModel ? { selectedModel } : {},
6677
6863
  queue: Promise.resolve(),
6678
6864
  appendQueue: Promise.resolve(),
6679
6865
  followUpWaiters: /* @__PURE__ */ new Map()
@@ -6688,6 +6874,106 @@ var init_src4 = __esm({
6688
6874
  listNames: () => Object.keys(lane.session.tree.controlState.skillIndex).sort()
6689
6875
  })
6690
6876
  );
6877
+ lane.runtime.registerTool(
6878
+ createSnipTool({
6879
+ snip: async (input) => this.#snipUserTurn(lane, input.userMessageId, input.reason)
6880
+ })
6881
+ );
6882
+ }
6883
+ async #snipUserTurn(lane, userMessageId, reason) {
6884
+ const leafId = lane.session.activeLeafId;
6885
+ if (!leafId) {
6886
+ throw new Error("snip requires an active conversation");
6887
+ }
6888
+ const path = lane.session.tree.getPath(leafId);
6889
+ const anchorUserEventId = this.#resolveSnipUserMessageId(lane, path, userMessageId);
6890
+ const anchorIndex = path.findIndex((id) => id === anchorUserEventId);
6891
+ if (anchorIndex < 0) {
6892
+ throw new Error(`snip target is not on the active conversation path: ${anchorUserEventId}`);
6893
+ }
6894
+ const anchor = lane.session.tree.get(anchorUserEventId)?.event;
6895
+ if (anchor?.type !== "user_message") {
6896
+ throw new Error(`snip target must be a user_message: ${anchorUserEventId}`);
6897
+ }
6898
+ const nextUserIndex = path.findIndex(
6899
+ (id, index) => index > anchorIndex && lane.session.tree.get(id)?.event.type === "user_message"
6900
+ );
6901
+ if (nextUserIndex < 0) {
6902
+ throw new Error("snip cannot hide the current user turn before the next user message exists");
6903
+ }
6904
+ const throughEventId = path[nextUserIndex - 1];
6905
+ if (!throughEventId || throughEventId === anchorUserEventId) {
6906
+ throw new Error(`snip target has no completed turn content: ${anchorUserEventId}`);
6907
+ }
6908
+ const clientId = lane.snipClientId;
6909
+ if (!clientId) {
6910
+ throw new Error("snip is only available while a user turn is running");
6911
+ }
6912
+ await this.#appendPersistent(lane, {
6913
+ type: "context_control",
6914
+ id: asEventId(this.#createId()),
6915
+ parentId: null,
6916
+ sessionId: lane.session.header.sessionId,
6917
+ clientId,
6918
+ ts: this.#now(),
6919
+ operation: "hide_user_turn",
6920
+ anchorUserEventId,
6921
+ throughEventId,
6922
+ actor: "agent",
6923
+ ...reason ? { reason } : {}
6924
+ });
6925
+ await this.#appendDiagnostic(lane.session.header.sessionId, "context_snipped", {
6926
+ anchorUserEventId,
6927
+ throughEventId,
6928
+ hiddenEventCount: nextUserIndex - anchorIndex
6929
+ });
6930
+ return {
6931
+ anchorUserEventId,
6932
+ throughEventId,
6933
+ hiddenEventCount: nextUserIndex - anchorIndex
6934
+ };
6935
+ }
6936
+ #resolveSnipUserMessageId(lane, path, userMessageId) {
6937
+ if (path.includes(userMessageId)) {
6938
+ return userMessageId;
6939
+ }
6940
+ const matches = path.filter((id) => {
6941
+ const event = lane.session.tree.get(id)?.event;
6942
+ return event?.type === "user_message" && snipUserMessageAlias(id) === userMessageId;
6943
+ });
6944
+ if (matches.length === 1) {
6945
+ return matches[0];
6946
+ }
6947
+ if (matches.length > 1) {
6948
+ throw new Error(`snip target short id is ambiguous: ${userMessageId}`);
6949
+ }
6950
+ return asEventId(userMessageId);
6951
+ }
6952
+ async #selectChatRuntime(lane, modelSelection) {
6953
+ if (!modelSelection) {
6954
+ return;
6955
+ }
6956
+ const selectedModel = await this.#selectedModelFromMeta(
6957
+ { projectId: lane.project.projectId, modelSelection },
6958
+ lane.project
6959
+ );
6960
+ if (!selectedModel || lane.selectedModel?.modelId === selectedModel.modelId) {
6961
+ return;
6962
+ }
6963
+ lane.runtime = await this.#createRuntime({
6964
+ sessionId: lane.session.header.sessionId,
6965
+ project: lane.project,
6966
+ selectedModel,
6967
+ purpose: "chat"
6968
+ });
6969
+ lane.selectedModel = selectedModel;
6970
+ this.#registerLaneTools(lane);
6971
+ await this.#appendDiagnostic(lane.session.header.sessionId, "chat_model_selected", {
6972
+ projectId: lane.project.projectId,
6973
+ workDir: lane.project.workDir,
6974
+ selectedModelId: selectedModel.modelId,
6975
+ role: selectedModel.role
6976
+ });
6691
6977
  }
6692
6978
  #syncChannelTool(lane, channelContext) {
6693
6979
  if (!channelContext) {
@@ -7249,9 +7535,10 @@ var init_src4 = __esm({
7249
7535
  }
7250
7536
  const persistedSelection = "selectedModel" in meta ? meta.selectedModel : void 0;
7251
7537
  const requestedSelection = "modelSelection" in meta ? meta.modelSelection : void 0;
7538
+ const selectionInput = persistedSelection ? config.models[persistedSelection.modelId] ? { modelId: persistedSelection.modelId, role: persistedSelection.role } : persistedSelection.role ? { role: persistedSelection.role } : void 0 : requestedSelection;
7252
7539
  const selection = resolveModelSelection(
7253
7540
  config,
7254
- persistedSelection ? { modelId: persistedSelection.modelId, role: persistedSelection.role } : requestedSelection
7541
+ selectionInput
7255
7542
  );
7256
7543
  const model = resolvePiAiModel(selection.config);
7257
7544
  return {
@@ -7423,7 +7710,14 @@ var init_src4 = __esm({
7423
7710
  };
7424
7711
  countContentBlocks = (message, type) => message.content.filter((block) => block.type === type).length;
7425
7712
  normalizeContent = (content) => typeof content === "string" ? [{ type: "text", text: content }] : content;
7426
- inputText = (message) => message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
7713
+ snipUserMessageIdBlock = (userEventId) => ({
7714
+ type: "text",
7715
+ text: `<system-reminder>
7716
+ snip.userMessageId: ${snipUserMessageAlias(userEventId)}
7717
+ </system-reminder>`,
7718
+ visibility: "model"
7719
+ });
7720
+ inputText = (message) => message.content.flatMap((block) => block.type === "text" && block.visibility !== "model" ? [block.text] : []).join("\n").trim();
7427
7721
  assistantText = (message) => message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
7428
7722
  messageText = (message) => {
7429
7723
  const text = message.content.map((block) => {
@@ -7452,7 +7746,7 @@ var init_src4 = __esm({
7452
7746
  return void 0;
7453
7747
  }
7454
7748
  const parsed = JSON.parse(text);
7455
- if (!isRecord8(parsed)) {
7749
+ if (!isRecord9(parsed)) {
7456
7750
  return void 0;
7457
7751
  }
7458
7752
  return {
@@ -7540,7 +7834,7 @@ var init_src4 = __esm({
7540
7834
  }
7541
7835
  };
7542
7836
  parseRuntimeStats = (value) => {
7543
- if (!isRecord8(value) || !isRecord8(value.rtk)) {
7837
+ if (!isRecord9(value) || !isRecord9(value.rtk)) {
7544
7838
  return emptyRuntimeStats();
7545
7839
  }
7546
7840
  return {
@@ -7554,13 +7848,13 @@ var init_src4 = __esm({
7554
7848
  };
7555
7849
  };
7556
7850
  parseRuntimeStatsBuckets = (value) => {
7557
- if (!isRecord8(value)) {
7851
+ if (!isRecord9(value)) {
7558
7852
  return {};
7559
7853
  }
7560
7854
  return Object.fromEntries(
7561
7855
  Object.entries(value).map(([key, bucket]) => [
7562
7856
  key,
7563
- isRecord8(bucket) ? {
7857
+ isRecord9(bucket) ? {
7564
7858
  outputTokens: nonNegativeInteger2(bucket.outputTokens),
7565
7859
  savedTokens: nonNegativeInteger2(bucket.savedTokens)
7566
7860
  } : { outputTokens: 0, savedTokens: 0 }
@@ -7578,11 +7872,11 @@ var init_src4 = __esm({
7578
7872
  return bucket;
7579
7873
  };
7580
7874
  rtkSavingsFromToolResult = (result) => {
7581
- if (!isRecord8(result) || !isRecord8(result.details)) {
7875
+ if (!isRecord9(result) || !isRecord9(result.details)) {
7582
7876
  return void 0;
7583
7877
  }
7584
7878
  const rtk = result.details.rtk;
7585
- if (!isRecord8(rtk) || rtk.applied !== true) {
7879
+ if (!isRecord9(rtk) || rtk.applied !== true) {
7586
7880
  return void 0;
7587
7881
  }
7588
7882
  const outputTokens = nonNegativeInteger2(rtk.estimatedOutputTokens);
@@ -7627,7 +7921,7 @@ var init_src4 = __esm({
7627
7921
  ...context.data ? { data: context.data } : {}
7628
7922
  });
7629
7923
  parseQueuedChannelContext = (value) => {
7630
- if (!isRecord8(value)) {
7924
+ if (!isRecord9(value)) {
7631
7925
  return void 0;
7632
7926
  }
7633
7927
  if (typeof value.channel !== "string" || typeof value.externalConversationId !== "string") {
@@ -7639,9 +7933,22 @@ var init_src4 = __esm({
7639
7933
  ...typeof value.conversationType === "string" ? { conversationType: value.conversationType } : {},
7640
7934
  ...typeof value.senderDisplayName === "string" ? { senderDisplayName: value.senderDisplayName } : {},
7641
7935
  ...typeof value.mentionedBot === "boolean" ? { mentionedBot: value.mentionedBot } : {},
7642
- ...isRecord8(value.data) ? { data: value.data } : {}
7936
+ ...isRecord9(value.data) ? { data: value.data } : {}
7643
7937
  });
7644
7938
  };
7939
+ parseQueuedModelSelection = (value) => {
7940
+ if (!isRecord9(value)) {
7941
+ return void 0;
7942
+ }
7943
+ const selection = {};
7944
+ if (typeof value.modelId === "string") {
7945
+ selection.modelId = value.modelId;
7946
+ }
7947
+ if (value.role === "primary" || value.role === "standard" || value.role === "auxiliary") {
7948
+ selection.role = value.role;
7949
+ }
7950
+ return selection.modelId || selection.role ? selection : void 0;
7951
+ };
7645
7952
  imBindingKey = (extensionId, externalConversationId) => `${extensionId}:${externalConversationId}`;
7646
7953
  defaultBuiltinExtensionsDir = () => findBuiltinExtensionsDir([
7647
7954
  runtimeModuleDir(),
@@ -7672,7 +7979,7 @@ var init_src4 = __esm({
7672
7979
  };
7673
7980
  isSteerMessage = (text) => /^\/(?:steer|interrupt)\b/i.test(text.trim());
7674
7981
  stripImCommandPrefix = (text) => text.trim().replace(/^\/(?:steer|interrupt)\s*/i, "").trim() || text;
7675
- isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7982
+ isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7676
7983
  parseMemoryUpdate = (raw) => {
7677
7984
  const text = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim();
7678
7985
  if (!text) {
@@ -7777,25 +8084,131 @@ var init_relay_cli = __esm({
7777
8084
  }
7778
8085
  });
7779
8086
 
8087
+ // apps/cli/src/update-cli.ts
8088
+ import { execFile as execFileCallback } from "node:child_process";
8089
+ import { readFile as readFile12 } from "node:fs/promises";
8090
+ import { dirname as dirname9, join as join12 } from "node:path";
8091
+ import { fileURLToPath } from "node:url";
8092
+ import { promisify as promisify3 } from "node:util";
8093
+ var SCOREL_PACKAGE_NAME, AUTO_UPDATE_INTERVAL_MS, ACTIVE_WORK_STALE_MS, execFileAsync3, compareSemver, shouldRunAutoUpdate, createNpmPackageUpdater, readInstalledScorelVersion, runCliUpdate, writeUpdateUsage, parseSemver;
8094
+ var init_update_cli = __esm({
8095
+ "apps/cli/src/update-cli.ts"() {
8096
+ "use strict";
8097
+ SCOREL_PACKAGE_NAME = "@chanlerdev/scorel";
8098
+ AUTO_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
8099
+ ACTIVE_WORK_STALE_MS = 3 * 60 * 60 * 1e3;
8100
+ execFileAsync3 = promisify3(execFileCallback);
8101
+ compareSemver = (a, b) => {
8102
+ const left = parseSemver(a);
8103
+ const right = parseSemver(b);
8104
+ for (let index = 0; index < 3; index += 1) {
8105
+ const delta = left[index] - right[index];
8106
+ if (delta !== 0) return delta;
8107
+ }
8108
+ return 0;
8109
+ };
8110
+ shouldRunAutoUpdate = (activity) => !activity.activeWork || activity.now - activity.lastActiveWorkAt >= ACTIVE_WORK_STALE_MS;
8111
+ createNpmPackageUpdater = (options) => {
8112
+ const packageName = options.packageName ?? SCOREL_PACKAGE_NAME;
8113
+ const execFile3 = options.execFile ?? ((command, argv) => execFileAsync3(command, argv));
8114
+ return {
8115
+ async checkLatest() {
8116
+ const result = await execFile3("npm", ["view", packageName, "version"]);
8117
+ const latest = result.stdout.trim();
8118
+ if (!latest) {
8119
+ throw new Error(`npm did not return a latest version for ${packageName}`);
8120
+ }
8121
+ parseSemver(latest);
8122
+ return latest;
8123
+ },
8124
+ async update() {
8125
+ const latestVersion = await this.checkLatest();
8126
+ if (compareSemver(options.currentVersion, latestVersion) >= 0) {
8127
+ return { status: "current", currentVersion: options.currentVersion, latestVersion };
8128
+ }
8129
+ await execFile3("npm", ["install", "-g", `${packageName}@${latestVersion}`]);
8130
+ return { status: "updated", currentVersion: options.currentVersion, latestVersion };
8131
+ }
8132
+ };
8133
+ };
8134
+ readInstalledScorelVersion = async () => {
8135
+ const here = dirname9(fileURLToPath(import.meta.url));
8136
+ for (const candidate of [
8137
+ join12(here, "..", "package.json"),
8138
+ join12(here, "..", "..", "package.json"),
8139
+ join12(process.cwd(), "package.json")
8140
+ ]) {
8141
+ try {
8142
+ const parsed = JSON.parse(await readFile12(candidate, "utf8"));
8143
+ if (typeof parsed.version === "string" && (parsed.name === SCOREL_PACKAGE_NAME || parsed.name === "@scorel/app-cli")) {
8144
+ return parsed.version;
8145
+ }
8146
+ } catch {
8147
+ }
8148
+ }
8149
+ return "0.0.0";
8150
+ };
8151
+ runCliUpdate = async (argv, io, options = {}) => {
8152
+ if (argv.includes("--help") || argv.includes("-h")) {
8153
+ writeUpdateUsage(io.output);
8154
+ return 0;
8155
+ }
8156
+ if (argv.length > 0) {
8157
+ writeUpdateUsage(io.error);
8158
+ return 1;
8159
+ }
8160
+ const currentVersion = options.currentVersion ?? await readInstalledScorelVersion();
8161
+ const updater = options.updater ?? createNpmPackageUpdater({ currentVersion });
8162
+ try {
8163
+ const result = await updater.update();
8164
+ if (result.status === "current") {
8165
+ io.output.write(`scorel is current (${result.currentVersion})
8166
+ `);
8167
+ } else {
8168
+ io.output.write(`updated scorel ${result.currentVersion} -> ${result.latestVersion}
8169
+ `);
8170
+ }
8171
+ return 0;
8172
+ } catch (cause) {
8173
+ io.error.write(`scorel update error: ${cause instanceof Error ? cause.message : String(cause)}
8174
+ `);
8175
+ return 1;
8176
+ }
8177
+ };
8178
+ writeUpdateUsage = (output) => {
8179
+ output.write("Usage: scorel update\n scorel upgrade\n");
8180
+ };
8181
+ parseSemver = (version) => {
8182
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
8183
+ if (!match) {
8184
+ throw new Error(`Invalid semver version: ${version}`);
8185
+ }
8186
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
8187
+ };
8188
+ }
8189
+ });
8190
+
7780
8191
  // apps/cli/src/daemon-cli.ts
7781
8192
  import { randomUUID as randomUUID4 } from "node:crypto";
7782
8193
  import { spawn } from "node:child_process";
7783
8194
  import { homedir as homedir6 } from "node:os";
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;
8195
+ import { dirname as dirname10, join as join13 } from "node:path";
8196
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
8197
+ var DEFAULT_HOST, DEFAULT_PORT, STOP_POLL_INTERVAL_MS, STOP_GRACE_MS, START_READY_TIMEOUT_MS, AUTO_STARTED_IDLE_SHUTDOWN_MS, FOREGROUND_IDLE_SHUTDOWN_MS, defaultStateDir2, isLoopbackHost, formatTimestamp, runCliDaemon, runStartCommand, runServeCommand, startAutoUpdateLoop, stopRunningDaemon, runStatusCommand, runStopCommand, runResetCommand, formatStatusLine, parseServeFlags, parseStatusFlags, requireValue2, sleep, waitForDaemonReady, detachBackgroundDaemon, nodeEntrypointArgs, writeDaemonUsage;
7787
8198
  var init_daemon_cli = __esm({
7788
8199
  "apps/cli/src/daemon-cli.ts"() {
7789
8200
  "use strict";
7790
8201
  init_src4();
7791
8202
  init_relay_cli();
8203
+ init_update_cli();
7792
8204
  DEFAULT_HOST = "127.0.0.1";
7793
8205
  DEFAULT_PORT = 7777;
7794
8206
  STOP_POLL_INTERVAL_MS = 200;
7795
8207
  STOP_GRACE_MS = 5e3;
7796
- START_READY_TIMEOUT_MS = 1e4;
7797
- DEFAULT_IDLE_SHUTDOWN_MS = 15 * 60 * 1e3;
7798
- defaultStateDir2 = () => join12(homedir6(), ".scorel");
8208
+ START_READY_TIMEOUT_MS = 3e4;
8209
+ AUTO_STARTED_IDLE_SHUTDOWN_MS = 15 * 60 * 1e3;
8210
+ FOREGROUND_IDLE_SHUTDOWN_MS = 0;
8211
+ defaultStateDir2 = () => join13(homedir6(), ".scorel");
7799
8212
  isLoopbackHost = (host) => host === "127.0.0.1" || host === "::1" || host === "localhost";
7800
8213
  formatTimestamp = (epochMs) => new Date(epochMs).toISOString();
7801
8214
  runCliDaemon = async (argv, options) => {
@@ -7824,7 +8237,7 @@ var init_daemon_cli = __esm({
7824
8237
  runStartCommand = async (argv, options) => {
7825
8238
  let flags;
7826
8239
  try {
7827
- flags = parseServeFlags(argv, options.cwd ?? process.cwd(), options.env ?? process.env);
8240
+ flags = parseServeFlags(argv, options.cwd ?? process.cwd(), options.env ?? process.env, FOREGROUND_IDLE_SHUTDOWN_MS);
7828
8241
  } catch (cause) {
7829
8242
  options.error.write(`scorel daemon start error: ${cause.message}
7830
8243
  `);
@@ -7837,7 +8250,7 @@ var init_daemon_cli = __esm({
7837
8250
  `);
7838
8251
  return 0;
7839
8252
  }
7840
- const cliEntrypoint = options.cliEntrypoint ?? fileURLToPath(import.meta.url).replace(/daemon-cli\.ts$/, "index.ts");
8253
+ const cliEntrypoint = options.cliEntrypoint ?? fileURLToPath2(import.meta.url).replace(/daemon-cli\.ts$/, "index.ts");
7841
8254
  const child = (options.spawn ?? spawn)(process.execPath, [
7842
8255
  ...nodeEntrypointArgs(cliEntrypoint),
7843
8256
  "host",
@@ -7854,7 +8267,7 @@ var init_daemon_cli = __esm({
7854
8267
  ...flags.relayUrl ? ["--relay", flags.relayUrl] : ["--no-relay"],
7855
8268
  ...flags.replace ? ["--replace"] : []
7856
8269
  ], {
7857
- cwd: dirname9(cliEntrypoint),
8270
+ cwd: dirname10(cliEntrypoint),
7858
8271
  env: { ...process.env, ...options.env ?? {} },
7859
8272
  detached: true,
7860
8273
  stdio: ["ignore", "pipe", "pipe"]
@@ -7881,7 +8294,7 @@ var init_daemon_cli = __esm({
7881
8294
  runServeCommand = async (argv, options) => {
7882
8295
  let flags;
7883
8296
  try {
7884
- flags = parseServeFlags(argv, options.cwd ?? process.cwd(), options.env ?? process.env);
8297
+ flags = parseServeFlags(argv, options.cwd ?? process.cwd(), options.env ?? process.env, FOREGROUND_IDLE_SHUTDOWN_MS);
7885
8298
  } catch (cause) {
7886
8299
  options.error.write(`scorel daemon serve error: ${cause.message}
7887
8300
  `);
@@ -7914,9 +8327,10 @@ Use --replace to stop it and start a new one.
7914
8327
  stopRequested = true;
7915
8328
  resolveStopWaiter?.();
7916
8329
  };
8330
+ const sessionsDir = options.sessionsDir ?? scorelSessionsDir(homedir6());
7917
8331
  const daemon = new ScorelHost({
7918
- sessionsDir: options.sessionsDir ?? scorelSessionsDir(homedir6()),
7919
- projectsPath: join12(options.stateDir, "projects.json"),
8332
+ sessionsDir,
8333
+ projectsPath: join13(options.stateDir, "projects.json"),
7920
8334
  deviceId: identity.deviceId,
7921
8335
  deviceDisplayName: identity.displayName,
7922
8336
  idleShutdownMs: flags.idleShutdownMs,
@@ -7924,15 +8338,25 @@ Use --replace to stop it and start a new one.
7924
8338
  scorelHomeDir: options.stateDir,
7925
8339
  loadConfig: async ({ project }) => loadScorelConfig({ cwd: project.workDir, ...configScope }),
7926
8340
  loadConfigProfile: async ({ project }) => loadScorelConfigProfile({ cwd: project.workDir, ...configScope }),
7927
- createRuntime: async ({ project, selectedModel, purpose }) => createRealRuntime({
8341
+ createRuntime: async ({ sessionId, project, selectedModel, purpose }) => createRealRuntime({
7928
8342
  cwd: project.workDir,
7929
8343
  config: await loadScorelConfig({ cwd: project.workDir, ...configScope }),
8344
+ sessionsDir,
8345
+ sessionId,
7930
8346
  modelSelection: selectedModel ? { modelId: selectedModel.modelId, role: selectedModel.role } : void 0,
7931
8347
  includeTools: purpose === "chat"
7932
8348
  })
7933
8349
  });
7934
8350
  await daemon.start();
7935
8351
  await daemon.registerProject(flags.cwd);
8352
+ const autoUpdater = await startAutoUpdateLoop({
8353
+ host: daemon,
8354
+ requestStop,
8355
+ output: options.output,
8356
+ error: options.error,
8357
+ updater: options.packageUpdater,
8358
+ intervalMs: options.autoUpdateIntervalMs ?? AUTO_UPDATE_INTERVAL_MS
8359
+ });
7936
8360
  const server = await startScorelHostWebSocketServer({
7937
8361
  hostService: daemon,
7938
8362
  host: flags.host,
@@ -7978,6 +8402,7 @@ Use --replace to stop it and start a new one.
7978
8402
  }
7979
8403
  const shutdown = async () => {
7980
8404
  try {
8405
+ autoUpdater.stop();
7981
8406
  relayClient?.close();
7982
8407
  await server.close();
7983
8408
  } finally {
@@ -8028,6 +8453,42 @@ Use --replace to stop it and start a new one.
8028
8453
  `);
8029
8454
  return 0;
8030
8455
  };
8456
+ startAutoUpdateLoop = async (options) => {
8457
+ const updater = options.updater ?? createNpmPackageUpdater({ currentVersion: await readInstalledScorelVersion() });
8458
+ let timer;
8459
+ let running = false;
8460
+ const tick = async () => {
8461
+ if (running) return;
8462
+ running = true;
8463
+ try {
8464
+ const activity = options.host.activityStatus();
8465
+ if (!shouldRunAutoUpdate({ ...activity, now: Date.now() })) {
8466
+ return;
8467
+ }
8468
+ const result = await updater.update();
8469
+ if (result.status === "updated") {
8470
+ options.output.write(`scorel auto-updated ${result.currentVersion} -> ${result.latestVersion}; restarting host
8471
+ `);
8472
+ options.requestStop("auto-update");
8473
+ }
8474
+ } catch (cause) {
8475
+ options.error.write(`scorel auto-update error: ${cause instanceof Error ? cause.message : String(cause)}
8476
+ `);
8477
+ } finally {
8478
+ running = false;
8479
+ }
8480
+ };
8481
+ timer = setInterval(() => void tick(), options.intervalMs);
8482
+ timer.unref?.();
8483
+ return {
8484
+ stop() {
8485
+ if (timer) {
8486
+ clearInterval(timer);
8487
+ timer = void 0;
8488
+ }
8489
+ }
8490
+ };
8491
+ };
8031
8492
  stopRunningDaemon = async (state, options) => {
8032
8493
  try {
8033
8494
  process.kill(state.pid, "SIGTERM");
@@ -8131,14 +8592,14 @@ Use --replace to stop it and start a new one.
8131
8592
  const stoppedAt = state.stoppedAt !== null ? formatTimestamp(state.stoppedAt) : "unknown";
8132
8593
  return `stopped url=${state.wsUrl} last-pid=${state.pid} stoppedAt=${stoppedAt} liveness=${liveness}`;
8133
8594
  };
8134
- parseServeFlags = (argv, defaultCwd, env) => {
8595
+ parseServeFlags = (argv, defaultCwd, env, defaultIdleShutdownMs) => {
8135
8596
  let host = DEFAULT_HOST;
8136
8597
  let port = DEFAULT_PORT;
8137
8598
  let cwd = defaultCwd;
8138
8599
  let token;
8139
8600
  let relayUrl = resolveDefaultRelayUrl(env);
8140
8601
  let replace = false;
8141
- let idleShutdownMs = DEFAULT_IDLE_SHUTDOWN_MS;
8602
+ let idleShutdownMs = defaultIdleShutdownMs;
8142
8603
  for (let index = 0; index < argv.length; index += 1) {
8143
8604
  const arg = argv[index];
8144
8605
  if (arg === "--host") {
@@ -8475,8 +8936,8 @@ var init_routing = __esm({
8475
8936
  });
8476
8937
 
8477
8938
  // apps/relay/src/store.ts
8478
- import { mkdir as mkdir7, readFile as readFile12, writeFile as writeFile7 } from "node:fs/promises";
8479
- import { join as join13 } from "node:path";
8939
+ import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile7 } from "node:fs/promises";
8940
+ import { join as join14 } from "node:path";
8480
8941
  var FileRelayStore, emptyStoreFile;
8481
8942
  var init_store = __esm({
8482
8943
  "apps/relay/src/store.ts"() {
@@ -8486,7 +8947,7 @@ var init_store = __esm({
8486
8947
  #now;
8487
8948
  #queue = Promise.resolve();
8488
8949
  constructor(options) {
8489
- this.#filePath = join13(options.dataDir, "relay-store.json");
8950
+ this.#filePath = join14(options.dataDir, "relay-store.json");
8490
8951
  this.#now = options.now ?? Date.now;
8491
8952
  }
8492
8953
  async upsertDevice(record) {
@@ -8529,7 +8990,7 @@ var init_store = __esm({
8529
8990
  this.#queue = this.#queue.then(async () => {
8530
8991
  const file = await this.#read();
8531
8992
  mutator(file);
8532
- await mkdir7(join13(this.#filePath, ".."), { recursive: true });
8993
+ await mkdir7(join14(this.#filePath, ".."), { recursive: true });
8533
8994
  await writeFile7(this.#filePath, `${JSON.stringify(file, null, 2)}
8534
8995
  `);
8535
8996
  });
@@ -8537,7 +8998,7 @@ var init_store = __esm({
8537
8998
  }
8538
8999
  async #read() {
8539
9000
  try {
8540
- const raw = JSON.parse(await readFile12(this.#filePath, "utf8"));
9001
+ const raw = JSON.parse(await readFile13(this.#filePath, "utf8"));
8541
9002
  if (raw.version !== 1 || !Array.isArray(raw.devices) || !Array.isArray(raw.clients) || !Array.isArray(raw.bindings)) {
8542
9003
  return emptyStoreFile();
8543
9004
  }
@@ -8790,7 +9251,7 @@ var init_library = __esm({
8790
9251
 
8791
9252
  // apps/cli/src/relay-server-cli.ts
8792
9253
  import { homedir as homedir7 } from "node:os";
8793
- import { join as join14 } from "node:path";
9254
+ import { join as join15 } from "node:path";
8794
9255
  var DEFAULT_HOST2, DEFAULT_PORT2, runCliRelay, runRelayServe, parseRelayServeFlags, waitForStop, requireValue3, writeRelayUsage;
8795
9256
  var init_relay_server_cli = __esm({
8796
9257
  "apps/cli/src/relay-server-cli.ts"() {
@@ -8842,7 +9303,7 @@ var init_relay_server_cli = __esm({
8842
9303
  parseRelayServeFlags = (argv) => {
8843
9304
  let host = DEFAULT_HOST2;
8844
9305
  let port = DEFAULT_PORT2;
8845
- let dataDir = join14(homedir7(), ".scorel", "relay");
9306
+ let dataDir = join15(homedir7(), ".scorel", "relay");
8846
9307
  for (let index = 0; index < argv.length; index += 1) {
8847
9308
  const arg = argv[index];
8848
9309
  if (arg === "--host") {
@@ -8900,17 +9361,18 @@ var init_relay_server_cli = __esm({
8900
9361
  // apps/cli/src/up-cli.ts
8901
9362
  import { spawn as spawn2 } from "node:child_process";
8902
9363
  import { homedir as homedir8 } from "node:os";
8903
- import { dirname as dirname10, join as join15 } from "node:path";
8904
- import { fileURLToPath as fileURLToPath2 } from "node:url";
9364
+ import { dirname as dirname11, join as join16 } from "node:path";
9365
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
8905
9366
  var DEFAULT_DAEMON_PORT, DEFAULT_WEBUI_PORT, DEFAULT_DAEMON_READY_TIMEOUT_MS, defaultStateDir3, defaultAttachSigint, runCliUp, parseUpFlags, requireValue4, waitForDaemonReady2, pipeWithPrefix, detachBackgroundDaemon2, nodeEntrypointArgs2, pipeStreamLines, once;
8906
9367
  var init_up_cli = __esm({
8907
9368
  "apps/cli/src/up-cli.ts"() {
8908
9369
  "use strict";
8909
9370
  init_src4();
9371
+ init_daemon_cli();
8910
9372
  DEFAULT_DAEMON_PORT = 7777;
8911
9373
  DEFAULT_WEBUI_PORT = 3e3;
8912
- DEFAULT_DAEMON_READY_TIMEOUT_MS = 1e4;
8913
- defaultStateDir3 = () => join15(homedir8(), ".scorel");
9374
+ DEFAULT_DAEMON_READY_TIMEOUT_MS = 3e4;
9375
+ defaultStateDir3 = () => join16(homedir8(), ".scorel");
8914
9376
  defaultAttachSigint = (listener) => {
8915
9377
  process.on("SIGINT", listener);
8916
9378
  return () => process.off("SIGINT", listener);
@@ -8925,7 +9387,7 @@ var init_up_cli = __esm({
8925
9387
  return 1;
8926
9388
  }
8927
9389
  const stateDir = options.stateDir ?? defaultStateDir3();
8928
- const cliEntrypoint = options.cliEntrypoint ?? fileURLToPath2(import.meta.url).replace(/up-cli\.ts$/, "index.ts");
9390
+ const cliEntrypoint = options.cliEntrypoint ?? fileURLToPath3(import.meta.url).replace(/up-cli\.ts$/, "index.ts");
8929
9391
  const spawnFn = options.spawn ?? spawn2;
8930
9392
  const readState = options.readState ?? ((dir) => readLocalDaemonState({ stateDir: dir }));
8931
9393
  const attachSigint = options.attachSigint ?? defaultAttachSigint;
@@ -8944,10 +9406,12 @@ var init_up_cli = __esm({
8944
9406
  String(flags.daemonPort),
8945
9407
  "--cwd",
8946
9408
  flags.cwd,
9409
+ "--idle-timeout-ms",
9410
+ String(AUTO_STARTED_IDLE_SHUTDOWN_MS),
8947
9411
  "--no-relay"
8948
9412
  ];
8949
9413
  daemonChild = spawnFn(process.execPath, daemonArgs, {
8950
- cwd: dirname10(cliEntrypoint),
9414
+ cwd: dirname11(cliEntrypoint),
8951
9415
  env: { ...process.env },
8952
9416
  detached: true,
8953
9417
  stdio: ["ignore", "pipe", "pipe"]
@@ -8977,7 +9441,7 @@ var init_up_cli = __esm({
8977
9441
  String(flags.webuiPort)
8978
9442
  ];
8979
9443
  const webuiChild = spawnFn(process.execPath, webuiArgs, {
8980
- cwd: dirname10(cliEntrypoint),
9444
+ cwd: dirname11(cliEntrypoint),
8981
9445
  env: { ...process.env },
8982
9446
  stdio: ["ignore", "pipe", "pipe"]
8983
9447
  });
@@ -9142,8 +9606,8 @@ var init_up_cli = __esm({
9142
9606
  // apps/cli/src/webui-cli.ts
9143
9607
  import { spawn as spawn3 } from "node:child_process";
9144
9608
  import { existsSync as existsSync4 } from "node:fs";
9145
- import { dirname as dirname11, resolve as resolve6 } from "node:path";
9146
- import { fileURLToPath as fileURLToPath3 } from "node:url";
9609
+ import { dirname as dirname12, resolve as resolve6 } from "node:path";
9610
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
9147
9611
  var DEFAULT_PORT3, DEFAULT_HOST3, runCliWebUi, findWebuiAppDir, buildWebUiSpawnPlan, parseWebUiFlags, requireValue5, waitForChildExit;
9148
9612
  var init_webui_cli = __esm({
9149
9613
  "apps/cli/src/webui-cli.ts"() {
@@ -9174,7 +9638,7 @@ var init_webui_cli = __esm({
9174
9638
  return await waitForChildExit(child, options);
9175
9639
  };
9176
9640
  findWebuiAppDir = () => {
9177
- let cursor = dirname11(fileURLToPath3(import.meta.url));
9641
+ let cursor = dirname12(fileURLToPath4(import.meta.url));
9178
9642
  for (let depth = 0; depth < 8; depth += 1) {
9179
9643
  const candidate = resolve6(cursor, "apps/webui/package.json");
9180
9644
  if (existsSync4(candidate)) {
@@ -9262,12 +9726,12 @@ __export(index_exports, {
9262
9726
  runChat: () => runChat,
9263
9727
  runCli: () => runCli
9264
9728
  });
9265
- import { createHash as createHash3 } from "node:crypto";
9266
- import { appendFile as appendFile4, mkdir as mkdir8, readFile as readFile13, realpath as realpath3, readdir as readdir7, writeFile as writeFile8 } from "node:fs/promises";
9729
+ import { createHash as createHash4 } from "node:crypto";
9730
+ import { appendFile as appendFile4, mkdir as mkdir8, readFile as readFile14, realpath as realpath3, readdir as readdir7, writeFile as writeFile8 } from "node:fs/promises";
9267
9731
  import { createInterface } from "node:readline/promises";
9268
9732
  import { homedir as homedir9 } from "node:os";
9269
- import { fileURLToPath as fileURLToPath4 } from "node:url";
9270
- import { basename as basename4, dirname as dirname12, join as join16 } from "node:path";
9733
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
9734
+ import { basename as basename4, dirname as dirname13, join as join17 } from "node:path";
9271
9735
  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;
9272
9736
  var init_index = __esm({
9273
9737
  async "apps/cli/src/index.ts"() {
@@ -9280,13 +9744,19 @@ var init_index = __esm({
9280
9744
  init_relay_server_cli();
9281
9745
  init_up_cli();
9282
9746
  init_webui_cli();
9747
+ init_update_cli();
9283
9748
  cliAppName = "@scorel/app-cli";
9284
9749
  cliClientDependency = clientPackageName;
9285
9750
  cliDaemonDependency = daemonPackageName;
9286
9751
  defaultSessionsDir = () => scorelSessionsDir(homedir9());
9287
- defaultStateDir4 = () => join16(homedir9(), ".scorel");
9752
+ defaultStateDir4 = () => join17(homedir9(), ".scorel");
9288
9753
  runCli = async (argv, io = { input: process.stdin, output: process.stdout, error: process.stderr }, runOptions = {}) => {
9289
9754
  const [command, ...rest] = argv;
9755
+ if (command === "--version" || command === "-v" || command === "version") {
9756
+ io.output.write(`${await readInstalledScorelVersion()}
9757
+ `);
9758
+ return 0;
9759
+ }
9290
9760
  if (!command || command === "chat") {
9291
9761
  if (rest.includes("--help") || rest.includes("-h")) {
9292
9762
  writeUsage(io.output);
@@ -9332,6 +9802,9 @@ var init_index = __esm({
9332
9802
  error: io.error
9333
9803
  });
9334
9804
  }
9805
+ if (command === "update" || command === "upgrade") {
9806
+ return runCliUpdate(rest, { output: io.output, error: io.error });
9807
+ }
9335
9808
  if (command === "attach") {
9336
9809
  try {
9337
9810
  return runAttach(parseAttachOptions(rest), {
@@ -9419,10 +9892,10 @@ var init_index = __esm({
9419
9892
  }
9420
9893
  };
9421
9894
  runLogs = async (options, io) => {
9422
- const filePath = options.attach ? await findAttachDiagnosticsFilePath(io.stateDir, options.sessionId, options.remoteUrl) : join16(io.sessionsDir, `${options.sessionId}.log`);
9895
+ const filePath = options.attach ? await findAttachDiagnosticsFilePath(io.stateDir, options.sessionId, options.remoteUrl) : join17(io.sessionsDir, `${options.sessionId}.log`);
9423
9896
  let content;
9424
9897
  try {
9425
- content = await readFile13(filePath, "utf8");
9898
+ content = await readFile14(filePath, "utf8");
9426
9899
  } catch (cause) {
9427
9900
  io.error.write(`scorel logs error: ${cause instanceof Error ? cause.message : String(cause)}
9428
9901
  `);
@@ -9575,32 +10048,32 @@ var init_index = __esm({
9575
10048
  };
9576
10049
  };
9577
10050
  attachCacheFilePath = (stateDir, scope, sessionId) => {
9578
- const scopeKey = createHash3("sha256").update(`${scope.kind}\0${scope.locator}`).digest("hex").slice(0, 24);
9579
- return join16(stateDir, "attach-cache", scopeKey, `${sessionId}.json`);
10051
+ const scopeKey = createHash4("sha256").update(`${scope.kind}\0${scope.locator}`).digest("hex").slice(0, 24);
10052
+ return join17(stateDir, "attach-cache", scopeKey, `${sessionId}.json`);
9580
10053
  };
9581
10054
  attachDiagnosticsFilePath = (stateDir, scope, sessionId) => {
9582
- const scopeKey = createHash3("sha256").update(`${scope.kind}\0${scope.locator}`).digest("hex").slice(0, 24);
9583
- return join16(stateDir, "attach-cache", scopeKey, `${sessionId}.log`);
10055
+ const scopeKey = createHash4("sha256").update(`${scope.kind}\0${scope.locator}`).digest("hex").slice(0, 24);
10056
+ return join17(stateDir, "attach-cache", scopeKey, `${sessionId}.log`);
9584
10057
  };
9585
10058
  findAttachDiagnosticsFilePath = async (stateDir, sessionId, _remoteUrl) => {
9586
- const root = join16(stateDir, "attach-cache");
10059
+ const root = join17(stateDir, "attach-cache");
9587
10060
  const scopes = await readdir7(root).catch(() => []);
9588
10061
  for (const scope of scopes) {
9589
- const candidate = join16(root, scope, `${sessionId}.log`);
10062
+ const candidate = join17(root, scope, `${sessionId}.log`);
9590
10063
  try {
9591
- await readFile13(candidate, "utf8");
10064
+ await readFile14(candidate, "utf8");
9592
10065
  return candidate;
9593
10066
  } catch {
9594
10067
  continue;
9595
10068
  }
9596
10069
  }
9597
- return join16(root, "__missing__", `${sessionId}.log`);
10070
+ return join17(root, "__missing__", `${sessionId}.log`);
9598
10071
  };
9599
10072
  stateDirFromSessionsDir = (sessionsDir) => {
9600
10073
  if (!sessionsDir) {
9601
10074
  return defaultStateDir4();
9602
10075
  }
9603
- return basename4(sessionsDir) === "sessions" ? dirname12(sessionsDir) : sessionsDir;
10076
+ return basename4(sessionsDir) === "sessions" ? dirname13(sessionsDir) : sessionsDir;
9604
10077
  };
9605
10078
  AttachDiagnostics = class {
9606
10079
  #stateDir;
@@ -9652,14 +10125,14 @@ var init_index = __esm({
9652
10125
  }
9653
10126
  const filePath = attachDiagnosticsFilePath(this.#stateDir, this.#scope, this.#sessionId);
9654
10127
  this.#writes.push(
9655
- mkdir8(dirname12(filePath), { recursive: true }).then(() => appendFile4(filePath, `${line}
10128
+ mkdir8(dirname13(filePath), { recursive: true }).then(() => appendFile4(filePath, `${line}
9656
10129
  `, "utf8"))
9657
10130
  );
9658
10131
  }
9659
10132
  };
9660
10133
  readAttachCache = async (stateDir, scope, sessionId) => {
9661
10134
  try {
9662
- const raw = JSON.parse(await readFile13(attachCacheFilePath(stateDir, scope, sessionId), "utf8"));
10135
+ const raw = JSON.parse(await readFile14(attachCacheFilePath(stateDir, scope, sessionId), "utf8"));
9663
10136
  if (raw.version !== 1 || raw.sessionId !== String(sessionId) || raw.scope.kind !== scope.kind || raw.scope.locator !== scope.locator || !Array.isArray(raw.events)) {
9664
10137
  return emptyAttachCacheSnapshot();
9665
10138
  }
@@ -9682,7 +10155,7 @@ var init_index = __esm({
9682
10155
  const filePath = attachCacheFilePath(stateDir, scope, sessionId);
9683
10156
  const uniqueEvents = mergePersistentEvents(snapshot.events);
9684
10157
  const transients = removeCompletedTransients(snapshot.transients, uniqueEvents);
9685
- await mkdir8(dirname12(filePath), { recursive: true });
10158
+ await mkdir8(dirname13(filePath), { recursive: true });
9686
10159
  await writeFile8(
9687
10160
  filePath,
9688
10161
  `${JSON.stringify({ version: 1, scope, sessionId: String(sessionId), events: uniqueEvents, transients }, null, 2)}
@@ -9824,14 +10297,16 @@ var init_index = __esm({
9824
10297
  const loadProjectConfigProfile = async (project2) => options.config ?? await loadScorelConfigProfile({ cwd: project2.workDir, ...configScope });
9825
10298
  const daemon = new ScorelHost({
9826
10299
  sessionsDir: options.sessionsDir,
9827
- projectsPath: join16(options.stateDir, "projects.json"),
10300
+ projectsPath: join17(options.stateDir, "projects.json"),
9828
10301
  deviceId: asDeviceId("device_local"),
9829
10302
  scorelHomeDir: options.stateDir,
9830
10303
  loadConfig: async ({ project: project2 }) => loadProjectConfig(project2),
9831
10304
  loadConfigProfile: async ({ project: project2 }) => loadProjectConfigProfile(project2),
9832
- createRuntime: async ({ project: project2, selectedModel, purpose }) => createRealRuntime({
10305
+ createRuntime: async ({ sessionId, project: project2, selectedModel, purpose }) => createRealRuntime({
9833
10306
  cwd: project2.workDir,
9834
10307
  config: await loadProjectConfig(project2),
10308
+ sessionsDir: options.sessionsDir,
10309
+ sessionId,
9835
10310
  modelSelection: selectedModel ? { modelId: selectedModel.modelId, role: selectedModel.role } : void 0,
9836
10311
  includeTools: purpose === "chat"
9837
10312
  })
@@ -9971,6 +10446,9 @@ var init_index = __esm({
9971
10446
  " scorel relay serve [--host <h>] [--port <p>] [--data-dir <dir>]",
9972
10447
  " scorel webui [--port <p>] [--host <h>]",
9973
10448
  " scorel up [--daemon-port <p>] [--webui-port <p>] [--cwd <d>]",
10449
+ " scorel update",
10450
+ " scorel upgrade",
10451
+ " scorel version",
9974
10452
  " scorel logs [--attach] --session <id> [--remote <ws-url>] [--tail <n>]",
9975
10453
  " scorel project list",
9976
10454
  " scorel project add <dir>",
@@ -10102,7 +10580,7 @@ ${text}
10102
10580
  if (!process.argv[1]) return false;
10103
10581
  const [argvPath, modulePath] = await Promise.all([
10104
10582
  realpath3(process.argv[1]).catch(() => process.argv[1]),
10105
- realpath3(fileURLToPath4(import.meta.url)).catch(() => fileURLToPath4(import.meta.url))
10583
+ realpath3(fileURLToPath5(import.meta.url)).catch(() => fileURLToPath5(import.meta.url))
10106
10584
  ]);
10107
10585
  return argvPath === modulePath;
10108
10586
  };