@pi-unipi/unipi 2.4.2 → 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 (69) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -0
  3. package/docs/prefix-cache-architecture.md +89 -0
  4. package/package.json +24 -22
  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/core/sandbox.ts +7 -6
  23. package/packages/footer/package.json +2 -2
  24. package/packages/image/package.json +2 -2
  25. package/packages/info-screen/package.json +2 -2
  26. package/packages/input-shortcuts/package.json +2 -2
  27. package/packages/kanboard/package.json +2 -2
  28. package/packages/mcp/README.md +13 -0
  29. package/packages/mcp/package.json +5 -2
  30. package/packages/mcp/src/bridge/registry.ts +244 -137
  31. package/packages/mcp/src/bridge/translator.ts +96 -21
  32. package/packages/mcp/src/index.ts +43 -67
  33. package/packages/mcp/src/tui/settings-overlay.ts +3 -9
  34. package/packages/memory/README.md +15 -11
  35. package/packages/memory/bridge/mempalace_bridge.py +636 -0
  36. package/packages/memory/index.ts +56 -34
  37. package/packages/memory/mempalace.ts +169 -18
  38. package/packages/memory/package.json +3 -3
  39. package/packages/memory/storage.ts +27 -10
  40. package/packages/milestone/README.md +11 -3
  41. package/packages/milestone/hooks.ts +103 -29
  42. package/packages/milestone/index.ts +1 -1
  43. package/packages/milestone/package.json +5 -2
  44. package/packages/notify/package.json +2 -2
  45. package/packages/ralph/index.ts +12 -18
  46. package/packages/ralph/package.json +6 -3
  47. package/packages/ralph/reminder.ts +40 -0
  48. package/packages/ralph/tools.ts +5 -1
  49. package/packages/subagents/README.md +2 -0
  50. package/packages/subagents/package.json +1 -1
  51. package/packages/subagents/src/agent-manager.ts +5 -1
  52. package/packages/subagents/src/agent-runner.ts +2 -2
  53. package/packages/subagents/src/core-compat.ts +73 -0
  54. package/packages/subagents/src/custom-agents.ts +10 -2
  55. package/packages/subagents/src/index.ts +14 -3
  56. package/packages/subagents/src/types.ts +2 -0
  57. package/packages/unipi/bundled.js +1445 -686
  58. package/packages/updater/package.json +2 -2
  59. package/packages/utility/README.md +9 -0
  60. package/packages/utility/package.json +2 -2
  61. package/packages/utility/src/index.ts +48 -0
  62. package/packages/utility/src/lifecycle/cleanup.ts +29 -0
  63. package/packages/utility/src/prefix-cache.ts +263 -0
  64. package/packages/utility/src/types.ts +1 -1
  65. package/packages/web-api/package.json +2 -2
  66. package/packages/workflow/README.md +8 -0
  67. package/packages/workflow/commands.ts +16 -26
  68. package/packages/workflow/index.ts +165 -85
  69. package/packages/workflow/package.json +5 -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,17 +954,46 @@ 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
 
869
961
  // packages/memory/mempalace.ts
870
962
  import { spawnSync, spawn } from "node:child_process";
963
+ import { createHash } from "node:crypto";
964
+ import { createRequire } from "node:module";
871
965
  import * as fs4 from "node:fs";
872
966
  import * as path4 from "node:path";
873
967
  import * as os from "node:os";
874
968
  import { fileURLToPath as fileURLToPath3 } from "node:url";
