@pi-unipi/unipi 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -0
  3. package/docs/prefix-cache-architecture.md +89 -0
  4. package/package.json +22 -21
  5. package/packages/ask-user/package.json +2 -2
  6. package/packages/autocomplete/package.json +1 -1
  7. package/packages/autocomplete/src/constants.ts +2 -0
  8. package/packages/btw/package.json +2 -2
  9. package/packages/cocoindex/README.md +2 -1
  10. package/packages/cocoindex/index.ts +6 -13
  11. package/packages/cocoindex/package.json +4 -3
  12. package/packages/cocoindex/tools.ts +45 -11
  13. package/packages/compactor/README.md +4 -2
  14. package/packages/compactor/package.json +3 -3
  15. package/packages/compactor/src/session/snapshot.ts +3 -2
  16. package/packages/compactor/src/tools/register.ts +6 -2
  17. package/packages/compactor/src/tools/vcc-recall.ts +18 -3
  18. package/packages/core/bounded-output.ts +106 -0
  19. package/packages/core/constants.ts +2 -0
  20. package/packages/core/index.ts +1 -0
  21. package/packages/core/package.json +1 -1
  22. package/packages/footer/package.json +2 -2
  23. package/packages/image/package.json +2 -2
  24. package/packages/info-screen/package.json +2 -2
  25. package/packages/input-shortcuts/package.json +2 -2
  26. package/packages/kanboard/package.json +2 -2
  27. package/packages/mcp/README.md +4 -0
  28. package/packages/mcp/package.json +2 -2
  29. package/packages/mcp/src/bridge/translator.ts +19 -12
  30. package/packages/memory/index.ts +56 -34
  31. package/packages/memory/package.json +3 -3
  32. package/packages/milestone/hooks.ts +1 -1
  33. package/packages/milestone/package.json +2 -2
  34. package/packages/notify/package.json +2 -2
  35. package/packages/ralph/index.ts +12 -18
  36. package/packages/ralph/package.json +6 -3
  37. package/packages/ralph/reminder.ts +40 -0
  38. package/packages/ralph/tools.ts +5 -1
  39. package/packages/subagents/README.md +2 -0
  40. package/packages/subagents/package.json +1 -1
  41. package/packages/subagents/src/agent-manager.ts +5 -1
  42. package/packages/subagents/src/agent-runner.ts +2 -2
  43. package/packages/subagents/src/core-compat.ts +73 -0
  44. package/packages/subagents/src/custom-agents.ts +10 -2
  45. package/packages/subagents/src/index.ts +14 -3
  46. package/packages/subagents/src/types.ts +2 -0
  47. package/packages/unipi/bundled.js +786 -312
  48. package/packages/updater/package.json +2 -2
  49. package/packages/utility/README.md +9 -0
  50. package/packages/utility/package.json +2 -2
  51. package/packages/utility/src/index.ts +48 -0
  52. package/packages/utility/src/lifecycle/cleanup.ts +29 -0
  53. package/packages/utility/src/prefix-cache.ts +263 -0
  54. package/packages/utility/src/types.ts +1 -1
  55. package/packages/web-api/package.json +2 -2
  56. package/packages/workflow/index.ts +2 -2
  57. package/packages/workflow/package.json +2 -2