875
- function dirname5(p) {
876
- return path4.dirname(p);
969
+ function isReadableFile(candidate) {
970
+ try {
971
+ return fs4.statSync(candidate).isFile() && fs4.accessSync(candidate, fs4.constants.R_OK) === void 0;
972
+ } catch {
973
+ return false;
974
+ }
975
+ }
976
+ function resolveMempalaceBridgePath(moduleUrl = import.meta.url) {
977
+ const moduleDir = path4.dirname(fileURLToPath3(moduleUrl));
978
+ const candidates = [];
979
+ if (process.env.UNIPI_MEMPALACE_BRIDGE) {
980
+ candidates.push(path4.resolve(process.env.UNIPI_MEMPALACE_BRIDGE));
981
+ }
982
+ candidates.push(
983
+ path4.join(moduleDir, "bridge", "mempalace_bridge.py"),
984
+ path4.join(moduleDir, "..", "memory", "bridge", "mempalace_bridge.py")
985
+ );
986
+ try {
987
+ const require2 = createRequire(moduleUrl);
988
+ const memoryPackage = require2.resolve("@pi-unipi/memory/package.json");
989
+ candidates.push(path4.join(path4.dirname(memoryPackage), "bridge", "mempalace_bridge.py"));
990
+ } catch {
991
+ }
992
+ return candidates.find(isReadableFile) ?? null;
993
+ }
994
+ function getBridgePath() {
995
+ if (cachedBridgePath === void 0) cachedBridgePath = resolveMempalaceBridgePath();
996
+ return cachedBridgePath;
877
997
  }
878
998
  function which(bin) {
879
999
  try {
@@ -973,17 +1093,71 @@ function invalidatePingVerified() {
973
1093
  } catch {
974
1094
  }
975
1095
  }
976
- function isMigrated() {
977
- return fs4.existsSync(MIGRATED_FLAG);
1096
+ function getMemorySourceFingerprint(sourceDir = path4.join(os.homedir(), ".unipi", "memory")) {
1097
+ const hash = createHash("sha256");
1098
+ if (!fs4.existsSync(sourceDir)) return hash.update("missing").digest("hex");
1099
+ const visit = (dir) => {
1100
+ let entries;
1101
+ try {
1102
+ entries = fs4.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1103
+ } catch {
1104
+ hash.update(`unreadable:${dir}`);
1105
+ return;
1106
+ }
1107
+ for (const entry of entries) {
1108
+ if (entry.name.startsWith(".")) continue;
1109
+ const full = path4.join(dir, entry.name);
1110
+ if (entry.isDirectory()) {
1111
+ visit(full);
1112
+ continue;
1113
+ }
1114
+ if (!entry.isFile() || entry.name !== "memory.db" && !entry.name.endsWith(".md")) continue;
1115
+ try {
1116
+ const stat = fs4.statSync(full);
1117
+ hash.update(`${path4.relative(sourceDir, full)}\0${stat.size}\0${stat.mtimeMs}
1118
+ `);
1119
+ } catch {
1120
+ hash.update(`unreadable:${path4.relative(sourceDir, full)}
1121
+ `);
1122
+ }
1123
+ }
1124
+ };
1125
+ visit(sourceDir);
1126
+ return hash.digest("hex");
978
1127
  }
979
- function markMigrated() {
1128
+ function readMigrationState(flagPath = MIGRATED_FLAG) {
980
1129
  try {
981
- fs4.mkdirSync(path4.dirname(MIGRATED_FLAG), { recursive: true });
982
- fs4.writeFileSync(MIGRATED_FLAG, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
1130
+ const parsed = JSON.parse(fs4.readFileSync(flagPath, "utf-8"));
1131
+ if (parsed?.version !== MIGRATION_STATE_VERSION || typeof parsed.completedAt !== "string" || typeof parsed.sourceFingerprint !== "string" || !parsed.result || parsed.result.failed !== 0 || parsed.result.verified !== parsed.result.discovered) return null;
1132
+ return parsed;
983
1133
  } catch {
1134
+ return null;
1135
+ }
1136
+ }
1137
+ function isMigrated(sourceFingerprint = getMemorySourceFingerprint(), flagPath = MIGRATED_FLAG) {
1138
+ return readMigrationState(flagPath)?.sourceFingerprint === sourceFingerprint;
1139
+ }
1140
+ function markMigrated(sourceFingerprint, result, flagPath = MIGRATED_FLAG) {
1141
+ if (result.failed !== 0 || result.verified !== result.discovered) return false;
1142
+ try {
1143
+ fs4.mkdirSync(path4.dirname(flagPath), { recursive: true });
1144
+ const state = {
1145
+ version: MIGRATION_STATE_VERSION,
1146
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
1147
+ sourceFingerprint,
1148
+ result
1149
+ };
1150
+ const temp = `${flagPath}.${process.pid}.tmp`;
1151
+ fs4.writeFileSync(temp, JSON.stringify(state, null, 2), "utf-8");
1152
+ fs4.renameSync(temp, flagPath);
1153
+ return true;
1154
+ } catch {
1155
+ return false;
984
1156
  }
985
1157
  }
986
- function runBridge(install, palace, cmd, args = {}) {
1158
+ function runBridge(install, palace, cmd, args = {}, timeoutMs = 6e4) {
1159
+ const bridgePath = getBridgePath();
1160
+ if (!bridgePath) return null;
987
1161
  let argsJson;
988
1162
  try {
989
1163
  argsJson = JSON.stringify(args);
@@ -992,9 +1166,9 @@ function runBridge(install, palace, cmd, args = {}) {
992
1166
  }
993
1167
  let res;
994
1168
  try {
995
- res = spawnSync(install.python, [BRIDGE_PATH, palace, cmd, argsJson], {
1169
+ res = spawnSync(install.python, [bridgePath, palace, cmd, argsJson], {
996
1170
  encoding: "utf-8",
997
- timeout: 6e4,
1171
+ timeout: timeoutMs,
998
1172
  maxBuffer: 64 * 1024 * 1024
999
1173
  });
1000
1174
  } catch {
@@ -1013,22 +1187,27 @@ function runBridge(install, palace, cmd, args = {}) {
1013
1187
  return null;
1014
1188
  }
1015
1189
  }
1016
- function runBridgeAsync(install, palace, cmd, args = {}) {
1017
- return new Promise((resolve13) => {
1190
+ function runBridgeAsync(install, palace, cmd, args = {}, timeoutMs = 6e4) {
1191
+ return new Promise((resolve14) => {
1192
+ const bridgePath = getBridgePath();
1193
+ if (!bridgePath) {
1194
+ resolve14(null);
1195
+ return;
1196
+ }
1018
1197
  let argsJson;
1019
1198
  try {
1020
1199
  argsJson = JSON.stringify(args);
1021
1200
  } catch {
1022
- resolve13(null);
1201
+ resolve14(null);
1023
1202
  return;
1024
1203
  }
1025
1204
  let child;
1026
1205
  try {
1027
- child = spawn(install.python, [BRIDGE_PATH, palace, cmd, argsJson], {
1206
+ child = spawn(install.python, [bridgePath, palace, cmd, argsJson], {
1028
1207
  stdio: ["ignore", "pipe", "ignore"]
1029
1208
  });
1030
1209
  } catch {
1031
- resolve13(null);
1210
+ resolve14(null);
1032
1211
  return;
1033
1212
  }
1034
1213
  let out = "";
@@ -1037,7 +1216,7 @@ function runBridgeAsync(install, palace, cmd, args = {}) {
1037
1216
  if (settled) return;
1038
1217
  settled = true;
1039
1218
  clearTimeout(timer);
1040
- resolve13(value);
1219
+ resolve14(value);
1041
1220
  };
1042
1221
  const timer = setTimeout(() => {
1043
1222
  try {
@@ -1045,7 +1224,7 @@ function runBridgeAsync(install, palace, cmd, args = {}) {
1045
1224
  } catch {
1046
1225
  }
1047
1226
  finish(null);
1048
- }, 6e4);
1227
+ }, timeoutMs);
1049
1228
  timer.unref?.();
1050
1229
  child.stdout?.setEncoding("utf-8");
1051
1230
  child.stdout?.on("data", (chunk) => {
@@ -1065,7 +1244,7 @@ function runBridgeAsync(install, palace, cmd, args = {}) {
1065
1244
  });
1066
1245
  });
1067
1246
  }
1068
- var DEFAULT_PALACE, INSTALL_FLAG, MIGRATED_FLAG, PING_VERIFIED_FLAG, PING_VERIFIED_TTL_MS, BRIDGE_PATH;
1247
+ var DEFAULT_PALACE, INSTALL_FLAG, MIGRATED_FLAG, PING_VERIFIED_FLAG, PING_VERIFIED_TTL_MS, MIGRATION_STATE_VERSION, cachedBridgePath;
1069
1248
  var init_mempalace = __esm({
1070
1249
  "packages/memory/mempalace.ts"() {
1071
1250
  "use strict";
@@ -1074,7 +1253,7 @@ var init_mempalace = __esm({
1074
1253
  MIGRATED_FLAG = path4.join(os.homedir(), ".unipi", "memory", ".mempalace-migrated");
1075
1254
  PING_VERIFIED_FLAG = path4.join(os.homedir(), ".unipi", "memory", ".mempalace-ping-verified");
1076
1255
  PING_VERIFIED_TTL_MS = 24 * 60 * 60 * 1e3;
1077
- BRIDGE_PATH = path4.join(dirname5(fileURLToPath3(import.meta.url)), "bridge", "mempalace_bridge.py");
1256
+ MIGRATION_STATE_VERSION = 2;
1078
1257
  }
1079
1258
  });
1080
1259
 
@@ -1174,7 +1353,7 @@ function parseMemoryContent(content) {
1174
1353
  const [, frontmatterStr, body] = match;
1175
1354
  const frontmatter = yaml.load(frontmatterStr);
1176
1355
  return {
1177
- id: "",
1356
+ id: frontmatter.id || "",
1178
1357
  title: frontmatter.title,
1179
1358
  content: body.trim(),
1180
1359
  tags: frontmatter.tags || [],
@@ -1186,6 +1365,7 @@ function parseMemoryContent(content) {
1186
1365
  }
1187
1366
  function writeMemoryFile(filePath, record) {
1188
1367
  const frontmatter = {
1368
+ id: record.id,
1189
1369
  title: record.title,
1190
1370
  tags: record.tags,
1191
1371
  project: record.project,
@@ -1405,12 +1585,15 @@ var init_storage = __esm({
1405
1585
  }
1406
1586
  this.mempalaceInstall = install;
1407
1587
  this.backend = "mempalace";
1408
- if (!isMigrated()) {
1588
+ const sourceFingerprint = getMemorySourceFingerprint(getMemoryBaseDir());
1589
+ if (!isMigrated(sourceFingerprint)) {
1409
1590
  try {
1410
- runBridge(install, this.palacePath, "migrate", {
1591
+ const result = runBridge(install, this.palacePath, "migrate", {
1411
1592
  source_dir: getMemoryBaseDir()
1412
- });
1413
- markMigrated();
1593
+ }, 15 * 6e4);
1594
+ if (result && result.failed === 0 && result.verified === result.discovered) {
1595
+ markMigrated(sourceFingerprint, result);
1596
+ }
1414
1597
  } catch {
1415
1598
  }
1416
1599
  }
@@ -1597,7 +1780,7 @@ ${record.content}
1597
1780
  const filePath = path5.join(this.scopeDir, file);
1598
1781
  const record = parseMemoryFile(filePath);
1599
1782
  if (!record) continue;
1600
- const id = record.title.toLowerCase().replace(/[^a-z0-9]+/g, "_");
1783
+ const id = record.id || record.title.toLowerCase().replace(/[^a-z0-9]+/g, "_");
1601
1784
  if (existingIds.has(id)) continue;
1602
1785
  try {
1603
1786
  record.id = id;
@@ -2239,12 +2422,12 @@ var init_embedding = __esm({
2239
2422
  });
2240
2423
 
2241
2424
  // packages/info-screen/usage-parser.ts
2242
- import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync4, existsSync as existsSync9, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8, renameSync as renameSync2, openSync, readSync, closeSync } from "node:fs";
2243
- import { join as join9, dirname as dirname10 } from "node:path";
2244
- 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";
2245
2428
  function getCachePath() {
2246
- const base = process.env.UNIPI_DIR || join9(homedir5(), ".unipi");
2247
- 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");
2248
2431
  }
2249
2432
  function readCache() {
2250
2433
  const empty = { version: CACHE_VERSION, models: [], files: {} };
@@ -2262,16 +2445,16 @@ function readCache() {
2262
2445
  function writeCache(cache) {
2263
2446
  try {
2264
2447
  const path28 = getCachePath();
2265
- mkdirSync7(dirname10(path28), { recursive: true });
2448
+ mkdirSync8(dirname9(path28), { recursive: true });
2266
2449
  const tmp = `${path28}.${process.pid}.tmp`;
2267
- writeFileSync8(tmp, JSON.stringify(cache), "utf-8");
2268
- renameSync2(tmp, path28);
2450
+ writeFileSync9(tmp, JSON.stringify(cache), "utf-8");
2451
+ renameSync3(tmp, path28);
2269
2452
  } catch {
2270
2453
  }
2271
2454
  }
2272
2455
  function getSessionsDir() {
2273
- const agentDir = process.env.PI_CODING_AGENT_DIR || join9(homedir5(), ".pi", "agent");
2274
- return join9(agentDir, "sessions");
2456
+ const agentDir = process.env.PI_CODING_AGENT_DIR || join10(homedir6(), ".pi", "agent");
2457
+ return join10(agentDir, "sessions");
2275
2458
  }
2276
2459
  function getPeriodBounds() {
2277
2460
  const now2 = /* @__PURE__ */ new Date();
@@ -2361,9 +2544,9 @@ function extractRecords(filePath, modelIndex, models) {
2361
2544
  }
2362
2545
  function collectSessionFiles(dir, files) {
2363
2546
  try {
2364
- const entries = readdirSync4(dir, { withFileTypes: true });
2547
+ const entries = readdirSync5(dir, { withFileTypes: true });
2365
2548
  for (const entry of entries) {
2366
- const entryPath = join9(dir, entry.name);
2549
+ const entryPath = join10(dir, entry.name);
2367
2550
  if (entry.isDirectory()) {
2368
2551
  collectSessionFiles(entryPath, files);
2369
2552
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -2375,7 +2558,7 @@ function collectSessionFiles(dir, files) {
2375
2558
  }
2376
2559
  async function parseUsageStatsAsync() {
2377
2560
  const yielder = async () => {
2378
- await new Promise((resolve13) => setImmediate(resolve13));
2561
+ await new Promise((resolve14) => setImmediate(resolve14));
2379
2562
  };
2380
2563
  const { stats, pending } = collectStats(yielder);
2381
2564
  if (pending) await pending;
@@ -2411,7 +2594,7 @@ function collectStats(yielder) {
2411
2594
  let mtimeMs = 0;
2412
2595
  let size = 0;
2413
2596
  try {
2414
- const st = statSync4(filePath);
2597
+ const st = statSync5(filePath);
2415
2598
  mtimeMs = st.mtimeMs;
2416
2599
  size = st.size;
2417
2600
  } catch {
@@ -2592,21 +2775,21 @@ __export(env_exports, {
2592
2775
  formatEnvironmentInfo: () => formatEnvironmentInfo,
2593
2776
  getEnvironmentInfo: () => getEnvironmentInfo
2594
2777
  });
2595
- import { existsSync as existsSync18, readdirSync as readdirSync10 } from "node:fs";
2596
- import { join as join18, resolve as resolve5 } from "node:path";
2597
- import { homedir as homedir14 } from "node:os";
2778
+ import { existsSync as existsSync18, readdirSync as readdirSync11 } from "node:fs";
2779
+ import { join as join20, resolve as resolve6 } from "node:path";
2780
+ import { homedir as homedir16 } from "node:os";
2598
2781
  function getEnvironmentInfo() {
2599
2782
  const unipiModules = [];
2600
2783
  const configPaths = [];
2601
2784
  const extensionPaths = [];
2602
2785
  try {
2603
- const nodeModules = resolve5(process.cwd(), "node_modules");
2786
+ const nodeModules = resolve6(process.cwd(), "node_modules");
2604
2787
  if (existsSync18(nodeModules)) {
2605
- const scopePath = join18(nodeModules, "@pi-unipi");
2788
+ const scopePath = join20(nodeModules, "@pi-unipi");
2606
2789
  if (existsSync18(scopePath)) {
2607
- for (const entry of readdirSync10(scopePath)) {
2790
+ for (const entry of readdirSync11(scopePath)) {
2608
2791
  if (entry.startsWith(".")) continue;
2609
- const pkgPath = join18(scopePath, entry, "package.json");
2792
+ const pkgPath = join20(scopePath, entry, "package.json");
2610
2793
  if (existsSync18(pkgPath)) {
2611
2794
  unipiModules.push(`@pi-unipi/${entry}`);
2612
2795
  }
@@ -2615,8 +2798,8 @@ function getEnvironmentInfo() {
2615
2798
  }
2616
2799
  } catch {
2617
2800
  }
2618
- const globalConfig = join18(homedir14(), ".unipi", "config");
2619
- const projectConfig = resolve5(process.cwd(), ".unipi", "config");
2801
+ const globalConfig = join20(homedir16(), ".unipi", "config");
2802
+ const projectConfig = resolve6(process.cwd(), ".unipi", "config");
2620
2803
  if (existsSync18(globalConfig)) {
2621
2804
  configPaths.push(globalConfig);
2622
2805
  }
@@ -2624,7 +2807,7 @@ function getEnvironmentInfo() {
2624
2807
  configPaths.push(projectConfig);
2625
2808
  }
2626
2809
  try {
2627
- const piDir = join18(homedir14(), ".pi");
2810
+ const piDir = join20(homedir16(), ".pi");
2628
2811
  if (existsSync18(piDir)) {
2629
2812
  extensionPaths.push(piDir);
2630
2813
  }
@@ -3861,11 +4044,11 @@ var init_schema = __esm({
3861
4044
  });
3862
4045
 
3863
4046
  // packages/compactor/src/config/manager.ts
3864
- import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync29, writeFileSync as writeFileSync23 } from "node:fs";
3865
- import { homedir as homedir20 } from "node:os";
3866
- import { dirname as dirname25, 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";
3867
4050
  function projectConfigPath(cwd) {
3868
- return join32(cwd, ".unipi", "config", "compactor.json");
4051
+ return join34(cwd, ".unipi", "config", "compactor.json");
3869
4052
  }
3870
4053
  function deepMerge(base, override) {
3871
4054
  const result = { ...base };
@@ -3900,9 +4083,9 @@ function loadConfig4(cwd) {
3900
4083
  function saveConfig3(config, opts) {
3901
4084
  try {
3902
4085
  const targetPath = opts?.perProject && opts?.cwd ? projectConfigPath(opts.cwd) : COMPACTOR_CONFIG_PATH;
3903
- const dir = dirname25(targetPath);
3904
- if (!existsSync31(dir)) mkdirSync19(dir, { recursive: true });
3905
- writeFileSync23(targetPath, `${JSON.stringify(config, null, 2)}
4086
+ const dir = dirname24(targetPath);
4087
+ if (!existsSync31(dir)) mkdirSync21(dir, { recursive: true });
4088
+ writeFileSync25(targetPath, `${JSON.stringify(config, null, 2)}
3906
4089
  `);
3907
4090
  return { success: true };
3908
4091
  } catch (err) {
@@ -3935,10 +4118,10 @@ function migrateConfig(partial) {
3935
4118
  }
3936
4119
  function scaffoldConfig() {
3937
4120
  try {
3938
- const dir = dirname25(COMPACTOR_CONFIG_PATH);
3939
- if (!existsSync31(dir)) mkdirSync19(dir, { recursive: true });
4121
+ const dir = dirname24(COMPACTOR_CONFIG_PATH);
4122
+ if (!existsSync31(dir)) mkdirSync21(dir, { recursive: true });
3940
4123
  if (!existsSync31(COMPACTOR_CONFIG_PATH)) {
3941
- writeFileSync23(COMPACTOR_CONFIG_PATH, `${JSON.stringify(DEFAULT_COMPACTOR_CONFIG, null, 2)}
4124
+ writeFileSync25(COMPACTOR_CONFIG_PATH, `${JSON.stringify(DEFAULT_COMPACTOR_CONFIG, null, 2)}
3942
4125
  `);
3943
4126
  }
3944
4127
  } catch {
@@ -3949,7 +4132,7 @@ var init_manager = __esm({
3949
4132
  "packages/compactor/src/config/manager.ts"() {
3950
4133
  "use strict";
3951
4134
  init_schema();
3952
- COMPACTOR_CONFIG_PATH = join32(homedir20(), ".unipi", "config", "compactor", "config.json");
4135
+ COMPACTOR_CONFIG_PATH = join34(homedir22(), ".unipi", "config", "compactor", "config.json");
3953
4136
  readJson3 = (path28) => {
3954
4137
  try {
3955
4138
  return JSON.parse(readFileSync29(path28, "utf-8"));
@@ -5354,7 +5537,6 @@ function buildIntentSection(intentEvents) {
5354
5537
  function buildResumeSnapshot(events, opts) {
5355
5538
  const compactCount = opts?.compactCount ?? 1;
5356
5539
  const searchTool = opts?.searchTool ?? "ctx_search";
5357
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
5358
5540
  const fileEvents = [];
5359
5541
  const taskEvents = [];
5360
5542
  const ruleEvents = [];
@@ -5430,7 +5612,7 @@ function buildResumeSnapshot(events, opts) {
5430
5612
  if (skills) sections.push(skills);
5431
5613
  const intent = buildIntentSection(intentEvents);
5432
5614
  if (intent) sections.push(intent);
5433
- const header = `<session_resume events="${events.length}" compact_count="${compactCount}" generated_at="${now2}">`;
5615
+ const header = `<session_resume events="${events.length}" compact_count="${compactCount}">`;
5434
5616
  const footer = `</session_resume>`;
5435
5617
  const body = sections.join("\n\n");
5436
5618
  if (body) {
@@ -5452,9 +5634,9 @@ var init_snapshot = __esm({
5452
5634
  });
5453
5635
 
5454
5636
  // packages/compactor/src/config/presets.ts
5455
- import { createHash as createHash3 } from "node:crypto";
5637
+ import { createHash as createHash4 } from "node:crypto";
5456
5638
  function presetHash(config) {
5457
- return createHash3("sha256").update(JSON.stringify(config)).digest("hex");
5639
+ return createHash4("sha256").update(JSON.stringify(config)).digest("hex");
5458
5640
  }
5459
5641
  function configsEqual(a, b) {
5460
5642
  const aHash = presetHash(a);
@@ -6225,7 +6407,7 @@ __export(policy_exports, {
6225
6407
  readsOrCreatesPolicy: () => readsOrCreatesPolicy
6226
6408
  });
6227
6409
  import { existsSync as existsSync36, readFileSync as readFileSync31 } from "node:fs";
6228
- import { join as join35 } from "node:path";
6410
+ import { join as join37 } from "node:path";
6229
6411
  function parseBashPattern(pattern) {
6230
6412
  const match = pattern.match(/^Bash\((.+)\)$/);
6231
6413
  return match ? match[1] : null;
@@ -6280,7 +6462,7 @@ function fileGlobToRegex(glob, caseInsensitive = false) {
6280
6462
  return new RegExp(`^${regexStr}$`, caseInsensitive ? "i" : "");
6281
6463
  }
6282
6464
  function readsOrCreatesPolicy(cwd) {
6283
- const settingsPath = join35(cwd, ".pi", "settings.json");
6465
+ const settingsPath = join37(cwd, ".pi", "settings.json");
6284
6466
  if (!existsSync36(settingsPath)) return { allow: [], deny: [], ask: [] };
6285
6467
  try {
6286
6468
  const settings = JSON.parse(readFileSync31(settingsPath, "utf-8"));
@@ -6309,10 +6491,10 @@ __export(evaluator_exports, {
6309
6491
  splitChainedCommands: () => splitChainedCommands
6310
6492
  });
6311
6493
  import { realpathSync as realpathSync2, existsSync as existsSync37, readFileSync as readFileSync32 } from "node:fs";
6312
- import { resolve as resolve11, join as join36 } from "node:path";
6313
- 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";
6314
6496
  function loadProjectPermissions(cwd, basePolicy) {
6315
- const settingsPath = join36(cwd, ".pi", "settings.json");
6497
+ const settingsPath = join38(cwd, ".pi", "settings.json");
6316
6498
  if (!existsSync37(settingsPath)) return basePolicy;
6317
6499
  try {
6318
6500
  const raw = readFileSync32(settingsPath, "utf-8");
@@ -6381,10 +6563,10 @@ function splitChainedCommands(command) {
6381
6563
  return commands;
6382
6564
  }
6383
6565
  function evaluateFilePath(filePath, policy, cwd = process.cwd()) {
6384
- const resolved = resolve11(cwd, filePath);
6566
+ const resolved = resolve12(cwd, filePath);
6385
6567
  try {
6386
6568
  const real = realpathSync2(resolved);
6387
- const home = homedir22();
6569
+ const home = homedir24();
6388
6570
  if (!real.startsWith(cwd) && !real.startsWith(home)) {
6389
6571
  return "deny";
6390
6572
  }
@@ -6551,17 +6733,20 @@ var init_thinking_label = __esm({
6551
6733
  init_core();
6552
6734
  import { dirname as dirname2 } from "node:path";
6553
6735
  import { fileURLToPath } from "node:url";
6736
+ import {
6737
+ buildSessionContext
6738
+ } from "@earendil-works/pi-coding-agent";
6554
6739
 
6555
6740
  // packages/workflow/commands.ts
6556
6741
  init_core();
6557
6742
  import { readFileSync as readFileSync3, readdirSync, existsSync as existsSync3, statSync as statSync2 } from "fs";
6558
- import { join as join3, basename } from "path";
6743
+ import { join as join4, basename } from "path";
6559
6744
  function suggestSpecFiles(prefix) {
6560
- const specsDir = join3(process.cwd(), ".unipi", "docs", "specs");
6745
+ const specsDir = join4(process.cwd(), ".unipi", "docs", "specs");
6561
6746
  if (!existsSync3(specsDir)) return [];
6562
6747
  try {
6563
6748
  const search2 = prefix?.trim().split(/\s+/).pop() ?? "";
6564
- 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);
6565
6750
  return files.filter((f) => !search2 || f.name.includes(search2)).map((f) => ({
6566
6751
  value: `specs:${f.name}`,
6567
6752
  label: basename(f.name, ".md"),
@@ -6572,11 +6757,11 @@ function suggestSpecFiles(prefix) {
6572
6757
  }
6573
6758
  }
6574
6759
  function suggestPlanFiles(prefix) {
6575
- const plansDir = join3(process.cwd(), ".unipi", "docs", "plans");
6760
+ const plansDir = join4(process.cwd(), ".unipi", "docs", "plans");
6576
6761
  if (!existsSync3(plansDir)) return [];
6577
6762
  try {
6578
6763
  const search2 = prefix?.trim().split(/\s+/).pop() ?? "";
6579
- 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);
6580
6765
  return files.filter((f) => !search2 || f.name.includes(search2)).map((f) => ({
6581
6766
  value: `plan:${f.name}`,
6582
6767
  label: basename(f.name, ".md"),
@@ -6587,11 +6772,11 @@ function suggestPlanFiles(prefix) {
6587
6772
  }
6588
6773
  }
6589
6774
  function suggestDebugFiles(prefix) {
6590
- const debugDir = join3(process.cwd(), ".unipi", "docs", "debug");
6775
+ const debugDir = join4(process.cwd(), ".unipi", "docs", "debug");
6591
6776
  if (!existsSync3(debugDir)) return [];
6592
6777
  try {
6593
6778
  const search2 = prefix?.trim().split(/\s+/).pop() ?? "";
6594
- 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);
6595
6780
  return files.filter((f) => !search2 || f.name.includes(search2)).map((f) => ({
6596
6781
  value: `debug:${f.name}`,
6597
6782
  label: basename(f.name, ".md"),
@@ -6602,11 +6787,11 @@ function suggestDebugFiles(prefix) {
6602
6787
  }
6603
6788
  }
6604
6789
  function suggestChoreFiles(prefix) {
6605
- const choreDir = join3(process.cwd(), ".unipi", "docs", "chore");
6790
+ const choreDir = join4(process.cwd(), ".unipi", "docs", "chore");
6606
6791
  if (!existsSync3(choreDir)) return [];
6607
6792
  try {
6608
6793
  const search2 = prefix?.trim().split(/\s+/).pop() ?? "";
6609
- 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);
6610
6795
  return files.filter((f) => !search2 || f.name.includes(search2)).map((f) => ({
6611
6796
  value: `chore:${f.name}`,
6612
6797
  label: basename(f.name, ".md"),
@@ -6622,7 +6807,7 @@ function suggestWorktrees() {
6622
6807
  if (worktreeSuggestionsCache?.cwd === cwd) {
6623
6808
  return worktreeSuggestionsCache.items;
6624
6809
  }
6625
- const worktreesDir = join3(cwd, ".unipi", "worktrees");
6810
+ const worktreesDir = join4(cwd, ".unipi", "worktrees");
6626
6811
  if (!existsSync3(worktreesDir)) {
6627
6812
  worktreeSuggestionsCache = { cwd, items: [] };
6628
6813
  return worktreeSuggestionsCache.items;
@@ -6631,7 +6816,7 @@ function suggestWorktrees() {
6631
6816
  let findWorktrees2 = function(dir, relativePath) {
6632
6817
  const entries = readdirSync(dir, { withFileTypes: true });
6633
6818
  for (const entry of entries) {
6634
- const fullPath = join3(dir, entry.name);
6819
+ const fullPath = join4(dir, entry.name);
6635
6820
  const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
6636
6821
  if (entry.name === ".git" && entry.isFile()) {
6637
6822
  try {
@@ -6824,18 +7009,13 @@ function registerWorkflowCommands(pi, options) {
6824
7009
  fullCommand: `/${fullCommand}`,
6825
7010
  args: args?.trim() ?? ""
6826
7011
  };
6827
- if (!options.startWorkflow(workflowEvent)) {
7012
+ if (!options.activateSandbox(workflowEvent)) {
6828
7013
  if (ctx.hasUI) ctx.ui.notify("Another UniPi workflow is still active", "warning");
6829
7014
  return;
6830
7015
  }
6831
- const currentTools = options.getActiveTools();
6832
- options.saveTools(currentTools);
6833
- const sandboxTools = getToolsForCommand(cmd.name, currentTools);
6834
- const sandboxLevel = getSandboxLevel(cmd.name);
6835
- options.setActiveTools([...sandboxTools], sandboxLevel);
6836
7016
  let skillContent = "";
6837
7017
  try {
6838
- const skillPath = join3(
7018
+ const skillPath = join4(
6839
7019
  new URL("./skills", import.meta.url).pathname,
6840
7020
  cmd.skillName,
6841
7021
  "SKILL.md"
@@ -6861,7 +7041,12 @@ Arguments: ${args.trim()}`;
6861
7041
  ${skillContent}
6862
7042
  </skill_content>`;
6863
7043
  }
6864
- pi.sendUserMessage(message, { deliverAs: "followUp" });
7044
+ try {
7045
+ pi.sendUserMessage(message, { deliverAs: "followUp" });
7046
+ } catch (error) {
7047
+ options.abortWorkflow();
7048
+ throw error;
7049
+ }
6865
7050
  if (ctx.hasUI) {
6866
7051
  ctx.ui.notify(`Running /${fullCommand}`, "info");
6867
7052
  const ralphStatus = options.isRalphDetected() ? "\u2713 rl" : "\u25CB rl";
@@ -6929,90 +7114,147 @@ var WorkflowLifecycle = class {
6929
7114
 
6930
7115
  // packages/workflow/index.ts
6931
7116
  var VERSION = getPackageVersion(dirname2(fileURLToPath(import.meta.url)));
6932
- var ralphDetected = false;
6933
- var savedTools = null;
6934
- var sandboxActive = false;
6935
- var currentSandboxLevel = null;
6936
- var currentSandboxTools = null;
7117
+ var WORKFLOW_SANDBOX_SNAPSHOT_TYPE = "unipi-workflow-sandbox-snapshot";
7118
+ function sandboxRestrictions(level) {
7119
+ const common = [
7120
+ "Do not attempt to call tools blocked by name.",
7121
+ "If the user requests an action that requires a blocked tool, explain that the workflow sandbox does not allow it."
7122
+ ];
7123
+ if (level === "brainstorm") {
7124
+ return [
7125
+ "The write tool is restricted to .unipi/docs/specs/ only.",
7126
+ "Use bash only for specific setup operations such as git init or mkdir; use grep, find, and ls for discovery instead of bash.",
7127
+ ...common
7128
+ ];
7129
+ }
7130
+ if (level === "write_unipi") {
7131
+ return [
7132
+ "The write tool is restricted to .unipi/docs/ only (specs and plans).",
7133
+ "Use grep, find, and ls for file discovery instead of guessing filenames.",
7134
+ "Bash is blocked; use read, write, edit, grep, find, and ls only.",
7135
+ ...common
7136
+ ];
7137
+ }
7138
+ return common;
7139
+ }
7140
+ function formatActiveSandboxSnapshot(command, level) {
7141
+ const blocked = getBlockedToolsForLevel(level);
7142
+ const blockedLine = blocked.length > 0 ? blocked.join(", ") : "none";
7143
+ return [
7144
+ "# UniPi Workflow Sandbox Snapshot",
7145
+ "This snapshot supersedes all prior UniPi workflow sandbox snapshots; use only this snapshot for workflow sandbox status and restrictions.",
7146
+ "Status: active",
7147
+ `Workflow: /unipi:${command}`,
7148
+ `Sandbox level: ${level}`,
7149
+ `Blocked tool names: ${blockedLine}`,
7150
+ "Pi's provider tool schemas and tool order remain unchanged. Calls to blocked tool names are rejected by the workflow sandbox.",
7151
+ ["Restrictions:", ...sandboxRestrictions(level).map((restriction) => `- ${restriction}`)].join("\n")
7152
+ ].join("\n\n");
7153
+ }
7154
+ function formatInactiveSandboxSnapshot() {
7155
+ return [
7156
+ "# UniPi Workflow Sandbox Snapshot",
7157
+ "This snapshot supersedes all prior UniPi workflow sandbox snapshots; use only this snapshot for workflow sandbox status and restrictions.",
7158
+ "Status: inactive",
7159
+ "No UniPi workflow sandbox is active. Prior workflow sandbox restrictions no longer apply."
7160
+ ].join("\n\n");
7161
+ }
7162
+ function latestEffectiveSandboxSnapshot(branch) {
7163
+ const messages = buildSessionContext(branch).messages;
7164
+ for (let index = messages.length - 1; index >= 0; index--) {
7165
+ const message = messages[index];
7166
+ if (message.role === "custom" && message.customType === WORKFLOW_SANDBOX_SNAPSHOT_TYPE) {
7167
+ return {
7168
+ content: message.content,
7169
+ details: message.details
7170
+ };
7171
+ }
7172
+ }
7173
+ return void 0;
7174
+ }
7175
+ function latestHistoricalSandboxSnapshot(branch) {
7176
+ for (let index = branch.length - 1; index >= 0; index--) {
7177
+ const entry = branch[index];
7178
+ if (entry.type === "custom_message" && entry.customType === WORKFLOW_SANDBOX_SNAPSHOT_TYPE) {
7179
+ return {
7180
+ content: entry.content,
7181
+ details: entry.details
7182
+ };
7183
+ }
7184
+ }
7185
+ return void 0;
7186
+ }
7187
+ function isActiveSnapshot(snapshot) {
7188
+ if (typeof snapshot.details?.active === "boolean") return snapshot.details.active;
7189
+ return typeof snapshot.content === "string" && snapshot.content.includes("Status: active");
7190
+ }
6937
7191
  function workflow_default(pi) {
6938
7192
  const workflowLifecycle = new WorkflowLifecycle();
7193
+ let ralphDetected = false;
7194
+ let sandboxCommand = null;
6939
7195
  registerWorkflowCommands(pi, {
6940
7196
  isRalphDetected: () => ralphDetected,
6941
- getActiveTools: () => pi.getActiveTools(),
6942
- setActiveTools: (tools, level) => {
6943
- pi.setActiveTools(tools);
6944
- sandboxActive = true;
6945
- currentSandboxLevel = level;
6946
- currentSandboxTools = tools;
6947
- },
6948
- saveTools: (tools) => {
6949
- savedTools = tools;
6950
- },
6951
- startWorkflow: (event) => {
7197
+ activateSandbox: (event) => {
6952
7198
  if (!workflowLifecycle.start(event)) return false;
7199
+ sandboxCommand = event.command;
6953
7200
  emitEvent(pi, UNIPI_EVENTS.WORKFLOW_START, event);
6954
7201
  return true;
7202
+ },
7203
+ abortWorkflow: () => {
7204
+ sandboxCommand = null;
7205
+ workflowLifecycle.reset();
6955
7206
  }
6956
7207
  });
6957
7208
  pi.on("tool_call", async (event, _ctx) => {
6958
- if (!sandboxActive || !currentSandboxLevel) return;
6959
- const allowed = currentSandboxTools ?? [];
6960
- if (!allowed.includes(event.toolName)) {
7209
+ if (!sandboxCommand) return;
7210
+ const level = getSandboxLevel(sandboxCommand);
7211
+ if (!isToolAllowed(level, event.toolName)) {
7212
+ const blocked = getBlockedToolsForLevel(level);
6961
7213
  return {
6962
7214
  block: true,
6963
- reason: `Tool "${event.toolName}" is not allowed in ${currentSandboxLevel} sandbox. Allowed: ${allowed.join(", ")}`
7215
+ reason: `Tool "${event.toolName}" is not allowed in ${level} sandbox. Blocked: ${blocked.join(", ")}`
6964
7216
  };
6965
7217
  }
6966
7218
  });
6967
- pi.on("before_agent_start", async (event, _ctx) => {
6968
- if (!sandboxActive || !currentSandboxLevel) return;
6969
- const allowed = currentSandboxTools ?? pi.getActiveTools();
6970
- const blocked = getBlockedToolsForLevel(currentSandboxLevel);
6971
- const blockedLine = blocked.length > 0 ? `
6972
- Blocked tools: ${blocked.join(", ")} \u2014 removed from your tool list.` : "\nNo tools were blocked by this sandbox.";
6973
- const base = `
6974
-
6975
- <sandbox>
6976
- Sandbox mode: ${currentSandboxLevel}.
6977
- Available tools: ${allowed.join(", ")}.${blockedLine}`;
6978
- if (currentSandboxLevel === "brainstorm") {
6979
- return {
6980
- systemPrompt: event.systemPrompt + base + `
6981
- The write tool is available but restricted to .unipi/docs/specs/ only.
6982
- bash is available ONLY for specific setup use case (e.g., git init, mkdir). Do NOT use bash for reading files or listing directories \u2014 use grep, find, ls instead.
6983
- Do NOT attempt to call blocked tools. Do NOT output tool call XML for them.
6984
- If the user requests an action that requires a blocked tool, respond that you do not have access.
6985
- </sandbox>`
6986
- };
6987
- }
6988
- if (currentSandboxLevel === "write_unipi") {
7219
+ pi.on("before_agent_start", async (_event, ctx) => {
7220
+ const branch = ctx.sessionManager.getBranch();
7221
+ const latest = latestEffectiveSandboxSnapshot(branch);
7222
+ const historical = latestHistoricalSandboxSnapshot(branch);
7223
+ if (sandboxCommand) {
7224
+ const level = getSandboxLevel(sandboxCommand);
7225
+ const content = formatActiveSandboxSnapshot(sandboxCommand, level);
7226
+ if (latest?.content === content) return void 0;
6989
7227
  return {
6990
- systemPrompt: event.systemPrompt + base + `
6991
- Write tool is restricted to .unipi/docs/ only (specs and plans).
6992
- Use grep, find, ls for file discovery \u2014 do NOT guess filenames.
6993
- bash is blocked \u2014 use read, write, edit, grep, find, ls only.
6994
- Do NOT attempt to call blocked tools. Do NOT output tool call XML for them.
6995
- If the user requests an action that requires a blocked tool, respond that you do not have access.
6996
- </sandbox>`
7228
+ message: {
7229
+ customType: WORKFLOW_SANDBOX_SNAPSHOT_TYPE,
7230
+ content,
7231
+ display: false,
7232
+ details: {
7233
+ active: true,
7234
+ command: sandboxCommand,
7235
+ level
7236
+ }
7237
+ }
6997
7238
  };
6998
7239
  }
7240
+ const prior = latest ?? historical;
7241
+ if (!prior || !isActiveSnapshot(prior)) return void 0;
6999
7242
  return {
7000
- systemPrompt: event.systemPrompt + base + `
7001
- Do NOT attempt to call blocked tools. Do NOT output tool call XML for them.
7002
- If the user requires an action that requires a blocked tool, respond that you do not have access.
7003
- </sandbox>`
7243
+ message: {
7244
+ customType: WORKFLOW_SANDBOX_SNAPSHOT_TYPE,
7245
+ content: formatInactiveSandboxSnapshot(),
7246
+ display: false,
7247
+ details: {
7248
+ active: false
7249
+ }
7250
+ }
7004
7251
  };
7005
7252
  });
7006
7253
  pi.on("agent_end", async (event, _ctx) => {
7007
- if (sandboxActive && savedTools) {
7008
- pi.setActiveTools(savedTools);
7009
- savedTools = null;
7010
- sandboxActive = false;
7011
- currentSandboxLevel = null;
7012
- currentSandboxTools = null;
7013
- }
7014
7254
  const completedWorkflow = workflowLifecycle.complete(event.messages);
7015
- if (completedWorkflow) emitEvent(pi, UNIPI_EVENTS.WORKFLOW_END, completedWorkflow);
7255
+ if (!completedWorkflow) return;
7256
+ sandboxCommand = null;
7257
+ emitEvent(pi, UNIPI_EVENTS.WORKFLOW_END, completedWorkflow);
7016
7258
  });
7017
7259
  pi.on("session_start", async (_event, ctx) => {
7018
7260
  initUnipiDirs();
@@ -7025,7 +7267,7 @@ If the user requires an action that requires a blocked tool, respond that you do
7025
7267
  if (!ralphDetected) {
7026
7268
  try {
7027
7269
  const allTools = pi.getAllTools();
7028
- ralphDetected = allTools.some((t) => t.name === "ralph_start");
7270
+ ralphDetected = allTools.some((tool) => tool.name === "ralph_start");
7029
7271
  } catch {
7030
7272
  }
7031
7273
  }
@@ -7036,14 +7278,11 @@ If the user requires an action that requires a blocked tool, respond that you do
7036
7278
  });
7037
7279
  pi.events.on(UNIPI_EVENTS.MODULE_READY, (data) => {
7038
7280
  const event = data;
7039
- if (event?.name === MODULES.RALPH) {
7040
- ralphDetected = true;
7041
- }
7281
+ if (event?.name === MODULES.RALPH) ralphDetected = true;
7042
7282
  });
7043
7283
  pi.on("session_shutdown", async () => {
7044
7284
  ralphDetected = false;
7045
- savedTools = null;
7046
- sandboxActive = false;
7285
+ sandboxCommand = null;
7047
7286
  workflowLifecycle.reset();
7048
7287
  });
7049
7288
  }
@@ -7365,7 +7604,7 @@ Use /unipi:ralph-resume <name> to continue`, "info");
7365
7604
  // packages/ralph/tools.ts
7366
7605
  init_core();
7367
7606
  import { Type } from "typebox";
7368
- function registerRalphTools(pi, manager2) {
7607
+ function registerRalphTools(pi, getManager2) {
7369
7608
  pi.registerTool({
7370
7609
  name: RALPH_TOOLS.START,
7371
7610
  label: "Start Ralph Loop",
@@ -7394,6 +7633,7 @@ function registerRalphTools(pi, manager2) {
7394
7633
  )
7395
7634
  }),
7396
7635
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
7636
+ const manager2 = getManager2(ctx);
7397
7637
  const taskFile = `.unipi/ralph/${params.name.replace(/[^a-zA-Z0-9_-]/g, "_")}.md`;
7398
7638
  if (manager2.loadState(params.name)?.status === "active") {
7399
7639
  return {
@@ -7434,6 +7674,7 @@ function registerRalphTools(pi, manager2) {
7434
7674
  ],
7435
7675
  parameters: Type.Object({}),
7436
7676
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
7677
+ const manager2 = getManager2(ctx);
7437
7678
  if (!manager2.getCurrentLoop()) {
7438
7679
  return {
7439
7680
  content: [{ type: "text", text: "No active Ralph loop." }],
@@ -7491,6 +7732,38 @@ function registerRalphTools(pi, manager2) {
7491
7732
  });
7492
7733
  }
7493
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
+
7494
7767
  // packages/ralph/index.ts
7495
7768
  function getInfoRegistry() {
7496
7769
  return globalThis.__unipi_info_registry;
@@ -7507,6 +7780,7 @@ function getManager(ctx, pi) {
7507
7780
  return manager;
7508
7781
  }
7509
7782
  function ralph_default(pi) {
7783
+ registerRalphTools(pi, (ctx) => getManager(ctx, pi));
7510
7784
  registerCommands(pi);
7511
7785
  pi.on("session_start", async (_event, ctx) => {
7512
7786
  manager = null;
@@ -7587,24 +7861,12 @@ function ralph_default(pi) {
7587
7861
  if (!currentLoop) return;
7588
7862
  const state = mgr.loadState(currentLoop);
7589
7863
  if (!state || state.status !== "active") return;
7590
- const iterStr = `${state.iteration}${state.maxIterations > 0 ? `/${state.maxIterations}` : ""}`;
7591
- let instructions = `You are in a Ralph loop working on: ${state.taskFile}
7592
- `;
7593
- if (state.itemsPerIteration > 0) {
7594
- instructions += `- Work on ~${state.itemsPerIteration} items this iteration
7595
- `;
7596
- }
7597
- instructions += `- Update the task file as you progress
7598
- `;
7599
- instructions += `- When FULLY COMPLETE: ${RALPH_COMPLETE_MARKER}
7600
- `;
7601
- instructions += `- Otherwise, call ralph_done tool to proceed to next iteration`;
7864
+ const content = buildRalphLoopReminder(state);
7865
+ if (latestRalphReminder(ctx) === content) return;
7602
7866
  return {
7603
7867
  message: {
7604
- customType: "unipi-ralph-loop-reminder",
7605
- content: `[RALPH LOOP - ${state.name} - Iteration ${iterStr}]
7606
-
7607
- ${instructions}`,
7868
+ customType: RALPH_REMINDER_TYPE,
7869
+ content,
7608
7870
  display: false
7609
7871
  }
7610
7872
  };
@@ -7618,10 +7880,6 @@ ${instructions}`,
7618
7880
  }
7619
7881
  manager = null;
7620
7882
  });
7621
- pi.on("session_start", async (_event, ctx) => {
7622
- const mgr = getManager(ctx, pi);
7623
- registerRalphTools(pi, mgr);
7624
- });
7625
7883
  }
7626
7884
  function registerCommands(pi) {
7627
7885
  const HELP = `Ralph Loop \u2014 Long-running development loops
@@ -7922,7 +8180,7 @@ function handleNuke(rest, ctx, pi) {
7922
8180
  // packages/memory/index.ts
7923
8181
  init_core();
7924
8182
  init_storage();
7925
- import { dirname as dirname8 } from "node:path";
8183
+ import { dirname as dirname7 } from "node:path";
7926
8184
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7927
8185
 
7928
8186
  // packages/memory/tools.ts
@@ -8630,7 +8888,37 @@ init_settings();
8630
8888
  function getInfoRegistry2() {
8631
8889
  return globalThis.__unipi_info_registry;
8632
8890
  }
8633
- var VERSION3 = getPackageVersion(dirname8(fileURLToPath4(import.meta.url)));
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
+ }
8634
8922
  var projectStorage = null;
8635
8923
  var orphanSyncPending = false;
8636
8924
  function ensureOrphanSync(storage) {
@@ -8784,41 +9072,17 @@ function memory_default(pi) {
8784
9072
  recallDone = true;
8785
9073
  return;
8786
9074
  }
8787
- const lines = [
8788
- "## \u{1F9E0} Memory System Active",
8789
- "",
8790
- `You have ${projectMemories.length} memories stored for project "${projectName}".`
8791
- ];
8792
- if (canSearch && projectMemories.length > 0) {
8793
- const titleList = projectMemories.slice(0, 20).map((m) => `- ${m.title}`).join("\n");
8794
- const extra = projectMemories.length > 20 ? `
8795
- ... and ${projectMemories.length - 20} more` : "";
8796
- lines.push(
8797
- "**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
8798
- "",
8799
- "Available memories:",
8800
- titleList + extra
8801
- );
8802
- } else {
8803
- recallDone = true;
8804
- }
8805
- if (canStore) {
8806
- lines.push(
8807
- "",
8808
- "**AFTER completing the task**, if you learned something non-obvious,",
8809
- "call `memory_store` to save it for future sessions."
8810
- );
8811
- } else {
8812
- storeDone = true;
8813
- }
8814
- lines.push(
8815
- "",
8816
- "Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates."
8817
- );
9075
+ if (!canSearch || projectMemories.length === 0) recallDone = true;
9076
+ if (!canStore) storeDone = true;
8818
9077
  return {
8819
9078
  message: {
8820
9079
  customType: "unipi-memory-recall-reminder",
8821
- content: lines.join("\n"),
9080
+ content: buildMemoryRecallReminder({
9081
+ projectName,
9082
+ memories: projectMemories,
9083
+ canSearch,
9084
+ canStore
9085
+ }),
8822
9086
  display: false
8823
9087
  }
8824
9088
  };
@@ -8852,13 +9116,13 @@ function memory_default(pi) {
8852
9116
 
8853
9117
  // packages/info-screen/index.ts
8854
9118
  init_core();
8855
- import { dirname as dirname11 } from "node:path";
9119
+ import { dirname as dirname10 } from "node:path";
8856
9120
  import { fileURLToPath as fileURLToPath5 } from "node:url";
8857
9121
 
8858
9122
  // packages/info-screen/config.ts
8859
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync6 } from "node:fs";
8860
- import { join as join8, dirname as dirname9 } from "node:path";
8861
- 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";
8862
9126
 
8863
9127
  // packages/info-screen/types.ts
8864
9128
  var BOOT_MODES = ["on", "auto-close", "off"];
@@ -8870,7 +9134,7 @@ var DEFAULT_SETTINGS = {
8870
9134
  };
8871
9135
 
8872
9136
  // packages/info-screen/config.ts
8873
- var SETTINGS_PATH = join8(homedir4(), ".pi", "agent", "settings.json");
9137
+ var SETTINGS_PATH = join9(homedir5(), ".pi", "agent", "settings.json");
8874
9138
  var SETTINGS_KEY = "unipi";
8875
9139
  var cachedSettings = null;
8876
9140
  function isRecord(value) {
@@ -8886,11 +9150,11 @@ function readSettingsFile() {
8886
9150
  }
8887
9151
  }
8888
9152
  function writeSettingsFile(data) {
8889
- const dir = dirname9(SETTINGS_PATH);
9153
+ const dir = dirname8(SETTINGS_PATH);
8890
9154
  if (!existsSync8(dir)) {
8891
- mkdirSync6(dir, { recursive: true });
9155
+ mkdirSync7(dir, { recursive: true });
8892
9156
  }
8893
- writeFileSync7(SETTINGS_PATH, JSON.stringify(data, null, 2) + "\n", "utf-8");
9157
+ writeFileSync8(SETTINGS_PATH, JSON.stringify(data, null, 2) + "\n", "utf-8");
8894
9158
  }
8895
9159
  function getInfoSettings() {
8896
9160
  if (cachedSettings) return cachedSettings;
@@ -9187,17 +9451,17 @@ if (!globalThis.__unipi_info_registry) {
9187
9451
 
9188
9452
  // packages/info-screen/core-groups.ts
9189
9453
  init_core();
9190
- import { readFileSync as readFileSync9, readdirSync as readdirSync5, existsSync as existsSync10 } from "node:fs";
9191
- import { join as join10 } from "node:path";
9192
- import { homedir as homedir6 } from "node:os";
9454
+ import { readFileSync as readFileSync9, readdirSync as readdirSync6, existsSync as existsSync10 } from "node:fs";
9455
+ import { join as join11 } from "node:path";
9456
+ import { homedir as homedir7 } from "node:os";
9193
9457
  init_usage_parser();
9194
9458
  function discoverExtensions() {
9195
9459
  const extensions = [];
9196
- const homeDir = process.env.HOME || process.env.USERPROFILE || homedir6();
9460
+ const homeDir = process.env.HOME || process.env.USERPROFILE || homedir7();
9197
9461
  const cwd = process.cwd();
9198
9462
  const settingsPaths = [
9199
- join10(homeDir, ".pi", "agent", "settings.json"),
9200
- join10(cwd, ".pi", "settings.json")
9463
+ join11(homeDir, ".pi", "agent", "settings.json"),
9464
+ join11(cwd, ".pi", "settings.json")
9201
9465
  ];
9202
9466
  const counted = /* @__PURE__ */ new Set();
9203
9467
  for (const settingsPath of settingsPaths) {
@@ -9242,13 +9506,13 @@ function discoverExtensions() {
9242
9506
  }
9243
9507
  }
9244
9508
  const extensionDirs = [
9245
- join10(homeDir, ".pi", "agent", "extensions"),
9246
- join10(cwd, ".pi", "extensions")
9509
+ join11(homeDir, ".pi", "agent", "extensions"),
9510
+ join11(cwd, ".pi", "extensions")
9247
9511
  ];
9248
9512
  for (const dir of extensionDirs) {
9249
9513
  if (!existsSync10(dir)) continue;
9250
9514
  try {
9251
- const entries = readdirSync5(dir, { withFileTypes: true });
9515
+ const entries = readdirSync6(dir, { withFileTypes: true });
9252
9516
  for (const entry of entries) {
9253
9517
  const name = entry.name;
9254
9518
  if (counted.has(name)) continue;
@@ -9310,11 +9574,11 @@ function getTotalLoadTime() {
9310
9574
  var extraSkillDirs = [];
9311
9575
  function discoverSkills() {
9312
9576
  const skills = [];
9313
- const homeDir = process.env.HOME || process.env.USERPROFILE || homedir6();
9577
+ const homeDir = process.env.HOME || process.env.USERPROFILE || homedir7();
9314
9578
  const cwd = process.cwd();
9315
9579
  const skillDirs = [
9316
- join10(homeDir, ".pi", "agent", "skills"),
9317
- join10(cwd, ".pi", "skills"),
9580
+ join11(homeDir, ".pi", "agent", "skills"),
9581
+ join11(cwd, ".pi", "skills"),
9318
9582
  // Add extra dirs from extensions
9319
9583
  ...extraSkillDirs
9320
9584
  ];
@@ -9322,18 +9586,18 @@ function discoverSkills() {
9322
9586
  for (const dir of skillDirs) {
9323
9587
  if (!existsSync10(dir)) continue;
9324
9588
  try {
9325
- const entries = readdirSync5(dir, { withFileTypes: true });
9589
+ const entries = readdirSync6(dir, { withFileTypes: true });
9326
9590
  for (const entry of entries) {
9327
9591
  if (!entry.isDirectory()) continue;
9328
9592
  const name = entry.name;
9329
9593
  if (counted.has(name)) continue;
9330
- const skillPath = join10(dir, name, "SKILL.md");
9594
+ const skillPath = join11(dir, name, "SKILL.md");
9331
9595
  if (existsSync10(skillPath)) {
9332
9596
  counted.add(name);
9333
9597
  let source = "extension";
9334
- if (dir.includes(join10(homeDir, ".pi"))) {
9598
+ if (dir.includes(join11(homeDir, ".pi"))) {
9335
9599
  source = "global";
9336
- } else if (dir === join10(cwd, ".pi", "skills")) {
9600
+ } else if (dir === join11(cwd, ".pi", "skills")) {
9337
9601
  source = "project";
9338
9602
  }
9339
9603
  skills.push({ name, source });
@@ -9384,7 +9648,7 @@ function registerCoreGroups() {
9384
9648
  },
9385
9649
  dataProvider: async () => {
9386
9650
  const cwd = process.cwd();
9387
- const homeDir = process.env.HOME || process.env.USERPROFILE || homedir6();
9651
+ const homeDir = process.env.HOME || process.env.USERPROFILE || homedir7();
9388
9652
  const shortCwd = cwd.startsWith(homeDir) ? `~${cwd.slice(homeDir.length)}` : cwd;
9389
9653
  const announced = getAnnouncedModules();
9390
9654
  const registeredGroups = infoRegistry.getAllGroups();
@@ -10433,7 +10697,7 @@ var SettingsOverlay = class _SettingsOverlay {
10433
10697
  };
10434
10698
 
10435
10699
  // packages/info-screen/index.ts
10436
- var VERSION4 = getPackageVersion(dirname11(fileURLToPath5(import.meta.url)));
10700
+ var VERSION4 = getPackageVersion(dirname10(fileURLToPath5(import.meta.url)));
10437
10701
  function info_screen_default(pi) {
10438
10702
  setPiApi(pi);
10439
10703
  registerCoreGroups();
@@ -10576,11 +10840,15 @@ function info_screen_default(pi) {
10576
10840
  import { defineTool } from "@earendil-works/pi-coding-agent";
10577
10841
  import { Text } from "@earendil-works/pi-tui";
10578
10842
  import { Type as Type3 } from "typebox";
10579
- import { existsSync as existsSync13, readdirSync as readdirSync7 } from "node:fs";
10580
- import { join as join13 } from "node:path";
10581
- import { homedir as homedir9 } from "node:os";
10843
+ import { existsSync as existsSync13, readdirSync as readdirSync8 } from "node:fs";
10844
+ import { join as join15 } from "node:path";
10845
+ import { homedir as homedir11 } from "node:os";
10582
10846
 
10583
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";
10584
10852
  var MODULES2 = { SUBAGENTS: "subagents" };
10585
10853
  var UNIPI_EVENTS2 = {
10586
10854
  MODULE_READY: "unipi:module:ready",
@@ -10594,6 +10862,61 @@ function emitEvent2(pi, eventName, payload) {
10594
10862
  return false;
10595
10863
  }
10596
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
+ }
10597
10920
  async function withHerdrBlocked2(pi, label, fn) {
10598
10921
  emitEvent2(pi, "herdr:blocked", { active: true, label });
10599
10922
  try {
@@ -10604,7 +10927,7 @@ async function withHerdrBlocked2(pi, label, fn) {
10604
10927
  }
10605
10928
 
10606
10929
  // packages/subagents/src/agent-manager.ts
10607
- import { randomUUID } from "node:crypto";
10930
+ import { randomUUID as randomUUID3 } from "node:crypto";
10608
10931
 
10609
10932
  // packages/subagents/src/agent-runner.ts
10610
10933
  import {
@@ -10657,7 +10980,7 @@ var BUILTIN_CONFIGS = {
10657
10980
  };
10658
10981
 
10659
10982
  // packages/subagents/src/agent-runner.ts
10660
- var EXCLUDED_TOOL_NAMES = ["Agent", "get_result"];
10983
+ var EXCLUDED_TOOL_NAMES = ["spawn_helper", "get_helper_result", "Agent", "get_result"];
10661
10984
  var BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
10662
10985
  var defaultMaxTurns;
10663
10986
  var graceTurns = 5;
@@ -10815,22 +11138,25 @@ ${agentPrompt}`;
10815
11138
  init_model_resolver();
10816
11139
 
10817
11140
  // packages/subagents/src/custom-agents.ts
10818
- import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync10, renameSync as renameSync3 } from "node:fs";
10819
- import { join as join11 } from "node:path";
10820
- import { homedir as homedir7 } from "node:os";
11141
+ import { existsSync as existsSync11, readdirSync as readdirSync7, readFileSync as readFileSync10, renameSync as renameSync4 } from "node:fs";
11142
+ import { join as join13 } from "node:path";
11143
+ import { homedir as homedir9 } from "node:os";
10821
11144
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
11145
+ function compareCodeUnits(a, b) {
11146
+ return a < b ? -1 : a > b ? 1 : 0;
11147
+ }
10822
11148
  function backupCorrupted(filePath) {
10823
11149
  const backupPath = filePath + ".bak";
10824
11150
  try {
10825
- renameSync3(filePath, backupPath);
11151
+ renameSync4(filePath, backupPath);
10826
11152
  } catch {
10827
11153
  }
10828
11154
  }
10829
11155
  function getProjectAgentsDir(cwd) {
10830
- return join11(cwd, ".unipi", "config", "agents");
11156
+ return join13(cwd, ".unipi", "config", "agents");
10831
11157
  }
10832
11158
  function getGlobalAgentsDir() {
10833
- return join11(homedir7(), ".unipi", "config", "agents");
11159
+ return join13(homedir9(), ".unipi", "config", "agents");
10834
11160
  }
10835
11161
  var BUILTIN_TOOL_NAMES2 = ["read", "bash", "edit", "write", "grep", "find", "ls"];
10836
11162
  function loadAgentFromFile(filePath, source) {
@@ -10871,9 +11197,9 @@ function loadCustomAgents(cwd) {
10871
11197
  const agents = /* @__PURE__ */ new Map();
10872
11198
  const globalDir = getGlobalAgentsDir();
10873
11199
  if (existsSync11(globalDir)) {
10874
- const files = readdirSync6(globalDir).filter((f) => f.endsWith(".md"));
11200
+ const files = readdirSync7(globalDir).filter((f) => f.endsWith(".md")).sort(compareCodeUnits);
10875
11201
  for (const file of files) {
10876
- const agent = loadAgentFromFile(join11(globalDir, file), "global");
11202
+ const agent = loadAgentFromFile(join13(globalDir, file), "global");
10877
11203
  if (agent) {
10878
11204
  agents.set(agent.name, agent);
10879
11205
  }
@@ -10881,9 +11207,9 @@ function loadCustomAgents(cwd) {
10881
11207
  }
10882
11208
  const projectDir = getProjectAgentsDir(cwd);
10883
11209
  if (existsSync11(projectDir)) {
10884
- const files = readdirSync6(projectDir).filter((f) => f.endsWith(".md"));
11210
+ const files = readdirSync7(projectDir).filter((f) => f.endsWith(".md")).sort(compareCodeUnits);
10885
11211
  for (const file of files) {
10886
- const agent = loadAgentFromFile(join11(projectDir, file), "project");
11212
+ const agent = loadAgentFromFile(join13(projectDir, file), "project");
10887
11213
  if (agent) {
10888
11214
  agents.set(agent.name, agent);
10889
11215
  }
@@ -10908,17 +11234,17 @@ var FileLock = class {
10908
11234
  */
10909
11235
  async acquire(filePath, agentId) {
10910
11236
  while (this.locks.has(filePath)) {
10911
- await new Promise((resolve13) => {
11237
+ await new Promise((resolve14) => {
10912
11238
  const queue = this.queues.get(filePath) ?? [];
10913
- queue.push(resolve13);
11239
+ queue.push(resolve14);
10914
11240
  this.queues.set(filePath, queue);
10915
11241
  });
10916
11242
  }
10917
11243
  let releaseFn;
10918
- const promise = new Promise((resolve13) => {
11244
+ const promise = new Promise((resolve14) => {
10919
11245
  releaseFn = () => {
10920
11246
  this.locks.delete(filePath);
10921
- resolve13();
11247
+ resolve14();
10922
11248
  const queue = this.queues.get(filePath);
10923
11249
  if (queue && queue.length > 0) {
10924
11250
  const next = queue.shift();
@@ -10976,6 +11302,9 @@ var FileLock = class {
10976
11302
  };
10977
11303
 
10978
11304
  // packages/subagents/src/agent-manager.ts
11305
+ function compareCodeUnits2(a, b) {
11306
+ return a < b ? -1 : a > b ? 1 : 0;
11307
+ }
10979
11308
  var DEFAULT_MAX_CONCURRENT = 4;
10980
11309
  var AgentManager = class {
10981
11310
  agents = /* @__PURE__ */ new Map();
@@ -11009,7 +11338,7 @@ var AgentManager = class {
11009
11338
  ...Object.keys(BUILTIN_CONFIGS).filter((type) => type !== "name-gen"),
11010
11339
  ...this.customAgents.keys(),
11011
11340
  ...Object.keys(this.typeSettings)
11012
- ])];
11341
+ ])].sort(compareCodeUnits2);
11013
11342
  }
11014
11343
  /** A type is enabled only when both JSON config and agent frontmatter allow it. */
11015
11344
  isTypeEnabled(type) {
@@ -11033,7 +11362,7 @@ var AgentManager = class {
11033
11362
  */
11034
11363
  spawn(pi, ctx, type, prompt, options) {
11035
11364
  this.assertTypeEnabled(type);
11036
- const id = randomUUID().slice(0, 17);
11365
+ const id = randomUUID3().slice(0, 17);
11037
11366
  const abortController = new AbortController();
11038
11367
  const record = {
11039
11368
  id,
@@ -11236,9 +11565,9 @@ var AgentManager = class {
11236
11565
  };
11237
11566
 
11238
11567
  // packages/subagents/src/config.ts
11239
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync9 } from "node:fs";
11240
- import { join as join12 } from "node:path";
11241
- 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";
11242
11571
  var DEFAULT_CONFIG2 = {
11243
11572
  maxConcurrent: 4,
11244
11573
  enabled: true,
@@ -11248,20 +11577,20 @@ var DEFAULT_CONFIG2 = {
11248
11577
  }
11249
11578
  };
11250
11579
  function getGlobalConfigPath() {
11251
- return join12(homedir8(), ".unipi", "config", "subagents.json");
11580
+ return join14(homedir10(), ".unipi", "config", "subagents.json");
11252
11581
  }
11253
11582
  function getWorkspaceConfigPath(cwd) {
11254
- return join12(cwd, ".unipi", "config", "subagents.json");
11583
+ return join14(cwd, ".unipi", "config", "subagents.json");
11255
11584
  }
11256
11585
  function ensureDirExists(dirPath) {
11257
11586
  if (!existsSync12(dirPath)) {
11258
- mkdirSync8(dirPath, { recursive: true });
11587
+ mkdirSync10(dirPath, { recursive: true });
11259
11588
  }
11260
11589
  }
11261
11590
  function writeConfigAtomic(filePath, config) {
11262
11591
  const tmpPath = filePath + ".tmp";
11263
- writeFileSync9(tmpPath, JSON.stringify(config, null, 2), "utf-8");
11264
- renameSync4(tmpPath, filePath);
11592
+ writeFileSync11(tmpPath, JSON.stringify(config, null, 2), "utf-8");
11593
+ renameSync5(tmpPath, filePath);
11265
11594
  }
11266
11595
  function loadConfigFromPath(filePath) {
11267
11596
  if (!existsSync12(filePath)) return null;
@@ -11277,7 +11606,7 @@ function loadConfigFromPath(filePath) {
11277
11606
  function repairCorrupted(filePath) {
11278
11607
  const backupPath = filePath + ".bak";
11279
11608
  try {
11280
- renameSync4(filePath, backupPath);
11609
+ renameSync5(filePath, backupPath);
11281
11610
  } catch {
11282
11611
  }
11283
11612
  writeConfigAtomic(filePath, DEFAULT_CONFIG2);
@@ -11285,17 +11614,17 @@ function repairCorrupted(filePath) {
11285
11614
  }
11286
11615
  function initConfig(cwd) {
11287
11616
  const globalPath = getGlobalConfigPath();
11288
- const globalDir = join12(homedir8(), ".unipi", "config");
11289
- const globalAgentsDir = join12(homedir8(), ".unipi", "config", "agents");
11617
+ const globalDir = join14(homedir10(), ".unipi", "config");
11618
+ const globalAgentsDir = join14(homedir10(), ".unipi", "config", "agents");
11290
11619
  ensureDirExists(globalDir);
11291
11620
  ensureDirExists(globalAgentsDir);
11292
11621
  let globalConfig = loadConfigFromPath(globalPath);
11293
11622
  if (globalConfig === null) {
11294
11623
  globalConfig = repairCorrupted(globalPath);
11295
11624
  }
11296
- const workspaceDir = join12(cwd, ".unipi", "config");
11297
- const workspaceAgentsDir = join12(cwd, ".unipi", "config", "agents");
11298
- 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())) {
11299
11628
  ensureDirExists(workspaceDir);
11300
11629
  ensureDirExists(workspaceAgentsDir);
11301
11630
  }
@@ -11959,10 +12288,10 @@ function getStatusLabel(status2, error) {
11959
12288
  function src_default(pi) {
11960
12289
  const config = initConfig(process.cwd());
11961
12290
  if (!config.enabled) return;
11962
- const homeDir = homedir9();
12291
+ const homeDir = homedir11();
11963
12292
  const cwd = process.cwd();
11964
- const globalAgentsDir = join13(homeDir, ".unipi", "config", "agents");
11965
- const workspaceAgentsDir = join13(cwd, ".unipi", "config", "agents");
12293
+ const globalAgentsDir = join15(homeDir, ".unipi", "config", "agents");
12294
+ const workspaceAgentsDir = join15(cwd, ".unipi", "config", "agents");
11966
12295
  const agentActivity = /* @__PURE__ */ new Map();
11967
12296
  let sessionEnded = false;
11968
12297
  const manager2 = new AgentManager(
@@ -12096,7 +12425,7 @@ function src_default(pi) {
12096
12425
  for (const dir of [globalAgentsDir, workspaceAgentsDir]) {
12097
12426
  try {
12098
12427
  if (existsSync13(dir)) {
12099
- for (const file of readdirSync7(dir)) {
12428
+ for (const file of readdirSync8(dir)) {
12100
12429
  if (file.endsWith(".md") && !customTypes.includes(file.replace(".md", ""))) {
12101
12430
  customTypes.push(file.replace(".md", ""));
12102
12431
  }
@@ -12442,17 +12771,22 @@ Use get_result to retrieve full results.`,
12442
12771
  error: record.error
12443
12772
  });
12444
12773
  }
12774
+ const output = boundHelperOutput(record.result?.trim() || "No output.");
12775
+ record.resultArtifactPath = output.artifactPath;
12445
12776
  return textResult(
12446
12777
  `Agent completed in ${(durationMs / 1e3).toFixed(1)}s (${record.toolUses} tool uses${tokenText ? `, ${tokenText} tokens` : ""}).
12447
12778
 
12448
- ` + (record.result?.trim() || "No output."),
12779
+ ` + output.text,
12449
12780
  {
12450
12781
  status: "completed",
12451
12782
  toolUses: record.toolUses,
12452
12783
  tokens: tokenText,
12453
12784
  durationMs,
12454
12785
  turnCount: fgState.turnCount,
12455
- maxTurns: fgState.maxTurns
12786
+ maxTurns: fgState.maxTurns,
12787
+ truncated: output.truncated,
12788
+ originalBytes: output.originalBytes,
12789
+ artifactPath: output.artifactPath
12456
12790
  }
12457
12791
  );
12458
12792
  }
@@ -12528,7 +12862,13 @@ Description: ${record.description}
12528
12862
  } else if (record.status === "error") {
12529
12863
  output += `Error: ${record.error}`;
12530
12864
  } else {
12531
- 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;
12532
12872
  }
12533
12873
  if (record.status !== "running" && record.status !== "queued") {
12534
12874
  record.resultConsumed = true;
@@ -12542,7 +12882,7 @@ Description: ${record.description}
12542
12882
  // packages/btw/extensions/btw.ts
12543
12883
  init_core();
12544
12884
  import {
12545
- buildSessionContext,
12885
+ buildSessionContext as buildSessionContext2,
12546
12886
  createAgentSession as createAgentSession2,
12547
12887
  createExtensionRuntime,
12548
12888
  SessionManager as SessionManager2
@@ -12628,7 +12968,7 @@ function buildBtwSeedState(ctx, thread, mode) {
12628
12968
  if (mode === "contextual") {
12629
12969
  try {
12630
12970
  messages.push(
12631
- ...buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages.filter(
12971
+ ...buildSessionContext2(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages.filter(
12632
12972
  (message) => !isVisibleBtwMessage(message)
12633
12973
  )
12634
12974
  );
@@ -14031,7 +14371,7 @@ ${summary}`;
14031
14371
 
14032
14372
  // packages/web-api/src/index.ts
14033
14373
  init_core();
14034
- import { dirname as dirname12 } from "node:path";
14374
+ import { dirname as dirname11 } from "node:path";
14035
14375
  import { fileURLToPath as fileURLToPath6 } from "node:url";
14036
14376
 
14037
14377
  // packages/web-api/src/tools.ts
@@ -16701,7 +17041,7 @@ var llmSummarizeProvider = {
16701
17041
  registry.register(llmSummarizeProvider);
16702
17042
 
16703
17043
  // packages/web-api/src/index.ts
16704
- var VERSION5 = getPackageVersion(dirname12(fileURLToPath6(import.meta.url)));
17044
+ var VERSION5 = getPackageVersion(dirname11(fileURLToPath6(import.meta.url)));
16705
17045
  function getInfoRegistry4() {
16706
17046
  return globalThis.__unipi_info_registry;
16707
17047
  }
@@ -16773,16 +17113,16 @@ function src_default2(pi) {
16773
17113
 
16774
17114
  // packages/utility/src/index.ts
16775
17115
  init_core();
16776
- import { dirname as dirname15 } from "node:path";
17116
+ import { dirname as dirname14 } from "node:path";
16777
17117
  import { fileURLToPath as fileURLToPath7 } from "node:url";
16778
17118
 
16779
17119
  // packages/utility/src/commands.ts
16780
17120
  init_core();
16781
17121
 
16782
17122
  // packages/utility/src/lifecycle/cleanup.ts
16783
- import { existsSync as existsSync16, statSync as statSync7, readdirSync as readdirSync9, unlinkSync as unlinkSync5, rmdirSync } from "node:fs";
16784
- import { join as join16 } from "node:path";
16785
- import { homedir as homedir12, tmpdir } from "node:os";
17123
+ import { existsSync as existsSync16, statSync as statSync8, readdirSync as readdirSync10, unlinkSync as unlinkSync5, rmdirSync } from "node:fs";
17124
+ import { join as join18 } from "node:path";
17125
+ import { homedir as homedir14, tmpdir } from "node:os";
16786
17126
  var DEFAULTS = {
16787
17127
  dbMaxAgeDays: 14,
16788
17128
  tempMaxAgeDays: 7,
@@ -16791,13 +17131,13 @@ var DEFAULTS = {
16791
17131
  };
16792
17132
  function expandHome(path28) {
16793
17133
  if (path28.startsWith("~/")) {
16794
- return join16(homedir12(), path28.slice(2));
17134
+ return join18(homedir14(), path28.slice(2));
16795
17135
  }
16796
17136
  return path28;
16797
17137
  }
16798
17138
  function isStale(path28, maxAgeDays) {
16799
17139
  try {
16800
- const stats = statSync7(path28);
17140
+ const stats = statSync8(path28);
16801
17141
  const ageMs = Date.now() - stats.mtime.getTime();
16802
17142
  return ageMs > maxAgeDays * 24 * 60 * 60 * 1e3;
16803
17143
  } catch {
@@ -16809,7 +17149,7 @@ function hasWalLock(dbPath) {
16809
17149
  const shmPath = dbPath + "-shm";
16810
17150
  if (existsSync16(walPath)) {
16811
17151
  try {
16812
- const walStats = statSync7(walPath);
17152
+ const walStats = statSync8(walPath);
16813
17153
  const ageMs = Date.now() - walStats.mtime.getTime();
16814
17154
  return ageMs > 5 * 60 * 1e3;
16815
17155
  } catch {
@@ -16830,14 +17170,14 @@ function cleanDbs(options) {
16830
17170
  const scanDir = (dir) => {
16831
17171
  let entries;
16832
17172
  try {
16833
- entries = readdirSync9(dir);
17173
+ entries = readdirSync10(dir);
16834
17174
  } catch {
16835
17175
  return;
16836
17176
  }
16837
17177
  for (const entry of entries) {
16838
- const fullPath = join16(dir, entry);
17178
+ const fullPath = join18(dir, entry);
16839
17179
  try {
16840
- const stats = statSync7(fullPath);
17180
+ const stats = statSync8(fullPath);
16841
17181
  if (stats.isDirectory()) {
16842
17182
  scanDir(fullPath);
16843
17183
  continue;
@@ -16853,7 +17193,7 @@ function cleanDbs(options) {
16853
17193
  const companion = fullPath + suffix;
16854
17194
  if (existsSync16(companion)) {
16855
17195
  unlinkSync5(companion);
16856
- result.bytesFreed += statSync7(companion).size;
17196
+ result.bytesFreed += statSync8(companion).size;
16857
17197
  }
16858
17198
  }
16859
17199
  result.removed++;
@@ -16871,6 +17211,36 @@ function cleanDbs(options) {
16871
17211
  scanDir(unipiDir);
16872
17212
  return result;
16873
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
+ }
16874
17244
  function cleanTemps(options) {
16875
17245
  const result = {
16876
17246
  category: "temp",
@@ -16882,15 +17252,15 @@ function cleanTemps(options) {
16882
17252
  const tmpDir = tmpdir();
16883
17253
  let entries;
16884
17254
  try {
16885
- entries = readdirSync9(tmpDir);
17255
+ entries = readdirSync10(tmpDir);
16886
17256
  } catch {
16887
17257
  return result;
16888
17258
  }
16889
17259
  for (const entry of entries) {
16890
17260
  if (!patterns.some((p) => p.test(entry))) continue;
16891
- const fullPath = join16(tmpDir, entry);
17261
+ const fullPath = join18(tmpDir, entry);
16892
17262
  try {
16893
- const stats = statSync7(fullPath);
17263
+ const stats = statSync8(fullPath);
16894
17264
  if (!stats.isFile()) continue;
16895
17265
  if (isStale(fullPath, options.tempMaxAgeDays)) {
16896
17266
  result.bytesFreed += stats.size;
@@ -16918,18 +17288,18 @@ function cleanSessions(options) {
16918
17288
  paths: []
16919
17289
  };
16920
17290
  const unipiDir = expandHome("~/.unipi");
16921
- const sessionsDir = join16(unipiDir, "sessions");
17291
+ const sessionsDir = join18(unipiDir, "sessions");
16922
17292
  if (!existsSync16(sessionsDir)) return result;
16923
17293
  let entries;
16924
17294
  try {
16925
- entries = readdirSync9(sessionsDir);
17295
+ entries = readdirSync10(sessionsDir);
16926
17296
  } catch {
16927
17297
  return result;
16928
17298
  }
16929
17299
  for (const entry of entries) {
16930
- const fullPath = join16(sessionsDir, entry);
17300
+ const fullPath = join18(sessionsDir, entry);
16931
17301
  try {
16932
- const stats = statSync7(fullPath);
17302
+ const stats = statSync8(fullPath);
16933
17303
  if (!stats.isDirectory()) continue;
16934
17304
  if (isStale(fullPath, options.sessionMaxAgeDays)) {
16935
17305
  result.bytesFreed += stats.size;
@@ -16937,10 +17307,10 @@ function cleanSessions(options) {
16937
17307
  if (!options.dryRun) {
16938
17308
  try {
16939
17309
  const removeRecursive = (dir) => {
16940
- const items = readdirSync9(dir);
17310
+ const items = readdirSync10(dir);
16941
17311
  for (const item of items) {
16942
- const itemPath = join16(dir, item);
16943
- const itemStats = statSync7(itemPath);
17312
+ const itemPath = join18(dir, item);
17313
+ const itemStats = statSync8(itemPath);
16944
17314
  if (itemStats.isDirectory()) {
16945
17315
  removeRecursive(itemPath);
16946
17316
  } else {
@@ -16973,14 +17343,14 @@ function cleanCache(options) {
16973
17343
  if (!existsSync16(cacheDir)) return result;
16974
17344
  let entries;
16975
17345
  try {
16976
- entries = readdirSync9(cacheDir);
17346
+ entries = readdirSync10(cacheDir);
16977
17347
  } catch {
16978
17348
  return result;
16979
17349
  }
16980
17350
  for (const entry of entries) {
16981
- const fullPath = join16(cacheDir, entry);
17351
+ const fullPath = join18(cacheDir, entry);
16982
17352
  try {
16983
- const stats = statSync7(fullPath);
17353
+ const stats = statSync8(fullPath);
16984
17354
  if (!stats.isFile()) continue;
16985
17355
  if (isStale(fullPath, options.tempMaxAgeDays)) {
16986
17356
  result.bytesFreed += stats.size;
@@ -17005,6 +17375,7 @@ function cleanupStale(options = {}) {
17005
17375
  const results = [
17006
17376
  cleanDbs(opts),
17007
17377
  cleanTemps(opts),
17378
+ cleanToolResults(opts),
17008
17379
  cleanSessions(opts),
17009
17380
  cleanCache(opts)
17010
17381
  ];
@@ -17044,18 +17415,18 @@ function formatCleanupReport(report) {
17044
17415
  }
17045
17416
 
17046
17417
  // packages/utility/src/diagnostics/engine.ts
17047
- import { existsSync as existsSync17, accessSync, constants } from "node:fs";
17048
- import { join as join17 } from "node:path";
17049
- import { homedir as homedir13 } from "node:os";
17418
+ import { existsSync as existsSync17, accessSync as accessSync2, constants as constants2 } from "node:fs";
17419
+ import { join as join19 } from "node:path";
17420
+ import { homedir as homedir15 } from "node:os";
17050
17421
  function expandHome2(path28) {
17051
17422
  if (path28.startsWith("~/")) {
17052
- return join17(homedir13(), path28.slice(2));
17423
+ return join19(homedir15(), path28.slice(2));
17053
17424
  }
17054
17425
  return path28;
17055
17426
  }
17056
17427
  function isReadable(path28) {
17057
17428
  try {
17058
- accessSync(path28, constants.R_OK);
17429
+ accessSync2(path28, constants2.R_OK);
17059
17430
  return true;
17060
17431
  } catch {
17061
17432
  return false;
@@ -17063,7 +17434,7 @@ function isReadable(path28) {
17063
17434
  }
17064
17435
  function isWritable(path28) {
17065
17436
  try {
17066
- accessSync(path28, constants.W_OK);
17437
+ accessSync2(path28, constants2.W_OK);
17067
17438
  return true;
17068
17439
  } catch {
17069
17440
  return false;
@@ -19115,7 +19486,7 @@ function getLifecycle(options) {
19115
19486
  }
19116
19487
 
19117
19488
  // packages/utility/src/analytics/collector.ts
19118
- import { randomUUID as randomUUID2 } from "node:crypto";
19489
+ import { randomUUID as randomUUID4 } from "node:crypto";
19119
19490
  var DEFAULTS3 = {
19120
19491
  dbPath: "~/.unipi/analytics/events.db",
19121
19492
  maxEvents: 1e4,
@@ -19124,7 +19495,7 @@ var DEFAULTS3 = {
19124
19495
  var eventBuffer = [];
19125
19496
  var bufferFlushTimer = null;
19126
19497
  function generateId() {
19127
- return randomUUID2();
19498
+ return randomUUID4();
19128
19499
  }
19129
19500
  function today() {
19130
19501
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -19332,8 +19703,189 @@ function registerInfoScreen(pi) {
19332
19703
  });
19333
19704
  }
19334
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
+
19335
19887
  // packages/utility/src/index.ts
19336
- var VERSION6 = getPackageVersion(dirname15(fileURLToPath7(import.meta.url)));
19888
+ var VERSION6 = getPackageVersion(dirname14(fileURLToPath7(import.meta.url)));
19337
19889
  var firstMessageSeen = false;
19338
19890
  var firstUserText = "";
19339
19891
  var firstInputCtx = null;
@@ -19348,12 +19900,44 @@ var ALL_COMMANDS = [
19348
19900
  UTILITY_COMMANDS.BADGE_GEN,
19349
19901
  UTILITY_COMMANDS.BADGE_TOGGLE,
19350
19902
  UTILITY_COMMANDS.BADGE_SETTINGS,
19351
- UTILITY_COMMANDS.UTIL_SETTINGS
19903
+ UTILITY_COMMANDS.UTIL_SETTINGS,
19904
+ UTILITY_COMMANDS.PREFIX_CACHE
19352
19905
  ].map((cmd) => `unipi:${cmd}`);
19353
19906
  var ALL_TOOLS2 = [UTILITY_TOOLS.BATCH, UTILITY_TOOLS.ENV, UTILITY_TOOLS.SET_SESSION_NAME];
19354
19907
  function src_default3(pi) {
19355
19908
  const lifecycle = getLifecycle();
19356
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"));
19357
19941
  lifecycle.registerCleanup(async () => {
19358
19942
  analytics.disable();
19359
19943
  });
@@ -19545,7 +20129,7 @@ function registerUtilityTools(pi, nameBadgeState) {
19545
20129
 
19546
20130
  // packages/ask-user/index.ts
19547
20131
  init_core();
19548
- import { dirname as dirname16 } from "node:path";
20132
+ import { dirname as dirname15 } from "node:path";
19549
20133
  import { fileURLToPath as fileURLToPath8 } from "node:url";
19550
20134
 
19551
20135
  // packages/ask-user/tools.ts
@@ -20201,9 +20785,9 @@ function renderLauncherUI(params) {
20201
20785
  }
20202
20786
 
20203
20787
  // packages/ask-user/config.ts
20204
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync14, existsSync as existsSync21 } from "node:fs";
20205
- import { join as join19 } from "node:path";
20206
- 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";
20207
20791
  var DEFAULT_SETTINGS3 = {
20208
20792
  enabled: true,
20209
20793
  allowedFormats: {
@@ -20213,7 +20797,7 @@ var DEFAULT_SETTINGS3 = {
20213
20797
  },
20214
20798
  notifyOnAsk: true
20215
20799
  };
20216
- var SETTINGS_PATH2 = join19(homedir15(), ".pi", "agent", "settings.json");
20800
+ var SETTINGS_PATH2 = join21(homedir17(), ".pi", "agent", "settings.json");
20217
20801
  var SETTINGS_KEY2 = "unipi";
20218
20802
  var cachedSettings2 = null;
20219
20803
  function isRecord3(value) {
@@ -20233,7 +20817,7 @@ function writeSettingsFile2(data) {
20233
20817
  if (!existsSync21(dir)) {
20234
20818
  __require("node:fs").mkdirSync(dir, { recursive: true });
20235
20819
  }
20236
- writeFileSync14(SETTINGS_PATH2, JSON.stringify(data, null, 2) + "\n", "utf-8");
20820
+ writeFileSync16(SETTINGS_PATH2, JSON.stringify(data, null, 2) + "\n", "utf-8");
20237
20821
  }
20238
20822
  function getAskUserSettings() {
20239
20823
  if (cachedSettings2) return cachedSettings2;
@@ -20930,7 +21514,7 @@ function registerAskUserCommands(pi) {
20930
21514
  }
20931
21515
 
20932
21516
  // packages/ask-user/index.ts
20933
- var VERSION7 = getPackageVersion(dirname16(fileURLToPath8(import.meta.url)));
21517
+ var VERSION7 = getPackageVersion(dirname15(fileURLToPath8(import.meta.url)));
20934
21518
  function ask_user_default(pi) {
20935
21519
  registerAskUserTools(pi);
20936
21520
  registerAskUserCommands(pi);
@@ -20946,7 +21530,7 @@ function ask_user_default(pi) {
20946
21530
 
20947
21531
  // packages/mcp/src/index.ts
20948
21532
  init_core();
20949
- import { dirname as dirname19 } from "node:path";
21533
+ import { dirname as dirname18 } from "node:path";
20950
21534
  import { fileURLToPath as fileURLToPath9 } from "node:url";
20951
21535
 
20952
21536
  // packages/mcp/src/config/manager.ts
@@ -21444,7 +22028,7 @@ var McpClient = class {
21444
22028
  if (this.connected) {
21445
22029
  throw new Error("McpClient is already connected");
21446
22030
  }
21447
- return new Promise((resolve13, reject) => {
22031
+ return new Promise((resolve14, reject) => {
21448
22032
  try {
21449
22033
  const mergedEnv = { ...process.env, ...env };
21450
22034
  this.process = spawn2(command, args, {
@@ -21493,7 +22077,7 @@ Stderr: ${this.stderrBuffer}`
21493
22077
  }).then(() => {
21494
22078
  this.sendNotification("notifications/initialized", {});
21495
22079
  this.connected = true;
21496
- resolve13();
22080
+ resolve14();
21497
22081
  }).catch((err) => {
21498
22082
  this.cleanup();
21499
22083
  reject(
@@ -21537,16 +22121,16 @@ Stderr: ${this.stderrBuffer}`
21537
22121
  this.sendNotification("shutdown", {});
21538
22122
  } catch {
21539
22123
  }
21540
- await new Promise((resolve13) => {
21541
- const timer = setTimeout(() => resolve13(), 500);
22124
+ await new Promise((resolve14) => {
22125
+ const timer = setTimeout(() => resolve14(), 500);
21542
22126
  this.process.on("exit", () => {
21543
22127
  clearTimeout(timer);
21544
- resolve13();
22128
+ resolve14();
21545
22129
  });
21546
22130
  try {
21547
22131
  this.process.kill("SIGTERM");
21548
22132
  } catch {
21549
- resolve13();
22133
+ resolve14();
21550
22134
  }
21551
22135
  });
21552
22136
  if (this.process && !this.process.killed) {
@@ -21576,7 +22160,7 @@ Stderr: ${this.stderrBuffer}`
21576
22160
  }
21577
22161
  }
21578
22162
  sendRequest(method, params) {
21579
- return new Promise((resolve13, reject) => {
22163
+ return new Promise((resolve14, reject) => {
21580
22164
  if (!this.process?.stdin) {
21581
22165
  reject(new Error("MCP server process not available"));
21582
22166
  return;
@@ -21596,7 +22180,7 @@ Stderr: ${this.stderrBuffer}`
21596
22180
  )
21597
22181
  );
21598
22182
  }, this.timeoutMs);
21599
- this.pending.set(id, { resolve: resolve13, reject, timer });
22183
+ this.pending.set(id, { resolve: resolve14, reject, timer });
21600
22184
  const message = JSON.stringify(request) + "\n";
21601
22185
  this.process.stdin.write(message);
21602
22186
  });
@@ -21659,15 +22243,45 @@ Stderr: ${this.stderrBuffer}`
21659
22243
 
21660
22244
  // packages/mcp/src/bridge/translator.ts
21661
22245
  init_core();
22246
+ function compareCodeUnits3(left, right) {
22247
+ return left < right ? -1 : left > right ? 1 : 0;
22248
+ }
22249
+ function isObject(value) {
22250
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22251
+ }
22252
+ var LITERAL_VALUE_KEYWORDS = /* @__PURE__ */ new Set(["const", "default", "enum", "examples"]);
22253
+ function canonicalizeValue(value, key, normalizeSchemaKeywords = true) {
22254
+ if (Array.isArray(value)) {
22255
+ if (normalizeSchemaKeywords && key === "required" && value.every((item) => typeof item === "string")) {
22256
+ return [...new Set(value)].sort(compareCodeUnits3);
22257
+ }
22258
+ return value.map((item) => canonicalizeValue(item, void 0, normalizeSchemaKeywords));
22259
+ }
22260
+ if (!isObject(value)) return value;
22261
+ const canonical = {};
22262
+ for (const objectKey of Object.keys(value).sort(compareCodeUnits3)) {
22263
+ const childNormalizesSchemaKeywords = normalizeSchemaKeywords && !LITERAL_VALUE_KEYWORDS.has(objectKey);
22264
+ canonical[objectKey] = canonicalizeValue(
22265
+ value[objectKey],
22266
+ objectKey,
22267
+ childNormalizesSchemaKeywords
22268
+ );
22269
+ }
22270
+ return canonical;
22271
+ }
22272
+ function canonicalizeJsonSchema(schema) {
22273
+ return canonicalizeValue(schema);
22274
+ }
21662
22275
  function translateMcpTool(mcpTool, serverName, client) {
21663
22276
  const separator = MCP_DEFAULTS.TOOL_NAME_SEPARATOR;
21664
22277
  const toolName = `${serverName}${separator}${mcpTool.name}`;
21665
- const inputSchema = mcpTool.inputSchema ?? {};
21666
- const parameters = {
22278
+ const inputSchema = isObject(mcpTool.inputSchema) ? mcpTool.inputSchema : {};
22279
+ const normalizedSchema = {
21667
22280
  type: "object",
21668
- properties: inputSchema.properties ?? {},
21669
- required: inputSchema.required
22281
+ properties: isObject(inputSchema.properties) ? inputSchema.properties : {},
22282
+ required: Array.isArray(inputSchema.required) ? inputSchema.required : []
21670
22283
  };
22284
+ const parameters = canonicalizeJsonSchema(normalizedSchema);
21671
22285
  const description = [
21672
22286
  mcpTool.description || `MCP tool: ${mcpTool.name}`,
21673
22287
  `[Server: ${serverName}]`
@@ -21695,19 +22309,26 @@ function translateMcpTool(mcpTool, serverName, client) {
21695
22309
  });
21696
22310
  }
21697
22311
  }
21698
- if (result.isError) {
21699
- const errorText = blocks.map((b) => b.text).join("\n") || "Unknown error";
21700
- return {
21701
- content: [{ type: "text", text: `MCP tool error from ${serverName}: ${errorText}` }],
21702
- details: { error: true, server: serverName, tool: mcpTool.name }
21703
- };
21704
- }
21705
22312
  if (blocks.length === 0) {
21706
- blocks.push({ type: "text", text: "(no output)" });
22313
+ blocks.push({ type: "text", text: result.isError ? "Unknown error" : "(no output)" });
21707
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}`;
21708
22322
  return {
21709
- content: blocks,
21710
- 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
+ }
21711
22332
  };
21712
22333
  } catch (err) {
21713
22334
  const message = err instanceof Error ? err.message : String(err);
@@ -21725,6 +22346,7 @@ Check server status via /unipi:mcp-settings`
21725
22346
  };
21726
22347
  return {
21727
22348
  name: toolName,
22349
+ label: toolName,
21728
22350
  description,
21729
22351
  parameters,
21730
22352
  execute: execute2
@@ -21737,26 +22359,125 @@ var ServerRegistry = class {
21737
22359
  emitEvent;
21738
22360
  registerTool;
21739
22361
  unregisterTool;
22362
+ canUnregisterTools;
21740
22363
  timeoutMs;
22364
+ createClient;
21741
22365
  constructor(options) {
21742
22366
  this.emitEvent = options.emitEvent;
21743
22367
  this.registerTool = options.registerTool;
21744
22368
  this.unregisterTool = options.unregisterTool;
22369
+ this.canUnregisterTools = options.canUnregisterTools ?? true;
21745
22370
  this.timeoutMs = options.timeoutMs ?? MCP_DEFAULTS.STARTUP_TIMEOUT_MS;
22371
+ this.createClient = options.createClient ?? (() => new McpClient({ timeoutMs: this.timeoutMs }));
21746
22372
  }
21747
22373
  /**
21748
- * Start an MCP server: spawn process, initialize, discover tools, register.
22374
+ * Start one MCP server. Discovery completes before its tools are registered,
22375
+ * and registration always follows final Pi tool-name order.
21749
22376
  */
21750
22377
  async startServer(resolved) {
21751
- const { name, def } = resolved;
21752
- if (this.entries.size >= MCP_DEFAULTS.MAX_SERVERS) {
22378
+ await this.startServers([resolved]);
22379
+ const state = this.getServerState(resolved.name);
22380
+ if (state?.status === "error") {
22381
+ throw new Error(state.error ?? `MCP server "${resolved.name}" failed to start`);
22382
+ }
22383
+ }
22384
+ /**
22385
+ * Start MCP servers behind a discovery barrier.
22386
+ *
22387
+ * Connections and tool discovery run in parallel. Once every server has
22388
+ * either prepared or failed, tools from all successful servers are checked
22389
+ * for duplicate final names, sorted, and only then registered with Pi.
22390
+ * Individual connection failures remain represented by error registry
22391
+ * entries and do not prevent tools from other servers being registered.
22392
+ */
22393
+ async startServers(resolvedServers) {
22394
+ if (resolvedServers.length === 0) return;
22395
+ const names = /* @__PURE__ */ new Set();
22396
+ for (const { name } of resolvedServers) {
22397
+ if (names.has(name)) {
22398
+ throw new Error(`Duplicate MCP server name in startup batch: "${name}"`);
22399
+ }
22400
+ names.add(name);
22401
+ }
22402
+ const replacementCount = [...names].filter((name) => this.entries.has(name)).length;
22403
+ if (this.entries.size - replacementCount + resolvedServers.length > MCP_DEFAULTS.MAX_SERVERS) {
21753
22404
  throw new Error(
21754
22405
  `Maximum number of MCP servers (${MCP_DEFAULTS.MAX_SERVERS}) reached. Stop a server before starting a new one.`
21755
22406
  );
21756
22407
  }
21757
- if (this.entries.has(name)) {
21758
- await this.stopServer(name);
22408
+ await Promise.all(
22409
+ [...names].filter((name) => this.entries.has(name)).map((name) => this.stopServer(name))
22410
+ );
22411
+ const settled = await Promise.allSettled(
22412
+ resolvedServers.map((resolved) => this.prepareServer(resolved))
22413
+ );
22414
+ const prepared = settled.filter((result) => result.status === "fulfilled").map((result) => result.value);
22415
+ if (prepared.length === 0) return;
22416
+ const registrations = prepared.flatMap(({ entry, tools }) => tools.map((tool) => ({ entry, tool }))).sort((left, right) => compareCodeUnits3(left.tool.name, right.tool.name));
22417
+ try {
22418
+ this.assertUniqueFinalToolNames(registrations.map(({ tool }) => tool.name));
22419
+ } catch (error) {
22420
+ await this.failPreparedServers(prepared, error);
22421
+ throw error;
22422
+ }
22423
+ const registeredNames = [];
22424
+ try {
22425
+ for (const { tool } of registrations) {
22426
+ this.registerTool(tool);
22427
+ registeredNames.push(tool.name);
22428
+ }
22429
+ } catch (error) {
22430
+ if (this.canUnregisterTools) {
22431
+ for (const toolName of registeredNames.reverse()) {
22432
+ try {
22433
+ this.unregisterTool(toolName);
22434
+ } catch {
22435
+ }
22436
+ }
22437
+ await this.failPreparedServers(prepared, error);
22438
+ } else {
22439
+ const message = `${error instanceof Error ? error.message : String(error)}; some MCP tools remain registered until Pi restarts`;
22440
+ for (const { entry, client, tools } of prepared) {
22441
+ const toolNames = tools.map((tool) => tool.name).filter((name) => registeredNames.includes(name)).sort(compareCodeUnits3);
22442
+ entry.toolNames = toolNames;
22443
+ entry.state = {
22444
+ ...entry.state,
22445
+ status: "error",
22446
+ pid: client.pid,
22447
+ toolCount: toolNames.length,
22448
+ error: message
22449
+ };
22450
+ this.emitEvent(UNIPI_EVENTS.MCP_SERVER_ERROR, {
22451
+ name: entry.name,
22452
+ error: message
22453
+ });
22454
+ }
22455
+ }
22456
+ throw error;
22457
+ }
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);
22460
+ entry.toolNames = toolNames;
22461
+ entry.state = {
22462
+ ...entry.state,
22463
+ status: "running",
22464
+ pid: client.pid,
22465
+ toolCount: toolNames.length
22466
+ };
22467
+ this.emitEvent(UNIPI_EVENTS.MCP_SERVER_STARTED, {
22468
+ name: entry.name,
22469
+ toolCount: toolNames.length
22470
+ });
22471
+ if (toolNames.length > 0) {
22472
+ this.emitEvent(UNIPI_EVENTS.MCP_TOOLS_REGISTERED, {
22473
+ serverName: entry.name,
22474
+ toolNames
22475
+ });
22476
+ }
21759
22477
  }
22478
+ }
22479
+ async prepareServer(resolved) {
22480
+ const { name, def } = resolved;
21760
22481
  const state = {
21761
22482
  name,
21762
22483
  status: "starting",
@@ -21771,73 +22492,74 @@ var ServerRegistry = class {
21771
22492
  toolNames: []
21772
22493
  };
21773
22494
  this.entries.set(name, entry);
22495
+ let client = null;
21774
22496
  try {
21775
22497
  const safeCommand = typeof def.command === "string" ? def.command : String(def.command);
21776
22498
  const safeArgs = Array.isArray(def.args) ? def.args : [];
21777
22499
  let safeEnv;
21778
- if (def.env !== void 0 && def.env !== null) {
21779
- if (typeof def.env === "object" && !Array.isArray(def.env)) {
21780
- safeEnv = {};
21781
- for (const [k, v] of Object.entries(def.env)) {
21782
- safeEnv[k] = typeof v === "string" ? v : String(v);
21783
- }
21784
- } else {
21785
- }
21786
- }
21787
- const client = new McpClient({ timeoutMs: this.timeoutMs });
21788
- await client.connect(safeCommand, safeArgs, safeEnv);
21789
- entry.client = client;
21790
- const mcpTools = await client.listTools();
21791
- const toolNames = [];
21792
- for (const mcpTool of mcpTools) {
21793
- const piTool = translateMcpTool(mcpTool, name, client);
21794
- this.registerTool(piTool);
21795
- toolNames.push(piTool.name);
21796
- }
21797
- entry.state = {
21798
- ...state,
21799
- status: "running",
21800
- pid: client.pid,
21801
- toolCount: toolNames.length
21802
- };
21803
- entry.toolNames = toolNames;
21804
- this.emitEvent(UNIPI_EVENTS.MCP_SERVER_STARTED, {
21805
- name,
21806
- toolCount: toolNames.length
21807
- });
21808
- if (toolNames.length > 0) {
21809
- this.emitEvent(UNIPI_EVENTS.MCP_TOOLS_REGISTERED, {
21810
- serverName: name,
21811
- toolNames
21812
- });
21813
- }
21814
- } catch (err) {
21815
- const error = err instanceof Error ? err.message : String(err);
21816
- entry.state = {
21817
- ...state,
21818
- status: "error",
21819
- error
21820
- };
21821
- if (entry.client) {
22500
+ if (def.env !== void 0 && def.env !== null && typeof def.env === "object" && !Array.isArray(def.env)) {
22501
+ safeEnv = {};
22502
+ for (const [key, value] of Object.entries(def.env)) {
22503
+ safeEnv[key] = typeof value === "string" ? value : String(value);
22504
+ }
22505
+ }
22506
+ client = this.createClient();
22507
+ const connectedClient = client;
22508
+ await connectedClient.connect(safeCommand, safeArgs, safeEnv);
22509
+ entry.client = connectedClient;
22510
+ const mcpTools = await connectedClient.listTools();
22511
+ const tools = mcpTools.map((tool) => translateMcpTool(tool, name, connectedClient)).sort((left, right) => compareCodeUnits3(left.name, right.name));
22512
+ return { entry, client: connectedClient, tools };
22513
+ } catch (error) {
22514
+ const message = error instanceof Error ? error.message : String(error);
22515
+ entry.state = { ...state, status: "error", error: message };
22516
+ if (client) {
21822
22517
  try {
21823
- await entry.client.disconnect();
22518
+ await client.disconnect();
21824
22519
  } catch {
21825
22520
  }
21826
- entry.client = null;
21827
22521
  }
21828
- this.emitEvent(UNIPI_EVENTS.MCP_SERVER_ERROR, {
21829
- name,
21830
- error
21831
- });
21832
- throw err;
22522
+ entry.client = null;
22523
+ this.emitEvent(UNIPI_EVENTS.MCP_SERVER_ERROR, { name, error: message });
22524
+ throw error;
21833
22525
  }
21834
22526
  }
21835
- /**
21836
- * Stop an MCP server: unregister tools, disconnect client.
21837
- */
22527
+ assertUniqueFinalToolNames(newNames) {
22528
+ const existingNames = this.getActive().flatMap((state) => this.entries.get(state.name)?.toolNames ?? []);
22529
+ const seen = /* @__PURE__ */ new Set();
22530
+ const duplicates = /* @__PURE__ */ new Set();
22531
+ for (const name of [...existingNames, ...newNames]) {
22532
+ if (seen.has(name)) duplicates.add(name);
22533
+ seen.add(name);
22534
+ }
22535
+ if (duplicates.size > 0) {
22536
+ throw new Error(
22537
+ `Duplicate final MCP tool name(s): ${[...duplicates].sort(compareCodeUnits3).join(", ")}`
22538
+ );
22539
+ }
22540
+ }
22541
+ async failPreparedServers(prepared, error) {
22542
+ const message = error instanceof Error ? error.message : String(error);
22543
+ await Promise.all(prepared.map(async ({ entry, client }) => {
22544
+ entry.state = { ...entry.state, status: "error", toolCount: 0, error: message };
22545
+ entry.toolNames = [];
22546
+ try {
22547
+ await client.disconnect();
22548
+ } catch {
22549
+ }
22550
+ entry.client = null;
22551
+ this.emitEvent(UNIPI_EVENTS.MCP_SERVER_ERROR, { name: entry.name, error: message });
22552
+ }));
22553
+ }
22554
+ /** Stop an MCP server: unregister tools, disconnect client. */
21838
22555
  async stopServer(name) {
21839
22556
  const entry = this.entries.get(name);
21840
22557
  if (!entry) return;
22558
+ if (entry.toolNames.length > 0 && !this.canUnregisterTools) {
22559
+ throw new Error(
22560
+ "This Pi version cannot remove MCP tools at runtime; restart Pi to change the MCP tool set."
22561
+ );
22562
+ }
21841
22563
  for (const toolName of entry.toolNames) {
21842
22564
  this.unregisterTool(toolName);
21843
22565
  }
@@ -21854,78 +22576,63 @@ var ServerRegistry = class {
21854
22576
  }
21855
22577
  entry.client = null;
21856
22578
  }
21857
- entry.state = {
21858
- ...entry.state,
21859
- status: "stopped",
21860
- toolCount: 0
21861
- };
22579
+ entry.state = { ...entry.state, status: "stopped", toolCount: 0 };
21862
22580
  entry.toolNames = [];
21863
22581
  this.emitEvent(UNIPI_EVENTS.MCP_SERVER_STOPPED, { name });
21864
22582
  }
21865
- /**
21866
- * Restart an MCP server: stop then start.
21867
- */
22583
+ /** Restart an MCP server: stop then start. */
21868
22584
  async restartServer(name) {
21869
22585
  const entry = this.entries.get(name);
21870
- if (!entry) {
21871
- throw new Error(`Server '${name}' not found in registry`);
21872
- }
22586
+ if (!entry) throw new Error(`Server '${name}' not found in registry`);
21873
22587
  const resolved = entry.resolved;
21874
22588
  await this.stopServer(name);
21875
22589
  await this.startServer(resolved);
21876
22590
  }
21877
- /**
21878
- * Stop all running servers.
21879
- */
22591
+ /** Stop all running servers when runtime unregistration is supported. */
21880
22592
  async stopAll() {
21881
22593
  const names = Array.from(this.entries.keys());
21882
22594
  await Promise.allSettled(names.map((name) => this.stopServer(name)));
21883
22595
  }
21884
- /**
21885
- * Get all registered server states.
21886
- */
22596
+ /** Disconnect clients during extension shutdown without claiming tools were removed. */
22597
+ async disconnectAll() {
22598
+ await Promise.allSettled([...this.entries.values()].map(async (entry) => {
22599
+ if (!entry.client) return;
22600
+ try {
22601
+ await entry.client.disconnect();
22602
+ } finally {
22603
+ entry.client = null;
22604
+ }
22605
+ }));
22606
+ }
22607
+ /** Get all registered server states. */
21887
22608
  getAll() {
21888
- return Array.from(this.entries.values()).map((e) => e.state);
22609
+ return Array.from(this.entries.values()).map((entry) => entry.state);
21889
22610
  }
21890
- /**
21891
- * Get states of running servers.
21892
- */
22611
+ /** Get states of running servers. */
21893
22612
  getActive() {
21894
- return this.getAll().filter((s) => s.status === "running");
22613
+ return this.getAll().filter((state) => state.status === "running");
21895
22614
  }
21896
- /**
21897
- * Get states of servers in error state.
21898
- */
22615
+ /** Get states of servers in error state. */
21899
22616
  getFailed() {
21900
- return this.getAll().filter((s) => s.status === "error");
22617
+ return this.getAll().filter((state) => state.status === "error");
21901
22618
  }
21902
- /**
21903
- * Get total number of tools across all active servers.
21904
- */
22619
+ /** Get total number of tools across all active servers. */
21905
22620
  getTotalToolCount() {
21906
- return this.getActive().reduce((sum, s) => sum + s.toolCount, 0);
22621
+ return this.getActive().reduce((sum, state) => sum + state.toolCount, 0);
21907
22622
  }
21908
- /**
21909
- * Get the state of a specific server.
21910
- */
22623
+ /** Get the state of a specific server. */
21911
22624
  getServerState(name) {
21912
22625
  return this.entries.get(name)?.state ?? null;
21913
22626
  }
21914
- /**
21915
- * Get the full registry entry for a server.
21916
- */
22627
+ /** Get the full registry entry for a server. */
21917
22628
  getEntry(name) {
21918
22629
  return this.entries.get(name) ?? null;
21919
22630
  }
21920
- /**
21921
- * Check if a server exists in the registry.
21922
- */
22631
+ /** Check if a server exists in the registry. */
21923
22632
  hasServer(name) {
21924
22633
  return this.entries.has(name);
21925
22634
  }
21926
- /**
21927
- * Get the number of registered servers.
21928
- */
22635
+ /** Get the number of registered servers. */
21929
22636
  get size() {
21930
22637
  return this.entries.size;
21931
22638
  }
@@ -22538,12 +23245,6 @@ function renderMcpSettingsOverlay(params) {
22538
23245
  addedAt: meta.servers[server.name]?.addedAt ?? (/* @__PURE__ */ new Date()).toISOString()
22539
23246
  };
22540
23247
  saveMetadata(configDir, meta);
22541
- if (!newEnabled && registry3) {
22542
- try {
22543
- await registry3.stopServer(server.name);
22544
- } catch {
22545
- }
22546
- }
22547
23248
  refreshServers();
22548
23249
  refresh();
22549
23250
  } catch (err) {
@@ -22690,7 +23391,7 @@ function renderMcpSettingsOverlay(params) {
22690
23391
  }
22691
23392
 
22692
23393
  // packages/mcp/src/index.ts
22693
- var VERSION8 = getPackageVersion(dirname19(fileURLToPath9(import.meta.url)));
23394
+ var VERSION8 = getPackageVersion(dirname18(fileURLToPath9(import.meta.url)));
22694
23395
  var registry2 = null;
22695
23396
  function getInfoRegistry5() {
22696
23397
  return globalThis.__unipi_info_registry;
@@ -22700,20 +23401,18 @@ function getRegistry() {
22700
23401
  }
22701
23402
  function src_default4(pi) {
22702
23403
  pi.on("session_start", async (_event, ctx) => {
23404
+ const toolApi = pi;
23405
+ const registerTool = typeof toolApi.registerTool === "function" ? (tool) => toolApi.registerTool(tool) : typeof toolApi.registerExternalTool === "function" ? (tool) => toolApi.registerExternalTool(tool) : () => {
23406
+ throw new Error("Pi does not expose a supported MCP tool registration API");
23407
+ };
23408
+ const canUnregisterTools = typeof toolApi.unregisterTool === "function" || typeof toolApi.unregisterExternalTool === "function";
23409
+ const unregisterTool = typeof toolApi.unregisterTool === "function" ? (toolName) => toolApi.unregisterTool(toolName) : typeof toolApi.unregisterExternalTool === "function" ? (toolName) => toolApi.unregisterExternalTool(toolName) : () => {
23410
+ };
22703
23411
  registry2 = new ServerRegistry({
22704
23412
  emitEvent: (event, payload) => emitEvent(pi, event, payload),
22705
- registerTool: (tool) => {
22706
- try {
22707
- pi.registerTool?.(tool) ?? pi.registerExternalTool?.(tool);
22708
- } catch {
22709
- }
22710
- },
22711
- unregisterTool: (toolName) => {
22712
- try {
22713
- pi.unregisterTool?.(toolName) ?? pi.unregisterExternalTool?.(toolName);
22714
- } catch {
22715
- }
22716
- }
23413
+ registerTool,
23414
+ unregisterTool,
23415
+ canUnregisterTools
22717
23416
  });
22718
23417
  const cwd = ctx.cwd ?? process.cwd();
22719
23418
  let servers = [];
@@ -22722,13 +23421,10 @@ function src_default4(pi) {
22722
23421
  servers = result.servers;
22723
23422
  } catch (_err) {
22724
23423
  }
22725
- const startPromises = servers.filter((s) => s.enabled).map(async (server) => {
22726
- try {
22727
- await registry2.startServer(server);
22728
- } catch (err) {
22729
- }
22730
- });
22731
- await Promise.allSettled(startPromises);
23424
+ try {
23425
+ await registry2.startServers(servers.filter((server) => server.enabled));
23426
+ } catch (_err) {
23427
+ }
22732
23428
  const infoRegistry2 = getInfoRegistry5();
22733
23429
  if (infoRegistry2 && registry2) {
22734
23430
  const reg = registry2;
@@ -22785,14 +23481,12 @@ function src_default4(pi) {
22785
23481
  `unipi:${MCP_COMMANDS.STATUS}`,
22786
23482
  `unipi:${MCP_COMMANDS.RELOAD}`
22787
23483
  ],
22788
- tools: activeServers.flatMap(
22789
- (s) => registry2?.getEntry(s.name)?.toolNames ?? []
22790
- )
23484
+ tools: activeServers.flatMap((server) => registry2?.getEntry(server.name)?.toolNames ?? []).sort(compareCodeUnits3)
22791
23485
  });
22792
23486
  });
22793
23487
  pi.on("session_shutdown", async (_event, _ctx) => {
22794
23488
  if (registry2) {
22795
- await registry2.stopAll();
23489
+ await registry2.disconnectAll();
22796
23490
  registry2 = null;
22797
23491
  }
22798
23492
  });
@@ -22906,38 +23600,16 @@ Updated: ${catalog.lastUpdated}`,
22906
23600
  }
22907
23601
  });
22908
23602
  pi.registerCommand(`unipi:${MCP_COMMANDS.RELOAD}`, {
22909
- description: "Reload all MCP servers (restart with current config)",
23603
+ description: "Explain how to reload MCP servers safely",
22910
23604
  handler: async (_args, ctx) => {
22911
- const reg = getRegistry();
22912
- if (!reg) {
22913
- ctx.ui.notify("MCP extension not initialized", "warning");
22914
- return;
22915
- }
22916
- const all = reg.getAll();
22917
- if (all.length === 0) {
22918
- ctx.ui.notify("No MCP servers configured. Use /unipi:mcp-add to add one.", "info");
22919
- return;
22920
- }
22921
- ctx.ui.notify(`Reloading ${all.length} MCP server(s)...`, "info");
22922
- let restarted = 0;
22923
- let failed = 0;
22924
- for (const state of all) {
22925
- try {
22926
- await reg.restartServer(state.name);
22927
- restarted++;
22928
- } catch (_err) {
22929
- failed++;
22930
- }
22931
- }
22932
- const msg = failed > 0 ? `Reloaded: ${restarted} ok, ${failed} failed` : `Reloaded ${restarted} MCP server(s) successfully`;
22933
- ctx.ui.notify(msg, failed > 0 ? "warning" : "info");
23605
+ ctx.ui.notify("Restart Pi to reload MCP servers and tool schemas safely.", "info");
22934
23606
  }
22935
23607
  });
22936
23608
  }
22937
23609
 
22938
23610
  // packages/notify/index.ts
22939
23611
  init_core();
22940
- import { dirname as dirname22 } from "node:path";
23612
+ import { dirname as dirname21 } from "node:path";
22941
23613
  import { fileURLToPath as fileURLToPath10 } from "node:url";
22942
23614
 
22943
23615
  // packages/notify/tools.ts
@@ -22946,12 +23618,12 @@ import { Type as Type6 } from "typebox";
22946
23618
 
22947
23619
  // packages/notify/settings.ts
22948
23620
  init_core();
22949
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, existsSync as existsSync24 } from "fs";
22950
- import { dirname as dirname20, join as join22 } from "path";
22951
- 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";
22952
23624
  function resolveConfigPath() {
22953
- const base = NOTIFY_DIRS.CONFIG.replace("~", homedir17());
22954
- return join22(base, "config.json");
23625
+ const base = NOTIFY_DIRS.CONFIG.replace("~", homedir19());
23626
+ return join24(base, "config.json");
22955
23627
  }
22956
23628
  var DEFAULT_CONFIG4 = {
22957
23629
  defaultPlatforms: ["native"],
@@ -23001,9 +23673,9 @@ function loadConfig2() {
23001
23673
  }
23002
23674
  function saveConfig2(config) {
23003
23675
  const configPath = resolveConfigPath();
23004
- const dir = dirname20(configPath);
23005
- mkdirSync15(dir, { recursive: true });
23006
- writeFileSync17(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23676
+ const dir = dirname19(configPath);
23677
+ mkdirSync17(dir, { recursive: true });
23678
+ writeFileSync19(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23007
23679
  }
23008
23680
  function updateConfig(partial) {
23009
23681
  const current = loadConfig2();
@@ -23061,19 +23733,19 @@ function mergeWithDefaults(loaded) {
23061
23733
  init_core();
23062
23734
 
23063
23735
  // packages/notify/ntfy-config.ts
23064
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync18, mkdirSync as mkdirSync16, existsSync as existsSync25 } from "fs";
23065
- import { dirname as dirname21, join as join23 } from "path";
23066
- 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";
23067
23739
  var DEFAULT_NTFY_CONFIG = {
23068
23740
  enabled: false,
23069
23741
  serverUrl: "https://ntfy.sh",
23070
23742
  priority: 3
23071
23743
  };
23072
23744
  function getGlobalNtfyPath() {
23073
- return join23(homedir18(), ".unipi", "config", "notify", "ntfy.json");
23745
+ return join25(homedir20(), ".unipi", "config", "notify", "ntfy.json");
23074
23746
  }
23075
23747
  function getProjectNtfyPath(cwd) {
23076
- return join23(cwd, ".unipi", "config", "notify", "ntfy.json");
23748
+ return join25(cwd, ".unipi", "config", "notify", "ntfy.json");
23077
23749
  }
23078
23750
  function readNtfyJson(filePath) {
23079
23751
  try {
@@ -23102,9 +23774,9 @@ function loadNtfyConfig(cwd) {
23102
23774
  }
23103
23775
  function saveNtfyConfig(scope, cwd, config) {
23104
23776
  const filePath = scope === "project" ? getProjectNtfyPath(cwd) : getGlobalNtfyPath();
23105
- const dir = dirname21(filePath);
23106
- mkdirSync16(dir, { recursive: true });
23107
- writeFileSync18(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23777
+ const dir = dirname20(filePath);
23778
+ mkdirSync18(dir, { recursive: true });
23779
+ writeFileSync20(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23108
23780
  }
23109
23781
  function getNtfyConfigScope(cwd) {
23110
23782
  if (existsSync25(getProjectNtfyPath(cwd))) {
@@ -23120,8 +23792,8 @@ function migrateFromLegacyConfig() {
23120
23792
  if (existsSync25(globalNtfyPath)) {
23121
23793
  return;
23122
23794
  }
23123
- const legacyConfigPath = join23(
23124
- homedir18(),
23795
+ const legacyConfigPath = join25(
23796
+ homedir20(),
23125
23797
  ".unipi",
23126
23798
  "config",
23127
23799
  "notify",
@@ -23142,9 +23814,9 @@ function migrateFromLegacyConfig() {
23142
23814
  token: ntfySection.token,
23143
23815
  priority: ntfySection.priority ?? 3
23144
23816
  };
23145
- const dir = dirname21(globalNtfyPath);
23146
- mkdirSync16(dir, { recursive: true });
23147
- writeFileSync18(
23817
+ const dir = dirname20(globalNtfyPath);
23818
+ mkdirSync18(dir, { recursive: true });
23819
+ writeFileSync20(
23148
23820
  globalNtfyPath,
23149
23821
  JSON.stringify(migratedConfig, null, 2) + "\n",
23150
23822
  "utf-8"
@@ -23158,8 +23830,8 @@ import notifier from "node-notifier";
23158
23830
 
23159
23831
  // packages/notify/platforms/focus-win.ts
23160
23832
  import { execFile } from "child_process";
23161
- import { writeFileSync as writeFileSync19, rmSync as rmSync2, mkdtempSync } from "fs";
23162
- import { join as join24 } from "path";
23833
+ import { writeFileSync as writeFileSync21, rmSync as rmSync2, mkdtempSync } from "fs";
23834
+ import { join as join26 } from "path";
23163
23835
  import { tmpdir as tmpdir2 } from "os";
23164
23836
  var POWERCHECK_SCRIPT = `
23165
23837
  param($targetPid)
@@ -23195,10 +23867,10 @@ async function isWindowFocusedOnWindows() {
23195
23867
  }
23196
23868
  let tmpDir = null;
23197
23869
  try {
23198
- tmpDir = mkdtempSync(join24(tmpdir2(), "pi-focus-"));
23199
- const scriptPath = join24(tmpDir, "check.ps1");
23200
- writeFileSync19(scriptPath, POWERCHECK_SCRIPT, "utf-8");
23201
- const stdout = await new Promise((resolve13, reject) => {
23870
+ tmpDir = mkdtempSync(join26(tmpdir2(), "pi-focus-"));
23871
+ const scriptPath = join26(tmpDir, "check.ps1");
23872
+ writeFileSync21(scriptPath, POWERCHECK_SCRIPT, "utf-8");
23873
+ const stdout = await new Promise((resolve14, reject) => {
23202
23874
  execFile(
23203
23875
  "powershell.exe",
23204
23876
  [
@@ -23213,7 +23885,7 @@ async function isWindowFocusedOnWindows() {
23213
23885
  { timeout: 5e3, encoding: "utf-8" },
23214
23886
  (err, out) => {
23215
23887
  if (err) reject(err);
23216
- else resolve13(out);
23888
+ else resolve14(out);
23217
23889
  }
23218
23890
  );
23219
23891
  });
@@ -23255,7 +23927,7 @@ async function sendNativeNotification(title, message, options) {
23255
23927
  if (options?.suppressWhenFocused && await isWindowFocused()) {
23256
23928
  throw new SuppressedError2();
23257
23929
  }
23258
- return new Promise((resolve13, reject) => {
23930
+ return new Promise((resolve14, reject) => {
23259
23931
  notifier.notify(
23260
23932
  {
23261
23933
  title,
@@ -23270,7 +23942,7 @@ async function sendNativeNotification(title, message, options) {
23270
23942
  )
23271
23943
  );
23272
23944
  } else {
23273
- resolve13();
23945
+ resolve14();
23274
23946
  }
23275
23947
  }
23276
23948
  );
@@ -26006,7 +26678,7 @@ ${results.join("\n")}`, "info");
26006
26678
  }
26007
26679
 
26008
26680
  // packages/notify/index.ts
26009
- var VERSION9 = getPackageVersion(dirname22(fileURLToPath10(import.meta.url)));
26681
+ var VERSION9 = getPackageVersion(dirname21(fileURLToPath10(import.meta.url)));
26010
26682
  function notify_default(pi) {
26011
26683
  registerNotifyTools(pi);
26012
26684
  registerNotifyCommands(pi);
@@ -26035,6 +26707,9 @@ init_core();
26035
26707
  init_core();
26036
26708
  import * as fs14 from "node:fs";
26037
26709
  import * as path13 from "node:path";
26710
+ import {
26711
+ buildSessionContext as buildSessionContext3
26712
+ } from "@earendil-works/pi-coding-agent";
26038
26713
 
26039
26714
  // packages/milestone/milestone.ts
26040
26715
  init_core();
@@ -26180,7 +26855,7 @@ function getProgressSummary(filePath) {
26180
26855
  }
26181
26856
 
26182
26857
  // packages/milestone/hooks.ts
26183
- var sessionStartMs = 0;
26858
+ var MILESTONE_SNAPSHOT_TYPE = "unipi-milestone-snapshot";
26184
26859
  function formatMilestoneContext(filePath) {
26185
26860
  const summary = getProgressSummary(filePath);
26186
26861
  if (summary.totalItems === 0) return null;
@@ -26193,16 +26868,67 @@ function formatMilestoneContext(filePath) {
26193
26868
  focus
26194
26869
  ].filter(Boolean).join("\n");
26195
26870
  }
26871
+ function formatMilestoneSnapshot(workspace, context) {
26872
+ return [
26873
+ "# UniPi Milestone Snapshot",
26874
+ "This snapshot supersedes all prior UniPi milestone snapshots; use only this snapshot for milestone status.",
26875
+ `Workspace: ${workspace}`,
26876
+ `Status: ${context ? "active" : "inactive"}`,
26877
+ context ?? "No milestones are active for this workspace."
26878
+ ].join("\n\n");
26879
+ }
26880
+ function latestEffectiveSnapshot(branch) {
26881
+ const messages = buildSessionContext3(branch).messages;
26882
+ for (let index = messages.length - 1; index >= 0; index--) {
26883
+ const message = messages[index];
26884
+ if (message.role === "custom" && message.customType === MILESTONE_SNAPSHOT_TYPE) {
26885
+ return {
26886
+ content: message.content,
26887
+ details: message.details
26888
+ };
26889
+ }
26890
+ }
26891
+ return void 0;
26892
+ }
26893
+ function latestHistoricalSnapshot(branch) {
26894
+ for (let index = branch.length - 1; index >= 0; index--) {
26895
+ const entry = branch[index];
26896
+ if (entry.type === "custom_message" && entry.customType === MILESTONE_SNAPSHOT_TYPE) {
26897
+ return {
26898
+ content: entry.content,
26899
+ details: entry.details
26900
+ };
26901
+ }
26902
+ }
26903
+ return void 0;
26904
+ }
26905
+ function isActiveSnapshot2(snapshot) {
26906
+ if (typeof snapshot.details?.active === "boolean") return snapshot.details.active;
26907
+ return typeof snapshot.content === "string" && snapshot.content.includes("Status: active");
26908
+ }
26196
26909
  function registerSessionStartHook(pi) {
26197
- pi.on("before_agent_start", (event) => {
26198
- sessionStartMs = Date.now();
26199
- const cwd = process.cwd();
26200
- const milestonesPath = path13.join(cwd, MILESTONE_DIRS.MILESTONES);
26910
+ pi.on("before_agent_start", (_event, ctx) => {
26911
+ const workspace = ctx.cwd;
26912
+ const milestonesPath = path13.join(workspace, MILESTONE_DIRS.MILESTONES);
26201
26913
  const context = formatMilestoneContext(milestonesPath);
26202
- if (!context) return void 0;
26203
- const currentPrompt = event.systemPrompt ?? "";
26914
+ const branch = ctx.sessionManager.getBranch();
26915
+ const latest = latestEffectiveSnapshot(branch);
26916
+ const historical = latestHistoricalSnapshot(branch);
26917
+ if (!context && !latest && !historical) return void 0;
26918
+ const prior = latest ?? historical;
26919
+ if (!context && prior && !isActiveSnapshot2(prior)) return void 0;
26920
+ const content = formatMilestoneSnapshot(workspace, context);
26921
+ if (latest?.content === content) return void 0;
26204
26922
  return {
26205
- systemPrompt: currentPrompt + "\n\n" + context
26923
+ message: {
26924
+ customType: MILESTONE_SNAPSHOT_TYPE,
26925
+ content,
26926
+ display: false,
26927
+ details: {
26928
+ active: context !== null,
26929
+ workspace
26930
+ }
26931
+ }
26206
26932
  };
26207
26933
  });
26208
26934
  }
@@ -26258,14 +26984,16 @@ function scanModifiedDocs(dirs, since) {
26258
26984
  }
26259
26985
  function registerSessionEndHook(pi) {
26260
26986
  const baselineSnapshots = /* @__PURE__ */ new Map();
26261
- pi.on("session_start", () => {
26987
+ let sessionStartMs = 0;
26988
+ let sessionWorkspace = null;
26989
+ pi.on("session_start", (_event, ctx) => {
26262
26990
  sessionStartMs = Date.now();
26991
+ sessionWorkspace = ctx.cwd;
26263
26992
  baselineSnapshots.clear();
26264
- const cwd = process.cwd();
26265
26993
  const scanDirs = [
26266
- path13.join(cwd, ".unipi/docs/specs"),
26267
- path13.join(cwd, ".unipi/docs/plans"),
26268
- path13.join(cwd, ".unipi/docs/quick-work")
26994
+ path13.join(sessionWorkspace, ".unipi/docs/specs"),
26995
+ path13.join(sessionWorkspace, ".unipi/docs/plans"),
26996
+ path13.join(sessionWorkspace, ".unipi/docs/quick-work")
26269
26997
  ];
26270
26998
  for (const dir of scanDirs) {
26271
26999
  if (!fs14.existsSync(dir)) continue;
@@ -26279,13 +27007,13 @@ function registerSessionEndHook(pi) {
26279
27007
  }
26280
27008
  });
26281
27009
  const syncModifiedDocs = () => {
26282
- const cwd = process.cwd();
26283
- const milestonesPath = path13.join(cwd, MILESTONE_DIRS.MILESTONES);
27010
+ if (!sessionWorkspace) return;
27011
+ const milestonesPath = path13.join(sessionWorkspace, MILESTONE_DIRS.MILESTONES);
26284
27012
  if (!fs14.existsSync(milestonesPath)) return;
26285
27013
  const scanDirs = [
26286
- path13.join(cwd, ".unipi/docs/specs"),
26287
- path13.join(cwd, ".unipi/docs/plans"),
26288
- path13.join(cwd, ".unipi/docs/quick-work")
27014
+ path13.join(sessionWorkspace, ".unipi/docs/specs"),
27015
+ path13.join(sessionWorkspace, ".unipi/docs/plans"),
27016
+ path13.join(sessionWorkspace, ".unipi/docs/quick-work")
26289
27017
  ];
26290
27018
  const modifiedFiles = scanModifiedDocs(scanDirs, sessionStartMs);
26291
27019
  for (const filePath of modifiedFiles) {
@@ -26303,13 +27031,13 @@ function registerSessionEndHook(pi) {
26303
27031
 
26304
27032
  // packages/milestone/commands.ts
26305
27033
  init_core();
26306
- import { dirname as dirname23, join as join26 } from "node:path";
27034
+ import { dirname as dirname22, join as join28 } from "node:path";
26307
27035
  import { fileURLToPath as fileURLToPath11 } from "node:url";
26308
27036
  import { readFileSync as readFileSync21 } from "node:fs";
26309
- var SKILLS_DIR = join26(dirname23(fileURLToPath11(import.meta.url)), "skills");
27037
+ var SKILLS_DIR = join28(dirname22(fileURLToPath11(import.meta.url)), "skills");
26310
27038
  function loadSkill(skillName) {
26311
27039
  try {
26312
- return readFileSync21(join26(SKILLS_DIR, skillName, "SKILL.md"), "utf-8");
27040
+ return readFileSync21(join28(SKILLS_DIR, skillName, "SKILL.md"), "utf-8");
26313
27041
  } catch {
26314
27042
  return "";
26315
27043
  }
@@ -26362,7 +27090,7 @@ ${skillContent}
26362
27090
  },
26363
27091
  getArgumentCompletions: () => {
26364
27092
  const cwd = process.cwd();
26365
- const milestonesPath = join26(cwd, MILESTONE_DIRS.MILESTONES);
27093
+ const milestonesPath = join28(cwd, MILESTONE_DIRS.MILESTONES);
26366
27094
  const doc = parseMilestones(milestonesPath);
26367
27095
  const suggestions = doc.phases.map((phase) => ({
26368
27096
  value: phase.name,
@@ -26533,9 +27261,9 @@ var KanboardServer = class {
26533
27261
  }
26534
27262
  /** Bind server to a port */
26535
27263
  listen(port) {
26536
- return new Promise((resolve13, reject) => {
27264
+ return new Promise((resolve14, reject) => {
26537
27265
  this.server.once("error", reject);
26538
- this.server.listen(port, () => resolve13());
27266
+ this.server.listen(port, () => resolve14());
26539
27267
  });
26540
27268
  }
26541
27269
  /** Handle incoming HTTP request */
@@ -26818,6 +27546,7 @@ var COMMAND_REGISTRY = {
26818
27546
  "unipi:worktree-list": "workflow",
26819
27547
  "unipi:worktree-merge": "workflow",
26820
27548
  "unipi:consultant": "workflow",
27549
+ "unipi:prefix-cache": "utility",
26821
27550
  "unipi:quick-work": "workflow",
26822
27551
  "unipi:gather-context": "workflow",
26823
27552
  "unipi:document": "workflow",
@@ -26925,6 +27654,7 @@ var COMMAND_DESCRIPTIONS = {
26925
27654
  "unipi:worktree-list": "List all unipi worktrees",
26926
27655
  "unipi:worktree-merge": "Merge worktree branches back to main",
26927
27656
  "unipi:consultant": "Expert consultation \u2014 advisory analysis",
27657
+ "unipi:prefix-cache": "Show privacy-safe provider prefix-cache diagnostics",
26928
27658
  "unipi:quick-work": "Fast single-task execution \u2014 one shot",
26929
27659
  "unipi:gather-context": "Research codebase \u2014 surface patterns",
26930
27660
  "unipi:document": "Generate documentation \u2014 README, API docs",
@@ -27504,11 +28234,11 @@ function markAutoCompactionError(state, nowMs) {
27504
28234
  }
27505
28235
 
27506
28236
  // packages/compactor/src/session/db.ts
27507
- import { createHash as createHash2 } from "node:crypto";
28237
+ import { createHash as createHash3 } from "node:crypto";
27508
28238
  import { execFileSync } from "node:child_process";
27509
- import { existsSync as existsSync32, mkdirSync as mkdirSync20 } from "node:fs";
27510
- import { homedir as homedir21 } from "node:os";
27511
- import { dirname as dirname26, 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";
27512
28242
  function getWorktreeSuffix() {
27513
28243
  const envSuffix = process.env.COMPACTOR_SESSION_SUFFIX;
27514
28244
  if (envSuffix !== void 0) {
@@ -27522,7 +28252,7 @@ function getWorktreeSuffix() {
27522
28252
  { encoding: "utf-8", timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] }
27523
28253
  ).split(/\r?\n/).find((l) => l.startsWith("worktree "))?.replace("worktree ", "")?.trim();
27524
28254
  if (mainWorktree && cwd !== mainWorktree) {
27525
- const suffix = createHash2("sha256").update(cwd).digest("hex").slice(0, 8);
28255
+ const suffix = createHash3("sha256").update(cwd).digest("hex").slice(0, 8);
27526
28256
  return `__${suffix}`;
27527
28257
  }
27528
28258
  } catch {
@@ -27530,7 +28260,7 @@ function getWorktreeSuffix() {
27530
28260
  return "";
27531
28261
  }
27532
28262
  function defaultDBPath(name) {
27533
- return join33(homedir21(), ".unipi", "db", "compactor", `${name}.db`);
28263
+ return join35(homedir23(), ".unipi", "db", "compactor", `${name}.db`);
27534
28264
  }
27535
28265
  var sqliteLib = null;
27536
28266
  async function getSQLite() {
@@ -27553,8 +28283,8 @@ var SessionDB = class {
27553
28283
  this.dbPath = opts?.dbPath ?? defaultDBPath("session");
27554
28284
  }
27555
28285
  async init() {
27556
- const dir = dirname26(this.dbPath);
27557
- if (!existsSync32(dir)) mkdirSync20(dir, { recursive: true });
28286
+ const dir = dirname25(this.dbPath);
28287
+ if (!existsSync32(dir)) mkdirSync22(dir, { recursive: true });
27558
28288
  const sqlite = await getSQLite();
27559
28289
  const Database2 = sqlite.Database ?? sqlite.default?.Database ?? sqlite.default ?? sqlite;
27560
28290
  this.db = new Database2(this.dbPath);
@@ -27664,7 +28394,7 @@ var SessionDB = class {
27664
28394
  }
27665
28395
  insertEvent(sessionId, event, sourceHook = "PostToolUse") {
27666
28396
  if (!this.stmts) return;
27667
- const dataHash = createHash2("sha256").update(event.data).digest("hex").slice(0, 16).toUpperCase();
28397
+ const dataHash = createHash3("sha256").update(event.data).digest("hex").slice(0, 16).toUpperCase();
27668
28398
  const projectDir = String(event.project_dir ?? "").trim();
27669
28399
  const attributionSource = String(event.attribution_source ?? "unknown");
27670
28400
  const rawConfidence = Number(event.attribution_confidence ?? 0);
@@ -27947,8 +28677,8 @@ async function buildResumeContextMessage(db, sessionId) {
27947
28677
 
27948
28678
  // packages/compactor/src/executor/executor.ts
27949
28679
  import { spawn as spawn3, execSync, execFileSync as execFileSync2 } from "node:child_process";
27950
- import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync24, rmSync as rmSync3 } from "node:fs";
27951
- import { join as join34, resolve as resolve10 } 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";
27952
28682
  import { tmpdir as tmpdir3 } from "node:os";
27953
28683
 
27954
28684
  // packages/compactor/src/executor/runtime.ts
@@ -28040,7 +28770,7 @@ var OS_TMPDIR = (() => {
28040
28770
  process.platform === "darwin" ? ["DARWIN_USER_TEMP_DIR"] : ["-u", "-d"],
28041
28771
  { env: { ...process.env, TMPDIR: void 0 }, encoding: "utf-8" }
28042
28772
  ).trim();
28043
- const dir = process.platform === "darwin" ? result : resolve10(result, "..");
28773
+ const dir = process.platform === "darwin" ? result : resolve11(result, "..");
28044
28774
  if (dir && dir !== process.cwd()) return dir;
28045
28775
  } catch {
28046
28776
  }
@@ -28129,7 +28859,7 @@ var PolyglotExecutor = class {
28129
28859
  }
28130
28860
  async execute(opts) {
28131
28861
  const { language, code, timeout = 3e4, background = false } = opts;
28132
- const tmpDir = mkdtempSync2(join34(OS_TMPDIR, ".compactor-"));
28862
+ const tmpDir = mkdtempSync2(join36(OS_TMPDIR, ".compactor-"));
28133
28863
  try {
28134
28864
  const filePath = this.#writeScript(tmpDir, code, language);
28135
28865
  const cmd = buildCommand(this.#runtimes, language, filePath);
@@ -28161,7 +28891,7 @@ var PolyglotExecutor = class {
28161
28891
  async executeFile(opts) {
28162
28892
  const { language, path: path28, timeout = 3e4, background = false } = opts;
28163
28893
  const cmd = buildCommand(this.#runtimes, language, path28);
28164
- const cwd = language === "shell" ? this.#projectRoot : resolve10(path28, "..");
28894
+ const cwd = language === "shell" ? this.#projectRoot : resolve11(path28, "..");
28165
28895
  return this.#spawn(cmd, cwd, "", timeout, background);
28166
28896
  }
28167
28897
  #writeScript(tmpDir, code, language) {
@@ -28179,12 +28909,12 @@ var PolyglotExecutor = class {
28179
28909
  elixir: "ex"
28180
28910
  };
28181
28911
  const ext = extensions[language];
28182
- const filePath = join34(tmpDir, `script.${ext}`);
28183
- writeFileSync24(filePath, code, "utf-8");
28912
+ const filePath = join36(tmpDir, `script.${ext}`);
28913
+ writeFileSync26(filePath, code, "utf-8");
28184
28914
  return filePath;
28185
28915
  }
28186
28916
  async #spawn(cmd, cwd, tmpDir, timeout, background) {
28187
- return new Promise((resolve13) => {
28917
+ return new Promise((resolve14) => {
28188
28918
  const proc = spawn3(cmd[0], cmd.slice(1), {
28189
28919
  cwd,
28190
28920
  env: sanitizeEnv(),
@@ -28220,14 +28950,14 @@ var PolyglotExecutor = class {
28220
28950
  }, timeout);
28221
28951
  proc.on("error", (err) => {
28222
28952
  if (timer) clearTimeout(timer);
28223
- resolve13({ stdout, stderr: err.message, exitCode: 1, timedOut: false });
28953
+ resolve14({ stdout, stderr: err.message, exitCode: 1, timedOut: false });
28224
28954
  });
28225
28955
  proc.on("close", (code) => {
28226
28956
  if (timer) clearTimeout(timer);
28227
28957
  if (background && proc.pid) {
28228
28958
  this.#backgroundedPids.add(proc.pid);
28229
28959
  }
28230
- resolve13({
28960
+ resolve14({
28231
28961
  stdout: stdout.slice(0, this.#hardCapBytes),
28232
28962
  stderr: stderr.slice(0, this.#hardCapBytes),
28233
28963
  exitCode: code ?? 0,
@@ -28238,7 +28968,7 @@ var PolyglotExecutor = class {
28238
28968
  });
28239
28969
  }
28240
28970
  async #compileAndRun(filePath, tmpDir, timeout) {
28241
- const binPath = join34(tmpDir, "script");
28971
+ const binPath = join36(tmpDir, "script");
28242
28972
  try {
28243
28973
  execSync(`rustc "${filePath}" -o "${binPath}"`, {
28244
28974
  cwd: tmpDir,
@@ -28265,7 +28995,7 @@ init_core();
28265
28995
  init_hooks();
28266
28996
 
28267
28997
  // packages/compactor/src/compaction/search-entries.ts
28268
- import { createHash as createHash4 } from "node:crypto";
28998
+ import { createHash as createHash5 } from "node:crypto";
28269
28999
  function tokenize(text) {
28270
29000
  return text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1);
28271
29001
  }
@@ -28309,7 +29039,7 @@ function searchEntries(blocks, query, opts) {
28309
29039
  kind: b.kind
28310
29040
  }));
28311
29041
  const hashSource = docs.length > 0 ? `${docs.length}:${docs[0].text.slice(0, 80)}:${docs[docs.length - 1].text.slice(-80)}` : "empty";
28312
- const currentHash = createHash4("sha256").update(hashSource).digest("hex");
29042
+ const currentHash = createHash5("sha256").update(hashSource).digest("hex");
28313
29043
  let index;
28314
29044
  let docCount;
28315
29045
  let avgDocLen;
@@ -28348,15 +29078,29 @@ function searchEntries(blocks, query, opts) {
28348
29078
  }
28349
29079
 
28350
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
+ }
28351
29091
  function vccRecall(blocks, input) {
28352
- 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);
28353
29097
  let hits = [];
28354
29098
  if (mode === "bm25") {
28355
29099
  const results = searchEntries(blocks, query, { limit: limit + offset, offset: 0 });
28356
29100
  hits = results.map((r, i) => ({
28357
29101
  index: r.docId,
28358
29102
  score: r.score,
28359
- text: expand ? r.text : r.text.slice(0, 200),
29103
+ text: expand ? truncateExpandedHit(r.text) : r.text.slice(0, 200),
28360
29104
  kind: r.kind
28361
29105
  }));
28362
29106
  } else {
@@ -28368,7 +29112,7 @@ function vccRecall(blocks, input) {
28368
29112
  hits.push({
28369
29113
  index: i,
28370
29114
  score: 1,
28371
- text: expand ? text : text.slice(0, 200),
29115
+ text: expand ? truncateExpandedHit(text) : text.slice(0, 200),
28372
29116
  kind: b.kind
28373
29117
  });
28374
29118
  }
@@ -28850,7 +29594,11 @@ var RecallParams = Type7.Object({
28850
29594
  mode: Type7.Optional(Type7.Union([Type7.Literal("bm25"), Type7.Literal("regex")], {
28851
29595
  description: "Search mode: bm25 (default) or regex fallback"
28852
29596
  })),
28853
- 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
+ })),
28854
29602
  offset: Type7.Optional(Type7.Number({ description: "Pagination offset", minimum: 0 })),
28855
29603
  expand: Type7.Optional(Type7.Boolean({ description: "Return full message content for hits" }))
28856
29604
  });
@@ -29275,13 +30023,13 @@ function compactorExtension(pi) {
29275
30023
  debug("before_agent_start", { sessionId: currentSessionId, configDebug: config.debug });
29276
30024
  try {
29277
30025
  const { existsSync: existsSync48 } = await import("node:fs");
29278
- const { join: join48 } = await import("node:path");
30026
+ const { join: join50 } = await import("node:path");
29279
30027
  const strategies = [
29280
30028
  { key: "commits", config: config.commits }
29281
30029
  ];
29282
30030
  for (const { key, config: strat } of strategies) {
29283
30031
  if (strat.autoDetect === "git") {
29284
- const gitDir = join48(cwd, ".git");
30032
+ const gitDir = join50(cwd, ".git");
29285
30033
  if (!existsSync48(gitDir)) {
29286
30034
  debug("autoDetect_disable", { strategy: key, reason: "no .git dir" });
29287
30035
  strat.enabled = false;
@@ -32795,10 +33543,10 @@ import { Key as Key6, matchesKey as matchesKey14, truncateToWidth as truncateToW
32795
33543
  // packages/updater/src/readme.ts
32796
33544
  init_core();
32797
33545
  init_core();
32798
- import { dirname as dirname28 } from "node:path";
33546
+ import { dirname as dirname27 } from "node:path";
32799
33547
  import { fileURLToPath as fileURLToPath12 } from "node:url";
32800
33548
  import { existsSync as existsSync39, readFileSync as readFileSync34 } from "fs";
32801
- import { join as join38 } from "path";
33549
+ import { join as join40 } from "path";
32802
33550
  var PACKAGE_MAP = {
32803
33551
  core: MODULES.CORE,
32804
33552
  workflow: MODULES.WORKFLOW,
@@ -32822,13 +33570,13 @@ var PACKAGE_MAP = {
32822
33570
  updater: MODULES.UPDATER
32823
33571
  };
32824
33572
  function resolveUnipiRoot() {
32825
- const dir = dirname28(fileURLToPath12(import.meta.url));
33573
+ const dir = dirname27(fileURLToPath12(import.meta.url));
32826
33574
  const root = findPackageRoot(dir, "@pi-unipi/unipi");
32827
- return root ?? join38(dir, "..");
33575
+ return root ?? join40(dir, "..");
32828
33576
  }
32829
33577
  function getPackageVersion2(dir) {
32830
33578
  try {
32831
- const pkgPath = join38(dir, "package.json");
33579
+ const pkgPath = join40(dir, "package.json");
32832
33580
  if (existsSync39(pkgPath)) {
32833
33581
  const raw = readFileSync34(pkgPath, "utf-8");
32834
33582
  const pkg = JSON.parse(raw);
@@ -32841,7 +33589,7 @@ function getPackageVersion2(dir) {
32841
33589
  function discoverReadmes() {
32842
33590
  const root = resolveUnipiRoot();
32843
33591
  const entries = [];
32844
- const rootReadme = join38(root, "README.md");
33592
+ const rootReadme = join40(root, "README.md");
32845
33593
  if (existsSync39(rootReadme)) {
32846
33594
  entries.push({
32847
33595
  name: "unipi",
@@ -32851,16 +33599,16 @@ function discoverReadmes() {
32851
33599
  });
32852
33600
  }
32853
33601
  for (const [shortName, fullName] of Object.entries(PACKAGE_MAP)) {
32854
- const workspacePath = join38(root, "packages", shortName, "README.md");
32855
- 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");
32856
33604
  let readmePath = null;
32857
33605
  let versionDir = null;
32858
33606
  if (existsSync39(workspacePath)) {
32859
33607
  readmePath = workspacePath;
32860
- versionDir = join38(root, "packages", shortName);
33608
+ versionDir = join40(root, "packages", shortName);
32861
33609
  } else if (existsSync39(nodeModulesPath)) {
32862
33610
  readmePath = nodeModulesPath;
32863
- versionDir = join38(root, "node_modules", fullName);
33611
+ versionDir = join40(root, "node_modules", fullName);
32864
33612
  }
32865
33613
  if (readmePath && versionDir) {
32866
33614
  entries.push({
@@ -32876,14 +33624,14 @@ function discoverReadmes() {
32876
33624
  function resolveReadmePath(packageName) {
32877
33625
  const root = resolveUnipiRoot();
32878
33626
  if (!packageName || packageName === "unipi") {
32879
- const rootReadme = join38(root, "README.md");
33627
+ const rootReadme = join40(root, "README.md");
32880
33628
  return existsSync39(rootReadme) ? rootReadme : null;
32881
33629
  }
32882
33630
  const fullName = PACKAGE_MAP[packageName];
32883
33631
  if (!fullName) return null;
32884
- const workspacePath = join38(root, "packages", packageName, "README.md");
33632
+ const workspacePath = join40(root, "packages", packageName, "README.md");
32885
33633
  if (existsSync39(workspacePath)) return workspacePath;
32886
- const nodeModulesPath = join38(root, "node_modules", fullName, "README.md");
33634
+ const nodeModulesPath = join40(root, "node_modules", fullName, "README.md");
32887
33635
  if (existsSync39(nodeModulesPath)) return nodeModulesPath;
32888
33636
  return null;
32889
33637
  }
@@ -33159,7 +33907,7 @@ import { Key as Key7, matchesKey as matchesKey15, truncateToWidth as truncateToW
33159
33907
  // packages/updater/src/changelog.ts
33160
33908
  init_core();
33161
33909
  import { existsSync as existsSync40, readFileSync as readFileSync36 } from "fs";
33162
- import { dirname as dirname29, join as join39 } from "node:path";
33910
+ import { dirname as dirname28, join as join41 } from "node:path";
33163
33911
  import { fileURLToPath as fileURLToPath13 } from "node:url";
33164
33912
 
33165
33913
  // packages/updater/src/version.ts
@@ -33184,15 +33932,15 @@ function isNewerVersion(latest, current) {
33184
33932
  // packages/updater/src/changelog.ts
33185
33933
  function resolveChangelogPath() {
33186
33934
  try {
33187
- const dir = dirname29(fileURLToPath13(import.meta.url));
33935
+ const dir = dirname28(fileURLToPath13(import.meta.url));
33188
33936
  const root = findPackageRoot(dir, "@pi-unipi/unipi");
33189
33937
  if (root) {
33190
- const packaged = join39(root, "CHANGELOG.md");
33938
+ const packaged = join41(root, "CHANGELOG.md");
33191
33939
  if (existsSync40(packaged)) return packaged;
33192
33940
  }
33193
33941
  } catch {
33194
33942
  }
33195
- return join39(process.cwd(), "CHANGELOG.md");
33943
+ return join41(process.cwd(), "CHANGELOG.md");
33196
33944
  }
33197
33945
  var VERSION_HEADER_RE = /^## \[(.+?)\](?:\s*[-—–]\s*(.+))?$/;
33198
33946
  var SECTION_HEADER_RE = /^### (.+)$/;
@@ -33444,9 +34192,9 @@ function renderChangelogOverlay() {
33444
34192
 
33445
34193
  // packages/updater/src/settings.ts
33446
34194
  init_core();
33447
- import { readFileSync as readFileSync37, writeFileSync as writeFileSync26, mkdirSync as mkdirSync22, existsSync as existsSync42 } from "fs";
33448
- import { dirname as dirname30, join as join40 } from "path";
33449
- 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";
33450
34198
  var DEFAULT_CONFIG6 = {
33451
34199
  checkIntervalMs: 36e5,
33452
34200
  // 1 hour
@@ -33460,8 +34208,8 @@ var VALID_INTERVALS = {
33460
34208
  };
33461
34209
  var VALID_MODES = ["disabled", "notify", "auto"];
33462
34210
  function resolveConfigPath2() {
33463
- const base = UPDATER_DIRS.CONFIG.replace("~", homedir24());
33464
- return join40(base, "config.json");
34211
+ const base = UPDATER_DIRS.CONFIG.replace("~", homedir26());
34212
+ return join42(base, "config.json");
33465
34213
  }
33466
34214
  function loadConfig5() {
33467
34215
  const configPath = resolveConfigPath2();
@@ -33477,9 +34225,9 @@ function loadConfig5() {
33477
34225
  }
33478
34226
  function saveConfig4(config) {
33479
34227
  const configPath = resolveConfigPath2();
33480
- const dir = dirname30(configPath);
33481
- mkdirSync22(dir, { recursive: true });
33482
- writeFileSync26(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
34228
+ const dir = dirname29(configPath);
34229
+ mkdirSync24(dir, { recursive: true });
34230
+ writeFileSync28(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
33483
34231
  }
33484
34232
  function getIntervalLabel(ms) {
33485
34233
  for (const [label, value] of Object.entries(VALID_INTERVALS)) {
@@ -33712,12 +34460,12 @@ init_core();
33712
34460
 
33713
34461
  // packages/updater/src/cache.ts
33714
34462
  init_core();
33715
- import { readFileSync as readFileSync38, writeFileSync as writeFileSync27, mkdirSync as mkdirSync23, existsSync as existsSync43 } from "fs";
33716
- import { dirname as dirname31, join as join41 } from "path";
33717
- 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";
33718
34466
  function resolveCachePath() {
33719
- const base = UPDATER_DIRS.CACHE.replace("~", homedir25());
33720
- return join41(base, "last-check.json");
34467
+ const base = UPDATER_DIRS.CACHE.replace("~", homedir27());
34468
+ return join43(base, "last-check.json");
33721
34469
  }
33722
34470
  function readLastCheck() {
33723
34471
  const cachePath = resolveCachePath();
@@ -33735,9 +34483,9 @@ function readLastCheck() {
33735
34483
  }
33736
34484
  function writeLastCheck(cache) {
33737
34485
  const cachePath = resolveCachePath();
33738
- const dir = dirname31(cachePath);
33739
- mkdirSync23(dir, { recursive: true });
33740
- writeFileSync27(cachePath, JSON.stringify(cache, null, 2) + "\n", "utf-8");
34486
+ const dir = dirname30(cachePath);
34487
+ mkdirSync25(dir, { recursive: true });
34488
+ writeFileSync29(cachePath, JSON.stringify(cache, null, 2) + "\n", "utf-8");
33741
34489
  }
33742
34490
  function isCheckDue(intervalMs) {
33743
34491
  const cache = readLastCheck();
@@ -34145,8 +34893,8 @@ init_core();
34145
34893
  import { Key as Key10 } from "@earendil-works/pi-tui";
34146
34894
 
34147
34895
  // packages/input-shortcuts/src/registers.ts
34148
- import { existsSync as existsSync44, mkdirSync as mkdirSync24, readFileSync as readFileSync39, renameSync as renameSync7, writeFileSync as writeFileSync28 } from "node:fs";
34149
- import { dirname as dirname32, 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";
34150
34898
 
34151
34899
  // packages/input-shortcuts/src/types.ts
34152
34900
  var DEFAULT_CONFIG7 = {
@@ -34168,7 +34916,7 @@ var RegisterStore = class {
34168
34916
  filePath;
34169
34917
  loaded = false;
34170
34918
  constructor(baseDir) {
34171
- this.filePath = baseDir ? join42(baseDir, REGISTERS_FILE) : REGISTERS_FILE;
34919
+ this.filePath = baseDir ? join44(baseDir, REGISTERS_FILE) : REGISTERS_FILE;
34172
34920
  }
34173
34921
  /** Get the stash register contents. */
34174
34922
  getStash() {
@@ -34216,13 +34964,13 @@ var RegisterStore = class {
34216
34964
  /** Atomic write: write to .tmp then rename. */
34217
34965
  save() {
34218
34966
  try {
34219
- const dir = dirname32(this.filePath);
34967
+ const dir = dirname31(this.filePath);
34220
34968
  if (!existsSync44(dir)) {
34221
- mkdirSync24(dir, { recursive: true });
34969
+ mkdirSync26(dir, { recursive: true });
34222
34970
  }
34223
34971
  const tmpPath = this.filePath + ".tmp";
34224
- writeFileSync28(tmpPath, JSON.stringify(this.data, null, 2), "utf-8");
34225
- renameSync7(tmpPath, this.filePath);
34972
+ writeFileSync30(tmpPath, JSON.stringify(this.data, null, 2), "utf-8");
34973
+ renameSync8(tmpPath, this.filePath);
34226
34974
  } catch {
34227
34975
  }
34228
34976
  }
@@ -34443,8 +35191,8 @@ var ChordOverlay = class extends Container2 {
34443
35191
 
34444
35192
  // packages/input-shortcuts/src/settings-overlay.ts
34445
35193
  import { SettingsList as SettingsList3 } from "@earendil-works/pi-tui";
34446
- import { existsSync as existsSync45, mkdirSync as mkdirSync25, readFileSync as readFileSync40, renameSync as renameSync8, writeFileSync as writeFileSync29 } from "node:fs";
34447
- import { dirname as dirname33, 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";
34448
35196
  var ALT_KEY_OPTIONS = [
34449
35197
  "alt+a",
34450
35198
  "alt+b",
@@ -34476,7 +35224,7 @@ var ALT_KEY_OPTIONS = [
34476
35224
  var CONFLICTS = /* @__PURE__ */ new Set(["alt+e"]);
34477
35225
  var FREE_ALT_KEYS = ALT_KEY_OPTIONS.filter((k) => !CONFLICTS.has(k));
34478
35226
  function loadConfig6(baseDir) {
34479
- const filePath = baseDir ? join43(baseDir, CONFIG_FILE) : CONFIG_FILE;
35227
+ const filePath = baseDir ? join45(baseDir, CONFIG_FILE) : CONFIG_FILE;
34480
35228
  try {
34481
35229
  if (existsSync45(filePath)) {
34482
35230
  const raw = readFileSync40(filePath, "utf-8");
@@ -34491,15 +35239,15 @@ function loadConfig6(baseDir) {
34491
35239
  return { ...DEFAULT_CONFIG7 };
34492
35240
  }
34493
35241
  function saveConfig5(config, baseDir) {
34494
- const filePath = baseDir ? join43(baseDir, CONFIG_FILE) : CONFIG_FILE;
35242
+ const filePath = baseDir ? join45(baseDir, CONFIG_FILE) : CONFIG_FILE;
34495
35243
  try {
34496
- const dir = dirname33(filePath);
35244
+ const dir = dirname32(filePath);
34497
35245
  if (!existsSync45(dir)) {
34498
- mkdirSync25(dir, { recursive: true });
35246
+ mkdirSync27(dir, { recursive: true });
34499
35247
  }
34500
35248
  const tmpPath = filePath + ".tmp";
34501
- writeFileSync29(tmpPath, JSON.stringify(config, null, 2), "utf-8");
34502
- renameSync8(tmpPath, filePath);
35249
+ writeFileSync31(tmpPath, JSON.stringify(config, null, 2), "utf-8");
35250
+ renameSync9(tmpPath, filePath);
34503
35251
  } catch {
34504
35252
  }
34505
35253
  }
@@ -34927,9 +35675,9 @@ import { Type as Type8 } from "typebox";
34927
35675
  // packages/cocoindex/bridge.ts
34928
35676
  init_core();
34929
35677
  import { execFileSync as execFileSync3, spawn as spawn4 } from "node:child_process";
34930
- import { existsSync as existsSync46, mkdirSync as mkdirSync26, readFileSync as readFileSync41, writeFileSync as writeFileSync30, readdirSync as readdirSync13, statSync as statSync8 } from "node:fs";
34931
- import { homedir as homedir26 } from "node:os";
34932
- 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";
34933
35681
  var DEFAULT_PIPELINE_DIR = ".unipi/cocoindex";
34934
35682
  var DEFAULT_UPDATE_TIMEOUT_MS = 15 * 60 * 1e3;
34935
35683
  var DEFAULT_LEXICAL_SCAN_LIMIT = 5e4;
@@ -34999,13 +35747,13 @@ function resolveCocoindexBin() {
34999
35747
  if (resolved) return resolved;
35000
35748
  } catch {
35001
35749
  }
35002
- const uvToolBin = join44(homedir26(), ".local", "bin", "cocoindex");
35750
+ const uvToolBin = join46(homedir28(), ".local", "bin", "cocoindex");
35003
35751
  if (existsSync46(uvToolBin)) return uvToolBin;
35004
- const miseRoot = join44(homedir26(), ".local", "share", "mise", "installs", "python");
35752
+ const miseRoot = join46(homedir28(), ".local", "share", "mise", "installs", "python");
35005
35753
  try {
35006
- const versions = readdirSync13(miseRoot).sort().reverse();
35754
+ const versions = readdirSync14(miseRoot).sort().reverse();
35007
35755
  for (const ver of versions) {
35008
- const binPath = join44(miseRoot, ver, "bin", "cocoindex");
35756
+ const binPath = join46(miseRoot, ver, "bin", "cocoindex");
35009
35757
  if (existsSync46(binPath)) return binPath;
35010
35758
  }
35011
35759
  } catch {
@@ -35013,13 +35761,13 @@ function resolveCocoindexBin() {
35013
35761
  return "cocoindex";
35014
35762
  }
35015
35763
  function getPipelineDir(projectDir) {
35016
- return join44(projectDir, DEFAULT_PIPELINE_DIR);
35764
+ return join46(projectDir, DEFAULT_PIPELINE_DIR);
35017
35765
  }
35018
35766
  async function isPipelineInitialized(pipelineDir) {
35019
- return existsSync46(join44(pipelineDir, "main.py"));
35767
+ return existsSync46(join46(pipelineDir, "main.py"));
35020
35768
  }
35021
35769
  function detectTargetStore(pipelineDir) {
35022
- const mainPyPath = join44(pipelineDir, "main.py");
35770
+ const mainPyPath = join46(pipelineDir, "main.py");
35023
35771
  if (!existsSync46(mainPyPath)) return "unknown";
35024
35772
  try {
35025
35773
  const content = readFileSync41(mainPyPath, "utf-8");
@@ -35035,14 +35783,14 @@ function detectTargetStore(pipelineDir) {
35035
35783
  async function initPipeline(projectDir) {
35036
35784
  const pipelineDir = getPipelineDir(projectDir);
35037
35785
  if (!existsSync46(pipelineDir)) {
35038
- mkdirSync26(pipelineDir, { recursive: true });
35786
+ mkdirSync28(pipelineDir, { recursive: true });
35039
35787
  }
35040
- if (existsSync46(join44(pipelineDir, "main.py"))) {
35788
+ if (existsSync46(join46(pipelineDir, "main.py"))) {
35041
35789
  return { success: true };
35042
35790
  }
35043
35791
  const embeddingConfig = loadEmbeddingConfig2();
35044
35792
  const template = generatePipelineTemplate(projectDir, embeddingConfig);
35045
- writeFileSync30(join44(pipelineDir, "main.py"), template, "utf-8");
35793
+ writeFileSync32(join46(pipelineDir, "main.py"), template, "utf-8");
35046
35794
  return { success: true };
35047
35795
  }
35048
35796
  async function indexProject(projectDir) {
@@ -35056,7 +35804,7 @@ async function indexProject(projectDir) {
35056
35804
  };
35057
35805
  }
35058
35806
  const pipelineDir = getPipelineDir(projectDir);
35059
- if (!existsSync46(join44(pipelineDir, "main.py"))) {
35807
+ if (!existsSync46(join46(pipelineDir, "main.py"))) {
35060
35808
  return {
35061
35809
  success: false,
35062
35810
  chunksProcessed: 0,
@@ -35066,7 +35814,7 @@ async function indexProject(projectDir) {
35066
35814
  }
35067
35815
  const start = Date.now();
35068
35816
  const cocoindexBin = resolveCocoindexBin();
35069
- return new Promise((resolve13) => {
35817
+ return new Promise((resolve14) => {
35070
35818
  const proc = spawn4(cocoindexBin, ["update", "main.py"], {
35071
35819
  cwd: pipelineDir,
35072
35820
  stdio: ["pipe", "pipe", "pipe"]
@@ -35096,9 +35844,9 @@ async function indexProject(projectDir) {
35096
35844
  const durationMs = Date.now() - start;
35097
35845
  const chunksProcessed = parseChunksProcessed(stdout);
35098
35846
  if (code === 0) {
35099
- resolve13({ success: true, chunksProcessed, durationMs });
35847
+ resolve14({ success: true, chunksProcessed, durationMs });
35100
35848
  } else {
35101
- resolve13({
35849
+ resolve14({
35102
35850
  success: false,
35103
35851
  chunksProcessed,
35104
35852
  durationMs,
@@ -35109,7 +35857,7 @@ async function indexProject(projectDir) {
35109
35857
  proc.on("error", (err) => {
35110
35858
  settled = true;
35111
35859
  clearTimeout(timer);
35112
- resolve13({
35860
+ resolve14({
35113
35861
  success: false,
35114
35862
  chunksProcessed: 0,
35115
35863
  durationMs: Date.now() - start,
@@ -35167,16 +35915,16 @@ function parseChunksProcessed(output) {
35167
35915
  async function status(projectDir) {
35168
35916
  const pipelineDir = getPipelineDir(projectDir);
35169
35917
  const cliAvailable = await isAvailable();
35170
- const pipelineConfigured = existsSync46(join44(pipelineDir, "main.py"));
35918
+ const pipelineConfigured = existsSync46(join46(pipelineDir, "main.py"));
35171
35919
  const targetStore = detectTargetStore(pipelineDir);
35172
35920
  let docCount = 0;
35173
35921
  let lastRun = null;
35174
- const lancedbPath = join44(pipelineDir, ".lancedb");
35922
+ const lancedbPath = join46(pipelineDir, ".lancedb");
35175
35923
  if (existsSync46(lancedbPath)) {
35176
35924
  try {
35177
- const stat = statSync8(lancedbPath);
35925
+ const stat = statSync9(lancedbPath);
35178
35926
  lastRun = stat.mtime.toISOString();
35179
- const files = readdirSync13(lancedbPath, { recursive: true });
35927
+ const files = readdirSync14(lancedbPath, { recursive: true });
35180
35928
  docCount = files.filter((f) => f.endsWith(".lance")).length;
35181
35929
  } catch {
35182
35930
  }
@@ -35194,7 +35942,7 @@ async function search(projectDir, query, options) {
35194
35942
  const limit = options?.limit ?? 10;
35195
35943
  const offset = options?.offset ?? 0;
35196
35944
  try {
35197
- const lancedbPath = join44(getPipelineDir(projectDir), ".lancedb");
35945
+ const lancedbPath = join46(getPipelineDir(projectDir), ".lancedb");
35198
35946
  if (!existsSync46(lancedbPath)) {
35199
35947
  return [];
35200
35948
  }
@@ -35309,7 +36057,7 @@ function countOccurrences(value, needle) {
35309
36057
  }
35310
36058
  function loadEmbeddingConfig2() {
35311
36059
  const envKey = process.env.OPENROUTER_API_KEY ?? null;
35312
- const configPath = join44(homedir26(), ".unipi", "memory", "config.json");
36060
+ const configPath = join46(homedir28(), ".unipi", "memory", "config.json");
35313
36061
  try {
35314
36062
  if (existsSync46(configPath)) {
35315
36063
  const raw = readFileSync41(configPath, "utf-8");
@@ -35516,9 +36264,22 @@ app = coco.App(
35516
36264
  }
35517
36265
 
35518
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
+ }
35519
36276
  var SearchParams = Type8.Object({
35520
36277
  query: Type8.String({ description: "Search query against indexed content" }),
35521
- 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
+ })),
35522
36283
  offset: Type8.Optional(Type8.Number({ description: "Pagination offset", minimum: 0 }))
35523
36284
  });
35524
36285
  var StatusParams = Type8.Object({});
@@ -35534,7 +36295,7 @@ function registerCocoindexTools(pi, deps) {
35534
36295
  label: "CocoIndex Search",
35535
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.",
35536
36297
  parameters: SearchParams,
35537
- async execute(_toolCallId, params) {
36298
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
35538
36299
  try {
35539
36300
  const available = await isAvailable();
35540
36301
  if (!available) {
@@ -35543,9 +36304,10 @@ function registerCocoindexTools(pi, deps) {
35543
36304
  { cliAvailable: false }
35544
36305
  );
35545
36306
  }
35546
- const results = await search(deps.projectDir, params.query, {
35547
- limit: params.limit,
35548
- 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
35549
36311
  });
35550
36312
  if (results.length === 0) {
35551
36313
  return textResult3(
@@ -35560,7 +36322,8 @@ ${r.content.slice(0, 300)}`
35560
36322
  `Found ${results.length} results for "${params.query}":
35561
36323
 
35562
36324
  ${lines.join("\n\n")}`,
35563
- { results }
36325
+ // Avoid duplicating full result content in model-visible details.
36326
+ { count: results.length, query: params.query, limit, offset }
35564
36327
  );
35565
36328
  } catch (err) {
35566
36329
  return textResult3(`CocoIndex search error: ${err}`, { error: true });
@@ -35572,9 +36335,9 @@ ${lines.join("\n\n")}`,
35572
36335
  label: "CocoIndex Status",
35573
36336
  description: "Check CocoIndex indexing status. Diagnostic only; use commands for interactive install/update.",
35574
36337
  parameters: StatusParams,
35575
- async execute() {
36338
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
35576
36339
  try {
35577
- const info = await status(deps.projectDir);
36340
+ const info = await status(deps.getProjectDir(ctx));
35578
36341
  const lines = [
35579
36342
  `\u{1F4E6} CocoIndex Status`,
35580
36343
  `CLI available: ${info.cliAvailable ? "\u2705" : "\u274C"}`,
@@ -35607,8 +36370,8 @@ init_core();
35607
36370
  // packages/cocoindex/installer.ts
35608
36371
  init_core();
35609
36372
  import { execFileSync as execFileSync4, spawn as spawn5 } from "node:child_process";
35610
- import { homedir as homedir27 } from "node:os";
35611
- 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";
35612
36375
  var DEFAULT_STEP_TIMEOUT_MS = 10 * 60 * 1e3;
35613
36376
  function detectShell() {
35614
36377
  const shell = process.env.SHELL ?? "";
@@ -35836,7 +36599,7 @@ function buildUvInstallerFallback(shell) {
35836
36599
  ];
35837
36600
  }
35838
36601
  function runStep(step) {
35839
- return new Promise((resolve13) => {
36602
+ return new Promise((resolve14) => {
35840
36603
  const proc = spawn5(step.command, step.args ?? [], {
35841
36604
  stdio: ["ignore", "pipe", "pipe"],
35842
36605
  env: installerEnv()
@@ -35859,20 +36622,20 @@ Timed out after ${step.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS}ms.`;
35859
36622
  proc.on("close", (code, signal) => {
35860
36623
  settled = true;
35861
36624
  clearTimeout(timeout);
35862
- resolve13({ ok: code === 0, stdout, stderr, exitCode: code, signal });
36625
+ resolve14({ ok: code === 0, stdout, stderr, exitCode: code, signal });
35863
36626
  });
35864
36627
  proc.on("error", (err) => {
35865
36628
  settled = true;
35866
36629
  clearTimeout(timeout);
35867
- resolve13({ ok: false, stdout, stderr: stderr || err.message });
36630
+ resolve14({ ok: false, stdout, stderr: stderr || err.message });
35868
36631
  });
35869
36632
  });
35870
36633
  }
35871
36634
  function installerEnv() {
35872
- const home = homedir27();
36635
+ const home = homedir29();
35873
36636
  const extraPaths = [
35874
- join45(home, ".local", "bin"),
35875
- join45(home, ".local", "share", "mise", "shims")
36637
+ join47(home, ".local", "bin"),
36638
+ join47(home, ".local", "share", "mise", "shims")
35876
36639
  ];
35877
36640
  const currentPath = process.env.PATH ?? "";
35878
36641
  return {
@@ -36037,15 +36800,11 @@ ${lines.join("\n\n")}`,
36037
36800
  // packages/cocoindex/index.ts
36038
36801
  function cocoindexExtension(pi) {
36039
36802
  registerCocoindexCommands(pi);
36803
+ registerCocoindexTools(pi, {
36804
+ getProjectDir: (ctx) => ctx.cwd ?? process.cwd()
36805
+ });
36040
36806
  pi.on("session_start", async (_event, ctx) => {
36041
36807
  const projectDir = ctx.cwd ?? process.cwd();
36042
- const pipelineDir = getPipelineDir(projectDir);
36043
- const initialized = await isPipelineInitialized(pipelineDir);
36044
- registerCocoindexTools(pi, {
36045
- projectDir,
36046
- pipelineDir,
36047
- initialized
36048
- });
36049
36808
  emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
36050
36809
  name: MODULES.COCOINDEX,
36051
36810
  version: "0.1.0",
@@ -36066,7 +36825,7 @@ function cocoindexExtension(pi) {
36066
36825
 
36067
36826
  // packages/image/src/index.ts
36068
36827
  init_core();
36069
- import { dirname as dirname35 } from "node:path";
36828
+ import { dirname as dirname34 } from "node:path";
36070
36829
  import { fileURLToPath as fileURLToPath14 } from "node:url";
36071
36830
 
36072
36831
  // packages/image/src/commands.ts
@@ -37820,7 +38579,7 @@ function currentSessionModel(ctx) {
37820
38579
  }
37821
38580
 
37822
38581
  // packages/image/src/index.ts
37823
- var VERSION11 = getPackageVersion(dirname35(fileURLToPath14(import.meta.url)));
38582
+ var VERSION11 = getPackageVersion(dirname34(fileURLToPath14(import.meta.url)));
37824
38583
  function getInfoRegistry6() {
37825
38584
  return globalThis.__unipi_info_registry;
37826
38585
  }