@@ -162,7 +162,8 @@ var init_constants = __esm({
162
162
  BADGE_GEN: "badge-gen",
163
163
  BADGE_TOGGLE: "badge-toggle",
164
164
  BADGE_SETTINGS: "badge-settings",
165
- UTIL_SETTINGS: "util-settings"
165
+ UTIL_SETTINGS: "util-settings",
166
+ PREFIX_CACHE: "prefix-cache"
166
167
  };
167
168
  UTILITY_TOOLS = {
168
169
  CONTINUE: "continue_task",
@@ -206,7 +207,8 @@ var init_constants = __esm({
206
207
  SYNC_INTERVAL_MS: 864e5,
207
208
  STARTUP_TIMEOUT_MS: 1e4,
208
209
  MAX_SERVERS: 20,
209
- TOOL_NAME_SEPARATOR: "__"
210
+ TOOL_NAME_SEPARATOR: "__",
211
+ MAX_MODEL_OUTPUT_BYTES: 64 * 1024
210
212
  };
211
213
  COMPACTOR_INSTRUCTION = "__compactor__";
212
214
  COMPACTOR_TOOLS = {
@@ -758,6 +760,92 @@ var init_tui_width = __esm({
758
760
  }
759
761
  });
760
762
 
763
+ // packages/core/bounded-output.ts
764
+ import { chmodSync, lstatSync, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
765
+ import { homedir } from "node:os";
766
+ import { join as join3 } from "node:path";
767
+ import { randomUUID } from "node:crypto";
768
+ function byteSlice(text, start, end) {
769
+ return Buffer.from(text, "utf8").subarray(start, end).toString("utf8").replace(/\uFFFD+$/u, "");
770
+ }
771
+ function secureArtifactDir(customDir) {
772
+ const dir = customDir ?? join3(homedir(), ".unipi", "tool-results");
773
+ mkdirSync3(dir, { recursive: true, mode: 448 });
774
+ let stat = lstatSync(dir);
775
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
776
+ throw new Error(`Refusing unsafe tool-result directory: ${dir}`);
777
+ }
778
+ if ((stat.mode & 63) !== 0) {
779
+ chmodSync(dir, 448);
780
+ stat = lstatSync(dir);
781
+ if ((stat.mode & 63) !== 0) {
782
+ throw new Error(`Refusing non-private tool-result directory: ${dir}`);
783
+ }
784
+ }
785
+ return dir;
786
+ }
787
+ function safePrefix(prefix) {
788
+ const safe = prefix.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").slice(0, 48);
789
+ return safe || "tool-result";
790
+ }
791
+ function boundModelOutput(text, options = {}) {
792
+ const maxBytes = Math.max(1024, Math.floor(options.maxBytes ?? DEFAULT_MODEL_OUTPUT_BYTES));
793
+ const originalBytes = Buffer.byteLength(text, "utf8");
794
+ if (originalBytes <= maxBytes) {
795
+ return { text, truncated: false, originalBytes, visibleBytes: originalBytes };
796
+ }
797
+ let artifactPath;
798
+ let artifactWarning;
799
+ if (originalBytes <= MAX_RAW_ARTIFACT_BYTES) {
800
+ try {
801
+ const dir = secureArtifactDir(options.artifactDir);
802
+ artifactPath = join3(dir, `${safePrefix(options.artifactPrefix ?? "tool-result")}-${randomUUID()}.txt`);
803
+ writeFileSync3(artifactPath, text, { encoding: "utf8", mode: 384, flag: "wx" });
804
+ } catch (error) {
805
+ artifactWarning = `Full-output artifact unavailable: ${error instanceof Error ? error.message : String(error)}`;
806
+ artifactPath = void 0;
807
+ }
808
+ } else {
809
+ artifactWarning = `Full output exceeded the ${MAX_RAW_ARTIFACT_BYTES}-byte local artifact safety cap and was not retained.`;
810
+ }
811
+ const marker = [
812
+ "",
813
+ "--- output bounded by UniPi ---",
814
+ artifactPath ? `Full output: ${artifactPath}` : artifactWarning,
815
+ `Original size: ${originalBytes} bytes; model-visible ceiling: ${maxBytes} bytes.`,
816
+ ...artifactPath ? ["Use the read tool with offset/limit to inspect only the needed region."] : []
817
+ ].join("\n");
818
+ const omissionReserve = 80;
819
+ const markerBytes = Buffer.byteLength(marker, "utf8");
820
+ const contentBudget = Math.max(1, maxBytes - markerBytes - omissionReserve);
821
+ const headBytes = Math.ceil(contentBudget * 0.75);
822
+ const tailBytes = Math.max(0, contentBudget - headBytes);
823
+ const head = byteSlice(text, 0, headBytes);
824
+ const tail2 = tailBytes > 0 ? byteSlice(text, originalBytes - tailBytes) : "";
825
+ const omission = `
826
+ \u2026 ${Math.max(0, originalBytes - contentBudget)} bytes omitted \u2026
827
+ `;
828
+ let bounded = `${head}${omission}${tail2}${marker}`;
829
+ if (Buffer.byteLength(bounded, "utf8") > maxBytes) {
830
+ bounded = byteSlice(bounded, 0, maxBytes);
831
+ }
832
+ return {
833
+ text: bounded,
834
+ truncated: true,
835
+ originalBytes,
836
+ visibleBytes: Buffer.byteLength(bounded, "utf8"),
837
+ artifactPath
838
+ };
839
+ }
840
+ var DEFAULT_MODEL_OUTPUT_BYTES, MAX_RAW_ARTIFACT_BYTES;
841
+ var init_bounded_output = __esm({
842
+ "packages/core/bounded-output.ts"() {
843
+ "use strict";
844
+ DEFAULT_MODEL_OUTPUT_BYTES = 64 * 1024;
845
+ MAX_RAW_ARTIFACT_BYTES = 16 * 1024 * 1024;
846
+ }
847
+ });
848
+
761
849
  // packages/core/index.ts
762
850
  var core_exports = {};
763
851
  __export(core_exports, {
@@ -773,6 +861,7 @@ __export(core_exports, {
773
861
  COMPACTOR_DIRS: () => COMPACTOR_DIRS,
774
862
  COMPACTOR_INSTRUCTION: () => COMPACTOR_INSTRUCTION,
775
863
  COMPACTOR_TOOLS: () => COMPACTOR_TOOLS,
864
+ DEFAULT_MODEL_OUTPUT_BYTES: () => DEFAULT_MODEL_OUTPUT_BYTES,
776
865
  FOOTER_COMMANDS: () => FOOTER_COMMANDS,
777
866
  IMAGE_COMMANDS: () => IMAGE_COMMANDS,
778
867
  IMAGE_DIRS: () => IMAGE_DIRS,
@@ -781,6 +870,7 @@ __export(core_exports, {
781
870
  KANBOARD_COMMANDS: () => KANBOARD_COMMANDS,
782
871
  KANBOARD_DEFAULTS: () => KANBOARD_DEFAULTS,
783
872
  KANBOARD_DIRS: () => KANBOARD_DIRS,
873
+ MAX_RAW_ARTIFACT_BYTES: () => MAX_RAW_ARTIFACT_BYTES,
784
874
  MCP_COMMANDS: () => MCP_COMMANDS,
785
875
  MCP_DEFAULTS: () => MCP_DEFAULTS,
786
876
  MCP_DIRS: () => MCP_DIRS,
@@ -815,6 +905,7 @@ __export(core_exports, {
815
905
  WORKFLOW_COMMANDS: () => WORKFLOW_COMMANDS,
816
906
  WidthKeyedCache: () => WidthKeyedCache,
817
907
  adaptiveInnerWidth: () => adaptiveInnerWidth,
908
+ boundModelOutput: () => boundModelOutput,
818
909
  boxInnerWidth: () => boxInnerWidth,
819
910
  contentWidth: () => contentWidth,
820
911
  emitEvent: () => emitEvent,
@@ -863,6 +954,7 @@ var init_core = __esm({
863
954
  init_utils();
864
955
  init_model_cache();
865
956
  init_tui_width();
957
+ init_bounded_output();
866
958
  }
867
959
  });
868
960
 
@@ -2330,12 +2422,12 @@ var init_embedding = __esm({
2330
2422
  });
2331
2423
 
2332
2424
  // packages/info-screen/usage-parser.ts
2333
- import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5, existsSync as existsSync9, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8, renameSync as renameSync3, openSync, readSync, closeSync } from "node:fs";
2334
- import { join as join9, dirname as dirname9 } from "node:path";
2335
- import { homedir as homedir5 } from "node:os";
2425
+ import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5, existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9, renameSync as renameSync3, openSync, readSync, closeSync } from "node:fs";
2426
+ import { join as join10, dirname as dirname9 } from "node:path";
2427
+ import { homedir as homedir6 } from "node:os";
2336
2428
  function getCachePath() {
2337
- const base = process.env.UNIPI_DIR || join9(homedir5(), ".unipi");
2338
- return join9(base, "cache", "usage-stats.json");
2429
+ const base = process.env.UNIPI_DIR || join10(homedir6(), ".unipi");
2430
+ return join10(base, "cache", "usage-stats.json");
2339
2431
  }
2340
2432
  function readCache() {
2341
2433
  const empty = { version: CACHE_VERSION, models: [], files: {} };
@@ -2353,16 +2445,16 @@ function readCache() {
2353
2445
  function writeCache(cache) {
2354
2446
  try {
2355
2447
  const path28 = getCachePath();
2356
- mkdirSync7(dirname9(path28), { recursive: true });
2448
+ mkdirSync8(dirname9(path28), { recursive: true });
2357
2449
  const tmp = `${path28}.${process.pid}.tmp`;
2358
- writeFileSync8(tmp, JSON.stringify(cache), "utf-8");
2450
+ writeFileSync9(tmp, JSON.stringify(cache), "utf-8");
2359
2451
  renameSync3(tmp, path28);
2360
2452
  } catch {
2361
2453
  }
2362
2454
  }
2363
2455
  function getSessionsDir() {
2364
- const agentDir = process.env.PI_CODING_AGENT_DIR || join9(homedir5(), ".pi", "agent");
2365
- return join9(agentDir, "sessions");
2456
+ const agentDir = process.env.PI_CODING_AGENT_DIR || join10(homedir6(), ".pi", "agent");
2457
+ return join10(agentDir, "sessions");
2366
2458
  }
2367
2459
  function getPeriodBounds() {
2368
2460
  const now2 = /* @__PURE__ */ new Date();
@@ -2454,7 +2546,7 @@ function collectSessionFiles(dir, files) {
2454
2546
  try {
2455
2547
  const entries = readdirSync5(dir, { withFileTypes: true });
2456
2548
  for (const entry of entries) {
2457
- const entryPath = join9(dir, entry.name);
2549
+ const entryPath = join10(dir, entry.name);
2458
2550
  if (entry.isDirectory()) {
2459
2551
  collectSessionFiles(entryPath, files);
2460
2552
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -2684,8 +2776,8 @@ __export(env_exports, {
2684
2776
  getEnvironmentInfo: () => getEnvironmentInfo
2685
2777
  });
2686
2778
  import { existsSync as existsSync18, readdirSync as readdirSync11 } from "node:fs";
2687
- import { join as join18, resolve as resolve6 } from "node:path";
2688
- import { homedir as homedir14 } from "node:os";
2779
+ import { join as join20, resolve as resolve6 } from "node:path";
2780
+ import { homedir as homedir16 } from "node:os";
2689
2781
  function getEnvironmentInfo() {
2690
2782
  const unipiModules = [];
2691
2783
  const configPaths = [];
@@ -2693,11 +2785,11 @@ function getEnvironmentInfo() {
2693
2785
  try {
2694
2786
  const nodeModules = resolve6(process.cwd(), "node_modules");
2695
2787
  if (existsSync18(nodeModules)) {
2696
- const scopePath = join18(nodeModules, "@pi-unipi");
2788
+ const scopePath = join20(nodeModules, "@pi-unipi");
2697
2789
  if (existsSync18(scopePath)) {
2698
2790
  for (const entry of readdirSync11(scopePath)) {
2699
2791
  if (entry.startsWith(".")) continue;
2700
- const pkgPath = join18(scopePath, entry, "package.json");
2792
+ const pkgPath = join20(scopePath, entry, "package.json");
2701
2793
  if (existsSync18(pkgPath)) {
2702
2794
  unipiModules.push(`@pi-unipi/${entry}`);
2703
2795
  }
@@ -2706,7 +2798,7 @@ function getEnvironmentInfo() {
2706
2798
  }
2707
2799
  } catch {
2708
2800
  }
2709
- const globalConfig = join18(homedir14(), ".unipi", "config");
2801
+ const globalConfig = join20(homedir16(), ".unipi", "config");
2710
2802
  const projectConfig = resolve6(process.cwd(), ".unipi", "config");
2711
2803
  if (existsSync18(globalConfig)) {
2712
2804
  configPaths.push(globalConfig);
@@ -2715,7 +2807,7 @@ function getEnvironmentInfo() {
2715
2807
  configPaths.push(projectConfig);
2716
2808
  }
2717
2809
  try {
2718
- const piDir = join18(homedir14(), ".pi");
2810
+ const piDir = join20(homedir16(), ".pi");
2719
2811
  if (existsSync18(piDir)) {
2720
2812
  extensionPaths.push(piDir);
2721
2813
  }
@@ -3952,11 +4044,11 @@ var init_schema = __esm({
3952
4044
  });
3953
4045
 
3954
4046
  // packages/compactor/src/config/manager.ts
3955
- import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync29, writeFileSync as writeFileSync23 } from "node:fs";
3956
- import { homedir as homedir20 } from "node:os";
3957
- import { dirname as dirname24, join as join32 } from "node:path";
4047
+ import { existsSync as existsSync31, mkdirSync as mkdirSync21, readFileSync as readFileSync29, writeFileSync as writeFileSync25 } from "node:fs";
4048
+ import { homedir as homedir22 } from "node:os";
4049
+ import { dirname as dirname24, join as join34 } from "node:path";
3958
4050
  function projectConfigPath(cwd) {
3959
- return join32(cwd, ".unipi", "config", "compactor.json");
4051
+ return join34(cwd, ".unipi", "config", "compactor.json");
3960
4052
  }
3961
4053
  function deepMerge(base, override) {
3962
4054
  const result = { ...base };
@@ -3992,8 +4084,8 @@ function saveConfig3(config, opts) {
3992
4084
  try {
3993
4085
  const targetPath = opts?.perProject && opts?.cwd ? projectConfigPath(opts.cwd) : COMPACTOR_CONFIG_PATH;
3994
4086
  const dir = dirname24(targetPath);
3995
- if (!existsSync31(dir)) mkdirSync19(dir, { recursive: true });
3996
- writeFileSync23(targetPath, `${JSON.stringify(config, null, 2)}
4087
+ if (!existsSync31(dir)) mkdirSync21(dir, { recursive: true });
4088
+ writeFileSync25(targetPath, `${JSON.stringify(config, null, 2)}
3997
4089
  `);
3998
4090
  return { success: true };
3999
4091
  } catch (err) {
@@ -4027,9 +4119,9 @@ function migrateConfig(partial) {
4027
4119
  function scaffoldConfig() {
4028
4120
  try {
4029
4121
  const dir = dirname24(COMPACTOR_CONFIG_PATH);
4030
- if (!existsSync31(dir)) mkdirSync19(dir, { recursive: true });
4122
+ if (!existsSync31(dir)) mkdirSync21(dir, { recursive: true });
4031
4123
  if (!existsSync31(COMPACTOR_CONFIG_PATH)) {
4032
- writeFileSync23(COMPACTOR_CONFIG_PATH, `${JSON.stringify(DEFAULT_COMPACTOR_CONFIG, null, 2)}
4124
+ writeFileSync25(COMPACTOR_CONFIG_PATH, `${JSON.stringify(DEFAULT_COMPACTOR_CONFIG, null, 2)}
4033
4125
  `);
4034
4126
  }
4035
4127
  } catch {
@@ -4040,7 +4132,7 @@ var init_manager = __esm({
4040
4132
  "packages/compactor/src/config/manager.ts"() {
4041
4133
  "use strict";
4042
4134
  init_schema();
4043
- COMPACTOR_CONFIG_PATH = join32(homedir20(), ".unipi", "config", "compactor", "config.json");
4135
+ COMPACTOR_CONFIG_PATH = join34(homedir22(), ".unipi", "config", "compactor", "config.json");
4044
4136
  readJson3 = (path28) => {
4045
4137
  try {
4046
4138
  return JSON.parse(readFileSync29(path28, "utf-8"));
@@ -5445,7 +5537,6 @@ function buildIntentSection(intentEvents) {
5445
5537
  function buildResumeSnapshot(events, opts) {
5446
5538
  const compactCount = opts?.compactCount ?? 1;
5447
5539
  const searchTool = opts?.searchTool ?? "ctx_search";
5448
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
5449
5540
  const fileEvents = [];
5450
5541
  const taskEvents = [];
5451
5542
  const ruleEvents = [];
@@ -5521,7 +5612,7 @@ function buildResumeSnapshot(events, opts) {
5521
5612
  if (skills) sections.push(skills);
5522
5613
  const intent = buildIntentSection(intentEvents);
5523
5614
  if (intent) sections.push(intent);
5524
- const header = `<session_resume events="${events.length}" compact_count="${compactCount}" generated_at="${now2}">`;
5615
+ const header = `<session_resume events="${events.length}" compact_count="${compactCount}">`;
5525
5616
  const footer = `</session_resume>`;
5526
5617
  const body = sections.join("\n\n");
5527
5618
  if (body) {
@@ -6316,7 +6407,7 @@ __export(policy_exports, {
6316
6407
  readsOrCreatesPolicy: () => readsOrCreatesPolicy
6317
6408
  });
6318
6409
  import { existsSync as existsSync36, readFileSync as readFileSync31 } from "node:fs";
6319
- import { join as join35 } from "node:path";
6410
+ import { join as join37 } from "node:path";
6320
6411
  function parseBashPattern(pattern) {
6321
6412
  const match = pattern.match(/^Bash\((.+)\)$/);
6322
6413
  return match ? match[1] : null;
@@ -6371,7 +6462,7 @@ function fileGlobToRegex(glob, caseInsensitive = false) {
6371
6462
  return new RegExp(`^${regexStr}$`, caseInsensitive ? "i" : "");
6372
6463
  }
6373
6464
  function readsOrCreatesPolicy(cwd) {
6374
- const settingsPath = join35(cwd, ".pi", "settings.json");
6465
+ const settingsPath = join37(cwd, ".pi", "settings.json");
6375
6466
  if (!existsSync36(settingsPath)) return { allow: [], deny: [], ask: [] };
6376
6467
  try {
6377
6468
  const settings = JSON.parse(readFileSync31(settingsPath, "utf-8"));
@@ -6400,10 +6491,10 @@ __export(evaluator_exports, {
6400
6491
  splitChainedCommands: () => splitChainedCommands
6401
6492
  });
6402
6493
  import { realpathSync as realpathSync2, existsSync as existsSync37, readFileSync as readFileSync32 } from "node:fs";
6403
- import { resolve as resolve12, join as join36 } from "node:path";
6404
- import { homedir as homedir22 } from "node:os";
6494
+ import { resolve as resolve12, join as join38 } from "node:path";
6495
+ import { homedir as homedir24 } from "node:os";
6405
6496
  function loadProjectPermissions(cwd, basePolicy) {
6406
- const settingsPath = join36(cwd, ".pi", "settings.json");
6497
+ const settingsPath = join38(cwd, ".pi", "settings.json");
6407
6498
  if (!existsSync37(settingsPath)) return basePolicy;
6408
6499
  try {
6409
6500
  const raw = readFileSync32(settingsPath, "utf-8");
@@ -6475,7 +6566,7 @@ function evaluateFilePath(filePath, policy, cwd = process.cwd()) {
6475
6566
  const resolved = resolve12(cwd, filePath);
6476
6567
  try {
6477
6568
  const real = realpathSync2(resolved);
6478
- const home = homedir22();
6569
+ const home = homedir24();
6479
6570
  if (!real.startsWith(cwd) && !real.startsWith(home)) {
6480
6571
  return "deny";
6481
6572
  }
@@ -6649,13 +6740,13 @@ import {
6649
6740
  // packages/workflow/commands.ts
6650
6741
  init_core();
6651
6742
  import { readFileSync as readFileSync3, readdirSync, existsSync as existsSync3, statSync as statSync2 } from "fs";
6652
- import { join as join3, basename } from "path";
6743
+ import { join as join4, basename } from "path";
6653
6744
  function suggestSpecFiles(prefix) {
6654
- const specsDir = join3(process.cwd(), ".unipi", "docs", "specs");
6745
+ const specsDir = join4(process.cwd(), ".unipi", "docs", "specs");
6655
6746
  if (!existsSync3(specsDir)) return [];
6656
6747
  try {
6657
6748
  const search2 = prefix?.trim().split(/\s+/).pop() ?? "";
6658
- const files = readdirSync(specsDir).filter((f) => f.endsWith(".md")).map((f) => ({ name: f, time: statSync2(join3(specsDir, f)).mtimeMs })).sort((a, b) => b.time - a.time);
6749
+ const files = readdirSync(specsDir).filter((f) => f.endsWith(".md")).map((f) => ({ name: f, time: statSync2(join4(specsDir, f)).mtimeMs })).sort((a, b) => b.time - a.time);
6659
6750
  return files.filter((f) => !search2 || f.name.includes(search2)).map((f) => ({
6660
6751
  value: `specs:${f.name}`,
6661
6752
  label: basename(f.name, ".md"),
@@ -6666,11 +6757,11 @@ function suggestSpecFiles(prefix) {
6666
6757
  }
6667
6758
  }
6668
6759
  function suggestPlanFiles(prefix) {
6669
- const plansDir = join3(process.cwd(), ".unipi", "docs", "plans");
6760
+ const plansDir = join4(process.cwd(), ".unipi", "docs", "plans");
6670
6761
  if (!existsSync3(plansDir)) return [];
6671
6762
  try {
6672
6763
  const search2 = prefix?.trim().split(/\s+/).pop() ?? "";
6673
- const files = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => ({ name: f, time: statSync2(join3(plansDir, f)).mtimeMs })).sort((a, b) => b.time - a.time);
6764
+ const files = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => ({ name: f, time: statSync2(join4(plansDir, f)).mtimeMs })).sort((a, b) => b.time - a.time);
6674
6765
  return files.filter((f) => !search2 || f.name.includes(search2)).map((f) => ({
6675
6766
  value: `plan:${f.name}`,
6676
6767
  label: basename(f.name, ".md"),
@@ -6681,11 +6772,11 @@ function suggestPlanFiles(prefix) {
6681
6772
  }
6682
6773
  }
6683
6774
  function suggestDebugFiles(prefix) {
6684
- const debugDir = join3(process.cwd(), ".unipi", "docs", "debug");
6775
+ const debugDir = join4(process.cwd(), ".unipi", "docs", "debug");
6685
6776
  if (!existsSync3(debugDir)) return [];
6686
6777
  try {
6687
6778
  const search2 = prefix?.trim().split(/\s+/).pop() ?? "";
6688
- const files = readdirSync(debugDir).filter((f) => f.endsWith(".md")).map((f) => ({ name: f, time: statSync2(join3(debugDir, f)).mtimeMs })).sort((a, b) => b.time - a.time);
6779
+ const files = readdirSync(debugDir).filter((f) => f.endsWith(".md")).map((f) => ({ name: f, time: statSync2(join4(debugDir, f)).mtimeMs })).sort((a, b) => b.time - a.time);
6689
6780
  return files.filter((f) => !search2 || f.name.includes(search2)).map((f) => ({
6690
6781
  value: `debug:${f.name}`,
6691
6782
  label: basename(f.name, ".md"),
@@ -6696,11 +6787,11 @@ function suggestDebugFiles(prefix) {
6696
6787
  }
6697
6788
  }
6698
6789
  function suggestChoreFiles(prefix) {
6699
- const choreDir = join3(process.cwd(), ".unipi", "docs", "chore");
6790
+ const choreDir = join4(process.cwd(), ".unipi", "docs", "chore");
6700
6791
  if (!existsSync3(choreDir)) return [];
6701
6792
  try {
6702
6793
  const search2 = prefix?.trim().split(/\s+/).pop() ?? "";
6703
- const files = readdirSync(choreDir).filter((f) => f.endsWith(".md")).map((f) => ({ name: f, time: statSync2(join3(choreDir, f)).mtimeMs })).sort((a, b) => b.time - a.time);
6794
+ const files = readdirSync(choreDir).filter((f) => f.endsWith(".md")).map((f) => ({ name: f, time: statSync2(join4(choreDir, f)).mtimeMs })).sort((a, b) => b.time - a.time);
6704
6795
  return files.filter((f) => !search2 || f.name.includes(search2)).map((f) => ({
6705
6796
  value: `chore:${f.name}`,
6706
6797
  label: basename(f.name, ".md"),
@@ -6716,7 +6807,7 @@ function suggestWorktrees() {
6716
6807
  if (worktreeSuggestionsCache?.cwd === cwd) {
6717
6808
  return worktreeSuggestionsCache.items;
6718
6809
  }
6719
- const worktreesDir = join3(cwd, ".unipi", "worktrees");
6810
+ const worktreesDir = join4(cwd, ".unipi", "worktrees");
6720
6811
  if (!existsSync3(worktreesDir)) {
6721
6812
  worktreeSuggestionsCache = { cwd, items: [] };
6722
6813
  return worktreeSuggestionsCache.items;
@@ -6725,7 +6816,7 @@ function suggestWorktrees() {
6725
6816
  let findWorktrees2 = function(dir, relativePath) {
6726
6817
  const entries = readdirSync(dir, { withFileTypes: true });
6727
6818
  for (const entry of entries) {
6728
- const fullPath = join3(dir, entry.name);
6819
+ const fullPath = join4(dir, entry.name);
6729
6820
  const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
6730
6821
  if (entry.name === ".git" && entry.isFile()) {
6731
6822
  try {
@@ -6924,7 +7015,7 @@ function registerWorkflowCommands(pi, options) {
6924
7015
  }
6925
7016
  let skillContent = "";
6926
7017
  try {
6927
- const skillPath = join3(
7018
+ const skillPath = join4(
6928
7019
  new URL("./skills", import.meta.url).pathname,
6929
7020
  cmd.skillName,
6930
7021
  "SKILL.md"
@@ -7513,7 +7604,7 @@ Use /unipi:ralph-resume <name> to continue`, "info");
7513
7604
  // packages/ralph/tools.ts
7514
7605
  init_core();
7515
7606
  import { Type } from "typebox";
7516
- function registerRalphTools(pi, manager2) {
7607
+ function registerRalphTools(pi, getManager2) {
7517
7608
  pi.registerTool({
7518
7609
  name: RALPH_TOOLS.START,
7519
7610
  label: "Start Ralph Loop",
@@ -7542,6 +7633,7 @@ function registerRalphTools(pi, manager2) {
7542
7633
  )
7543
7634
  }),
7544
7635
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
7636
+ const manager2 = getManager2(ctx);
7545
7637
  const taskFile = `.unipi/ralph/${params.name.replace(/[^a-zA-Z0-9_-]/g, "_")}.md`;
7546
7638
  if (manager2.loadState(params.name)?.status === "active") {
7547
7639
  return {
@@ -7582,6 +7674,7 @@ function registerRalphTools(pi, manager2) {
7582
7674
  ],
7583
7675
  parameters: Type.Object({}),
7584
7676
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
7677
+ const manager2 = getManager2(ctx);
7585
7678
  if (!manager2.getCurrentLoop()) {
7586
7679
  return {
7587
7680
  content: [{ type: "text", text: "No active Ralph loop." }],
@@ -7639,6 +7732,38 @@ function registerRalphTools(pi, manager2) {
7639
7732
  });
7640
7733
  }
7641
7734
 
7735
+ // packages/ralph/reminder.ts
7736
+ init_core();
7737
+ var RALPH_REMINDER_TYPE = "unipi-ralph-loop-reminder";
7738
+ function buildRalphLoopReminder(state) {
7739
+ const iterStr = `${state.iteration}${state.maxIterations > 0 ? `/${state.maxIterations}` : ""}`;
7740
+ let instructions = "This snapshot supersedes all earlier Ralph loop reminders.\n";
7741
+ instructions += `You are in a Ralph loop working on: ${state.taskFile}
7742
+ `;
7743
+ if (state.itemsPerIteration > 0) {
7744
+ instructions += `- Work on ~${state.itemsPerIteration} items this iteration
7745
+ `;
7746
+ }
7747
+ instructions += "- Update the task file as you progress\n";
7748
+ instructions += `- When FULLY COMPLETE: ${RALPH_COMPLETE_MARKER}
7749
+ `;
7750
+ instructions += "- Otherwise, call ralph_done tool to proceed to next iteration";
7751
+ return `[RALPH LOOP - ${state.name} - Iteration ${iterStr}]
7752
+
7753
+ ${instructions}`;
7754
+ }
7755
+ function latestRalphReminder(ctx) {
7756
+ const branch = ctx.sessionManager.getBranch();
7757
+ for (let index = branch.length - 1; index >= 0; index--) {
7758
+ const entry = branch[index];
7759
+ if (entry.type === "custom_message" && entry.customType === RALPH_REMINDER_TYPE) {
7760
+ return typeof entry.content === "string" ? entry.content : null;
7761
+ }
7762
+ if (entry.type === "compaction") break;
7763
+ }
7764
+ return null;
7765
+ }
7766
+
7642
7767
  // packages/ralph/index.ts
7643
7768
  function getInfoRegistry() {
7644
7769
  return globalThis.__unipi_info_registry;
@@ -7655,6 +7780,7 @@ function getManager(ctx, pi) {
7655
7780
  return manager;
7656
7781
  }
7657
7782
  function ralph_default(pi) {
7783
+ registerRalphTools(pi, (ctx) => getManager(ctx, pi));
7658
7784
  registerCommands(pi);
7659
7785
  pi.on("session_start", async (_event, ctx) => {
7660
7786
  manager = null;
@@ -7735,24 +7861,12 @@ function ralph_default(pi) {
7735
7861
  if (!currentLoop) return;
7736
7862
  const state = mgr.loadState(currentLoop);
7737
7863
  if (!state || state.status !== "active") return;
7738
- const iterStr = `${state.iteration}${state.maxIterations > 0 ? `/${state.maxIterations}` : ""}`;
7739
- let instructions = `You are in a Ralph loop working on: ${state.taskFile}
7740
- `;
7741
- if (state.itemsPerIteration > 0) {
7742
- instructions += `- Work on ~${state.itemsPerIteration} items this iteration
7743
- `;
7744
- }
7745
- instructions += `- Update the task file as you progress
7746
- `;
7747
- instructions += `- When FULLY COMPLETE: ${RALPH_COMPLETE_MARKER}
7748
- `;
7749
- instructions += `- Otherwise, call ralph_done tool to proceed to next iteration`;
7864
+ const content = buildRalphLoopReminder(state);
7865
+ if (latestRalphReminder(ctx) === content) return;
7750
7866
  return {
7751
7867
  message: {
7752
- customType: "unipi-ralph-loop-reminder",
7753
- content: `[RALPH LOOP - ${state.name} - Iteration ${iterStr}]
7754
-
7755
- ${instructions}`,
7868
+ customType: RALPH_REMINDER_TYPE,
7869
+ content,
7756
7870
  display: false
7757
7871
  }
7758
7872
  };
@@ -7766,10 +7880,6 @@ ${instructions}`,
7766
7880
  }
7767
7881
  manager = null;
7768
7882
  });
7769
- pi.on("session_start", async (_event, ctx) => {
7770
- const mgr = getManager(ctx, pi);
7771
- registerRalphTools(pi, mgr);
7772
- });
7773
7883
  }
7774
7884
  function registerCommands(pi) {
7775
7885
  const HELP = `Ralph Loop \u2014 Long-running development loops
@@ -8779,6 +8889,36 @@ function getInfoRegistry2() {
8779
8889
  return globalThis.__unipi_info_registry;
8780
8890
  }
8781
8891
  var VERSION3 = getPackageVersion(dirname7(fileURLToPath4(import.meta.url)));
8892
+ function buildMemoryRecallReminder(input) {
8893
+ const lines = [
8894
+ "## \u{1F9E0} Memory System Active",
8895
+ "",
8896
+ `You have ${input.memories.length} memories stored for project "${input.projectName}".`
8897
+ ];
8898
+ if (input.canSearch && input.memories.length > 0) {
8899
+ const titleList = input.memories.slice(0, 20).map((memory) => `- ${memory.title}`).join("\n");
8900
+ const extra = input.memories.length > 20 ? `
8901
+ ... and ${input.memories.length - 20} more` : "";
8902
+ lines.push(
8903
+ "**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
8904
+ "",
8905
+ "Available memories:",
8906
+ titleList + extra
8907
+ );
8908
+ }
8909
+ if (input.canStore) {
8910
+ lines.push(
8911
+ "",
8912
+ "**AFTER completing the task**, if you learned something non-obvious,",
8913
+ "call `memory_store` to save it for future sessions."
8914
+ );
8915
+ }
8916
+ lines.push(
8917
+ "",
8918
+ "Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates."
8919
+ );
8920
+ return lines.join("\n");
8921
+ }
8782
8922
  var projectStorage = null;
8783
8923
  var orphanSyncPending = false;
8784
8924
  function ensureOrphanSync(storage) {
@@ -8932,41 +9072,17 @@ function memory_default(pi) {
8932
9072
  recallDone = true;
8933
9073
  return;
8934
9074
  }
8935
- const lines = [
8936
- "## \u{1F9E0} Memory System Active",
8937
- "",
8938
- `You have ${projectMemories.length} memories stored for project "${projectName}".`
8939
- ];
8940
- if (canSearch && projectMemories.length > 0) {
8941
- const titleList = projectMemories.slice(0, 20).map((m) => `- ${m.title}`).join("\n");
8942
- const extra = projectMemories.length > 20 ? `
8943
- ... and ${projectMemories.length - 20} more` : "";
8944
- lines.push(
8945
- "**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
8946
- "",
8947
- "Available memories:",
8948
- titleList + extra
8949
- );
8950
- } else {
8951
- recallDone = true;
8952
- }
8953
- if (canStore) {
8954
- lines.push(
8955
- "",
8956
- "**AFTER completing the task**, if you learned something non-obvious,",
8957
- "call `memory_store` to save it for future sessions."
8958
- );
8959
- } else {
8960
- storeDone = true;
8961
- }
8962
- lines.push(
8963
- "",
8964
- "Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates."
8965
- );
9075
+ if (!canSearch || projectMemories.length === 0) recallDone = true;
9076
+ if (!canStore) storeDone = true;
8966
9077
  return {
8967
9078
  message: {
8968
9079
  customType: "unipi-memory-recall-reminder",
8969
- content: lines.join("\n"),
9080
+ content: buildMemoryRecallReminder({
9081
+ projectName,
9082
+ memories: projectMemories,
9083
+ canSearch,
9084
+ canStore
9085
+ }),
8970
9086
  display: false
8971
9087
  }
8972
9088
  };
@@ -9004,9 +9120,9 @@ import { dirname as dirname10 } from "node:path";
9004
9120
  import { fileURLToPath as fileURLToPath5 } from "node:url";
9005
9121
 
9006
9122
  // packages/info-screen/config.ts
9007
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync6 } from "node:fs";
9008
- import { join as join8, dirname as dirname8 } from "node:path";
9009
- import { homedir as homedir4 } from "node:os";
9123
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync7 } from "node:fs";
9124
+ import { join as join9, dirname as dirname8 } from "node:path";
9125
+ import { homedir as homedir5 } from "node:os";
9010
9126
 
9011
9127
  // packages/info-screen/types.ts
9012
9128
  var BOOT_MODES = ["on", "auto-close", "off"];
@@ -9018,7 +9134,7 @@ var DEFAULT_SETTINGS = {
9018
9134
  };
9019
9135
 
9020
9136
  // packages/info-screen/config.ts
9021
- var SETTINGS_PATH = join8(homedir4(), ".pi", "agent", "settings.json");
9137
+ var SETTINGS_PATH = join9(homedir5(), ".pi", "agent", "settings.json");
9022
9138
  var SETTINGS_KEY = "unipi";
9023
9139
  var cachedSettings = null;
9024
9140
  function isRecord(value) {
@@ -9036,9 +9152,9 @@ function readSettingsFile() {
9036
9152
  function writeSettingsFile(data) {
9037
9153
  const dir = dirname8(SETTINGS_PATH);
9038
9154
  if (!existsSync8(dir)) {
9039
- mkdirSync6(dir, { recursive: true });
9155
+ mkdirSync7(dir, { recursive: true });
9040
9156
  }
9041
- writeFileSync7(SETTINGS_PATH, JSON.stringify(data, null, 2) + "\n", "utf-8");
9157
+ writeFileSync8(SETTINGS_PATH, JSON.stringify(data, null, 2) + "\n", "utf-8");
9042
9158
  }
9043
9159
  function getInfoSettings() {
9044
9160
  if (cachedSettings) return cachedSettings;
@@ -9336,16 +9452,16 @@ if (!globalThis.__unipi_info_registry) {
9336
9452
  // packages/info-screen/core-groups.ts
9337
9453
  init_core();
9338
9454
  import { readFileSync as readFileSync9, readdirSync as readdirSync6, existsSync as existsSync10 } from "node:fs";
9339
- import { join as join10 } from "node:path";
9340
- import { homedir as homedir6 } from "node:os";
9455
+ import { join as join11 } from "node:path";
9456
+ import { homedir as homedir7 } from "node:os";
9341
9457
  init_usage_parser();
9342
9458
  function discoverExtensions() {
9343
9459
  const extensions = [];
9344
- const homeDir = process.env.HOME || process.env.USERPROFILE || homedir6();
9460
+ const homeDir = process.env.HOME || process.env.USERPROFILE || homedir7();
9345
9461
  const cwd = process.cwd();
9346
9462
  const settingsPaths = [
9347
- join10(homeDir, ".pi", "agent", "settings.json"),
9348
- join10(cwd, ".pi", "settings.json")
9463
+ join11(homeDir, ".pi", "agent", "settings.json"),
9464
+ join11(cwd, ".pi", "settings.json")
9349
9465
  ];
9350
9466
  const counted = /* @__PURE__ */ new Set();
9351
9467
  for (const settingsPath of settingsPaths) {
@@ -9390,8 +9506,8 @@ function discoverExtensions() {
9390
9506
  }
9391
9507
  }
9392
9508
  const extensionDirs = [
9393
- join10(homeDir, ".pi", "agent", "extensions"),
9394
- join10(cwd, ".pi", "extensions")
9509
+ join11(homeDir, ".pi", "agent", "extensions"),
9510
+ join11(cwd, ".pi", "extensions")
9395
9511
  ];
9396
9512
  for (const dir of extensionDirs) {
9397
9513
  if (!existsSync10(dir)) continue;
@@ -9458,11 +9574,11 @@ function getTotalLoadTime() {
9458
9574
  var extraSkillDirs = [];
9459
9575
  function discoverSkills() {
9460
9576
  const skills = [];
9461
- const homeDir = process.env.HOME || process.env.USERPROFILE || homedir6();
9577
+ const homeDir = process.env.HOME || process.env.USERPROFILE || homedir7();
9462
9578
  const cwd = process.cwd();
9463
9579
  const skillDirs = [
9464
- join10(homeDir, ".pi", "agent", "skills"),
9465
- join10(cwd, ".pi", "skills"),
9580
+ join11(homeDir, ".pi", "agent", "skills"),
9581
+ join11(cwd, ".pi", "skills"),
9466
9582
  // Add extra dirs from extensions
9467
9583
  ...extraSkillDirs
9468
9584
  ];
@@ -9475,13 +9591,13 @@ function discoverSkills() {
9475
9591
  if (!entry.isDirectory()) continue;
9476
9592
  const name = entry.name;
9477
9593
  if (counted.has(name)) continue;
9478
- const skillPath = join10(dir, name, "SKILL.md");
9594
+ const skillPath = join11(dir, name, "SKILL.md");
9479
9595
  if (existsSync10(skillPath)) {
9480
9596
  counted.add(name);
9481
9597
  let source = "extension";
9482
- if (dir.includes(join10(homeDir, ".pi"))) {
9598
+ if (dir.includes(join11(homeDir, ".pi"))) {
9483
9599
  source = "global";
9484
- } else if (dir === join10(cwd, ".pi", "skills")) {
9600
+ } else if (dir === join11(cwd, ".pi", "skills")) {
9485
9601
  source = "project";
9486
9602
  }
9487
9603
  skills.push({ name, source });
@@ -9532,7 +9648,7 @@ function registerCoreGroups() {
9532
9648
  },
9533
9649
  dataProvider: async () => {
9534
9650
  const cwd = process.cwd();
9535
- const homeDir = process.env.HOME || process.env.USERPROFILE || homedir6();
9651
+ const homeDir = process.env.HOME || process.env.USERPROFILE || homedir7();
9536
9652
  const shortCwd = cwd.startsWith(homeDir) ? `~${cwd.slice(homeDir.length)}` : cwd;
9537
9653
  const announced = getAnnouncedModules();
9538
9654
  const registeredGroups = infoRegistry.getAllGroups();
@@ -10725,10 +10841,14 @@ import { defineTool } from "@earendil-works/pi-coding-agent";
10725
10841
  import { Text } from "@earendil-works/pi-tui";
10726
10842
  import { Type as Type3 } from "typebox";
10727
10843
  import { existsSync as existsSync13, readdirSync as readdirSync8 } from "node:fs";
10728
- import { join as join13 } from "node:path";
10729
- import { homedir as homedir9 } from "node:os";
10844
+ import { join as join15 } from "node:path";
10845
+ import { homedir as homedir11 } from "node:os";
10730
10846
 
10731
10847
  // packages/subagents/src/core-compat.ts
10848
+ import { chmodSync as chmodSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "node:fs";
10849
+ import { homedir as homedir8 } from "node:os";
10850
+ import { join as join12 } from "node:path";
10851
+ import { randomUUID as randomUUID2 } from "node:crypto";
10732
10852
  var MODULES2 = { SUBAGENTS: "subagents" };
10733
10853
  var UNIPI_EVENTS2 = {
10734
10854
  MODULE_READY: "unipi:module:ready",
@@ -10742,6 +10862,61 @@ function emitEvent2(pi, eventName, payload) {
10742
10862
  return false;
10743
10863
  }
10744
10864
  }
10865
+ var MAX_RAW_HELPER_ARTIFACT_BYTES = 16 * 1024 * 1024;
10866
+ function boundHelperOutput(text, maxBytes = 64 * 1024, existingArtifactPath) {
10867
+ const originalBytes = Buffer.byteLength(text, "utf8");
10868
+ if (originalBytes <= maxBytes) return { text, truncated: false, originalBytes };
10869
+ let artifactPath = existingArtifactPath;
10870
+ let artifactWarning;
10871
+ if (!artifactPath && originalBytes <= MAX_RAW_HELPER_ARTIFACT_BYTES) {
10872
+ try {
10873
+ const dir = join12(homedir8(), ".unipi", "tool-results");
10874
+ mkdirSync9(dir, { recursive: true, mode: 448 });
10875
+ let stat = lstatSync2(dir);
10876
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
10877
+ throw new Error(`Refusing unsafe tool-result directory: ${dir}`);
10878
+ }
10879
+ if ((stat.mode & 63) !== 0) {
10880
+ chmodSync2(dir, 448);
10881
+ stat = lstatSync2(dir);
10882
+ if ((stat.mode & 63) !== 0) throw new Error(`Refusing non-private tool-result directory: ${dir}`);
10883
+ }
10884
+ artifactPath = join12(dir, `helper-${randomUUID2()}.txt`);
10885
+ writeFileSync10(artifactPath, text, { encoding: "utf8", mode: 384, flag: "wx" });
10886
+ } catch (error) {
10887
+ artifactWarning = `Full-output artifact unavailable: ${error instanceof Error ? error.message : String(error)}`;
10888
+ artifactPath = void 0;
10889
+ }
10890
+ } else if (!artifactPath) {
10891
+ artifactWarning = `Full output exceeded the ${MAX_RAW_HELPER_ARTIFACT_BYTES}-byte local artifact safety cap and was not retained.`;
10892
+ }
10893
+ const marker = [
10894
+ "",
10895
+ "--- output bounded by UniPi ---",
10896
+ artifactPath ? `Full output: ${artifactPath}` : artifactWarning,
10897
+ `Original size: ${originalBytes} bytes; model-visible ceiling: ${maxBytes} bytes.`,
10898
+ ...artifactPath ? ["Use the read tool with offset/limit to inspect only the needed region."] : []
10899
+ ].join("\n");
10900
+ const markerBytes = Buffer.byteLength(marker, "utf8");
10901
+ const contentBudget = Math.max(1, maxBytes - markerBytes - 80);
10902
+ const bytes = Buffer.from(text, "utf8");
10903
+ const headBytes = Math.ceil(contentBudget * 0.75);
10904
+ const tailBytes = Math.max(0, contentBudget - headBytes);
10905
+ const head = bytes.subarray(0, headBytes).toString("utf8").replace(/\uFFFD+$/u, "");
10906
+ const tail2 = bytes.subarray(originalBytes - tailBytes).toString("utf8").replace(/^\uFFFD+/u, "");
10907
+ let bounded = `${head}
10908
+ \u2026 ${Math.max(0, originalBytes - contentBudget)} bytes omitted \u2026
10909
+ ${tail2}${marker}`;
10910
+ if (Buffer.byteLength(bounded, "utf8") > maxBytes) {
10911
+ bounded = Buffer.from(bounded, "utf8").subarray(0, maxBytes).toString("utf8").replace(/\uFFFD+$/u, "");
10912
+ }
10913
+ return {
10914
+ text: bounded,
10915
+ truncated: true,
10916
+ originalBytes,
10917
+ artifactPath
10918
+ };
10919
+ }
10745
10920
  async function withHerdrBlocked2(pi, label, fn) {
10746
10921
  emitEvent2(pi, "herdr:blocked", { active: true, label });
10747
10922
  try {
@@ -10752,7 +10927,7 @@ async function withHerdrBlocked2(pi, label, fn) {
10752
10927
  }
10753
10928
 
10754
10929
  // packages/subagents/src/agent-manager.ts
10755
- import { randomUUID } from "node:crypto";
10930
+ import { randomUUID as randomUUID3 } from "node:crypto";
10756
10931
 
10757
10932
  // packages/subagents/src/agent-runner.ts
10758
10933
  import {
@@ -10805,7 +10980,7 @@ var BUILTIN_CONFIGS = {
10805
10980
  };
10806
10981
 
10807
10982
  // packages/subagents/src/agent-runner.ts
10808
- var EXCLUDED_TOOL_NAMES = ["Agent", "get_result"];
10983
+ var EXCLUDED_TOOL_NAMES = ["spawn_helper", "get_helper_result", "Agent", "get_result"];
10809
10984
  var BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
10810
10985
  var defaultMaxTurns;
10811
10986
  var graceTurns = 5;
@@ -10964,9 +11139,12 @@ init_model_resolver();
10964
11139
 
10965
11140
  // packages/subagents/src/custom-agents.ts
10966
11141
  import { existsSync as existsSync11, readdirSync as readdirSync7, readFileSync as readFileSync10, renameSync as renameSync4 } from "node:fs";
10967
- import { join as join11 } from "node:path";
10968
- import { homedir as homedir7 } from "node:os";
11142
+ import { join as join13 } from "node:path";
11143
+ import { homedir as homedir9 } from "node:os";
10969
11144
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
11145
+ function compareCodeUnits(a, b) {
11146
+ return a < b ? -1 : a > b ? 1 : 0;
11147
+ }
10970
11148
  function backupCorrupted(filePath) {
10971
11149
  const backupPath = filePath + ".bak";
10972
11150
  try {
@@ -10975,10 +11153,10 @@ function backupCorrupted(filePath) {
10975
11153
  }
10976
11154
  }
10977
11155
  function getProjectAgentsDir(cwd) {
10978
- return join11(cwd, ".unipi", "config", "agents");
11156
+ return join13(cwd, ".unipi", "config", "agents");
10979
11157
  }
10980
11158
  function getGlobalAgentsDir() {
10981
- return join11(homedir7(), ".unipi", "config", "agents");
11159
+ return join13(homedir9(), ".unipi", "config", "agents");
10982
11160
  }
10983
11161
  var BUILTIN_TOOL_NAMES2 = ["read", "bash", "edit", "write", "grep", "find", "ls"];
10984
11162
  function loadAgentFromFile(filePath, source) {
@@ -11019,9 +11197,9 @@ function loadCustomAgents(cwd) {
11019
11197
  const agents = /* @__PURE__ */ new Map();
11020
11198
  const globalDir = getGlobalAgentsDir();
11021
11199
  if (existsSync11(globalDir)) {
11022
- const files = readdirSync7(globalDir).filter((f) => f.endsWith(".md"));
11200
+ const files = readdirSync7(globalDir).filter((f) => f.endsWith(".md")).sort(compareCodeUnits);
11023
11201
  for (const file of files) {
11024
- const agent = loadAgentFromFile(join11(globalDir, file), "global");
11202
+ const agent = loadAgentFromFile(join13(globalDir, file), "global");
11025
11203
  if (agent) {
11026
11204
  agents.set(agent.name, agent);
11027
11205
  }
@@ -11029,9 +11207,9 @@ function loadCustomAgents(cwd) {
11029
11207
  }
11030
11208
  const projectDir = getProjectAgentsDir(cwd);
11031
11209
  if (existsSync11(projectDir)) {
11032
- const files = readdirSync7(projectDir).filter((f) => f.endsWith(".md"));
11210
+ const files = readdirSync7(projectDir).filter((f) => f.endsWith(".md")).sort(compareCodeUnits);
11033
11211
  for (const file of files) {
11034
- const agent = loadAgentFromFile(join11(projectDir, file), "project");
11212
+ const agent = loadAgentFromFile(join13(projectDir, file), "project");
11035
11213
  if (agent) {
11036
11214
  agents.set(agent.name, agent);
11037
11215
  }
@@ -11124,6 +11302,9 @@ var FileLock = class {
11124
11302
  };
11125
11303
 
11126
11304
  // packages/subagents/src/agent-manager.ts
11305
+ function compareCodeUnits2(a, b) {
11306
+ return a < b ? -1 : a > b ? 1 : 0;
11307
+ }
11127
11308
  var DEFAULT_MAX_CONCURRENT = 4;
11128
11309
  var AgentManager = class {
11129
11310
  agents = /* @__PURE__ */ new Map();
@@ -11157,7 +11338,7 @@ var AgentManager = class {
11157
11338
  ...Object.keys(BUILTIN_CONFIGS).filter((type) => type !== "name-gen"),
11158
11339
  ...this.customAgents.keys(),
11159
11340
  ...Object.keys(this.typeSettings)
11160
- ])];
11341
+ ])].sort(compareCodeUnits2);
11161
11342
  }
11162
11343
  /** A type is enabled only when both JSON config and agent frontmatter allow it. */
11163
11344
  isTypeEnabled(type) {
@@ -11181,7 +11362,7 @@ var AgentManager = class {
11181
11362
  */
11182
11363
  spawn(pi, ctx, type, prompt, options) {
11183
11364
  this.assertTypeEnabled(type);
11184
- const id = randomUUID().slice(0, 17);
11365
+ const id = randomUUID3().slice(0, 17);
11185
11366
  const abortController = new AbortController();
11186
11367
  const record = {
11187
11368
  id,
@@ -11384,9 +11565,9 @@ var AgentManager = class {
11384
11565
  };
11385
11566
 
11386
11567
  // packages/subagents/src/config.ts
11387
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync11, renameSync as renameSync5, writeFileSync as writeFileSync9 } from "node:fs";
11388
- import { join as join12 } from "node:path";
11389
- import { homedir as homedir8 } from "node:os";
11568
+ import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync11, renameSync as renameSync5, writeFileSync as writeFileSync11 } from "node:fs";
11569
+ import { join as join14 } from "node:path";
11570
+ import { homedir as homedir10 } from "node:os";
11390
11571
  var DEFAULT_CONFIG2 = {
11391
11572
  maxConcurrent: 4,
11392
11573
  enabled: true,
@@ -11396,19 +11577,19 @@ var DEFAULT_CONFIG2 = {
11396
11577
  }
11397
11578
  };
11398
11579
  function getGlobalConfigPath() {
11399
- return join12(homedir8(), ".unipi", "config", "subagents.json");
11580
+ return join14(homedir10(), ".unipi", "config", "subagents.json");
11400
11581
  }
11401
11582
  function getWorkspaceConfigPath(cwd) {
11402
- return join12(cwd, ".unipi", "config", "subagents.json");
11583
+ return join14(cwd, ".unipi", "config", "subagents.json");
11403
11584
  }
11404
11585
  function ensureDirExists(dirPath) {
11405
11586
  if (!existsSync12(dirPath)) {
11406
- mkdirSync8(dirPath, { recursive: true });
11587
+ mkdirSync10(dirPath, { recursive: true });
11407
11588
  }
11408
11589
  }
11409
11590
  function writeConfigAtomic(filePath, config) {
11410
11591
  const tmpPath = filePath + ".tmp";
11411
- writeFileSync9(tmpPath, JSON.stringify(config, null, 2), "utf-8");
11592
+ writeFileSync11(tmpPath, JSON.stringify(config, null, 2), "utf-8");
11412
11593
  renameSync5(tmpPath, filePath);
11413
11594
  }
11414
11595
  function loadConfigFromPath(filePath) {
@@ -11433,17 +11614,17 @@ function repairCorrupted(filePath) {
11433
11614
  }
11434
11615
  function initConfig(cwd) {
11435
11616
  const globalPath = getGlobalConfigPath();
11436
- const globalDir = join12(homedir8(), ".unipi", "config");
11437
- const globalAgentsDir = join12(homedir8(), ".unipi", "config", "agents");
11617
+ const globalDir = join14(homedir10(), ".unipi", "config");
11618
+ const globalAgentsDir = join14(homedir10(), ".unipi", "config", "agents");
11438
11619
  ensureDirExists(globalDir);
11439
11620
  ensureDirExists(globalAgentsDir);
11440
11621
  let globalConfig = loadConfigFromPath(globalPath);
11441
11622
  if (globalConfig === null) {
11442
11623
  globalConfig = repairCorrupted(globalPath);
11443
11624
  }
11444
- const workspaceDir = join12(cwd, ".unipi", "config");
11445
- const workspaceAgentsDir = join12(cwd, ".unipi", "config", "agents");
11446
- if (cwd && !cwd.startsWith(homedir8())) {
11625
+ const workspaceDir = join14(cwd, ".unipi", "config");
11626
+ const workspaceAgentsDir = join14(cwd, ".unipi", "config", "agents");
11627
+ if (cwd && !cwd.startsWith(homedir10())) {
11447
11628
  ensureDirExists(workspaceDir);
11448
11629
  ensureDirExists(workspaceAgentsDir);
11449
11630
  }
@@ -12107,10 +12288,10 @@ function getStatusLabel(status2, error) {
12107
12288
  function src_default(pi) {
12108
12289
  const config = initConfig(process.cwd());
12109
12290
  if (!config.enabled) return;
12110
- const homeDir = homedir9();
12291
+ const homeDir = homedir11();
12111
12292
  const cwd = process.cwd();
12112
- const globalAgentsDir = join13(homeDir, ".unipi", "config", "agents");
12113
- const workspaceAgentsDir = join13(cwd, ".unipi", "config", "agents");
12293
+ const globalAgentsDir = join15(homeDir, ".unipi", "config", "agents");
12294
+ const workspaceAgentsDir = join15(cwd, ".unipi", "config", "agents");
12114
12295
  const agentActivity = /* @__PURE__ */ new Map();
12115
12296
  let sessionEnded = false;
12116
12297
  const manager2 = new AgentManager(
@@ -12590,17 +12771,22 @@ Use get_result to retrieve full results.`,
12590
12771
  error: record.error
12591
12772
  });
12592
12773
  }
12774
+ const output = boundHelperOutput(record.result?.trim() || "No output.");
12775
+ record.resultArtifactPath = output.artifactPath;
12593
12776
  return textResult(
12594
12777
  `Agent completed in ${(durationMs / 1e3).toFixed(1)}s (${record.toolUses} tool uses${tokenText ? `, ${tokenText} tokens` : ""}).
12595
12778
 
12596
- ` + (record.result?.trim() || "No output."),
12779
+ ` + output.text,
12597
12780
  {
12598
12781
  status: "completed",
12599
12782
  toolUses: record.toolUses,
12600
12783
  tokens: tokenText,
12601
12784
  durationMs,
12602
12785
  turnCount: fgState.turnCount,
12603
- maxTurns: fgState.maxTurns
12786
+ maxTurns: fgState.maxTurns,
12787
+ truncated: output.truncated,
12788
+ originalBytes: output.originalBytes,
12789
+ artifactPath: output.artifactPath
12604
12790
  }
12605
12791
  );
12606
12792
  }
@@ -12676,7 +12862,13 @@ Description: ${record.description}
12676
12862
  } else if (record.status === "error") {
12677
12863
  output += `Error: ${record.error}`;
12678
12864
  } else {
12679
- output += record.result?.trim() || "No output.";
12865
+ const bounded = boundHelperOutput(
12866
+ record.result?.trim() || "No output.",
12867
+ 64 * 1024,
12868
+ record.resultArtifactPath
12869
+ );
12870
+ record.resultArtifactPath = bounded.artifactPath;
12871
+ output += bounded.text;
12680
12872
  }
12681
12873
  if (record.status !== "running" && record.status !== "queued") {
12682
12874
  record.resultConsumed = true;
@@ -16929,8 +17121,8 @@ init_core();
16929
17121
 
16930
17122
  // packages/utility/src/lifecycle/cleanup.ts
16931
17123
  import { existsSync as existsSync16, statSync as statSync8, readdirSync as readdirSync10, unlinkSync as unlinkSync5, rmdirSync } from "node:fs";
16932
- import { join as join16 } from "node:path";
16933
- import { homedir as homedir12, tmpdir } from "node:os";
17124
+ import { join as join18 } from "node:path";
17125
+ import { homedir as homedir14, tmpdir } from "node:os";
16934
17126
  var DEFAULTS = {
16935
17127
  dbMaxAgeDays: 14,
16936
17128
  tempMaxAgeDays: 7,
@@ -16939,7 +17131,7 @@ var DEFAULTS = {
16939
17131
  };
16940
17132
  function expandHome(path28) {
16941
17133
  if (path28.startsWith("~/")) {
16942
- return join16(homedir12(), path28.slice(2));
17134
+ return join18(homedir14(), path28.slice(2));
16943
17135
  }
16944
17136
  return path28;
16945
17137
  }
@@ -16983,7 +17175,7 @@ function cleanDbs(options) {
16983
17175
  return;
16984
17176
  }
16985
17177
  for (const entry of entries) {
16986
- const fullPath = join16(dir, entry);
17178
+ const fullPath = join18(dir, entry);
16987
17179
  try {
16988
17180
  const stats = statSync8(fullPath);
16989
17181
  if (stats.isDirectory()) {
@@ -17019,6 +17211,36 @@ function cleanDbs(options) {
17019
17211
  scanDir(unipiDir);
17020
17212
  return result;
17021
17213
  }
17214
+ function cleanToolResults(options) {
17215
+ const result = {
17216
+ category: "tool-results",
17217
+ removed: 0,
17218
+ bytesFreed: 0,
17219
+ paths: []
17220
+ };
17221
+ const dir = expandHome("~/.unipi/tool-results");
17222
+ if (!existsSync16(dir)) return result;
17223
+ let entries;
17224
+ try {
17225
+ entries = readdirSync10(dir);
17226
+ } catch {
17227
+ return result;
17228
+ }
17229
+ for (const entry of entries) {
17230
+ if (!/^(?:tool-result|mcp-[a-zA-Z0-9_-]+|helper)-[a-f0-9-]+\.txt$/.test(entry)) continue;
17231
+ const path28 = join18(dir, entry);
17232
+ try {
17233
+ const stats = statSync8(path28);
17234
+ if (!stats.isFile() || !isStale(path28, options.tempMaxAgeDays)) continue;
17235
+ result.paths.push(path28);
17236
+ result.bytesFreed += stats.size;
17237
+ if (!options.dryRun) unlinkSync5(path28);
17238
+ result.removed++;
17239
+ } catch {
17240
+ }
17241
+ }
17242
+ return result;
17243
+ }
17022
17244
  function cleanTemps(options) {
17023
17245
  const result = {
17024
17246
  category: "temp",
@@ -17036,7 +17258,7 @@ function cleanTemps(options) {
17036
17258
  }
17037
17259
  for (const entry of entries) {
17038
17260
  if (!patterns.some((p) => p.test(entry))) continue;
17039
- const fullPath = join16(tmpDir, entry);
17261
+ const fullPath = join18(tmpDir, entry);
17040
17262
  try {
17041
17263
  const stats = statSync8(fullPath);
17042
17264
  if (!stats.isFile()) continue;
@@ -17066,7 +17288,7 @@ function cleanSessions(options) {
17066
17288
  paths: []
17067
17289
  };
17068
17290
  const unipiDir = expandHome("~/.unipi");
17069
- const sessionsDir = join16(unipiDir, "sessions");
17291
+ const sessionsDir = join18(unipiDir, "sessions");
17070
17292
  if (!existsSync16(sessionsDir)) return result;
17071
17293
  let entries;
17072
17294
  try {
@@ -17075,7 +17297,7 @@ function cleanSessions(options) {
17075
17297
  return result;
17076
17298
  }
17077
17299
  for (const entry of entries) {
17078
- const fullPath = join16(sessionsDir, entry);
17300
+ const fullPath = join18(sessionsDir, entry);
17079
17301
  try {
17080
17302
  const stats = statSync8(fullPath);
17081
17303
  if (!stats.isDirectory()) continue;
@@ -17087,7 +17309,7 @@ function cleanSessions(options) {
17087
17309
  const removeRecursive = (dir) => {
17088
17310
  const items = readdirSync10(dir);
17089
17311
  for (const item of items) {
17090
- const itemPath = join16(dir, item);
17312
+ const itemPath = join18(dir, item);
17091
17313
  const itemStats = statSync8(itemPath);
17092
17314
  if (itemStats.isDirectory()) {
17093
17315
  removeRecursive(itemPath);
@@ -17126,7 +17348,7 @@ function cleanCache(options) {
17126
17348
  return result;
17127
17349
  }
17128
17350
  for (const entry of entries) {
17129
- const fullPath = join16(cacheDir, entry);
17351
+ const fullPath = join18(cacheDir, entry);
17130
17352
  try {
17131
17353
  const stats = statSync8(fullPath);
17132
17354
  if (!stats.isFile()) continue;
@@ -17153,6 +17375,7 @@ function cleanupStale(options = {}) {
17153
17375
  const results = [
17154
17376
  cleanDbs(opts),
17155
17377
  cleanTemps(opts),
17378
+ cleanToolResults(opts),
17156
17379
  cleanSessions(opts),
17157
17380
  cleanCache(opts)
17158
17381
  ];
@@ -17193,11 +17416,11 @@ function formatCleanupReport(report) {
17193
17416
 
17194
17417
  // packages/utility/src/diagnostics/engine.ts
17195
17418
  import { existsSync as existsSync17, accessSync as accessSync2, constants as constants2 } from "node:fs";
17196
- import { join as join17 } from "node:path";
17197
- import { homedir as homedir13 } from "node:os";
17419
+ import { join as join19 } from "node:path";
17420
+ import { homedir as homedir15 } from "node:os";
17198
17421
  function expandHome2(path28) {
17199
17422
  if (path28.startsWith("~/")) {
17200
- return join17(homedir13(), path28.slice(2));
17423
+ return join19(homedir15(), path28.slice(2));
17201
17424
  }
17202
17425
  return path28;
17203
17426
  }
@@ -19263,7 +19486,7 @@ function getLifecycle(options) {
19263
19486
  }
19264
19487
 
19265
19488
  // packages/utility/src/analytics/collector.ts
19266
- import { randomUUID as randomUUID2 } from "node:crypto";
19489
+ import { randomUUID as randomUUID4 } from "node:crypto";
19267
19490
  var DEFAULTS3 = {
19268
19491
  dbPath: "~/.unipi/analytics/events.db",
19269
19492
  maxEvents: 1e4,
@@ -19272,7 +19495,7 @@ var DEFAULTS3 = {
19272
19495
  var eventBuffer = [];
19273
19496
  var bufferFlushTimer = null;
19274
19497
  function generateId() {
19275
- return randomUUID2();
19498
+ return randomUUID4();
19276
19499
  }
19277
19500
  function today() {
19278
19501
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -19480,6 +19703,187 @@ function registerInfoScreen(pi) {
19480
19703
  });
19481
19704
  }
19482
19705
 
19706
+ // packages/utility/src/prefix-cache.ts
19707
+ import { createHmac, randomBytes } from "node:crypto";
19708
+ var EMPTY_SNAPSHOT = {
19709
+ epoch: 0,
19710
+ requests: 0,
19711
+ prefixExtensions: 0,
19712
+ identicalRetries: 0,
19713
+ boundaries: 0,
19714
+ lastTransition: "none",
19715
+ requestFingerprint: "none",
19716
+ envelopeFingerprint: "none",
19717
+ sequenceField: "none",
19718
+ sequenceItems: 0,
19719
+ route: "unknown",
19720
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, responses: 0 }
19721
+ };
19722
+ function canonicalize(value, seen = /* @__PURE__ */ new WeakSet()) {
19723
+ if (value === void 0) return { $type: "undefined" };
19724
+ if (typeof value === "bigint") return { $type: "bigint", value: value.toString() };
19725
+ if (typeof value === "number" && !Number.isFinite(value)) {
19726
+ return { $type: "number", value: String(value) };
19727
+ }
19728
+ if (typeof value !== "object" || value === null) return value;
19729
+ if (seen.has(value)) return { $type: "cycle" };
19730
+ seen.add(value);
19731
+ if (Array.isArray(value)) {
19732
+ const result2 = value.map((item) => canonicalize(item, seen));
19733
+ seen.delete(value);
19734
+ return result2;
19735
+ }
19736
+ if (value instanceof Uint8Array) {
19737
+ seen.delete(value);
19738
+ return { $type: "bytes", length: value.byteLength };
19739
+ }
19740
+ const record = value;
19741
+ const result = {};
19742
+ for (const key of Object.keys(record).sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
19743
+ result[key] = canonicalize(record[key], seen);
19744
+ }
19745
+ seen.delete(value);
19746
+ return result;
19747
+ }
19748
+ function canonicalJson(value) {
19749
+ return JSON.stringify(canonicalize(value));
19750
+ }
19751
+ function routeLabel(model) {
19752
+ if (!model || typeof model !== "object") return "unknown";
19753
+ const value = model;
19754
+ return [value.provider, value.id, value.api].filter((part) => typeof part === "string" && part.length > 0).join("/") || "unknown";
19755
+ }
19756
+ function findSequence(payload) {
19757
+ for (const field of ["messages", "input", "contents"]) {
19758
+ if (Array.isArray(payload[field])) return { field, items: payload[field] };
19759
+ }
19760
+ return { field: "none", items: [] };
19761
+ }
19762
+ function isPrefix(previous, current) {
19763
+ return previous.length <= current.length && previous.every((item, index) => current[index] === item);
19764
+ }
19765
+ function usageNumber(value) {
19766
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
19767
+ }
19768
+ var PrefixCacheTracker = class {
19769
+ key;
19770
+ previous = null;
19771
+ seenResponses = /* @__PURE__ */ new WeakSet();
19772
+ snapshot = structuredClone(EMPTY_SNAPSHOT);
19773
+ constructor(key = randomBytes(32)) {
19774
+ this.key = key;
19775
+ }
19776
+ reset() {
19777
+ this.previous = null;
19778
+ this.seenResponses = /* @__PURE__ */ new WeakSet();
19779
+ this.snapshot = structuredClone(EMPTY_SNAPSHOT);
19780
+ }
19781
+ fingerprint(value) {
19782
+ return createHmac("sha256", this.key).update(canonicalJson(value)).digest("hex").slice(0, 16);
19783
+ }
19784
+ observeRequest(payload, model) {
19785
+ const object = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { payload };
19786
+ const { field, items } = findSequence(object);
19787
+ const route = routeLabel(model);
19788
+ const envelope = { route };
19789
+ for (const [key, value] of Object.entries(object)) {
19790
+ if (key !== field) envelope[key] = value;
19791
+ }
19792
+ const current = {
19793
+ envelopeFingerprint: this.fingerprint(envelope),
19794
+ requestFingerprint: this.fingerprint(object),
19795
+ sequenceField: field,
19796
+ sequenceItemFingerprints: items.map((item) => this.fingerprint(item))
19797
+ };
19798
+ let transition;
19799
+ let epoch = this.snapshot.epoch;
19800
+ if (!this.previous) {
19801
+ transition = "first_request";
19802
+ epoch = Math.max(1, epoch);
19803
+ } else if (current.envelopeFingerprint !== this.previous.envelopeFingerprint) {
19804
+ transition = "envelope_changed";
19805
+ epoch++;
19806
+ } else if (current.sequenceField !== this.previous.sequenceField) {
19807
+ transition = "payload_shape_changed";
19808
+ epoch++;
19809
+ } else if (current.requestFingerprint === this.previous.requestFingerprint) {
19810
+ transition = "identical_retry";
19811
+ } else if (isPrefix(this.previous.sequenceItemFingerprints, current.sequenceItemFingerprints)) {
19812
+ transition = "prefix_extended";
19813
+ } else {
19814
+ transition = "history_rewritten";
19815
+ epoch++;
19816
+ }
19817
+ this.snapshot = {
19818
+ ...this.snapshot,
19819
+ epoch,
19820
+ requests: this.snapshot.requests + 1,
19821
+ prefixExtensions: this.snapshot.prefixExtensions + (transition === "prefix_extended" ? 1 : 0),
19822
+ identicalRetries: this.snapshot.identicalRetries + (transition === "identical_retry" ? 1 : 0),
19823
+ boundaries: this.snapshot.boundaries + (transition === "envelope_changed" || transition === "history_rewritten" || transition === "payload_shape_changed" ? 1 : 0),
19824
+ lastTransition: transition,
19825
+ requestFingerprint: current.requestFingerprint,
19826
+ envelopeFingerprint: current.envelopeFingerprint,
19827
+ sequenceField: current.sequenceField,
19828
+ sequenceItems: current.sequenceItemFingerprints.length,
19829
+ route
19830
+ };
19831
+ this.previous = current;
19832
+ return this.getSnapshot();
19833
+ }
19834
+ observeMessages(messages) {
19835
+ for (const message of messages) {
19836
+ if (!message || typeof message !== "object") continue;
19837
+ const value = message;
19838
+ if (value.role !== "assistant" || !value.usage || typeof value.usage !== "object") continue;
19839
+ if (value.stopReason === "error" || value.stopReason === "aborted") continue;
19840
+ if (this.seenResponses.has(message)) continue;
19841
+ this.seenResponses.add(message);
19842
+ const usage = value.usage;
19843
+ this.snapshot.usage.input += usageNumber(usage.input);
19844
+ this.snapshot.usage.output += usageNumber(usage.output);
19845
+ this.snapshot.usage.cacheRead += usageNumber(usage.cacheRead);
19846
+ this.snapshot.usage.cacheWrite += usageNumber(usage.cacheWrite);
19847
+ this.snapshot.usage.responses++;
19848
+ }
19849
+ return this.getSnapshot();
19850
+ }
19851
+ markBoundary(transition) {
19852
+ if (this.previous) {
19853
+ this.snapshot.epoch++;
19854
+ this.snapshot.boundaries++;
19855
+ }
19856
+ this.snapshot.lastTransition = transition;
19857
+ this.previous = null;
19858
+ }
19859
+ getSnapshot() {
19860
+ return structuredClone(this.snapshot);
19861
+ }
19862
+ };
19863
+ function formatPrefixCacheStats(snapshot) {
19864
+ const usage = snapshot.usage;
19865
+ const totalPrompt = usage.input + usage.cacheRead + usage.cacheWrite;
19866
+ const readRate = totalPrompt > 0 ? `${(usage.cacheRead / totalPrompt * 100).toFixed(1)}%` : "n/a";
19867
+ return [
19868
+ "## Provider Prefix Cache",
19869
+ "",
19870
+ `- Epoch: ${snapshot.epoch || "not observed"}`,
19871
+ `- Requests observed: ${snapshot.requests}`,
19872
+ `- Prefix extensions: ${snapshot.prefixExtensions}`,
19873
+ `- Explicit boundaries: ${snapshot.boundaries}`,
19874
+ `- Last transition: ${snapshot.lastTransition}`,
19875
+ `- Route: ${snapshot.route}`,
19876
+ `- Request fingerprint: ${snapshot.requestFingerprint}`,
19877
+ `- Envelope fingerprint: ${snapshot.envelopeFingerprint}`,
19878
+ `- Provider cache read: ${usage.cacheRead} tokens`,
19879
+ `- Provider cache write: ${usage.cacheWrite} tokens`,
19880
+ `- Observed cache-read share: ${readRate}`,
19881
+ "",
19882
+ "Fingerprints are session-local keyed HMACs. Raw prompts, messages, tool arguments, and payloads are not retained or logged.",
19883
+ "A prefix extension indicates structural eligibility only; provider cache retention and routing still determine actual hits."
19884
+ ].join("\n");
19885
+ }
19886
+
19483
19887
  // packages/utility/src/index.ts
19484
19888
  var VERSION6 = getPackageVersion(dirname14(fileURLToPath7(import.meta.url)));
19485
19889
  var firstMessageSeen = false;
@@ -19496,12 +19900,44 @@ var ALL_COMMANDS = [
19496
19900
  UTILITY_COMMANDS.BADGE_GEN,
19497
19901
  UTILITY_COMMANDS.BADGE_TOGGLE,
19498
19902
  UTILITY_COMMANDS.BADGE_SETTINGS,
19499
- UTILITY_COMMANDS.UTIL_SETTINGS
19903
+ UTILITY_COMMANDS.UTIL_SETTINGS,
19904
+ UTILITY_COMMANDS.PREFIX_CACHE
19500
19905
  ].map((cmd) => `unipi:${cmd}`);
19501
19906
  var ALL_TOOLS2 = [UTILITY_TOOLS.BATCH, UTILITY_TOOLS.ENV, UTILITY_TOOLS.SET_SESSION_NAME];
19502
19907
  function src_default3(pi) {
19503
19908
  const lifecycle = getLifecycle();
19504
19909
  const analytics = getAnalyticsCollector();
19910
+ const prefixCache = new PrefixCacheTracker();
19911
+ pi.registerCommand(`unipi:${UTILITY_COMMANDS.PREFIX_CACHE}`, {
19912
+ description: "Show privacy-safe provider prefix-cache diagnostics",
19913
+ handler: async (_args, ctx) => {
19914
+ const report = formatPrefixCacheStats(prefixCache.getSnapshot());
19915
+ if (ctx.hasUI) {
19916
+ ctx.ui.notify(report, "info");
19917
+ } else {
19918
+ pi.sendMessage({ customType: "unipi-response", content: report, display: true }, { deliverAs: "followUp" });
19919
+ }
19920
+ }
19921
+ });
19922
+ pi.on("session_start", () => {
19923
+ prefixCache.reset();
19924
+ });
19925
+ pi.on("before_provider_request", (event, ctx) => {
19926
+ prefixCache.observeRequest(event.payload, ctx.model);
19927
+ });
19928
+ pi.on("agent_end", (event) => {
19929
+ prefixCache.observeMessages(event.messages);
19930
+ });
19931
+ pi.on("model_select", (event) => {
19932
+ if (event.previousModel && (event.previousModel.provider !== event.model.provider || event.previousModel.id !== event.model.id || event.previousModel.api !== event.model.api)) {
19933
+ prefixCache.markBoundary("envelope_changed");
19934
+ }
19935
+ });
19936
+ pi.on("thinking_level_select", (event) => {
19937
+ if (event.previousLevel !== event.level) prefixCache.markBoundary("envelope_changed");
19938
+ });
19939
+ pi.on("session_compact", () => prefixCache.markBoundary("history_rewritten"));
19940
+ pi.on("session_tree", () => prefixCache.markBoundary("history_rewritten"));
19505
19941
  lifecycle.registerCleanup(async () => {
19506
19942
  analytics.disable();
19507
19943
  });
@@ -20349,9 +20785,9 @@ function renderLauncherUI(params) {
20349
20785
  }
20350
20786
 
20351
20787
  // packages/ask-user/config.ts
20352
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync14, existsSync as existsSync21 } from "node:fs";
20353
- import { join as join19 } from "node:path";
20354
- import { homedir as homedir15 } from "node:os";
20788
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync16, existsSync as existsSync21 } from "node:fs";
20789
+ import { join as join21 } from "node:path";
20790
+ import { homedir as homedir17 } from "node:os";
20355
20791
  var DEFAULT_SETTINGS3 = {
20356
20792
  enabled: true,
20357
20793
  allowedFormats: {
@@ -20361,7 +20797,7 @@ var DEFAULT_SETTINGS3 = {
20361
20797
  },
20362
20798
  notifyOnAsk: true
20363
20799
  };
20364
- var SETTINGS_PATH2 = join19(homedir15(), ".pi", "agent", "settings.json");
20800
+ var SETTINGS_PATH2 = join21(homedir17(), ".pi", "agent", "settings.json");
20365
20801
  var SETTINGS_KEY2 = "unipi";
20366
20802
  var cachedSettings2 = null;
20367
20803
  function isRecord3(value) {
@@ -20381,7 +20817,7 @@ function writeSettingsFile2(data) {
20381
20817
  if (!existsSync21(dir)) {
20382
20818
  __require("node:fs").mkdirSync(dir, { recursive: true });
20383
20819
  }
20384
- writeFileSync14(SETTINGS_PATH2, JSON.stringify(data, null, 2) + "\n", "utf-8");
20820
+ writeFileSync16(SETTINGS_PATH2, JSON.stringify(data, null, 2) + "\n", "utf-8");
20385
20821
  }
20386
20822
  function getAskUserSettings() {
20387
20823
  if (cachedSettings2) return cachedSettings2;
@@ -21807,7 +22243,7 @@ Stderr: ${this.stderrBuffer}`
21807
22243
 
21808
22244
  // packages/mcp/src/bridge/translator.ts
21809
22245
  init_core();
21810
- function compareCodeUnits(left, right) {
22246
+ function compareCodeUnits3(left, right) {
21811
22247
  return left < right ? -1 : left > right ? 1 : 0;
21812
22248
  }
21813
22249
  function isObject(value) {
@@ -21817,13 +22253,13 @@ var LITERAL_VALUE_KEYWORDS = /* @__PURE__ */ new Set(["const", "default", "enum"
21817
22253
  function canonicalizeValue(value, key, normalizeSchemaKeywords = true) {
21818
22254
  if (Array.isArray(value)) {
21819
22255
  if (normalizeSchemaKeywords && key === "required" && value.every((item) => typeof item === "string")) {
21820
- return [...new Set(value)].sort(compareCodeUnits);
22256
+ return [...new Set(value)].sort(compareCodeUnits3);
21821
22257
  }
21822
22258
  return value.map((item) => canonicalizeValue(item, void 0, normalizeSchemaKeywords));
21823
22259
  }
21824
22260
  if (!isObject(value)) return value;
21825
22261
  const canonical = {};
21826
- for (const objectKey of Object.keys(value).sort(compareCodeUnits)) {
22262
+ for (const objectKey of Object.keys(value).sort(compareCodeUnits3)) {
21827
22263
  const childNormalizesSchemaKeywords = normalizeSchemaKeywords && !LITERAL_VALUE_KEYWORDS.has(objectKey);
21828
22264
  canonical[objectKey] = canonicalizeValue(
21829
22265
  value[objectKey],
@@ -21873,19 +22309,26 @@ function translateMcpTool(mcpTool, serverName, client) {
21873
22309
  });
21874
22310
  }
21875
22311
  }
21876
- if (result.isError) {
21877
- const errorText = blocks.map((b) => b.text).join("\n") || "Unknown error";
21878
- return {
21879
- content: [{ type: "text", text: `MCP tool error from ${serverName}: ${errorText}` }],
21880
- details: { error: true, server: serverName, tool: mcpTool.name }
21881
- };
21882
- }
21883
22312
  if (blocks.length === 0) {
21884
- blocks.push({ type: "text", text: "(no output)" });
22313
+ blocks.push({ type: "text", text: result.isError ? "Unknown error" : "(no output)" });
21885
22314
  }
22315
+ const rawText = blocks.map((block2) => block2.text).join("\n");
22316
+ const wrapper = result.isError ? `MCP tool error from ${serverName}: ` : "";
22317
+ const output = boundModelOutput(rawText, {
22318
+ maxBytes: Math.max(1024, MCP_DEFAULTS.MAX_MODEL_OUTPUT_BYTES - Buffer.byteLength(wrapper, "utf8")),
22319
+ artifactPrefix: `mcp-${serverName}-${mcpTool.name}`
22320
+ });
22321
+ const visibleText = `${wrapper}${output.text}`;
21886
22322
  return {
21887
- content: blocks,
21888
- details: { server: serverName, tool: mcpTool.name }
22323
+ content: [{ type: "text", text: visibleText }],
22324
+ details: {
22325
+ error: result.isError || void 0,
22326
+ server: serverName,
22327
+ tool: mcpTool.name,
22328
+ truncated: output.truncated,
22329
+ originalBytes: output.originalBytes,
22330
+ artifactPath: output.artifactPath
22331
+ }
21889
22332
  };
21890
22333
  } catch (err) {
21891
22334
  const message = err instanceof Error ? err.message : String(err);
@@ -21970,7 +22413,7 @@ var ServerRegistry = class {
21970
22413
  );
21971
22414
  const prepared = settled.filter((result) => result.status === "fulfilled").map((result) => result.value);
21972
22415
  if (prepared.length === 0) return;
21973
- const registrations = prepared.flatMap(({ entry, tools }) => tools.map((tool) => ({ entry, tool }))).sort((left, right) => compareCodeUnits(left.tool.name, right.tool.name));
22416
+ const registrations = prepared.flatMap(({ entry, tools }) => tools.map((tool) => ({ entry, tool }))).sort((left, right) => compareCodeUnits3(left.tool.name, right.tool.name));
21974
22417
  try {
21975
22418
  this.assertUniqueFinalToolNames(registrations.map(({ tool }) => tool.name));
21976
22419
  } catch (error) {
@@ -21995,7 +22438,7 @@ var ServerRegistry = class {
21995
22438
  } else {
21996
22439
  const message = `${error instanceof Error ? error.message : String(error)}; some MCP tools remain registered until Pi restarts`;
21997
22440
  for (const { entry, client, tools } of prepared) {
21998
- const toolNames = tools.map((tool) => tool.name).filter((name) => registeredNames.includes(name)).sort(compareCodeUnits);
22441
+ const toolNames = tools.map((tool) => tool.name).filter((name) => registeredNames.includes(name)).sort(compareCodeUnits3);
21999
22442
  entry.toolNames = toolNames;
22000
22443
  entry.state = {
22001
22444
  ...entry.state,
@@ -22012,8 +22455,8 @@ var ServerRegistry = class {
22012
22455
  }
22013
22456
  throw error;
22014
22457
  }
22015
- for (const { entry, client, tools } of [...prepared].sort((left, right) => compareCodeUnits(left.entry.name, right.entry.name))) {
22016
- const toolNames = tools.map((tool) => tool.name).sort(compareCodeUnits);
22458
+ for (const { entry, client, tools } of [...prepared].sort((left, right) => compareCodeUnits3(left.entry.name, right.entry.name))) {
22459
+ const toolNames = tools.map((tool) => tool.name).sort(compareCodeUnits3);
22017
22460
  entry.toolNames = toolNames;
22018
22461
  entry.state = {
22019
22462
  ...entry.state,
@@ -22065,7 +22508,7 @@ var ServerRegistry = class {
22065
22508
  await connectedClient.connect(safeCommand, safeArgs, safeEnv);
22066
22509
  entry.client = connectedClient;
22067
22510
  const mcpTools = await connectedClient.listTools();
22068
- const tools = mcpTools.map((tool) => translateMcpTool(tool, name, connectedClient)).sort((left, right) => compareCodeUnits(left.name, right.name));
22511
+ const tools = mcpTools.map((tool) => translateMcpTool(tool, name, connectedClient)).sort((left, right) => compareCodeUnits3(left.name, right.name));
22069
22512
  return { entry, client: connectedClient, tools };
22070
22513
  } catch (error) {
22071
22514
  const message = error instanceof Error ? error.message : String(error);
@@ -22091,7 +22534,7 @@ var ServerRegistry = class {
22091
22534
  }
22092
22535
  if (duplicates.size > 0) {
22093
22536
  throw new Error(
22094
- `Duplicate final MCP tool name(s): ${[...duplicates].sort(compareCodeUnits).join(", ")}`
22537
+ `Duplicate final MCP tool name(s): ${[...duplicates].sort(compareCodeUnits3).join(", ")}`
22095
22538
  );
22096
22539
  }
22097
22540
  }
@@ -23038,7 +23481,7 @@ function src_default4(pi) {
23038
23481
  `unipi:${MCP_COMMANDS.STATUS}`,
23039
23482
  `unipi:${MCP_COMMANDS.RELOAD}`
23040
23483
  ],
23041
- tools: activeServers.flatMap((server) => registry2?.getEntry(server.name)?.toolNames ?? []).sort(compareCodeUnits)
23484
+ tools: activeServers.flatMap((server) => registry2?.getEntry(server.name)?.toolNames ?? []).sort(compareCodeUnits3)
23042
23485
  });
23043
23486
  });
23044
23487
  pi.on("session_shutdown", async (_event, _ctx) => {
@@ -23175,12 +23618,12 @@ import { Type as Type6 } from "typebox";
23175
23618
 
23176
23619
  // packages/notify/settings.ts
23177
23620
  init_core();
23178
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, existsSync as existsSync24 } from "fs";
23179
- import { dirname as dirname19, join as join22 } from "path";
23180
- import { homedir as homedir17 } from "os";
23621
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync19, mkdirSync as mkdirSync17, existsSync as existsSync24 } from "fs";
23622
+ import { dirname as dirname19, join as join24 } from "path";
23623
+ import { homedir as homedir19 } from "os";
23181
23624
  function resolveConfigPath() {
23182
- const base = NOTIFY_DIRS.CONFIG.replace("~", homedir17());
23183
- return join22(base, "config.json");
23625
+ const base = NOTIFY_DIRS.CONFIG.replace("~", homedir19());
23626
+ return join24(base, "config.json");
23184
23627
  }
23185
23628
  var DEFAULT_CONFIG4 = {
23186
23629
  defaultPlatforms: ["native"],
@@ -23231,8 +23674,8 @@ function loadConfig2() {
23231
23674
  function saveConfig2(config) {
23232
23675
  const configPath = resolveConfigPath();
23233
23676
  const dir = dirname19(configPath);
23234
- mkdirSync15(dir, { recursive: true });
23235
- writeFileSync17(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23677
+ mkdirSync17(dir, { recursive: true });
23678
+ writeFileSync19(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23236
23679
  }
23237
23680
  function updateConfig(partial) {
23238
23681
  const current = loadConfig2();
@@ -23290,19 +23733,19 @@ function mergeWithDefaults(loaded) {
23290
23733
  init_core();
23291
23734
 
23292
23735
  // packages/notify/ntfy-config.ts
23293
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync18, mkdirSync as mkdirSync16, existsSync as existsSync25 } from "fs";
23294
- import { dirname as dirname20, join as join23 } from "path";
23295
- import { homedir as homedir18 } from "os";
23736
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync20, mkdirSync as mkdirSync18, existsSync as existsSync25 } from "fs";
23737
+ import { dirname as dirname20, join as join25 } from "path";
23738
+ import { homedir as homedir20 } from "os";
23296
23739
  var DEFAULT_NTFY_CONFIG = {
23297
23740
  enabled: false,
23298
23741
  serverUrl: "https://ntfy.sh",
23299
23742
  priority: 3
23300
23743
  };
23301
23744
  function getGlobalNtfyPath() {
23302
- return join23(homedir18(), ".unipi", "config", "notify", "ntfy.json");
23745
+ return join25(homedir20(), ".unipi", "config", "notify", "ntfy.json");
23303
23746
  }
23304
23747
  function getProjectNtfyPath(cwd) {
23305
- return join23(cwd, ".unipi", "config", "notify", "ntfy.json");
23748
+ return join25(cwd, ".unipi", "config", "notify", "ntfy.json");
23306
23749
  }
23307
23750
  function readNtfyJson(filePath) {
23308
23751
  try {
@@ -23332,8 +23775,8 @@ function loadNtfyConfig(cwd) {
23332
23775
  function saveNtfyConfig(scope, cwd, config) {
23333
23776
  const filePath = scope === "project" ? getProjectNtfyPath(cwd) : getGlobalNtfyPath();
23334
23777
  const dir = dirname20(filePath);
23335
- mkdirSync16(dir, { recursive: true });
23336
- writeFileSync18(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23778
+ mkdirSync18(dir, { recursive: true });
23779
+ writeFileSync20(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23337
23780
  }
23338
23781
  function getNtfyConfigScope(cwd) {
23339
23782
  if (existsSync25(getProjectNtfyPath(cwd))) {
@@ -23349,8 +23792,8 @@ function migrateFromLegacyConfig() {
23349
23792
  if (existsSync25(globalNtfyPath)) {
23350
23793
  return;
23351
23794
  }
23352
- const legacyConfigPath = join23(
23353
- homedir18(),
23795
+ const legacyConfigPath = join25(
23796
+ homedir20(),
23354
23797
  ".unipi",
23355
23798
  "config",
23356
23799
  "notify",
@@ -23372,8 +23815,8 @@ function migrateFromLegacyConfig() {
23372
23815
  priority: ntfySection.priority ?? 3
23373
23816
  };
23374
23817
  const dir = dirname20(globalNtfyPath);
23375
- mkdirSync16(dir, { recursive: true });
23376
- writeFileSync18(
23818
+ mkdirSync18(dir, { recursive: true });
23819
+ writeFileSync20(
23377
23820
  globalNtfyPath,
23378
23821
  JSON.stringify(migratedConfig, null, 2) + "\n",
23379
23822
  "utf-8"
@@ -23387,8 +23830,8 @@ import notifier from "node-notifier";
23387
23830
 
23388
23831
  // packages/notify/platforms/focus-win.ts
23389
23832
  import { execFile } from "child_process";
23390
- import { writeFileSync as writeFileSync19, rmSync as rmSync2, mkdtempSync } from "fs";
23391
- import { join as join24 } from "path";
23833
+ import { writeFileSync as writeFileSync21, rmSync as rmSync2, mkdtempSync } from "fs";
23834
+ import { join as join26 } from "path";
23392
23835
  import { tmpdir as tmpdir2 } from "os";
23393
23836
  var POWERCHECK_SCRIPT = `
23394
23837
  param($targetPid)
@@ -23424,9 +23867,9 @@ async function isWindowFocusedOnWindows() {
23424
23867
  }
23425
23868
  let tmpDir = null;
23426
23869
  try {
23427
- tmpDir = mkdtempSync(join24(tmpdir2(), "pi-focus-"));
23428
- const scriptPath = join24(tmpDir, "check.ps1");
23429
- writeFileSync19(scriptPath, POWERCHECK_SCRIPT, "utf-8");
23870
+ tmpDir = mkdtempSync(join26(tmpdir2(), "pi-focus-"));
23871
+ const scriptPath = join26(tmpDir, "check.ps1");
23872
+ writeFileSync21(scriptPath, POWERCHECK_SCRIPT, "utf-8");
23430
23873
  const stdout = await new Promise((resolve14, reject) => {
23431
23874
  execFile(
23432
23875
  "powershell.exe",
@@ -26588,13 +27031,13 @@ function registerSessionEndHook(pi) {
26588
27031
 
26589
27032
  // packages/milestone/commands.ts
26590
27033
  init_core();
26591
- import { dirname as dirname22, join as join26 } from "node:path";
27034
+ import { dirname as dirname22, join as join28 } from "node:path";
26592
27035
  import { fileURLToPath as fileURLToPath11 } from "node:url";
26593
27036
  import { readFileSync as readFileSync21 } from "node:fs";
26594
- var SKILLS_DIR = join26(dirname22(fileURLToPath11(import.meta.url)), "skills");
27037
+ var SKILLS_DIR = join28(dirname22(fileURLToPath11(import.meta.url)), "skills");
26595
27038
  function loadSkill(skillName) {
26596
27039
  try {
26597
- return readFileSync21(join26(SKILLS_DIR, skillName, "SKILL.md"), "utf-8");
27040
+ return readFileSync21(join28(SKILLS_DIR, skillName, "SKILL.md"), "utf-8");
26598
27041
  } catch {
26599
27042
  return "";
26600
27043
  }
@@ -26647,7 +27090,7 @@ ${skillContent}
26647
27090
  },
26648
27091
  getArgumentCompletions: () => {
26649
27092
  const cwd = process.cwd();
26650
- const milestonesPath = join26(cwd, MILESTONE_DIRS.MILESTONES);
27093
+ const milestonesPath = join28(cwd, MILESTONE_DIRS.MILESTONES);
26651
27094
  const doc = parseMilestones(milestonesPath);
26652
27095
  const suggestions = doc.phases.map((phase) => ({
26653
27096
  value: phase.name,
@@ -27103,6 +27546,7 @@ var COMMAND_REGISTRY = {
27103
27546
  "unipi:worktree-list": "workflow",
27104
27547
  "unipi:worktree-merge": "workflow",
27105
27548
  "unipi:consultant": "workflow",
27549
+ "unipi:prefix-cache": "utility",
27106
27550
  "unipi:quick-work": "workflow",
27107
27551
  "unipi:gather-context": "workflow",
27108
27552
  "unipi:document": "workflow",
@@ -27210,6 +27654,7 @@ var COMMAND_DESCRIPTIONS = {
27210
27654
  "unipi:worktree-list": "List all unipi worktrees",
27211
27655
  "unipi:worktree-merge": "Merge worktree branches back to main",
27212
27656
  "unipi:consultant": "Expert consultation \u2014 advisory analysis",
27657
+ "unipi:prefix-cache": "Show privacy-safe provider prefix-cache diagnostics",
27213
27658
  "unipi:quick-work": "Fast single-task execution \u2014 one shot",
27214
27659
  "unipi:gather-context": "Research codebase \u2014 surface patterns",
27215
27660
  "unipi:document": "Generate documentation \u2014 README, API docs",
@@ -27791,9 +28236,9 @@ function markAutoCompactionError(state, nowMs) {
27791
28236
  // packages/compactor/src/session/db.ts
27792
28237
  import { createHash as createHash3 } from "node:crypto";
27793
28238
  import { execFileSync } from "node:child_process";
27794
- import { existsSync as existsSync32, mkdirSync as mkdirSync20 } from "node:fs";
27795
- import { homedir as homedir21 } from "node:os";
27796
- import { dirname as dirname25, join as join33 } from "node:path";
28239
+ import { existsSync as existsSync32, mkdirSync as mkdirSync22 } from "node:fs";
28240
+ import { homedir as homedir23 } from "node:os";
28241
+ import { dirname as dirname25, join as join35 } from "node:path";
27797
28242
  function getWorktreeSuffix() {
27798
28243
  const envSuffix = process.env.COMPACTOR_SESSION_SUFFIX;
27799
28244
  if (envSuffix !== void 0) {
@@ -27815,7 +28260,7 @@ function getWorktreeSuffix() {
27815
28260
  return "";
27816
28261
  }
27817
28262
  function defaultDBPath(name) {
27818
- return join33(homedir21(), ".unipi", "db", "compactor", `${name}.db`);
28263
+ return join35(homedir23(), ".unipi", "db", "compactor", `${name}.db`);
27819
28264
  }
27820
28265
  var sqliteLib = null;
27821
28266
  async function getSQLite() {
@@ -27839,7 +28284,7 @@ var SessionDB = class {
27839
28284
  }
27840
28285
  async init() {
27841
28286
  const dir = dirname25(this.dbPath);
27842
- if (!existsSync32(dir)) mkdirSync20(dir, { recursive: true });
28287
+ if (!existsSync32(dir)) mkdirSync22(dir, { recursive: true });
27843
28288
  const sqlite = await getSQLite();
27844
28289
  const Database2 = sqlite.Database ?? sqlite.default?.Database ?? sqlite.default ?? sqlite;
27845
28290
  this.db = new Database2(this.dbPath);
@@ -28232,8 +28677,8 @@ async function buildResumeContextMessage(db, sessionId) {
28232
28677
 
28233
28678
  // packages/compactor/src/executor/executor.ts
28234
28679
  import { spawn as spawn3, execSync, execFileSync as execFileSync2 } from "node:child_process";
28235
- import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync24, rmSync as rmSync3 } from "node:fs";
28236
- import { join as join34, resolve as resolve11 } from "node:path";
28680
+ import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync26, rmSync as rmSync3 } from "node:fs";
28681
+ import { join as join36, resolve as resolve11 } from "node:path";
28237
28682
  import { tmpdir as tmpdir3 } from "node:os";
28238
28683
 
28239
28684
  // packages/compactor/src/executor/runtime.ts
@@ -28414,7 +28859,7 @@ var PolyglotExecutor = class {
28414
28859
  }
28415
28860
  async execute(opts) {
28416
28861
  const { language, code, timeout = 3e4, background = false } = opts;
28417
- const tmpDir = mkdtempSync2(join34(OS_TMPDIR, ".compactor-"));
28862
+ const tmpDir = mkdtempSync2(join36(OS_TMPDIR, ".compactor-"));
28418
28863
  try {
28419
28864
  const filePath = this.#writeScript(tmpDir, code, language);
28420
28865
  const cmd = buildCommand(this.#runtimes, language, filePath);
@@ -28464,8 +28909,8 @@ var PolyglotExecutor = class {
28464
28909
  elixir: "ex"
28465
28910
  };
28466
28911
  const ext = extensions[language];
28467
- const filePath = join34(tmpDir, `script.${ext}`);
28468
- writeFileSync24(filePath, code, "utf-8");
28912
+ const filePath = join36(tmpDir, `script.${ext}`);
28913
+ writeFileSync26(filePath, code, "utf-8");
28469
28914
  return filePath;
28470
28915
  }
28471
28916
  async #spawn(cmd, cwd, tmpDir, timeout, background) {
@@ -28523,7 +28968,7 @@ var PolyglotExecutor = class {
28523
28968
  });
28524
28969
  }
28525
28970
  async #compileAndRun(filePath, tmpDir, timeout) {
28526
- const binPath = join34(tmpDir, "script");
28971
+ const binPath = join36(tmpDir, "script");
28527
28972
  try {
28528
28973
  execSync(`rustc "${filePath}" -o "${binPath}"`, {
28529
28974
  cwd: tmpDir,
@@ -28633,15 +29078,29 @@ function searchEntries(blocks, query, opts) {
28633
29078
  }
28634
29079
 
28635
29080
  // packages/compactor/src/tools/vcc-recall.ts
29081
+ var MAX_RECALL_RESULTS = 50;
29082
+ var MAX_EXPANDED_HIT_BYTES = 16 * 1024;
29083
+ function truncateExpandedHit(text) {
29084
+ const bytes = Buffer.from(text, "utf8");
29085
+ if (bytes.byteLength <= MAX_EXPANDED_HIT_BYTES) return text;
29086
+ const omitted = bytes.byteLength - MAX_EXPANDED_HIT_BYTES;
29087
+ const visible = bytes.subarray(0, MAX_EXPANDED_HIT_BYTES).toString("utf8").replace(/\uFFFD+$/u, "");
29088
+ return `${visible}
29089
+ \u2026 ${omitted} bytes omitted from this hit; narrow the query to inspect more specific context \u2026`;
29090
+ }
28636
29091
  function vccRecall(blocks, input) {
28637
- const { query, mode = "bm25", limit = 10, offset = 0, expand = false } = input;
29092
+ const { query, mode = "bm25", expand = false } = input;
29093
+ const requestedLimit = Number.isFinite(input.limit) ? Math.floor(input.limit) : 10;
29094
+ const requestedOffset = Number.isFinite(input.offset) ? Math.floor(input.offset) : 0;
29095
+ const limit = Math.min(MAX_RECALL_RESULTS, Math.max(1, requestedLimit));
29096
+ const offset = Math.max(0, requestedOffset);
28638
29097
  let hits = [];
28639
29098
  if (mode === "bm25") {
28640
29099
  const results = searchEntries(blocks, query, { limit: limit + offset, offset: 0 });
28641
29100
  hits = results.map((r, i) => ({
28642
29101
  index: r.docId,
28643
29102
  score: r.score,
28644
- text: expand ? r.text : r.text.slice(0, 200),
29103
+ text: expand ? truncateExpandedHit(r.text) : r.text.slice(0, 200),
28645
29104
  kind: r.kind
28646
29105
  }));
28647
29106
  } else {
@@ -28653,7 +29112,7 @@ function vccRecall(blocks, input) {
28653
29112
  hits.push({
28654
29113
  index: i,
28655
29114
  score: 1,
28656
- text: expand ? text : text.slice(0, 200),
29115
+ text: expand ? truncateExpandedHit(text) : text.slice(0, 200),
28657
29116
  kind: b.kind
28658
29117
  });
28659
29118
  }
@@ -29135,7 +29594,11 @@ var RecallParams = Type7.Object({
29135
29594
  mode: Type7.Optional(Type7.Union([Type7.Literal("bm25"), Type7.Literal("regex")], {
29136
29595
  description: "Search mode: bm25 (default) or regex fallback"
29137
29596
  })),
29138
- limit: Type7.Optional(Type7.Number({ description: "Max results to return (default 10)", minimum: 1 })),
29597
+ limit: Type7.Optional(Type7.Number({
29598
+ description: `Max results to return (default 10, hard cap ${MAX_RECALL_RESULTS})`,
29599
+ minimum: 1,
29600
+ maximum: MAX_RECALL_RESULTS
29601
+ })),
29139
29602
  offset: Type7.Optional(Type7.Number({ description: "Pagination offset", minimum: 0 })),
29140
29603
  expand: Type7.Optional(Type7.Boolean({ description: "Return full message content for hits" }))
29141
29604
  });
@@ -29560,13 +30023,13 @@ function compactorExtension(pi) {
29560
30023
  debug("before_agent_start", { sessionId: currentSessionId, configDebug: config.debug });
29561
30024
  try {
29562
30025
  const { existsSync: existsSync48 } = await import("node:fs");
29563
- const { join: join48 } = await import("node:path");
30026
+ const { join: join50 } = await import("node:path");
29564
30027
  const strategies = [
29565
30028
  { key: "commits", config: config.commits }
29566
30029
  ];
29567
30030
  for (const { key, config: strat } of strategies) {
29568
30031
  if (strat.autoDetect === "git") {
29569
- const gitDir = join48(cwd, ".git");
30032
+ const gitDir = join50(cwd, ".git");
29570
30033
  if (!existsSync48(gitDir)) {
29571
30034
  debug("autoDetect_disable", { strategy: key, reason: "no .git dir" });
29572
30035
  strat.enabled = false;
@@ -33083,7 +33546,7 @@ init_core();
33083
33546
  import { dirname as dirname27 } from "node:path";
33084
33547
  import { fileURLToPath as fileURLToPath12 } from "node:url";
33085
33548
  import { existsSync as existsSync39, readFileSync as readFileSync34 } from "fs";
33086
- import { join as join38 } from "path";
33549
+ import { join as join40 } from "path";
33087
33550
  var PACKAGE_MAP = {
33088
33551
  core: MODULES.CORE,
33089
33552
  workflow: MODULES.WORKFLOW,
@@ -33109,11 +33572,11 @@ var PACKAGE_MAP = {
33109
33572
  function resolveUnipiRoot() {
33110
33573
  const dir = dirname27(fileURLToPath12(import.meta.url));
33111
33574
  const root = findPackageRoot(dir, "@pi-unipi/unipi");
33112
- return root ?? join38(dir, "..");
33575
+ return root ?? join40(dir, "..");
33113
33576
  }
33114
33577
  function getPackageVersion2(dir) {
33115
33578
  try {
33116
- const pkgPath = join38(dir, "package.json");
33579
+ const pkgPath = join40(dir, "package.json");
33117
33580
  if (existsSync39(pkgPath)) {
33118
33581
  const raw = readFileSync34(pkgPath, "utf-8");
33119
33582
  const pkg = JSON.parse(raw);
@@ -33126,7 +33589,7 @@ function getPackageVersion2(dir) {
33126
33589
  function discoverReadmes() {
33127
33590
  const root = resolveUnipiRoot();
33128
33591
  const entries = [];
33129
- const rootReadme = join38(root, "README.md");
33592
+ const rootReadme = join40(root, "README.md");
33130
33593
  if (existsSync39(rootReadme)) {
33131
33594
  entries.push({
33132
33595
  name: "unipi",
@@ -33136,16 +33599,16 @@ function discoverReadmes() {
33136
33599
  });
33137
33600
  }
33138
33601
  for (const [shortName, fullName] of Object.entries(PACKAGE_MAP)) {
33139
- const workspacePath = join38(root, "packages", shortName, "README.md");
33140
- const nodeModulesPath = join38(root, "node_modules", fullName, "README.md");
33602
+ const workspacePath = join40(root, "packages", shortName, "README.md");
33603
+ const nodeModulesPath = join40(root, "node_modules", fullName, "README.md");
33141
33604
  let readmePath = null;
33142
33605
  let versionDir = null;
33143
33606
  if (existsSync39(workspacePath)) {
33144
33607
  readmePath = workspacePath;
33145
- versionDir = join38(root, "packages", shortName);
33608
+ versionDir = join40(root, "packages", shortName);
33146
33609
  } else if (existsSync39(nodeModulesPath)) {
33147
33610
  readmePath = nodeModulesPath;
33148
- versionDir = join38(root, "node_modules", fullName);
33611
+ versionDir = join40(root, "node_modules", fullName);
33149
33612
  }
33150
33613
  if (readmePath && versionDir) {
33151
33614
  entries.push({
@@ -33161,14 +33624,14 @@ function discoverReadmes() {
33161
33624
  function resolveReadmePath(packageName) {
33162
33625
  const root = resolveUnipiRoot();
33163
33626
  if (!packageName || packageName === "unipi") {
33164
- const rootReadme = join38(root, "README.md");
33627
+ const rootReadme = join40(root, "README.md");
33165
33628
  return existsSync39(rootReadme) ? rootReadme : null;
33166
33629
  }
33167
33630
  const fullName = PACKAGE_MAP[packageName];
33168
33631
  if (!fullName) return null;
33169
- const workspacePath = join38(root, "packages", packageName, "README.md");
33632
+ const workspacePath = join40(root, "packages", packageName, "README.md");
33170
33633
  if (existsSync39(workspacePath)) return workspacePath;
33171
- const nodeModulesPath = join38(root, "node_modules", fullName, "README.md");
33634
+ const nodeModulesPath = join40(root, "node_modules", fullName, "README.md");
33172
33635
  if (existsSync39(nodeModulesPath)) return nodeModulesPath;
33173
33636
  return null;
33174
33637
  }
@@ -33444,7 +33907,7 @@ import { Key as Key7, matchesKey as matchesKey15, truncateToWidth as truncateToW
33444
33907
  // packages/updater/src/changelog.ts
33445
33908
  init_core();
33446
33909
  import { existsSync as existsSync40, readFileSync as readFileSync36 } from "fs";
33447
- import { dirname as dirname28, join as join39 } from "node:path";
33910
+ import { dirname as dirname28, join as join41 } from "node:path";
33448
33911
  import { fileURLToPath as fileURLToPath13 } from "node:url";
33449
33912
 
33450
33913
  // packages/updater/src/version.ts
@@ -33472,12 +33935,12 @@ function resolveChangelogPath() {
33472
33935
  const dir = dirname28(fileURLToPath13(import.meta.url));
33473
33936
  const root = findPackageRoot(dir, "@pi-unipi/unipi");
33474
33937
  if (root) {
33475
- const packaged = join39(root, "CHANGELOG.md");
33938
+ const packaged = join41(root, "CHANGELOG.md");
33476
33939
  if (existsSync40(packaged)) return packaged;
33477
33940
  }
33478
33941
  } catch {
33479
33942
  }
33480
- return join39(process.cwd(), "CHANGELOG.md");
33943
+ return join41(process.cwd(), "CHANGELOG.md");
33481
33944
  }
33482
33945
  var VERSION_HEADER_RE = /^## \[(.+?)\](?:\s*[-—–]\s*(.+))?$/;
33483
33946
  var SECTION_HEADER_RE = /^### (.+)$/;
@@ -33729,9 +34192,9 @@ function renderChangelogOverlay() {
33729
34192
 
33730
34193
  // packages/updater/src/settings.ts
33731
34194
  init_core();
33732
- import { readFileSync as readFileSync37, writeFileSync as writeFileSync26, mkdirSync as mkdirSync22, existsSync as existsSync42 } from "fs";
33733
- import { dirname as dirname29, join as join40 } from "path";
33734
- import { homedir as homedir24 } from "os";
34195
+ import { readFileSync as readFileSync37, writeFileSync as writeFileSync28, mkdirSync as mkdirSync24, existsSync as existsSync42 } from "fs";
34196
+ import { dirname as dirname29, join as join42 } from "path";
34197
+ import { homedir as homedir26 } from "os";
33735
34198
  var DEFAULT_CONFIG6 = {
33736
34199
  checkIntervalMs: 36e5,
33737
34200
  // 1 hour
@@ -33745,8 +34208,8 @@ var VALID_INTERVALS = {
33745
34208
  };
33746
34209
  var VALID_MODES = ["disabled", "notify", "auto"];
33747
34210
  function resolveConfigPath2() {
33748
- const base = UPDATER_DIRS.CONFIG.replace("~", homedir24());
33749
- return join40(base, "config.json");
34211
+ const base = UPDATER_DIRS.CONFIG.replace("~", homedir26());
34212
+ return join42(base, "config.json");
33750
34213
  }
33751
34214
  function loadConfig5() {
33752
34215
  const configPath = resolveConfigPath2();
@@ -33763,8 +34226,8 @@ function loadConfig5() {
33763
34226
  function saveConfig4(config) {
33764
34227
  const configPath = resolveConfigPath2();
33765
34228
  const dir = dirname29(configPath);
33766
- mkdirSync22(dir, { recursive: true });
33767
- writeFileSync26(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
34229
+ mkdirSync24(dir, { recursive: true });
34230
+ writeFileSync28(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
33768
34231
  }
33769
34232
  function getIntervalLabel(ms) {
33770
34233
  for (const [label, value] of Object.entries(VALID_INTERVALS)) {
@@ -33997,12 +34460,12 @@ init_core();
33997
34460
 
33998
34461
  // packages/updater/src/cache.ts
33999
34462
  init_core();
34000
- import { readFileSync as readFileSync38, writeFileSync as writeFileSync27, mkdirSync as mkdirSync23, existsSync as existsSync43 } from "fs";
34001
- import { dirname as dirname30, join as join41 } from "path";
34002
- import { homedir as homedir25 } from "os";
34463
+ import { readFileSync as readFileSync38, writeFileSync as writeFileSync29, mkdirSync as mkdirSync25, existsSync as existsSync43 } from "fs";
34464
+ import { dirname as dirname30, join as join43 } from "path";
34465
+ import { homedir as homedir27 } from "os";
34003
34466
  function resolveCachePath() {
34004
- const base = UPDATER_DIRS.CACHE.replace("~", homedir25());
34005
- return join41(base, "last-check.json");
34467
+ const base = UPDATER_DIRS.CACHE.replace("~", homedir27());
34468
+ return join43(base, "last-check.json");
34006
34469
  }
34007
34470
  function readLastCheck() {
34008
34471
  const cachePath = resolveCachePath();
@@ -34021,8 +34484,8 @@ function readLastCheck() {
34021
34484
  function writeLastCheck(cache) {
34022
34485
  const cachePath = resolveCachePath();
34023
34486
  const dir = dirname30(cachePath);
34024
- mkdirSync23(dir, { recursive: true });
34025
- writeFileSync27(cachePath, JSON.stringify(cache, null, 2) + "\n", "utf-8");
34487
+ mkdirSync25(dir, { recursive: true });
34488
+ writeFileSync29(cachePath, JSON.stringify(cache, null, 2) + "\n", "utf-8");
34026
34489
  }
34027
34490
  function isCheckDue(intervalMs) {
34028
34491
  const cache = readLastCheck();
@@ -34430,8 +34893,8 @@ init_core();
34430
34893
  import { Key as Key10 } from "@earendil-works/pi-tui";
34431
34894
 
34432
34895
  // packages/input-shortcuts/src/registers.ts
34433
- import { existsSync as existsSync44, mkdirSync as mkdirSync24, readFileSync as readFileSync39, renameSync as renameSync8, writeFileSync as writeFileSync28 } from "node:fs";
34434
- import { dirname as dirname31, join as join42 } from "node:path";
34896
+ import { existsSync as existsSync44, mkdirSync as mkdirSync26, readFileSync as readFileSync39, renameSync as renameSync8, writeFileSync as writeFileSync30 } from "node:fs";
34897
+ import { dirname as dirname31, join as join44 } from "node:path";
34435
34898
 
34436
34899
  // packages/input-shortcuts/src/types.ts
34437
34900
  var DEFAULT_CONFIG7 = {
@@ -34453,7 +34916,7 @@ var RegisterStore = class {
34453
34916
  filePath;
34454
34917
  loaded = false;
34455
34918
  constructor(baseDir) {
34456
- this.filePath = baseDir ? join42(baseDir, REGISTERS_FILE) : REGISTERS_FILE;
34919
+ this.filePath = baseDir ? join44(baseDir, REGISTERS_FILE) : REGISTERS_FILE;
34457
34920
  }
34458
34921
  /** Get the stash register contents. */
34459
34922
  getStash() {
@@ -34503,10 +34966,10 @@ var RegisterStore = class {
34503
34966
  try {
34504
34967
  const dir = dirname31(this.filePath);
34505
34968
  if (!existsSync44(dir)) {
34506
- mkdirSync24(dir, { recursive: true });
34969
+ mkdirSync26(dir, { recursive: true });
34507
34970
  }
34508
34971
  const tmpPath = this.filePath + ".tmp";
34509
- writeFileSync28(tmpPath, JSON.stringify(this.data, null, 2), "utf-8");
34972
+ writeFileSync30(tmpPath, JSON.stringify(this.data, null, 2), "utf-8");
34510
34973
  renameSync8(tmpPath, this.filePath);
34511
34974
  } catch {
34512
34975
  }
@@ -34728,8 +35191,8 @@ var ChordOverlay = class extends Container2 {
34728
35191
 
34729
35192
  // packages/input-shortcuts/src/settings-overlay.ts
34730
35193
  import { SettingsList as SettingsList3 } from "@earendil-works/pi-tui";
34731
- import { existsSync as existsSync45, mkdirSync as mkdirSync25, readFileSync as readFileSync40, renameSync as renameSync9, writeFileSync as writeFileSync29 } from "node:fs";
34732
- import { dirname as dirname32, join as join43 } from "node:path";
35194
+ import { existsSync as existsSync45, mkdirSync as mkdirSync27, readFileSync as readFileSync40, renameSync as renameSync9, writeFileSync as writeFileSync31 } from "node:fs";
35195
+ import { dirname as dirname32, join as join45 } from "node:path";
34733
35196
  var ALT_KEY_OPTIONS = [
34734
35197
  "alt+a",
34735
35198
  "alt+b",
@@ -34761,7 +35224,7 @@ var ALT_KEY_OPTIONS = [
34761
35224
  var CONFLICTS = /* @__PURE__ */ new Set(["alt+e"]);
34762
35225
  var FREE_ALT_KEYS = ALT_KEY_OPTIONS.filter((k) => !CONFLICTS.has(k));
34763
35226
  function loadConfig6(baseDir) {
34764
- const filePath = baseDir ? join43(baseDir, CONFIG_FILE) : CONFIG_FILE;
35227
+ const filePath = baseDir ? join45(baseDir, CONFIG_FILE) : CONFIG_FILE;
34765
35228
  try {
34766
35229
  if (existsSync45(filePath)) {
34767
35230
  const raw = readFileSync40(filePath, "utf-8");
@@ -34776,14 +35239,14 @@ function loadConfig6(baseDir) {
34776
35239
  return { ...DEFAULT_CONFIG7 };
34777
35240
  }
34778
35241
  function saveConfig5(config, baseDir) {
34779
- const filePath = baseDir ? join43(baseDir, CONFIG_FILE) : CONFIG_FILE;
35242
+ const filePath = baseDir ? join45(baseDir, CONFIG_FILE) : CONFIG_FILE;
34780
35243
  try {
34781
35244
  const dir = dirname32(filePath);
34782
35245
  if (!existsSync45(dir)) {
34783
- mkdirSync25(dir, { recursive: true });
35246
+ mkdirSync27(dir, { recursive: true });
34784
35247
  }
34785
35248
  const tmpPath = filePath + ".tmp";
34786
- writeFileSync29(tmpPath, JSON.stringify(config, null, 2), "utf-8");
35249
+ writeFileSync31(tmpPath, JSON.stringify(config, null, 2), "utf-8");
34787
35250
  renameSync9(tmpPath, filePath);
34788
35251
  } catch {
34789
35252
  }
@@ -35212,9 +35675,9 @@ import { Type as Type8 } from "typebox";
35212
35675
  // packages/cocoindex/bridge.ts
35213
35676
  init_core();
35214
35677
  import { execFileSync as execFileSync3, spawn as spawn4 } from "node:child_process";
35215
- import { existsSync as existsSync46, mkdirSync as mkdirSync26, readFileSync as readFileSync41, writeFileSync as writeFileSync30, readdirSync as readdirSync14, statSync as statSync9 } from "node:fs";
35216
- import { homedir as homedir26 } from "node:os";
35217
- import { join as join44 } from "node:path";
35678
+ import { existsSync as existsSync46, mkdirSync as mkdirSync28, readFileSync as readFileSync41, writeFileSync as writeFileSync32, readdirSync as readdirSync14, statSync as statSync9 } from "node:fs";
35679
+ import { homedir as homedir28 } from "node:os";
35680
+ import { join as join46 } from "node:path";
35218
35681
  var DEFAULT_PIPELINE_DIR = ".unipi/cocoindex";
35219
35682
  var DEFAULT_UPDATE_TIMEOUT_MS = 15 * 60 * 1e3;
35220
35683
  var DEFAULT_LEXICAL_SCAN_LIMIT = 5e4;
@@ -35284,13 +35747,13 @@ function resolveCocoindexBin() {
35284
35747
  if (resolved) return resolved;
35285
35748
  } catch {
35286
35749
  }
35287
- const uvToolBin = join44(homedir26(), ".local", "bin", "cocoindex");
35750
+ const uvToolBin = join46(homedir28(), ".local", "bin", "cocoindex");
35288
35751
  if (existsSync46(uvToolBin)) return uvToolBin;
35289
- const miseRoot = join44(homedir26(), ".local", "share", "mise", "installs", "python");
35752
+ const miseRoot = join46(homedir28(), ".local", "share", "mise", "installs", "python");
35290
35753
  try {
35291
35754
  const versions = readdirSync14(miseRoot).sort().reverse();
35292
35755
  for (const ver of versions) {
35293
- const binPath = join44(miseRoot, ver, "bin", "cocoindex");
35756
+ const binPath = join46(miseRoot, ver, "bin", "cocoindex");
35294
35757
  if (existsSync46(binPath)) return binPath;
35295
35758
  }
35296
35759
  } catch {
@@ -35298,13 +35761,13 @@ function resolveCocoindexBin() {
35298
35761
  return "cocoindex";
35299
35762
  }
35300
35763
  function getPipelineDir(projectDir) {
35301
- return join44(projectDir, DEFAULT_PIPELINE_DIR);
35764
+ return join46(projectDir, DEFAULT_PIPELINE_DIR);
35302
35765
  }
35303
35766
  async function isPipelineInitialized(pipelineDir) {
35304
- return existsSync46(join44(pipelineDir, "main.py"));
35767
+ return existsSync46(join46(pipelineDir, "main.py"));
35305
35768
  }
35306
35769
  function detectTargetStore(pipelineDir) {
35307
- const mainPyPath = join44(pipelineDir, "main.py");
35770
+ const mainPyPath = join46(pipelineDir, "main.py");
35308
35771
  if (!existsSync46(mainPyPath)) return "unknown";
35309
35772
  try {
35310
35773
  const content = readFileSync41(mainPyPath, "utf-8");
@@ -35320,14 +35783,14 @@ function detectTargetStore(pipelineDir) {
35320
35783
  async function initPipeline(projectDir) {
35321
35784
  const pipelineDir = getPipelineDir(projectDir);
35322
35785
  if (!existsSync46(pipelineDir)) {
35323
- mkdirSync26(pipelineDir, { recursive: true });
35786
+ mkdirSync28(pipelineDir, { recursive: true });
35324
35787
  }
35325
- if (existsSync46(join44(pipelineDir, "main.py"))) {
35788
+ if (existsSync46(join46(pipelineDir, "main.py"))) {
35326
35789
  return { success: true };
35327
35790
  }
35328
35791
  const embeddingConfig = loadEmbeddingConfig2();
35329
35792
  const template = generatePipelineTemplate(projectDir, embeddingConfig);
35330
- writeFileSync30(join44(pipelineDir, "main.py"), template, "utf-8");
35793
+ writeFileSync32(join46(pipelineDir, "main.py"), template, "utf-8");
35331
35794
  return { success: true };
35332
35795
  }
35333
35796
  async function indexProject(projectDir) {
@@ -35341,7 +35804,7 @@ async function indexProject(projectDir) {
35341
35804
  };
35342
35805
  }
35343
35806
  const pipelineDir = getPipelineDir(projectDir);
35344
- if (!existsSync46(join44(pipelineDir, "main.py"))) {
35807
+ if (!existsSync46(join46(pipelineDir, "main.py"))) {
35345
35808
  return {
35346
35809
  success: false,
35347
35810
  chunksProcessed: 0,
@@ -35452,11 +35915,11 @@ function parseChunksProcessed(output) {
35452
35915
  async function status(projectDir) {
35453
35916
  const pipelineDir = getPipelineDir(projectDir);
35454
35917
  const cliAvailable = await isAvailable();
35455
- const pipelineConfigured = existsSync46(join44(pipelineDir, "main.py"));
35918
+ const pipelineConfigured = existsSync46(join46(pipelineDir, "main.py"));
35456
35919
  const targetStore = detectTargetStore(pipelineDir);
35457
35920
  let docCount = 0;
35458
35921
  let lastRun = null;
35459
- const lancedbPath = join44(pipelineDir, ".lancedb");
35922
+ const lancedbPath = join46(pipelineDir, ".lancedb");
35460
35923
  if (existsSync46(lancedbPath)) {
35461
35924
  try {
35462
35925
  const stat = statSync9(lancedbPath);
@@ -35479,7 +35942,7 @@ async function search(projectDir, query, options) {
35479
35942
  const limit = options?.limit ?? 10;
35480
35943
  const offset = options?.offset ?? 0;
35481
35944
  try {
35482
- const lancedbPath = join44(getPipelineDir(projectDir), ".lancedb");
35945
+ const lancedbPath = join46(getPipelineDir(projectDir), ".lancedb");
35483
35946
  if (!existsSync46(lancedbPath)) {
35484
35947
  return [];
35485
35948
  }
@@ -35594,7 +36057,7 @@ function countOccurrences(value, needle) {
35594
36057
  }
35595
36058
  function loadEmbeddingConfig2() {
35596
36059
  const envKey = process.env.OPENROUTER_API_KEY ?? null;
35597
- const configPath = join44(homedir26(), ".unipi", "memory", "config.json");
36060
+ const configPath = join46(homedir28(), ".unipi", "memory", "config.json");
35598
36061
  try {
35599
36062
  if (existsSync46(configPath)) {
35600
36063
  const raw = readFileSync41(configPath, "utf-8");
@@ -35801,9 +36264,22 @@ app = coco.App(
35801
36264
  }
35802
36265
 
35803
36266
  // packages/cocoindex/tools.ts
36267
+ var MAX_SEARCH_RESULTS = 50;
36268
+ function normalizeSearchPage(limit, offset) {
36269
+ const requestedLimit = Number.isFinite(limit) ? Math.floor(limit) : 10;
36270
+ const requestedOffset = Number.isFinite(offset) ? Math.floor(offset) : 0;
36271
+ return {
36272
+ limit: Math.min(MAX_SEARCH_RESULTS, Math.max(1, requestedLimit)),
36273
+ offset: Math.max(0, requestedOffset)
36274
+ };
36275
+ }
35804
36276
  var SearchParams = Type8.Object({
35805
36277
  query: Type8.String({ description: "Search query against indexed content" }),
35806
- limit: Type8.Optional(Type8.Number({ description: "Max results (default 10)", minimum: 1 })),
36278
+ limit: Type8.Optional(Type8.Number({
36279
+ description: `Max results (default 10, hard cap ${MAX_SEARCH_RESULTS})`,
36280
+ minimum: 1,
36281
+ maximum: MAX_SEARCH_RESULTS
36282
+ })),
35807
36283
  offset: Type8.Optional(Type8.Number({ description: "Pagination offset", minimum: 0 }))
35808
36284
  });
35809
36285
  var StatusParams = Type8.Object({});
@@ -35819,7 +36295,7 @@ function registerCocoindexTools(pi, deps) {
35819
36295
  label: "CocoIndex Search",
35820
36296
  description: "Search indexed content using semantic vector search when available, with full-text/lexical fallbacks. Diagnostic/search only: this tool never installs CocoIndex. Use /unipi:cocoindex-init to set up/install, then /unipi:cocoindex-update to index.",
35821
36297
  parameters: SearchParams,
35822
- async execute(_toolCallId, params) {
36298
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
35823
36299
  try {
35824
36300
  const available = await isAvailable();
35825
36301
  if (!available) {
@@ -35828,9 +36304,10 @@ function registerCocoindexTools(pi, deps) {
35828
36304
  { cliAvailable: false }
35829
36305
  );
35830
36306
  }
35831
- const results = await search(deps.projectDir, params.query, {
35832
- limit: params.limit,
35833
- offset: params.offset
36307
+ const { limit, offset } = normalizeSearchPage(params.limit, params.offset);
36308
+ const results = await search(deps.getProjectDir(ctx), params.query, {
36309
+ limit,
36310
+ offset
35834
36311
  });
35835
36312
  if (results.length === 0) {
35836
36313
  return textResult3(
@@ -35845,7 +36322,8 @@ ${r.content.slice(0, 300)}`
35845
36322
  `Found ${results.length} results for "${params.query}":
35846
36323
 
35847
36324
  ${lines.join("\n\n")}`,
35848
- { results }
36325
+ // Avoid duplicating full result content in model-visible details.
36326
+ { count: results.length, query: params.query, limit, offset }
35849
36327
  );
35850
36328
  } catch (err) {
35851
36329
  return textResult3(`CocoIndex search error: ${err}`, { error: true });
@@ -35857,9 +36335,9 @@ ${lines.join("\n\n")}`,
35857
36335
  label: "CocoIndex Status",
35858
36336
  description: "Check CocoIndex indexing status. Diagnostic only; use commands for interactive install/update.",
35859
36337
  parameters: StatusParams,
35860
- async execute() {
36338
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
35861
36339
  try {
35862
- const info = await status(deps.projectDir);
36340
+ const info = await status(deps.getProjectDir(ctx));
35863
36341
  const lines = [
35864
36342
  `\u{1F4E6} CocoIndex Status`,
35865
36343
  `CLI available: ${info.cliAvailable ? "\u2705" : "\u274C"}`,
@@ -35892,8 +36370,8 @@ init_core();
35892
36370
  // packages/cocoindex/installer.ts
35893
36371
  init_core();
35894
36372
  import { execFileSync as execFileSync4, spawn as spawn5 } from "node:child_process";
35895
- import { homedir as homedir27 } from "node:os";
35896
- import { delimiter, join as join45 } from "node:path";
36373
+ import { homedir as homedir29 } from "node:os";
36374
+ import { delimiter, join as join47 } from "node:path";
35897
36375
  var DEFAULT_STEP_TIMEOUT_MS = 10 * 60 * 1e3;
35898
36376
  function detectShell() {
35899
36377
  const shell = process.env.SHELL ?? "";
@@ -36154,10 +36632,10 @@ Timed out after ${step.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS}ms.`;
36154
36632
  });
36155
36633
  }
36156
36634
  function installerEnv() {
36157
- const home = homedir27();
36635
+ const home = homedir29();
36158
36636
  const extraPaths = [
36159
- join45(home, ".local", "bin"),
36160
- join45(home, ".local", "share", "mise", "shims")
36637
+ join47(home, ".local", "bin"),
36638
+ join47(home, ".local", "share", "mise", "shims")
36161
36639
  ];
36162
36640
  const currentPath = process.env.PATH ?? "";
36163
36641
  return {
@@ -36322,15 +36800,11 @@ ${lines.join("\n\n")}`,
36322
36800
  // packages/cocoindex/index.ts
36323
36801
  function cocoindexExtension(pi) {
36324
36802
  registerCocoindexCommands(pi);
36803
+ registerCocoindexTools(pi, {
36804
+ getProjectDir: (ctx) => ctx.cwd ?? process.cwd()
36805
+ });
36325
36806
  pi.on("session_start", async (_event, ctx) => {
36326
36807
  const projectDir = ctx.cwd ?? process.cwd();
36327
- const pipelineDir = getPipelineDir(projectDir);
36328
- const initialized = await isPipelineInitialized(pipelineDir);
36329
- registerCocoindexTools(pi, {
36330
- projectDir,
36331
- pipelineDir,
36332
- initialized
36333
- });
36334
36808
  emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
36335
36809
  name: MODULES.COCOINDEX,
36336
36810
  version: "0.1.0",