@pi-unipi/unipi 2.4.1 → 2.5.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 (108) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/package.json +28 -23
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +4 -2
  5. package/packages/btw/package.json +2 -2
  6. package/packages/cocoindex/package.json +2 -2
  7. package/packages/cocoindex/tools.ts +0 -8
  8. package/packages/compactor/README.md +18 -22
  9. package/packages/compactor/package.json +3 -3
  10. package/packages/compactor/src/compaction/build-sections.ts +2 -2
  11. package/packages/compactor/src/compaction/format.ts +1 -1
  12. package/packages/compactor/src/config/presets.ts +19 -30
  13. package/packages/compactor/src/index.ts +39 -56
  14. package/packages/compactor/src/session/resume-inject.ts +27 -0
  15. package/packages/compactor/src/tools/ctx-batch-execute.ts +4 -2
  16. package/packages/compactor/src/tools/ctx-execute-file.ts +4 -2
  17. package/packages/compactor/src/tools/ctx-execute.ts +4 -2
  18. package/packages/compactor/src/tools/register.ts +22 -4
  19. package/packages/compactor/src/tui/settings-overlay.ts +9 -22
  20. package/packages/compactor/src/types.ts +24 -2
  21. package/packages/core/events.ts +13 -13
  22. package/packages/core/package.json +3 -2
  23. package/packages/core/sandbox.ts +7 -6
  24. package/packages/footer/README.md +2 -2
  25. package/packages/footer/package.json +3 -3
  26. package/packages/footer/src/events.ts +0 -10
  27. package/packages/footer/src/segments/core.ts +0 -9
  28. package/packages/image/package.json +2 -2
  29. package/packages/info-screen/core-groups.ts +0 -14
  30. package/packages/info-screen/package.json +2 -2
  31. package/packages/input-shortcuts/package.json +2 -2
  32. package/packages/kanboard/commands.ts +0 -1
  33. package/packages/kanboard/package.json +2 -2
  34. package/packages/mcp/README.md +9 -0
  35. package/packages/mcp/package.json +5 -2
  36. package/packages/mcp/src/bridge/registry.ts +244 -137
  37. package/packages/mcp/src/bridge/translator.ts +77 -9
  38. package/packages/mcp/src/index.ts +43 -67
  39. package/packages/mcp/src/tui/settings-overlay.ts +3 -9
  40. package/packages/memory/README.md +15 -11
  41. package/packages/memory/bridge/mempalace_bridge.py +636 -0
  42. package/packages/memory/mempalace.ts +169 -18
  43. package/packages/memory/package.json +3 -3
  44. package/packages/memory/storage.ts +27 -28
  45. package/packages/memory/tools.ts +0 -1
  46. package/packages/milestone/README.md +11 -3
  47. package/packages/milestone/hooks.ts +112 -44
  48. package/packages/milestone/index.ts +1 -1
  49. package/packages/milestone/package.json +5 -2
  50. package/packages/notify/README.md +2 -0
  51. package/packages/notify/events.ts +23 -13
  52. package/packages/notify/package.json +2 -2
  53. package/packages/notify/tools.ts +17 -8
  54. package/packages/notify/types.ts +5 -1
  55. package/packages/ralph/package.json +3 -3
  56. package/packages/ralph/ralph-loop.ts +10 -0
  57. package/packages/subagents/README.md +3 -1
  58. package/packages/subagents/package.json +10 -6
  59. package/packages/subagents/src/agent-manager.ts +46 -11
  60. package/packages/subagents/src/core-compat.ts +38 -0
  61. package/packages/subagents/src/global.d.ts +7 -0
  62. package/packages/subagents/src/index.ts +6 -3
  63. package/packages/subagents/src/widget.ts +0 -7
  64. package/packages/unipi/bundled.js +1052 -837
  65. package/packages/updater/package.json +4 -3
  66. package/packages/utility/README.md +1 -1
  67. package/packages/utility/package.json +7 -4
  68. package/packages/utility/src/commands.ts +2 -10
  69. package/packages/utility/src/diff/theme.ts +0 -3
  70. package/packages/utility/src/tui/name-badge.ts +0 -9
  71. package/packages/web-api/package.json +2 -2
  72. package/packages/web-api/src/engine/extract.ts +0 -26
  73. package/packages/web-api/src/tools.ts +0 -13
  74. package/packages/workflow/README.md +8 -0
  75. package/packages/workflow/commands.ts +25 -23
  76. package/packages/workflow/index.ts +172 -81
  77. package/packages/workflow/lifecycle.ts +43 -0
  78. package/packages/workflow/package.json +6 -2
  79. package/packages/autocomplete/index.ts +0 -7
  80. package/packages/autocomplete/src/__tests__/command-registry.audit.test.ts +0 -120
  81. package/packages/autocomplete/src/__tests__/provider.sorting.test.ts +0 -423
  82. package/packages/compactor/src/compaction/recall-scope.ts +0 -32
  83. package/packages/compactor/src/compaction/sections.ts +0 -15
  84. package/packages/compactor/src/display/bash-display.ts +0 -28
  85. package/packages/compactor/src/display/diff-presentation.ts +0 -25
  86. package/packages/compactor/src/display/pending-diff-preview.ts +0 -51
  87. package/packages/compactor/src/display/user-message-box.ts +0 -16
  88. package/packages/compactor/src/session/analytics.ts +0 -205
  89. package/packages/image/index.ts +0 -1
  90. package/packages/input-shortcuts/index.ts +0 -5
  91. package/packages/input-shortcuts/src/status.ts +0 -35
  92. package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
  93. package/packages/mcp/index.ts +0 -1
  94. package/packages/memory/search.ts +0 -134
  95. package/packages/milestone/coexist.ts +0 -114
  96. package/packages/notify/src/__tests__/ask-user-prompt-message.test.ts +0 -159
  97. package/packages/notify/src/__tests__/event-bus.test.ts +0 -119
  98. package/packages/notify/src/__tests__/permission-prompt-message.test.ts +0 -172
  99. package/packages/subagents/src/__tests__/badge-generation.test.ts +0 -315
  100. package/packages/subagents/src/__tests__/config.test.ts +0 -240
  101. package/packages/subagents/src/__tests__/esc-propagation.test.ts +0 -162
  102. package/packages/subagents/src/__tests__/file-lock.test.ts +0 -244
  103. package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
  104. package/packages/subagents/src/__tests__/workflow-integration.test.ts +0 -334
  105. package/packages/subagents/src/prompts.ts +0 -39
  106. package/packages/utility/index.ts +0 -1
  107. package/packages/web-api/src/tui/progress.ts +0 -168
  108. package/packages/web-api/src/tui/result.ts +0 -173
@@ -327,7 +327,7 @@ var init_events = __esm({
327
327
  UNIPI_EVENTS = {
328
328
  /** Module loaded and ready */
329
329
  MODULE_READY: "unipi:module:ready",
330
- /** Module unloading */
330
+ /** @deprecated No built-in emitter; retained for external compatibility. */
331
331
  MODULE_GONE: "unipi:module:gone",
332
332
  /** Workflow command started */
333
333
  WORKFLOW_START: "unipi:workflow:start",
@@ -339,9 +339,9 @@ var init_events = __esm({
339
339
  RALPH_LOOP_END: "unipi:ralph:loop:end",
340
340
  /** Ralph loop iteration completed */
341
341
  RALPH_ITERATION_DONE: "unipi:ralph:iteration:done",
342
- /** Request module status (for info-screen) */
342
+ /** @deprecated No built-in responders; use the info registry and /unipi:info. */
343
343
  MODULE_STATUS_REQUEST: "unipi:module:status:request",
344
- /** Module status response */
344
+ /** @deprecated No built-in aggregator; use the info registry and /unipi:info. */
345
345
  MODULE_STATUS_RESPONSE: "unipi:module:status:response",
346
346
  /** Info screen group registered */
347
347
  INFO_GROUP_REGISTERED: "unipi:info:group:registered",
@@ -351,7 +351,7 @@ var init_events = __esm({
351
351
  MEMORY_STORED: "unipi:memory:stored",
352
352
  /** Memory deleted */
353
353
  MEMORY_DELETED: "unipi:memory:deleted",
354
- /** Memory search performed */
354
+ /** @deprecated No built-in emitter; retained for external compatibility. */
355
355
  MEMORY_SEARCHED: "unipi:memory:searched",
356
356
  /** Memory consolidation completed */
357
357
  MEMORY_CONSOLIDATED: "unipi:memory:consolidated",
@@ -369,9 +369,9 @@ var init_events = __esm({
369
369
  MCP_CATALOG_SYNCED: "unipi:mcp:catalog:synced",
370
370
  /** Compactor: compaction completed */
371
371
  COMPACTOR_COMPACTED: "unipi:compactor:compacted",
372
- /** Compactor: stats updated */
372
+ /** @deprecated Footer reads live Pi session data; retained for compatibility. */
373
373
  COMPACTOR_STATS_UPDATED: "unipi:compactor:stats:updated",
374
- /** Utility module cleanup started */
374
+ /** @deprecated No built-in emitter; retained for external compatibility. */
375
375
  UTILITY_CLEANUP_START: "unipi:utility:cleanup:start",
376
376
  /** Utility module cleanup completed */
377
377
  UTILITY_CLEANUP_DONE: "unipi:utility:cleanup:done",
@@ -379,9 +379,9 @@ var init_events = __esm({
379
379
  UTILITY_DIAGNOSTICS_START: "unipi:utility:diagnostics:start",
380
380
  /** Utility diagnostics completed */
381
381
  UTILITY_DIAGNOSTICS_DONE: "unipi:utility:diagnostics:done",
382
- /** Utility cache invalidated */
382
+ /** @deprecated No built-in emitter; retained for external compatibility. */
383
383
  UTILITY_CACHE_INVALIDATED: "unipi:utility:cache:invalidated",
384
- /** Utility lifecycle state changed */
384
+ /** @deprecated No built-in emitter; retained for external compatibility. */
385
385
  UTILITY_LIFECYCLE_STATE: "unipi:utility:lifecycle:state",
386
386
  /** Notification sent */
387
387
  NOTIFICATION_SENT: "unipi:notify:sent",
@@ -397,11 +397,11 @@ var init_events = __esm({
397
397
  UPDATE_APPLIED: "unipi:update:applied",
398
398
  /** Update error */
399
399
  UPDATE_ERROR: "unipi:update:error",
400
- /** CocoIndex: update started */
400
+ /** @deprecated Reserved external contract; no built-in emitter. */
401
401
  COCOINDEX_UPDATE_STARTED: "unipi:cocoindex:update:started",
402
- /** CocoIndex: update completed */
402
+ /** @deprecated Reserved external contract; no built-in emitter. */
403
403
  COCOINDEX_UPDATE_COMPLETED: "unipi:cocoindex:update:completed",
404
- /** CocoIndex: search performed */
404
+ /** @deprecated Reserved external contract; no built-in emitter. */
405
405
  COCOINDEX_SEARCH_PERFORMED: "unipi:cocoindex:search:performed"
406
406
  };
407
407
  }
@@ -868,12 +868,40 @@ var init_core = __esm({
868
868
 
869
869
  // packages/memory/mempalace.ts
870
870
  import { spawnSync, spawn } from "node:child_process";
871
+ import { createHash } from "node:crypto";
872
+ import { createRequire } from "node:module";
871
873
  import * as fs4 from "node:fs";
872
874
  import * as path4 from "node:path";
873
875
  import * as os from "node:os";
874
876
  import { fileURLToPath as fileURLToPath3 } from "node:url";
875
- function dirname5(p) {
876
- return path4.dirname(p);
877
+ function isReadableFile(candidate) {
878
+ try {
879
+ return fs4.statSync(candidate).isFile() && fs4.accessSync(candidate, fs4.constants.R_OK) === void 0;
880
+ } catch {
881
+ return false;
882
+ }
883
+ }
884
+ function resolveMempalaceBridgePath(moduleUrl = import.meta.url) {
885
+ const moduleDir = path4.dirname(fileURLToPath3(moduleUrl));
886
+ const candidates = [];
887
+ if (process.env.UNIPI_MEMPALACE_BRIDGE) {
888
+ candidates.push(path4.resolve(process.env.UNIPI_MEMPALACE_BRIDGE));
889
+ }
890
+ candidates.push(
891
+ path4.join(moduleDir, "bridge", "mempalace_bridge.py"),
892
+ path4.join(moduleDir, "..", "memory", "bridge", "mempalace_bridge.py")
893
+ );
894
+ try {
895
+ const require2 = createRequire(moduleUrl);
896
+ const memoryPackage = require2.resolve("@pi-unipi/memory/package.json");
897
+ candidates.push(path4.join(path4.dirname(memoryPackage), "bridge", "mempalace_bridge.py"));
898
+ } catch {
899
+ }
900
+ return candidates.find(isReadableFile) ?? null;
901
+ }
902
+ function getBridgePath() {
903
+ if (cachedBridgePath === void 0) cachedBridgePath = resolveMempalaceBridgePath();
904
+ return cachedBridgePath;
877
905
  }
878
906
  function which(bin) {
879
907
  try {
@@ -973,17 +1001,71 @@ function invalidatePingVerified() {
973
1001
  } catch {
974
1002
  }
975
1003
  }
976
- function isMigrated() {
977
- return fs4.existsSync(MIGRATED_FLAG);
1004
+ function getMemorySourceFingerprint(sourceDir = path4.join(os.homedir(), ".unipi", "memory")) {
1005
+ const hash = createHash("sha256");
1006
+ if (!fs4.existsSync(sourceDir)) return hash.update("missing").digest("hex");
1007
+ const visit = (dir) => {
1008
+ let entries;
1009
+ try {
1010
+ entries = fs4.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1011
+ } catch {
1012
+ hash.update(`unreadable:${dir}`);
1013
+ return;
1014
+ }
1015
+ for (const entry of entries) {
1016
+ if (entry.name.startsWith(".")) continue;
1017
+ const full = path4.join(dir, entry.name);
1018
+ if (entry.isDirectory()) {
1019
+ visit(full);
1020
+ continue;
1021
+ }
1022
+ if (!entry.isFile() || entry.name !== "memory.db" && !entry.name.endsWith(".md")) continue;
1023
+ try {
1024
+ const stat = fs4.statSync(full);
1025
+ hash.update(`${path4.relative(sourceDir, full)}\0${stat.size}\0${stat.mtimeMs}
1026
+ `);
1027
+ } catch {
1028
+ hash.update(`unreadable:${path4.relative(sourceDir, full)}
1029
+ `);
1030
+ }
1031
+ }
1032
+ };
1033
+ visit(sourceDir);
1034
+ return hash.digest("hex");
978
1035
  }
979
- function markMigrated() {
1036
+ function readMigrationState(flagPath = MIGRATED_FLAG) {
980
1037
  try {
981
- fs4.mkdirSync(path4.dirname(MIGRATED_FLAG), { recursive: true });
982
- fs4.writeFileSync(MIGRATED_FLAG, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
1038
+ const parsed = JSON.parse(fs4.readFileSync(flagPath, "utf-8"));
1039
+ 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;
1040
+ return parsed;
983
1041
  } catch {
1042
+ return null;
984
1043
  }
985
1044
  }
986
- function runBridge(install, palace, cmd, args = {}) {
1045
+ function isMigrated(sourceFingerprint = getMemorySourceFingerprint(), flagPath = MIGRATED_FLAG) {
1046
+ return readMigrationState(flagPath)?.sourceFingerprint === sourceFingerprint;
1047
+ }
1048
+ function markMigrated(sourceFingerprint, result, flagPath = MIGRATED_FLAG) {
1049
+ if (result.failed !== 0 || result.verified !== result.discovered) return false;
1050
+ try {
1051
+ fs4.mkdirSync(path4.dirname(flagPath), { recursive: true });
1052
+ const state = {
1053
+ version: MIGRATION_STATE_VERSION,
1054
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
1055
+ sourceFingerprint,
1056
+ result
1057
+ };
1058
+ const temp = `${flagPath}.${process.pid}.tmp`;
1059
+ fs4.writeFileSync(temp, JSON.stringify(state, null, 2), "utf-8");
1060
+ fs4.renameSync(temp, flagPath);
1061
+ return true;
1062
+ } catch {
1063
+ return false;
1064
+ }
1065
+ }
1066
+ function runBridge(install, palace, cmd, args = {}, timeoutMs = 6e4) {
1067
+ const bridgePath = getBridgePath();
1068
+ if (!bridgePath) return null;
987
1069
  let argsJson;
988
1070
  try {
989
1071
  argsJson = JSON.stringify(args);
@@ -992,9 +1074,9 @@ function runBridge(install, palace, cmd, args = {}) {
992
1074
  }
993
1075
  let res;
994
1076
  try {
995
- res = spawnSync(install.python, [BRIDGE_PATH, palace, cmd, argsJson], {
1077
+ res = spawnSync(install.python, [bridgePath, palace, cmd, argsJson], {
996
1078
  encoding: "utf-8",
997
- timeout: 6e4,
1079
+ timeout: timeoutMs,
998
1080
  maxBuffer: 64 * 1024 * 1024
999
1081
  });
1000
1082
  } catch {
@@ -1013,22 +1095,27 @@ function runBridge(install, palace, cmd, args = {}) {
1013
1095
  return null;
1014
1096
  }
1015
1097
  }
1016
- function runBridgeAsync(install, palace, cmd, args = {}) {
1017
- return new Promise((resolve13) => {
1098
+ function runBridgeAsync(install, palace, cmd, args = {}, timeoutMs = 6e4) {
1099
+ return new Promise((resolve14) => {
1100
+ const bridgePath = getBridgePath();
1101
+ if (!bridgePath) {
1102
+ resolve14(null);
1103
+ return;
1104
+ }
1018
1105
  let argsJson;
1019
1106
  try {
1020
1107
  argsJson = JSON.stringify(args);
1021
1108
  } catch {
1022
- resolve13(null);
1109
+ resolve14(null);
1023
1110
  return;
1024
1111
  }
1025
1112
  let child;
1026
1113
  try {
1027
- child = spawn(install.python, [BRIDGE_PATH, palace, cmd, argsJson], {
1114
+ child = spawn(install.python, [bridgePath, palace, cmd, argsJson], {
1028
1115
  stdio: ["ignore", "pipe", "ignore"]
1029
1116
  });
1030
1117
  } catch {
1031
- resolve13(null);
1118
+ resolve14(null);
1032
1119
  return;
1033
1120
  }
1034
1121
  let out = "";
@@ -1037,7 +1124,7 @@ function runBridgeAsync(install, palace, cmd, args = {}) {
1037
1124
  if (settled) return;
1038
1125
  settled = true;
1039
1126
  clearTimeout(timer);
1040
- resolve13(value);
1127
+ resolve14(value);
1041
1128
  };
1042
1129
  const timer = setTimeout(() => {
1043
1130
  try {
@@ -1045,7 +1132,7 @@ function runBridgeAsync(install, palace, cmd, args = {}) {
1045
1132
  } catch {
1046
1133
  }
1047
1134
  finish(null);
1048
- }, 6e4);
1135
+ }, timeoutMs);
1049
1136
  timer.unref?.();
1050
1137
  child.stdout?.setEncoding("utf-8");
1051
1138
  child.stdout?.on("data", (chunk) => {
@@ -1065,7 +1152,7 @@ function runBridgeAsync(install, palace, cmd, args = {}) {
1065
1152
  });
1066
1153
  });
1067
1154
  }
1068
- var DEFAULT_PALACE, INSTALL_FLAG, MIGRATED_FLAG, PING_VERIFIED_FLAG, PING_VERIFIED_TTL_MS, BRIDGE_PATH;
1155
+ var DEFAULT_PALACE, INSTALL_FLAG, MIGRATED_FLAG, PING_VERIFIED_FLAG, PING_VERIFIED_TTL_MS, MIGRATION_STATE_VERSION, cachedBridgePath;
1069
1156
  var init_mempalace = __esm({
1070
1157
  "packages/memory/mempalace.ts"() {
1071
1158
  "use strict";
@@ -1074,7 +1161,7 @@ var init_mempalace = __esm({
1074
1161
  MIGRATED_FLAG = path4.join(os.homedir(), ".unipi", "memory", ".mempalace-migrated");
1075
1162
  PING_VERIFIED_FLAG = path4.join(os.homedir(), ".unipi", "memory", ".mempalace-ping-verified");
1076
1163
  PING_VERIFIED_TTL_MS = 24 * 60 * 60 * 1e3;
1077
- BRIDGE_PATH = path4.join(dirname5(fileURLToPath3(import.meta.url)), "bridge", "mempalace_bridge.py");
1164
+ MIGRATION_STATE_VERSION = 2;
1078
1165
  }
1079
1166
  });
1080
1167
 
@@ -1174,7 +1261,7 @@ function parseMemoryContent(content) {
1174
1261
  const [, frontmatterStr, body] = match;
1175
1262
  const frontmatter = yaml.load(frontmatterStr);
1176
1263
  return {
1177
- id: "",
1264
+ id: frontmatter.id || "",
1178
1265
  title: frontmatter.title,
1179
1266
  content: body.trim(),
1180
1267
  tags: frontmatter.tags || [],
@@ -1186,6 +1273,7 @@ function parseMemoryContent(content) {
1186
1273
  }
1187
1274
  function writeMemoryFile(filePath, record) {
1188
1275
  const frontmatter = {
1276
+ id: record.id,
1189
1277
  title: record.title,
1190
1278
  tags: record.tags,
1191
1279
  project: record.project,
@@ -1405,12 +1493,15 @@ var init_storage = __esm({
1405
1493
  }
1406
1494
  this.mempalaceInstall = install;
1407
1495
  this.backend = "mempalace";
1408
- if (!isMigrated()) {
1496
+ const sourceFingerprint = getMemorySourceFingerprint(getMemoryBaseDir());
1497
+ if (!isMigrated(sourceFingerprint)) {
1409
1498
  try {
1410
- runBridge(install, this.palacePath, "migrate", {
1499
+ const result = runBridge(install, this.palacePath, "migrate", {
1411
1500
  source_dir: getMemoryBaseDir()
1412
- });
1413
- markMigrated();
1501
+ }, 15 * 6e4);
1502
+ if (result && result.failed === 0 && result.verified === result.discovered) {
1503
+ markMigrated(sourceFingerprint, result);
1504
+ }
1414
1505
  } catch {
1415
1506
  }
1416
1507
  }
@@ -1457,21 +1548,6 @@ var init_storage = __esm({
1457
1548
  this.db = null;
1458
1549
  }
1459
1550
  }
1460
- /**
1461
- * Remove corrupted database files (db, wal, shm).
1462
- */
1463
- removeCorruptedDb() {
1464
- const dbPath = path5.join(this.scopeDir, MEMORY_DB_NAME);
1465
- const files = [dbPath, `${dbPath}-wal`, `${dbPath}-shm`];
1466
- for (const file of files) {
1467
- try {
1468
- if (fs5.existsSync(file)) {
1469
- fs5.unlinkSync(file);
1470
- }
1471
- } catch {
1472
- }
1473
- }
1474
- }
1475
1551
  /**
1476
1552
  * Check if database is healthy.
1477
1553
  */
@@ -1612,7 +1688,7 @@ ${record.content}
1612
1688
  const filePath = path5.join(this.scopeDir, file);
1613
1689
  const record = parseMemoryFile(filePath);
1614
1690
  if (!record) continue;
1615
- const id = record.title.toLowerCase().replace(/[^a-z0-9]+/g, "_");
1691
+ const id = record.id || record.title.toLowerCase().replace(/[^a-z0-9]+/g, "_");
1616
1692
  if (existingIds.has(id)) continue;
1617
1693
  try {
1618
1694
  record.id = id;
@@ -2254,8 +2330,8 @@ var init_embedding = __esm({
2254
2330
  });
2255
2331
 
2256
2332
  // packages/info-screen/usage-parser.ts
2257
- 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";
2258
- import { join as join9, dirname as dirname10 } from "node:path";
2333
+ import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5, existsSync as existsSync9, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8, renameSync as renameSync3, openSync, readSync, closeSync } from "node:fs";
2334
+ import { join as join9, dirname as dirname9 } from "node:path";
2259
2335
  import { homedir as homedir5 } from "node:os";
2260
2336
  function getCachePath() {
2261
2337
  const base = process.env.UNIPI_DIR || join9(homedir5(), ".unipi");
@@ -2277,10 +2353,10 @@ function readCache() {
2277
2353
  function writeCache(cache) {
2278
2354
  try {
2279
2355
  const path28 = getCachePath();
2280
- mkdirSync7(dirname10(path28), { recursive: true });
2356
+ mkdirSync7(dirname9(path28), { recursive: true });
2281
2357
  const tmp = `${path28}.${process.pid}.tmp`;
2282
2358
  writeFileSync8(tmp, JSON.stringify(cache), "utf-8");
2283
- renameSync2(tmp, path28);
2359
+ renameSync3(tmp, path28);
2284
2360
  } catch {
2285
2361
  }
2286
2362
  }
@@ -2376,7 +2452,7 @@ function extractRecords(filePath, modelIndex, models) {
2376
2452
  }
2377
2453
  function collectSessionFiles(dir, files) {
2378
2454
  try {
2379
- const entries = readdirSync4(dir, { withFileTypes: true });
2455
+ const entries = readdirSync5(dir, { withFileTypes: true });
2380
2456
  for (const entry of entries) {
2381
2457
  const entryPath = join9(dir, entry.name);
2382
2458
  if (entry.isDirectory()) {
@@ -2390,7 +2466,7 @@ function collectSessionFiles(dir, files) {
2390
2466
  }
2391
2467
  async function parseUsageStatsAsync() {
2392
2468
  const yielder = async () => {
2393
- await new Promise((resolve13) => setImmediate(resolve13));
2469
+ await new Promise((resolve14) => setImmediate(resolve14));
2394
2470
  };
2395
2471
  const { stats, pending } = collectStats(yielder);
2396
2472
  if (pending) await pending;
@@ -2426,7 +2502,7 @@ function collectStats(yielder) {
2426
2502
  let mtimeMs = 0;
2427
2503
  let size = 0;
2428
2504
  try {
2429
- const st = statSync4(filePath);
2505
+ const st = statSync5(filePath);
2430
2506
  mtimeMs = st.mtimeMs;
2431
2507
  size = st.size;
2432
2508
  } catch {
@@ -2607,19 +2683,19 @@ __export(env_exports, {
2607
2683
  formatEnvironmentInfo: () => formatEnvironmentInfo,
2608
2684
  getEnvironmentInfo: () => getEnvironmentInfo
2609
2685
  });
2610
- import { existsSync as existsSync18, readdirSync as readdirSync10 } from "node:fs";
2611
- import { join as join18, resolve as resolve5 } from "node:path";
2686
+ import { existsSync as existsSync18, readdirSync as readdirSync11 } from "node:fs";
2687
+ import { join as join18, resolve as resolve6 } from "node:path";
2612
2688
  import { homedir as homedir14 } from "node:os";
2613
2689
  function getEnvironmentInfo() {
2614
2690
  const unipiModules = [];
2615
2691
  const configPaths = [];
2616
2692
  const extensionPaths = [];
2617
2693
  try {
2618
- const nodeModules = resolve5(process.cwd(), "node_modules");
2694
+ const nodeModules = resolve6(process.cwd(), "node_modules");
2619
2695
  if (existsSync18(nodeModules)) {
2620
2696
  const scopePath = join18(nodeModules, "@pi-unipi");
2621
2697
  if (existsSync18(scopePath)) {
2622
- for (const entry of readdirSync10(scopePath)) {
2698
+ for (const entry of readdirSync11(scopePath)) {
2623
2699
  if (entry.startsWith(".")) continue;
2624
2700
  const pkgPath = join18(scopePath, entry, "package.json");
2625
2701
  if (existsSync18(pkgPath)) {
@@ -2631,7 +2707,7 @@ function getEnvironmentInfo() {
2631
2707
  } catch {
2632
2708
  }
2633
2709
  const globalConfig = join18(homedir14(), ".unipi", "config");
2634
- const projectConfig = resolve5(process.cwd(), ".unipi", "config");
2710
+ const projectConfig = resolve6(process.cwd(), ".unipi", "config");
2635
2711
  if (existsSync18(globalConfig)) {
2636
2712
  configPaths.push(globalConfig);
2637
2713
  }
@@ -3878,7 +3954,7 @@ var init_schema = __esm({
3878
3954
  // packages/compactor/src/config/manager.ts
3879
3955
  import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync29, writeFileSync as writeFileSync23 } from "node:fs";
3880
3956
  import { homedir as homedir20 } from "node:os";
3881
- import { dirname as dirname25, join as join32 } from "node:path";
3957
+ import { dirname as dirname24, join as join32 } from "node:path";
3882
3958
  function projectConfigPath(cwd) {
3883
3959
  return join32(cwd, ".unipi", "config", "compactor.json");
3884
3960
  }
@@ -3915,7 +3991,7 @@ function loadConfig4(cwd) {
3915
3991
  function saveConfig3(config, opts) {
3916
3992
  try {
3917
3993
  const targetPath = opts?.perProject && opts?.cwd ? projectConfigPath(opts.cwd) : COMPACTOR_CONFIG_PATH;
3918
- const dir = dirname25(targetPath);
3994
+ const dir = dirname24(targetPath);
3919
3995
  if (!existsSync31(dir)) mkdirSync19(dir, { recursive: true });
3920
3996
  writeFileSync23(targetPath, `${JSON.stringify(config, null, 2)}
3921
3997
  `);
@@ -3950,7 +4026,7 @@ function migrateConfig(partial) {
3950
4026
  }
3951
4027
  function scaffoldConfig() {
3952
4028
  try {
3953
- const dir = dirname25(COMPACTOR_CONFIG_PATH);
4029
+ const dir = dirname24(COMPACTOR_CONFIG_PATH);
3954
4030
  if (!existsSync31(dir)) mkdirSync19(dir, { recursive: true });
3955
4031
  if (!existsSync31(COMPACTOR_CONFIG_PATH)) {
3956
4032
  writeFileSync23(COMPACTOR_CONFIG_PATH, `${JSON.stringify(DEFAULT_COMPACTOR_CONFIG, null, 2)}
@@ -5466,57 +5542,10 @@ var init_snapshot = __esm({
5466
5542
  }
5467
5543
  });
5468
5544
 
5469
- // packages/compactor/src/session/auto-inject.ts
5470
- var auto_inject_exports = {};
5471
- __export(auto_inject_exports, {
5472
- buildAutoInjection: () => buildAutoInjection
5473
- });
5474
- function estimateTokens(text) {
5475
- return Math.ceil(text.length / 4);
5476
- }
5477
- function buildAutoInjection(events) {
5478
- const parts = [];
5479
- let tokenBudget = MAX_TOKENS2;
5480
- const roleEvents = events.filter((e) => e.category === "rule");
5481
- if (roleEvents.length > 0) {
5482
- const directive = roleEvents[roleEvents.length - 1].data;
5483
- const directiveText = `[Role Directive]
5484
- ${directive}`;
5485
- const tokens = estimateTokens(directiveText);
5486
- if (tokens <= tokenBudget) {
5487
- parts.push(directiveText);
5488
- tokenBudget -= tokens;
5489
- }
5490
- }
5491
- if (tokenBudget > 80) {
5492
- const intentEvents = events.filter((e) => e.category === "intent");
5493
- if (intentEvents.length > 0) {
5494
- const mode = intentEvents[intentEvents.length - 1].data;
5495
- const modeText = `[Session Mode]
5496
- ${mode}`;
5497
- const tokens = estimateTokens(modeText);
5498
- if (tokens <= tokenBudget) {
5499
- parts.push(modeText);
5500
- tokenBudget -= tokens;
5501
- }
5502
- }
5503
- }
5504
- if (parts.length === 0) return null;
5505
- const text = parts.join("\n\n");
5506
- return { text, tokens: estimateTokens(text) };
5507
- }
5508
- var MAX_TOKENS2;
5509
- var init_auto_inject = __esm({
5510
- "packages/compactor/src/session/auto-inject.ts"() {
5511
- "use strict";
5512
- MAX_TOKENS2 = 150;
5513
- }
5514
- });
5515
-
5516
5545
  // packages/compactor/src/config/presets.ts
5517
- import { createHash as createHash3 } from "node:crypto";
5546
+ import { createHash as createHash4 } from "node:crypto";
5518
5547
  function presetHash(config) {
5519
- return createHash3("sha256").update(JSON.stringify(config)).digest("hex");
5548
+ return createHash4("sha256").update(JSON.stringify(config)).digest("hex");
5520
5549
  }
5521
5550
  function configsEqual(a, b) {
5522
5551
  const aHash = presetHash(a);
@@ -5546,7 +5575,7 @@ function parsePreset(raw) {
5546
5575
  }
5547
5576
  return void 0;
5548
5577
  }
5549
- var pipeline, pipelineAllOn, preset, PRESET_CONFIGS, presetHashes, OLD_TO_NEW;
5578
+ var pipeline, pipelineWithAutoInjection, preset, PRESET_CONFIGS, presetHashes, OLD_TO_NEW;
5550
5579
  var init_presets = __esm({
5551
5580
  "packages/compactor/src/config/presets.ts"() {
5552
5581
  "use strict";
@@ -5556,13 +5585,8 @@ var init_presets = __esm({
5556
5585
  ...overrides,
5557
5586
  customNoisePatterns: [...overrides.customNoisePatterns ?? DEFAULT_COMPACTOR_CONFIG.pipeline.customNoisePatterns]
5558
5587
  });
5559
- pipelineAllOn = () => pipeline({
5560
- ttlCache: true,
5561
- autoInjection: true,
5562
- proximityReranking: true,
5563
- timelineSort: true,
5564
- progressiveThrottling: true,
5565
- mmapPragma: true
5588
+ pipelineWithAutoInjection = () => pipeline({
5589
+ autoInjection: true
5566
5590
  });
5567
5591
  preset = (overrides) => ({
5568
5592
  ...structuredClone(DEFAULT_COMPACTOR_CONFIG),
@@ -5574,23 +5598,23 @@ var init_presets = __esm({
5574
5598
  userPreferences: { ...DEFAULT_COMPACTOR_CONFIG.userPreferences, ...overrides.userPreferences ?? {} },
5575
5599
  briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, ...overrides.briefTranscript ?? {} },
5576
5600
  sessionContinuity: { ...DEFAULT_COMPACTOR_CONFIG.sessionContinuity, ...overrides.sessionContinuity ?? {} },
5577
- fts5Index: { ...DEFAULT_COMPACTOR_CONFIG.fts5Index, ...overrides.fts5Index ?? {} },
5601
+ // Legacy compatibility data only; presets never advertise or alter it.
5602
+ fts5Index: { ...DEFAULT_COMPACTOR_CONFIG.fts5Index },
5578
5603
  sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, ...overrides.sandboxExecution ?? {} },
5579
- toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, ...overrides.toolDisplay ?? {} },
5604
+ // Legacy compatibility data only; presets never advertise or alter it.
5605
+ toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay },
5580
5606
  pipeline: pipeline(overrides.pipeline),
5581
5607
  autoCompaction: { ...DEFAULT_COMPACTOR_CONFIG.autoCompaction, ...overrides.autoCompaction ?? {} }
5582
5608
  });
5583
5609
  PRESET_CONFIGS = {
5584
5610
  // New preset names
5585
5611
  precise: preset({
5586
- pipeline: pipeline({ ttlCache: true, mmapPragma: true }),
5587
- sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, mode: "safe-only" },
5588
- toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, mode: "opencode" }
5612
+ pipeline: pipeline(),
5613
+ sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, mode: "all" }
5589
5614
  }),
5590
5615
  thorough: preset({
5591
- pipeline: pipelineAllOn(),
5592
- briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, mode: "full" },
5593
- toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, mode: "verbose" }
5616
+ pipeline: pipelineWithAutoInjection(),
5617
+ briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, mode: "full" }
5594
5618
  }),
5595
5619
  lean: preset({
5596
5620
  pipeline: pipeline(),
@@ -5601,26 +5625,20 @@ var init_presets = __esm({
5601
5625
  userPreferences: { ...DEFAULT_COMPACTOR_CONFIG.userPreferences, enabled: false, mode: "off" },
5602
5626
  briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, enabled: true, mode: "minimal" },
5603
5627
  sessionContinuity: { ...DEFAULT_COMPACTOR_CONFIG.sessionContinuity, enabled: false, mode: "off" },
5604
- fts5Index: { ...DEFAULT_COMPACTOR_CONFIG.fts5Index, enabled: false, mode: "off" },
5605
- sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, enabled: false, mode: "off" },
5606
- toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, enabled: true, mode: "opencode" }
5628
+ sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, enabled: false, mode: "off" }
5607
5629
  }),
5608
5630
  balanced: preset({
5609
- pipeline: pipelineAllOn(),
5610
- briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, mode: "compact" },
5611
- toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, mode: "balanced" },
5612
- fts5Index: { ...DEFAULT_COMPACTOR_CONFIG.fts5Index, mode: "auto" }
5631
+ pipeline: pipelineWithAutoInjection(),
5632
+ briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, mode: "compact" }
5613
5633
  }),
5614
5634
  // Backward-compat aliases — map old names to new
5615
5635
  opencode: preset({
5616
- pipeline: pipeline({ ttlCache: true, mmapPragma: true }),
5617
- sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, mode: "safe-only" },
5618
- toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, mode: "opencode" }
5636
+ pipeline: pipeline(),
5637
+ sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, mode: "all" }
5619
5638
  }),
5620
5639
  verbose: preset({
5621
- pipeline: pipelineAllOn(),
5622
- briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, mode: "full" },
5623
- toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, mode: "verbose" }
5640
+ pipeline: pipelineWithAutoInjection(),
5641
+ briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, mode: "full" }
5624
5642
  }),
5625
5643
  minimal: preset({
5626
5644
  pipeline: pipeline(),
@@ -5631,9 +5649,7 @@ var init_presets = __esm({
5631
5649
  userPreferences: { ...DEFAULT_COMPACTOR_CONFIG.userPreferences, enabled: false, mode: "off" },
5632
5650
  briefTranscript: { ...DEFAULT_COMPACTOR_CONFIG.briefTranscript, enabled: true, mode: "minimal" },
5633
5651
  sessionContinuity: { ...DEFAULT_COMPACTOR_CONFIG.sessionContinuity, enabled: false, mode: "off" },
5634
- fts5Index: { ...DEFAULT_COMPACTOR_CONFIG.fts5Index, enabled: false, mode: "off" },
5635
- sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, enabled: false, mode: "off" },
5636
- toolDisplay: { ...DEFAULT_COMPACTOR_CONFIG.toolDisplay, enabled: true, mode: "opencode" }
5652
+ sandboxExecution: { ...DEFAULT_COMPACTOR_CONFIG.sandboxExecution, enabled: false, mode: "off" }
5637
5653
  }),
5638
5654
  custom: structuredClone(DEFAULT_COMPACTOR_CONFIG)
5639
5655
  };
@@ -5791,7 +5807,7 @@ var init_settings_overlay = __esm({
5791
5807
  key: "sessionContinuity",
5792
5808
  label: "Session Continuity",
5793
5809
  description: "XML resume snapshot for compaction survival",
5794
- modes: ["full", "essential-only", "off"],
5810
+ modes: ["full", "off"],
5795
5811
  getEnabled: (c) => c.sessionContinuity.enabled,
5796
5812
  setEnabled: (c, v) => c.sessionContinuity.enabled = v,
5797
5813
  getMode: (c) => c.sessionContinuity.mode,
@@ -5801,30 +5817,17 @@ var init_settings_overlay = __esm({
5801
5817
  key: "sandboxExecution",
5802
5818
  label: "Sandbox Execution",
5803
5819
  description: "Polyglot code execution",
5804
- modes: ["all", "safe-only", "off"],
5820
+ modes: ["all", "off"],
5805
5821
  getEnabled: (c) => c.sandboxExecution.enabled,
5806
5822
  setEnabled: (c, v) => c.sandboxExecution.enabled = v,
5807
5823
  getMode: (c) => c.sandboxExecution.mode,
5808
5824
  setMode: (c, v) => c.sandboxExecution.mode = v
5809
- },
5810
- {
5811
- key: "toolDisplay",
5812
- label: "Tool Display",
5813
- description: "Override tool output rendering",
5814
- modes: ["opencode", "balanced", "verbose", "custom"],
5815
- getEnabled: (c) => c.toolDisplay.enabled,
5816
- setEnabled: (c, v) => c.toolDisplay.enabled = v,
5817
- getMode: (c) => c.toolDisplay.mode,
5818
- setMode: (c, v) => c.toolDisplay.mode = v
5819
5825
  }
5820
5826
  ];
5821
5827
  PIPELINE_ITEMS = [
5822
- { key: "ttlCache", label: "TTL Cache", description: "Cache with time-based expiry", group: "On Compaction", getValue: (c) => c.pipeline.ttlCache, setValue: (c, v) => c.pipeline.ttlCache = v },
5823
- { key: "autoInjection", label: "Auto Injection", description: "Inject behavioral state after compaction", group: "On Compaction", getValue: (c) => c.pipeline.autoInjection, setValue: (c, v) => c.pipeline.autoInjection = v },
5824
- { key: "mmapPragma", label: "MMap Pragma", description: "Use mmap for SQLite I/O", group: "On Compaction", getValue: (c) => c.pipeline.mmapPragma, setValue: (c, v) => c.pipeline.mmapPragma = v },
5825
- { key: "proximityReranking", label: "Proximity Reranking", description: "Rerank search results by proximity", group: "On Search", getValue: (c) => c.pipeline.proximityReranking, setValue: (c, v) => c.pipeline.proximityReranking = v },
5826
- { key: "timelineSort", label: "Timeline Sort", description: "Sort session events chronologically", group: "On Search", getValue: (c) => c.pipeline.timelineSort, setValue: (c, v) => c.pipeline.timelineSort = v },
5827
- { key: "progressiveThrottling", label: "Progressive Throttling", description: "Slow down processing for large projects", group: "On Compaction", getValue: (c) => c.pipeline.progressiveThrottling, setValue: (c, v) => c.pipeline.progressiveThrottling = v }
5828
+ // Only expose implemented behavior. Reserved compatibility fields remain in
5829
+ // the persisted schema so existing config files continue to load.
5830
+ { key: "autoInjection", label: "Auto Injection", description: "Inject behavioral state after compaction", group: "On Compaction", getValue: (c) => c.pipeline.autoInjection, setValue: (c, v) => c.pipeline.autoInjection = v }
5828
5831
  ];
5829
5832
  PRESETS2 = ["precise", "balanced", "thorough", "lean"];
5830
5833
  THRESHOLD_VALUES = ["60%", "70%", "75%", "80%", "85%", "90%", "95%"];
@@ -5832,20 +5835,20 @@ var init_settings_overlay = __esm({
5832
5835
  REPEAT_GROWTH_VALUES = ["0", "1k", "4k", "8k", "16k", "32k"];
5833
5836
  PRESET_DESCRIPTIONS = {
5834
5837
  precise: {
5835
- summary: "Code-heavy, minimal waste \u2014 compaction: full, sandbox: safe-only",
5836
- detail: "Max token savings. Compaction: full. Display: minimal.\nSandbox: safe-only. Pipeline: ttlCache+mmap on."
5838
+ summary: "Code-heavy, minimal waste \u2014 compaction: full, sandbox: all",
5839
+ detail: "Max token savings. Compaction: full.\nSandbox: all. Auto injection: off."
5837
5840
  },
5838
5841
  balanced: {
5839
5842
  summary: "Daily use (default) \u2014 all strategies moderate",
5840
- detail: "Moderate all strategies. Display: balanced.\nSandbox: all. Pipeline: all 6 on."
5843
+ detail: "Moderate all strategies.\nSandbox: all. Auto injection: on."
5841
5844
  },
5842
5845
  thorough: {
5843
5846
  summary: "Debug/audit \u2014 everything on, full transcript",
5844
- detail: "Everything enabled. Display: verbose.\nSandbox: all. Pipeline: all 6 on."
5847
+ detail: "Everything enabled.\nSandbox: all. Auto injection: on."
5845
5848
  },
5846
5849
  lean: {
5847
5850
  summary: "Quick fixes, short sessions \u2014 compaction only",
5848
- detail: "Compaction only. Display: opencode.\nSandbox: off. Pipeline: all 6 off."
5851
+ detail: "Compaction only.\nSandbox: off. Auto injection: off."
5849
5852
  }
5850
5853
  };
5851
5854
  THEME = {
@@ -6397,7 +6400,7 @@ __export(evaluator_exports, {
6397
6400
  splitChainedCommands: () => splitChainedCommands
6398
6401
  });
6399
6402
  import { realpathSync as realpathSync2, existsSync as existsSync37, readFileSync as readFileSync32 } from "node:fs";
6400
- import { resolve as resolve11, join as join36 } from "node:path";
6403
+ import { resolve as resolve12, join as join36 } from "node:path";
6401
6404
  import { homedir as homedir22 } from "node:os";
6402
6405
  function loadProjectPermissions(cwd, basePolicy) {
6403
6406
  const settingsPath = join36(cwd, ".pi", "settings.json");
@@ -6469,7 +6472,7 @@ function splitChainedCommands(command) {
6469
6472
  return commands;
6470
6473
  }
6471
6474
  function evaluateFilePath(filePath, policy, cwd = process.cwd()) {
6472
- const resolved = resolve11(cwd, filePath);
6475
+ const resolved = resolve12(cwd, filePath);
6473
6476
  try {
6474
6477
  const real = realpathSync2(resolved);
6475
6478
  const home = homedir22();
@@ -6571,137 +6574,6 @@ var init_scanner = __esm({
6571
6574
  }
6572
6575
  });
6573
6576
 
6574
- // packages/compactor/src/display/render-utils.ts
6575
- function splitLines(text) {
6576
- return text.split(/\r?\n/);
6577
- }
6578
- function countNonEmptyLines(text) {
6579
- return splitLines(text).filter((l) => l.trim()).length;
6580
- }
6581
- function previewLines(text, lines) {
6582
- const all = splitLines(text);
6583
- if (all.length <= lines) return text;
6584
- return all.slice(0, lines).join("\n") + `
6585
- ...(${all.length - lines} more lines)`;
6586
- }
6587
- function shortenPath(path28, maxLen = 60) {
6588
- if (path28.length <= maxLen) return path28;
6589
- const parts = path28.split("/");
6590
- if (parts.length <= 2) return "..." + path28.slice(-(maxLen - 3));
6591
- return parts[0] + "/.../" + parts.slice(-2).join("/");
6592
- }
6593
- var init_render_utils = __esm({
6594
- "packages/compactor/src/display/render-utils.ts"() {
6595
- "use strict";
6596
- }
6597
- });
6598
-
6599
- // packages/compactor/src/display/tool-overrides.ts
6600
- var tool_overrides_exports = {};
6601
- __export(tool_overrides_exports, {
6602
- applyToolDisplayOverride: () => applyToolDisplayOverride,
6603
- renderBashResult: () => renderBashResult,
6604
- renderReadResult: () => renderReadResult,
6605
- renderSearchResult: () => renderSearchResult
6606
- });
6607
- function renderReadResult(content, filePath, config) {
6608
- switch (config.readOutputMode) {
6609
- case "hidden":
6610
- return `[Read: ${shortenPath(filePath)}]`;
6611
- case "summary":
6612
- return `[Read: ${shortenPath(filePath)} \u2014 ${countNonEmptyLines(content)} lines]`;
6613
- case "preview":
6614
- return previewLines(content, config.previewLines ?? 20);
6615
- default:
6616
- return content;
6617
- }
6618
- }
6619
- function renderSearchResult(results, config) {
6620
- switch (config.searchOutputMode) {
6621
- case "hidden":
6622
- return `[Search results hidden]`;
6623
- case "count":
6624
- return `[Search: ${countNonEmptyLines(results)} matches]`;
6625
- case "preview":
6626
- return previewLines(results, config.previewLines ?? 20);
6627
- default:
6628
- return results;
6629
- }
6630
- }
6631
- function renderBashResult(output, command, config) {
6632
- switch (config.bashOutputMode) {
6633
- case "hidden":
6634
- return `[Bash: ${command.slice(0, 60)}]`;
6635
- case "summary":
6636
- return `[Bash: ${command.slice(0, 60)} \u2014 ${countNonEmptyLines(output)} lines]`;
6637
- case "preview":
6638
- return previewLines(output, config.bashCollapsedLines ?? 5);
6639
- default:
6640
- return output;
6641
- }
6642
- }
6643
- function applyToolDisplayOverride(toolName, event, config) {
6644
- const content = event.content;
6645
- if (!content || !Array.isArray(content) || content.length === 0) return void 0;
6646
- const textContent = content[0]?.text ?? "";
6647
- const previewLinesCount = config.previewLines ?? 20;
6648
- const collapsedLines = config.bashCollapsedLines ?? 5;
6649
- let overridden;
6650
- switch (toolName) {
6651
- case "read": {
6652
- const filePath = String(event.args?.path ?? "");
6653
- const mode = config.readOutputMode ?? "full";
6654
- if (mode === "hidden") {
6655
- overridden = `[Read: ${shortenPath(filePath)}]`;
6656
- } else if (mode === "summary") {
6657
- overridden = `[Read: ${shortenPath(filePath)} \u2014 ${countNonEmptyLines(textContent)} lines]`;
6658
- } else if (mode === "preview") {
6659
- overridden = previewLines(textContent, previewLinesCount);
6660
- }
6661
- break;
6662
- }
6663
- case "grep":
6664
- case "find":
6665
- case "ls": {
6666
- const mode = config.searchOutputMode ?? "full";
6667
- if (mode === "hidden") {
6668
- overridden = `[${toolName} results hidden]`;
6669
- } else if (mode === "count") {
6670
- overridden = `[${toolName}: ${countNonEmptyLines(textContent)} matches]`;
6671
- } else if (mode === "preview") {
6672
- overridden = previewLines(textContent, previewLinesCount);
6673
- }
6674
- break;
6675
- }
6676
- case "bash": {
6677
- const command = String(event.args?.command ?? "");
6678
- const mode = config.bashOutputMode ?? "full";
6679
- if (mode === "hidden") {
6680
- overridden = `[Bash: ${command.slice(0, 60)}]`;
6681
- } else if (mode === "summary") {
6682
- overridden = `[Bash: ${command.slice(0, 60)} \u2014 ${countNonEmptyLines(textContent)} lines]`;
6683
- } else if (mode === "preview") {
6684
- overridden = previewLines(textContent, collapsedLines);
6685
- }
6686
- break;
6687
- }
6688
- default:
6689
- return void 0;
6690
- }
6691
- if (overridden !== void 0) {
6692
- return {
6693
- content: [{ type: "text", text: overridden }]
6694
- };
6695
- }
6696
- return void 0;
6697
- }
6698
- var init_tool_overrides = __esm({
6699
- "packages/compactor/src/display/tool-overrides.ts"() {
6700
- "use strict";
6701
- init_render_utils();
6702
- }
6703
- });
6704
-
6705
6577
  // packages/compactor/src/display/diff-width-safety.ts
6706
6578
  var diff_width_safety_exports = {};
6707
6579
  __export(diff_width_safety_exports, {
@@ -6770,6 +6642,9 @@ var init_thinking_label = __esm({
6770
6642
  init_core();
6771
6643
  import { dirname as dirname2 } from "node:path";
6772
6644
  import { fileURLToPath } from "node:url";
6645
+ import {
6646
+ buildSessionContext
6647
+ } from "@earendil-works/pi-coding-agent";
6773
6648
 
6774
6649
  // packages/workflow/commands.ts
6775
6650
  init_core();
@@ -7038,11 +6913,15 @@ function registerWorkflowCommands(pi, options) {
7038
6913
  return null;
7039
6914
  },
7040
6915
  handler: async (args, ctx) => {
7041
- const currentTools = options.getActiveTools();
7042
- options.saveTools(currentTools);
7043
- const sandboxTools = getToolsForCommand(cmd.name, currentTools);
7044
- const sandboxLevel = getSandboxLevel(cmd.name);
7045
- options.setActiveTools([...sandboxTools], sandboxLevel);
6916
+ const workflowEvent = {
6917
+ command: cmd.name,
6918
+ fullCommand: `/${fullCommand}`,
6919
+ args: args?.trim() ?? ""
6920
+ };
6921
+ if (!options.activateSandbox(workflowEvent)) {
6922
+ if (ctx.hasUI) ctx.ui.notify("Another UniPi workflow is still active", "warning");
6923
+ return;
6924
+ }
7046
6925
  let skillContent = "";
7047
6926
  try {
7048
6927
  const skillPath = join3(
@@ -7071,7 +6950,12 @@ Arguments: ${args.trim()}`;
7071
6950
  ${skillContent}
7072
6951
  </skill_content>`;
7073
6952
  }
7074
- pi.sendUserMessage(message, { deliverAs: "followUp" });
6953
+ try {
6954
+ pi.sendUserMessage(message, { deliverAs: "followUp" });
6955
+ } catch (error) {
6956
+ options.abortWorkflow();
6957
+ throw error;
6958
+ }
7075
6959
  if (ctx.hasUI) {
7076
6960
  ctx.ui.notify(`Running /${fullCommand}`, "info");
7077
6961
  const ralphStatus = options.isRalphDetected() ? "\u2713 rl" : "\u25CB rl";
@@ -7106,84 +6990,180 @@ ${taskContent}`,
7106
6990
  });
7107
6991
  }
7108
6992
 
6993
+ // packages/workflow/lifecycle.ts
6994
+ var WorkflowLifecycle = class {
6995
+ constructor(now2 = Date.now) {
6996
+ this.now = now2;
6997
+ }
6998
+ now;
6999
+ active = null;
7000
+ start(event) {
7001
+ if (this.active) return false;
7002
+ this.active = { ...event, startedAt: this.now() };
7003
+ return true;
7004
+ }
7005
+ complete(messages) {
7006
+ if (!this.active) return void 0;
7007
+ const workflow = this.active;
7008
+ this.active = null;
7009
+ const finalAssistant = [...messages].reverse().find((message) => message.role === "assistant");
7010
+ const success = finalAssistant?.stopReason !== "error" && finalAssistant?.stopReason !== "aborted";
7011
+ return {
7012
+ command: workflow.command,
7013
+ fullCommand: workflow.fullCommand,
7014
+ args: workflow.args,
7015
+ success,
7016
+ durationMs: this.now() - workflow.startedAt
7017
+ };
7018
+ }
7019
+ reset() {
7020
+ this.active = null;
7021
+ }
7022
+ };
7023
+
7109
7024
  // packages/workflow/index.ts
7110
7025
  var VERSION = getPackageVersion(dirname2(fileURLToPath(import.meta.url)));
7111
- var ralphDetected = false;
7112
- var savedTools = null;
7113
- var sandboxActive = false;
7114
- var currentSandboxLevel = null;
7115
- var currentSandboxTools = null;
7026
+ var WORKFLOW_SANDBOX_SNAPSHOT_TYPE = "unipi-workflow-sandbox-snapshot";
7027
+ function sandboxRestrictions(level) {
7028
+ const common = [
7029
+ "Do not attempt to call tools blocked by name.",
7030
+ "If the user requests an action that requires a blocked tool, explain that the workflow sandbox does not allow it."
7031
+ ];
7032
+ if (level === "brainstorm") {
7033
+ return [
7034
+ "The write tool is restricted to .unipi/docs/specs/ only.",
7035
+ "Use bash only for specific setup operations such as git init or mkdir; use grep, find, and ls for discovery instead of bash.",
7036
+ ...common
7037
+ ];
7038
+ }
7039
+ if (level === "write_unipi") {
7040
+ return [
7041
+ "The write tool is restricted to .unipi/docs/ only (specs and plans).",
7042
+ "Use grep, find, and ls for file discovery instead of guessing filenames.",
7043
+ "Bash is blocked; use read, write, edit, grep, find, and ls only.",
7044
+ ...common
7045
+ ];
7046
+ }
7047
+ return common;
7048
+ }
7049
+ function formatActiveSandboxSnapshot(command, level) {
7050
+ const blocked = getBlockedToolsForLevel(level);
7051
+ const blockedLine = blocked.length > 0 ? blocked.join(", ") : "none";
7052
+ return [
7053
+ "# UniPi Workflow Sandbox Snapshot",
7054
+ "This snapshot supersedes all prior UniPi workflow sandbox snapshots; use only this snapshot for workflow sandbox status and restrictions.",
7055
+ "Status: active",
7056
+ `Workflow: /unipi:${command}`,
7057
+ `Sandbox level: ${level}`,
7058
+ `Blocked tool names: ${blockedLine}`,
7059
+ "Pi's provider tool schemas and tool order remain unchanged. Calls to blocked tool names are rejected by the workflow sandbox.",
7060
+ ["Restrictions:", ...sandboxRestrictions(level).map((restriction) => `- ${restriction}`)].join("\n")
7061
+ ].join("\n\n");
7062
+ }
7063
+ function formatInactiveSandboxSnapshot() {
7064
+ return [
7065
+ "# UniPi Workflow Sandbox Snapshot",
7066
+ "This snapshot supersedes all prior UniPi workflow sandbox snapshots; use only this snapshot for workflow sandbox status and restrictions.",
7067
+ "Status: inactive",
7068
+ "No UniPi workflow sandbox is active. Prior workflow sandbox restrictions no longer apply."
7069
+ ].join("\n\n");
7070
+ }
7071
+ function latestEffectiveSandboxSnapshot(branch) {
7072
+ const messages = buildSessionContext(branch).messages;
7073
+ for (let index = messages.length - 1; index >= 0; index--) {
7074
+ const message = messages[index];
7075
+ if (message.role === "custom" && message.customType === WORKFLOW_SANDBOX_SNAPSHOT_TYPE) {
7076
+ return {
7077
+ content: message.content,
7078
+ details: message.details
7079
+ };
7080
+ }
7081
+ }
7082
+ return void 0;
7083
+ }
7084
+ function latestHistoricalSandboxSnapshot(branch) {
7085
+ for (let index = branch.length - 1; index >= 0; index--) {
7086
+ const entry = branch[index];
7087
+ if (entry.type === "custom_message" && entry.customType === WORKFLOW_SANDBOX_SNAPSHOT_TYPE) {
7088
+ return {
7089
+ content: entry.content,
7090
+ details: entry.details
7091
+ };
7092
+ }
7093
+ }
7094
+ return void 0;
7095
+ }
7096
+ function isActiveSnapshot(snapshot) {
7097
+ if (typeof snapshot.details?.active === "boolean") return snapshot.details.active;
7098
+ return typeof snapshot.content === "string" && snapshot.content.includes("Status: active");
7099
+ }
7116
7100
  function workflow_default(pi) {
7101
+ const workflowLifecycle = new WorkflowLifecycle();
7102
+ let ralphDetected = false;
7103
+ let sandboxCommand = null;
7117
7104
  registerWorkflowCommands(pi, {
7118
7105
  isRalphDetected: () => ralphDetected,
7119
- getActiveTools: () => pi.getActiveTools(),
7120
- setActiveTools: (tools, level) => {
7121
- pi.setActiveTools(tools);
7122
- sandboxActive = true;
7123
- currentSandboxLevel = level;
7124
- currentSandboxTools = tools;
7106
+ activateSandbox: (event) => {
7107
+ if (!workflowLifecycle.start(event)) return false;
7108
+ sandboxCommand = event.command;
7109
+ emitEvent(pi, UNIPI_EVENTS.WORKFLOW_START, event);
7110
+ return true;
7125
7111
  },
7126
- saveTools: (tools) => {
7127
- savedTools = tools;
7112
+ abortWorkflow: () => {
7113
+ sandboxCommand = null;
7114
+ workflowLifecycle.reset();
7128
7115
  }
7129
7116
  });
7130
7117
  pi.on("tool_call", async (event, _ctx) => {
7131
- if (!sandboxActive || !currentSandboxLevel) return;
7132
- const allowed = currentSandboxTools ?? [];
7133
- if (!allowed.includes(event.toolName)) {
7118
+ if (!sandboxCommand) return;
7119
+ const level = getSandboxLevel(sandboxCommand);
7120
+ if (!isToolAllowed(level, event.toolName)) {
7121
+ const blocked = getBlockedToolsForLevel(level);
7134
7122
  return {
7135
7123
  block: true,
7136
- reason: `Tool "${event.toolName}" is not allowed in ${currentSandboxLevel} sandbox. Allowed: ${allowed.join(", ")}`
7124
+ reason: `Tool "${event.toolName}" is not allowed in ${level} sandbox. Blocked: ${blocked.join(", ")}`
7137
7125
  };
7138
7126
  }
7139
7127
  });
7140
- pi.on("before_agent_start", async (event, _ctx) => {
7141
- if (!sandboxActive || !currentSandboxLevel) return;
7142
- const allowed = currentSandboxTools ?? pi.getActiveTools();
7143
- const blocked = getBlockedToolsForLevel(currentSandboxLevel);
7144
- const blockedLine = blocked.length > 0 ? `
7145
- Blocked tools: ${blocked.join(", ")} \u2014 removed from your tool list.` : "\nNo tools were blocked by this sandbox.";
7146
- const base = `
7147
-
7148
- <sandbox>
7149
- Sandbox mode: ${currentSandboxLevel}.
7150
- Available tools: ${allowed.join(", ")}.${blockedLine}`;
7151
- if (currentSandboxLevel === "brainstorm") {
7152
- return {
7153
- systemPrompt: event.systemPrompt + base + `
7154
- The write tool is available but restricted to .unipi/docs/specs/ only.
7155
- 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.
7156
- Do NOT attempt to call blocked tools. Do NOT output tool call XML for them.
7157
- If the user requests an action that requires a blocked tool, respond that you do not have access.
7158
- </sandbox>`
7159
- };
7160
- }
7161
- if (currentSandboxLevel === "write_unipi") {
7128
+ pi.on("before_agent_start", async (_event, ctx) => {
7129
+ const branch = ctx.sessionManager.getBranch();
7130
+ const latest = latestEffectiveSandboxSnapshot(branch);
7131
+ const historical = latestHistoricalSandboxSnapshot(branch);
7132
+ if (sandboxCommand) {
7133
+ const level = getSandboxLevel(sandboxCommand);
7134
+ const content = formatActiveSandboxSnapshot(sandboxCommand, level);
7135
+ if (latest?.content === content) return void 0;
7162
7136
  return {
7163
- systemPrompt: event.systemPrompt + base + `
7164
- Write tool is restricted to .unipi/docs/ only (specs and plans).
7165
- Use grep, find, ls for file discovery \u2014 do NOT guess filenames.
7166
- bash is blocked \u2014 use read, write, edit, grep, find, ls only.
7167
- Do NOT attempt to call blocked tools. Do NOT output tool call XML for them.
7168
- If the user requests an action that requires a blocked tool, respond that you do not have access.
7169
- </sandbox>`
7137
+ message: {
7138
+ customType: WORKFLOW_SANDBOX_SNAPSHOT_TYPE,
7139
+ content,
7140
+ display: false,
7141
+ details: {
7142
+ active: true,
7143
+ command: sandboxCommand,
7144
+ level
7145
+ }
7146
+ }
7170
7147
  };
7171
7148
  }
7149
+ const prior = latest ?? historical;
7150
+ if (!prior || !isActiveSnapshot(prior)) return void 0;
7172
7151
  return {
7173
- systemPrompt: event.systemPrompt + base + `
7174
- Do NOT attempt to call blocked tools. Do NOT output tool call XML for them.
7175
- If the user requires an action that requires a blocked tool, respond that you do not have access.
7176
- </sandbox>`
7152
+ message: {
7153
+ customType: WORKFLOW_SANDBOX_SNAPSHOT_TYPE,
7154
+ content: formatInactiveSandboxSnapshot(),
7155
+ display: false,
7156
+ details: {
7157
+ active: false
7158
+ }
7159
+ }
7177
7160
  };
7178
7161
  });
7179
- pi.on("agent_end", async (_event, _ctx) => {
7180
- if (sandboxActive && savedTools) {
7181
- pi.setActiveTools(savedTools);
7182
- savedTools = null;
7183
- sandboxActive = false;
7184
- currentSandboxLevel = null;
7185
- currentSandboxTools = null;
7186
- }
7162
+ pi.on("agent_end", async (event, _ctx) => {
7163
+ const completedWorkflow = workflowLifecycle.complete(event.messages);
7164
+ if (!completedWorkflow) return;
7165
+ sandboxCommand = null;
7166
+ emitEvent(pi, UNIPI_EVENTS.WORKFLOW_END, completedWorkflow);
7187
7167
  });
7188
7168
  pi.on("session_start", async (_event, ctx) => {
7189
7169
  initUnipiDirs();
@@ -7196,7 +7176,7 @@ If the user requires an action that requires a blocked tool, respond that you do
7196
7176
  if (!ralphDetected) {
7197
7177
  try {
7198
7178
  const allTools = pi.getAllTools();
7199
- ralphDetected = allTools.some((t) => t.name === "ralph_start");
7179
+ ralphDetected = allTools.some((tool) => tool.name === "ralph_start");
7200
7180
  } catch {
7201
7181
  }
7202
7182
  }
@@ -7207,14 +7187,12 @@ If the user requires an action that requires a blocked tool, respond that you do
7207
7187
  });
7208
7188
  pi.events.on(UNIPI_EVENTS.MODULE_READY, (data) => {
7209
7189
  const event = data;
7210
- if (event?.name === MODULES.RALPH) {
7211
- ralphDetected = true;
7212
- }
7190
+ if (event?.name === MODULES.RALPH) ralphDetected = true;
7213
7191
  });
7214
7192
  pi.on("session_shutdown", async () => {
7215
7193
  ralphDetected = false;
7216
- savedTools = null;
7217
- sandboxActive = false;
7194
+ sandboxCommand = null;
7195
+ workflowLifecycle.reset();
7218
7196
  });
7219
7197
  }
7220
7198
 
@@ -7480,12 +7458,20 @@ Use /unipi:ralph-resume <name> to continue`, "info");
7480
7458
  );
7481
7459
  return null;
7482
7460
  }
7461
+ this.emitIterationDone(state);
7483
7462
  const needsReflection = state.reflectEvery > 0 && (state.iteration - 1) % state.reflectEvery === 0;
7484
7463
  if (needsReflection) state.lastReflectionAt = state.iteration;
7485
7464
  this.saveState(state);
7486
7465
  this.updateUI();
7487
7466
  return { state, needsReflection };
7488
7467
  }
7468
+ emitIterationDone(state) {
7469
+ this.emitFn(UNIPI_EVENTS.RALPH_ITERATION_DONE, {
7470
+ name: state.name,
7471
+ iteration: state.iteration - 1,
7472
+ nextIteration: state.iteration
7473
+ });
7474
+ }
7489
7475
  archiveLoop(name) {
7490
7476
  const state = this.loadState(name);
7491
7477
  if (!state) return false;
@@ -8084,7 +8070,7 @@ function handleNuke(rest, ctx, pi) {
8084
8070
  // packages/memory/index.ts
8085
8071
  init_core();
8086
8072
  init_storage();
8087
- import { dirname as dirname8 } from "node:path";
8073
+ import { dirname as dirname7 } from "node:path";
8088
8074
  import { fileURLToPath as fileURLToPath4 } from "node:url";
8089
8075
 
8090
8076
  // packages/memory/tools.ts
@@ -8792,7 +8778,7 @@ init_settings();
8792
8778
  function getInfoRegistry2() {
8793
8779
  return globalThis.__unipi_info_registry;
8794
8780
  }
8795
- var VERSION3 = getPackageVersion(dirname8(fileURLToPath4(import.meta.url)));
8781
+ var VERSION3 = getPackageVersion(dirname7(fileURLToPath4(import.meta.url)));
8796
8782
  var projectStorage = null;
8797
8783
  var orphanSyncPending = false;
8798
8784
  function ensureOrphanSync(storage) {
@@ -9014,12 +9000,12 @@ function memory_default(pi) {
9014
9000
 
9015
9001
  // packages/info-screen/index.ts
9016
9002
  init_core();
9017
- import { dirname as dirname11 } from "node:path";
9003
+ import { dirname as dirname10 } from "node:path";
9018
9004
  import { fileURLToPath as fileURLToPath5 } from "node:url";
9019
9005
 
9020
9006
  // packages/info-screen/config.ts
9021
9007
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync6 } from "node:fs";
9022
- import { join as join8, dirname as dirname9 } from "node:path";
9008
+ import { join as join8, dirname as dirname8 } from "node:path";
9023
9009
  import { homedir as homedir4 } from "node:os";
9024
9010
 
9025
9011
  // packages/info-screen/types.ts
@@ -9048,7 +9034,7 @@ function readSettingsFile() {
9048
9034
  }
9049
9035
  }
9050
9036
  function writeSettingsFile(data) {
9051
- const dir = dirname9(SETTINGS_PATH);
9037
+ const dir = dirname8(SETTINGS_PATH);
9052
9038
  if (!existsSync8(dir)) {
9053
9039
  mkdirSync6(dir, { recursive: true });
9054
9040
  }
@@ -9349,7 +9335,7 @@ if (!globalThis.__unipi_info_registry) {
9349
9335
 
9350
9336
  // packages/info-screen/core-groups.ts
9351
9337
  init_core();
9352
- import { readFileSync as readFileSync9, readdirSync as readdirSync5, existsSync as existsSync10 } from "node:fs";
9338
+ import { readFileSync as readFileSync9, readdirSync as readdirSync6, existsSync as existsSync10 } from "node:fs";
9353
9339
  import { join as join10 } from "node:path";
9354
9340
  import { homedir as homedir6 } from "node:os";
9355
9341
  init_usage_parser();
@@ -9410,7 +9396,7 @@ function discoverExtensions() {
9410
9396
  for (const dir of extensionDirs) {
9411
9397
  if (!existsSync10(dir)) continue;
9412
9398
  try {
9413
- const entries = readdirSync5(dir, { withFileTypes: true });
9399
+ const entries = readdirSync6(dir, { withFileTypes: true });
9414
9400
  for (const entry of entries) {
9415
9401
  const name = entry.name;
9416
9402
  if (counted.has(name)) continue;
@@ -9484,7 +9470,7 @@ function discoverSkills() {
9484
9470
  for (const dir of skillDirs) {
9485
9471
  if (!existsSync10(dir)) continue;
9486
9472
  try {
9487
- const entries = readdirSync5(dir, { withFileTypes: true });
9473
+ const entries = readdirSync6(dir, { withFileTypes: true });
9488
9474
  for (const entry of entries) {
9489
9475
  if (!entry.isDirectory()) continue;
9490
9476
  const name = entry.name;
@@ -10595,7 +10581,7 @@ var SettingsOverlay = class _SettingsOverlay {
10595
10581
  };
10596
10582
 
10597
10583
  // packages/info-screen/index.ts
10598
- var VERSION4 = getPackageVersion(dirname11(fileURLToPath5(import.meta.url)));
10584
+ var VERSION4 = getPackageVersion(dirname10(fileURLToPath5(import.meta.url)));
10599
10585
  function info_screen_default(pi) {
10600
10586
  setPiApi(pi);
10601
10587
  registerCoreGroups();
@@ -10735,14 +10721,36 @@ function info_screen_default(pi) {
10735
10721
  }
10736
10722
 
10737
10723
  // packages/subagents/src/index.ts
10738
- init_core();
10739
10724
  import { defineTool } from "@earendil-works/pi-coding-agent";
10740
10725
  import { Text } from "@earendil-works/pi-tui";
10741
10726
  import { Type as Type3 } from "typebox";
10742
- import { existsSync as existsSync13, readdirSync as readdirSync7 } from "node:fs";
10727
+ import { existsSync as existsSync13, readdirSync as readdirSync8 } from "node:fs";
10743
10728
  import { join as join13 } from "node:path";
10744
10729
  import { homedir as homedir9 } from "node:os";
10745
10730
 
10731
+ // packages/subagents/src/core-compat.ts
10732
+ var MODULES2 = { SUBAGENTS: "subagents" };
10733
+ var UNIPI_EVENTS2 = {
10734
+ MODULE_READY: "unipi:module:ready",
10735
+ BADGE_GENERATE_REQUEST: "unipi:badge:generate:request"
10736
+ };
10737
+ function emitEvent2(pi, eventName, payload) {
10738
+ try {
10739
+ pi.events.emit(eventName, payload);
10740
+ return true;
10741
+ } catch {
10742
+ return false;
10743
+ }
10744
+ }
10745
+ async function withHerdrBlocked2(pi, label, fn) {
10746
+ emitEvent2(pi, "herdr:blocked", { active: true, label });
10747
+ try {
10748
+ return await fn();
10749
+ } finally {
10750
+ emitEvent2(pi, "herdr:blocked", { active: false, label });
10751
+ }
10752
+ }
10753
+
10746
10754
  // packages/subagents/src/agent-manager.ts
10747
10755
  import { randomUUID } from "node:crypto";
10748
10756
 
@@ -10955,14 +10963,14 @@ ${agentPrompt}`;
10955
10963
  init_model_resolver();
10956
10964
 
10957
10965
  // packages/subagents/src/custom-agents.ts
10958
- import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync10, renameSync as renameSync3 } from "node:fs";
10966
+ import { existsSync as existsSync11, readdirSync as readdirSync7, readFileSync as readFileSync10, renameSync as renameSync4 } from "node:fs";
10959
10967
  import { join as join11 } from "node:path";
10960
10968
  import { homedir as homedir7 } from "node:os";
10961
10969
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
10962
10970
  function backupCorrupted(filePath) {
10963
10971
  const backupPath = filePath + ".bak";
10964
10972
  try {
10965
- renameSync3(filePath, backupPath);
10973
+ renameSync4(filePath, backupPath);
10966
10974
  } catch {
10967
10975
  }
10968
10976
  }
@@ -11011,7 +11019,7 @@ function loadCustomAgents(cwd) {
11011
11019
  const agents = /* @__PURE__ */ new Map();
11012
11020
  const globalDir = getGlobalAgentsDir();
11013
11021
  if (existsSync11(globalDir)) {
11014
- const files = readdirSync6(globalDir).filter((f) => f.endsWith(".md"));
11022
+ const files = readdirSync7(globalDir).filter((f) => f.endsWith(".md"));
11015
11023
  for (const file of files) {
11016
11024
  const agent = loadAgentFromFile(join11(globalDir, file), "global");
11017
11025
  if (agent) {
@@ -11021,7 +11029,7 @@ function loadCustomAgents(cwd) {
11021
11029
  }
11022
11030
  const projectDir = getProjectAgentsDir(cwd);
11023
11031
  if (existsSync11(projectDir)) {
11024
- const files = readdirSync6(projectDir).filter((f) => f.endsWith(".md"));
11032
+ const files = readdirSync7(projectDir).filter((f) => f.endsWith(".md"));
11025
11033
  for (const file of files) {
11026
11034
  const agent = loadAgentFromFile(join11(projectDir, file), "project");
11027
11035
  if (agent) {
@@ -11048,17 +11056,17 @@ var FileLock = class {
11048
11056
  */
11049
11057
  async acquire(filePath, agentId) {
11050
11058
  while (this.locks.has(filePath)) {
11051
- await new Promise((resolve13) => {
11059
+ await new Promise((resolve14) => {
11052
11060
  const queue = this.queues.get(filePath) ?? [];
11053
- queue.push(resolve13);
11061
+ queue.push(resolve14);
11054
11062
  this.queues.set(filePath, queue);
11055
11063
  });
11056
11064
  }
11057
11065
  let releaseFn;
11058
- const promise = new Promise((resolve13) => {
11066
+ const promise = new Promise((resolve14) => {
11059
11067
  releaseFn = () => {
11060
11068
  this.locks.delete(filePath);
11061
- resolve13();
11069
+ resolve14();
11062
11070
  const queue = this.queues.get(filePath);
11063
11071
  if (queue && queue.length > 0) {
11064
11072
  const next = queue.shift();
@@ -11124,23 +11132,43 @@ var AgentManager = class {
11124
11132
  onStart;
11125
11133
  maxConcurrent;
11126
11134
  customAgents;
11135
+ typeSettings;
11127
11136
  /** Per-file transparent locking for write agents. */
11128
11137
  fileLock = new FileLock();
11129
11138
  /** Queue of background agents waiting to start. */
11130
11139
  queue = [];
11131
11140
  /** Number of currently running background agents. */
11132
11141
  runningBackground = 0;
11133
- constructor(onComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart) {
11142
+ constructor(onComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart, typeSettings = {}, agentsCwd = process.cwd()) {
11134
11143
  this.onComplete = onComplete;
11135
11144
  this.onStart = onStart;
11136
11145
  this.maxConcurrent = maxConcurrent;
11137
- this.customAgents = loadCustomAgents(process.cwd());
11146
+ this.typeSettings = typeSettings;
11147
+ this.customAgents = loadCustomAgents(agentsCwd);
11138
11148
  this.cleanupInterval = setInterval(() => this.cleanup(), 6e4);
11139
11149
  }
11140
11150
  /** Get resolved agent config for a type. */
11141
11151
  getAgentConfig(type) {
11142
11152
  return this.customAgents.get(type) ?? BUILTIN_CONFIGS[type];
11143
11153
  }
11154
+ /** Public agent types known from built-ins, custom files, or JSON config. */
11155
+ getKnownTypes() {
11156
+ return [.../* @__PURE__ */ new Set([
11157
+ ...Object.keys(BUILTIN_CONFIGS).filter((type) => type !== "name-gen"),
11158
+ ...this.customAgents.keys(),
11159
+ ...Object.keys(this.typeSettings)
11160
+ ])];
11161
+ }
11162
+ /** A type is enabled only when both JSON config and agent frontmatter allow it. */
11163
+ isTypeEnabled(type) {
11164
+ const agentConfig = this.getAgentConfig(type);
11165
+ return this.typeSettings[type]?.enabled !== false && agentConfig?.enabled !== false;
11166
+ }
11167
+ assertTypeEnabled(type) {
11168
+ if (!this.isTypeEnabled(type)) {
11169
+ throw new Error(`Agent type "${type}" is disabled by configuration.`);
11170
+ }
11171
+ }
11144
11172
  setMaxConcurrent(n) {
11145
11173
  this.maxConcurrent = Math.max(1, n);
11146
11174
  this.drainQueue();
@@ -11152,6 +11180,7 @@ var AgentManager = class {
11152
11180
  * Spawn an agent. Returns ID immediately for background, waits for foreground.
11153
11181
  */
11154
11182
  spawn(pi, ctx, type, prompt, options) {
11183
+ this.assertTypeEnabled(type);
11155
11184
  const id = randomUUID().slice(0, 17);
11156
11185
  const abortController = new AbortController();
11157
11186
  const record = {
@@ -11179,9 +11208,11 @@ var AgentManager = class {
11179
11208
  record.startedAt = Date.now();
11180
11209
  if (options.isBackground) this.runningBackground++;
11181
11210
  this.onStart?.(record);
11211
+ const agentConfig = this.getAgentConfig(type);
11182
11212
  let model = options.model;
11183
- if (options.modelInput && options.modelRegistry) {
11184
- const resolved = resolveModel(options.modelInput, options.modelRegistry);
11213
+ const modelInput = options.modelInput ?? agentConfig?.model;
11214
+ if (!model && modelInput && options.modelRegistry) {
11215
+ const resolved = resolveModel(modelInput, options.modelRegistry);
11185
11216
  if (typeof resolved === "string") {
11186
11217
  record.status = "error";
11187
11218
  record.error = resolved;
@@ -11194,15 +11225,14 @@ var AgentManager = class {
11194
11225
  }
11195
11226
  model = resolved;
11196
11227
  }
11197
- const agentConfig = this.getAgentConfig(type);
11198
11228
  const promise = runAgent(ctx, type, prompt, {
11199
11229
  pi,
11200
11230
  model,
11201
11231
  agentConfig,
11202
- maxTurns: options.maxTurns,
11203
- isolated: options.isolated,
11204
- inheritContext: options.inheritContext,
11205
- thinkingLevel: options.thinkingLevel,
11232
+ maxTurns: options.maxTurns ?? agentConfig?.maxTurns,
11233
+ isolated: options.isolated ?? agentConfig?.isolated,
11234
+ inheritContext: options.inheritContext ?? agentConfig?.inheritContext,
11235
+ thinkingLevel: options.thinkingLevel ?? agentConfig?.thinking,
11206
11236
  signal: record.abortController.signal,
11207
11237
  onToolActivity: (activity) => {
11208
11238
  if (activity.type === "end") record.toolUses++;
@@ -11354,7 +11384,7 @@ var AgentManager = class {
11354
11384
  };
11355
11385
 
11356
11386
  // packages/subagents/src/config.ts
11357
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync9 } from "node:fs";
11387
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync11, renameSync as renameSync5, writeFileSync as writeFileSync9 } from "node:fs";
11358
11388
  import { join as join12 } from "node:path";
11359
11389
  import { homedir as homedir8 } from "node:os";
11360
11390
  var DEFAULT_CONFIG2 = {
@@ -11379,7 +11409,7 @@ function ensureDirExists(dirPath) {
11379
11409
  function writeConfigAtomic(filePath, config) {
11380
11410
  const tmpPath = filePath + ".tmp";
11381
11411
  writeFileSync9(tmpPath, JSON.stringify(config, null, 2), "utf-8");
11382
- renameSync4(tmpPath, filePath);
11412
+ renameSync5(tmpPath, filePath);
11383
11413
  }
11384
11414
  function loadConfigFromPath(filePath) {
11385
11415
  if (!existsSync12(filePath)) return null;
@@ -11395,7 +11425,7 @@ function loadConfigFromPath(filePath) {
11395
11425
  function repairCorrupted(filePath) {
11396
11426
  const backupPath = filePath + ".bak";
11397
11427
  try {
11398
- renameSync4(filePath, backupPath);
11428
+ renameSync5(filePath, backupPath);
11399
11429
  } catch {
11400
11430
  }
11401
11431
  writeConfigAtomic(filePath, DEFAULT_CONFIG2);
@@ -12142,7 +12172,8 @@ function src_default(pi) {
12142
12172
  type: record.type,
12143
12173
  description: record.description
12144
12174
  });
12145
- }
12175
+ },
12176
+ config.types
12146
12177
  );
12147
12178
  function buildNotificationDetails(record, activity) {
12148
12179
  return {
@@ -12213,7 +12244,7 @@ function src_default(pi) {
12213
12244
  for (const dir of [globalAgentsDir, workspaceAgentsDir]) {
12214
12245
  try {
12215
12246
  if (existsSync13(dir)) {
12216
- for (const file of readdirSync7(dir)) {
12247
+ for (const file of readdirSync8(dir)) {
12217
12248
  if (file.endsWith(".md") && !customTypes.includes(file.replace(".md", ""))) {
12218
12249
  customTypes.push(file.replace(".md", ""));
12219
12250
  }
@@ -12245,14 +12276,14 @@ function src_default(pi) {
12245
12276
  let sessionCtx2 = null;
12246
12277
  pi.on("session_start", async (_event, ctx) => {
12247
12278
  sessionCtx2 = ctx;
12248
- emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
12249
- name: MODULES.SUBAGENTS || "subagents",
12279
+ emitEvent2(pi, UNIPI_EVENTS2.MODULE_READY, {
12280
+ name: MODULES2.SUBAGENTS || "subagents",
12250
12281
  version: "0.2.0",
12251
12282
  commands: [],
12252
12283
  tools: ["spawn_helper", "get_helper_result"]
12253
12284
  });
12254
12285
  });
12255
- pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST, async (data) => {
12286
+ pi.events.on(UNIPI_EVENTS2.BADGE_GENERATE_REQUEST, async (data) => {
12256
12287
  const event = data;
12257
12288
  if (!sessionCtx2) return;
12258
12289
  const summary = event?.conversationSummary ?? "";
@@ -12338,13 +12369,15 @@ ${summary}` : `Generate a concise session title (MAX 5 WORDS) for this session.
12338
12369
  return { state, callbacks };
12339
12370
  }
12340
12371
  const builtinTypes = BUILTIN_TYPES.join(", ");
12372
+ const enabledTypes = manager2.getKnownTypes().filter((type) => manager2.isTypeEnabled(type));
12373
+ const availableTypes = enabledTypes.join(", ") || "none";
12341
12374
  pi.registerTool(
12342
12375
  defineTool({
12343
12376
  name: "spawn_helper",
12344
12377
  label: "Spawn Helper",
12345
12378
  description: `Launch a sub-agent for parallel work.
12346
12379
 
12347
- Available agent types: ${builtinTypes}
12380
+ Available agent types: ${availableTypes}
12348
12381
  Custom types can be defined in:
12349
12382
  - ~/.unipi/config/agents/<name>.md (global)
12350
12383
  - <workspace>/.unipi/config/agents/<name>.md (project)
@@ -12357,7 +12390,7 @@ Guidelines:
12357
12390
  - Agents inherit the parent model by default`,
12358
12391
  parameters: Type3.Object({
12359
12392
  type: Type3.String({
12360
- description: `Agent type: ${builtinTypes}, or custom type from ~/.unipi/config/agents/*.md`
12393
+ description: `Enabled agent type: ${availableTypes}`
12361
12394
  }),
12362
12395
  prompt: Type3.String({
12363
12396
  description: "The task for the agent to perform."
@@ -12600,7 +12633,7 @@ Use get_result to retrieve full results.`,
12600
12633
  }
12601
12634
  if (params.view && record.session) {
12602
12635
  const activity = agentActivity.get(record.id);
12603
- await withHerdrBlocked(
12636
+ await withHerdrBlocked2(
12604
12637
  pi,
12605
12638
  "helper viewer",
12606
12639
  () => ctx.ui.custom(
@@ -12657,7 +12690,7 @@ Description: ${record.description}
12657
12690
  // packages/btw/extensions/btw.ts
12658
12691
  init_core();
12659
12692
  import {
12660
- buildSessionContext,
12693
+ buildSessionContext as buildSessionContext2,
12661
12694
  createAgentSession as createAgentSession2,
12662
12695
  createExtensionRuntime,
12663
12696
  SessionManager as SessionManager2
@@ -12743,7 +12776,7 @@ function buildBtwSeedState(ctx, thread, mode) {
12743
12776
  if (mode === "contextual") {
12744
12777
  try {
12745
12778
  messages.push(
12746
- ...buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages.filter(
12779
+ ...buildSessionContext2(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages.filter(
12747
12780
  (message) => !isVisibleBtwMessage(message)
12748
12781
  )
12749
12782
  );
@@ -14146,7 +14179,7 @@ ${summary}`;
14146
14179
 
14147
14180
  // packages/web-api/src/index.ts
14148
14181
  init_core();
14149
- import { dirname as dirname12 } from "node:path";
14182
+ import { dirname as dirname11 } from "node:path";
14150
14183
  import { fileURLToPath as fileURLToPath6 } from "node:url";
14151
14184
 
14152
14185
  // packages/web-api/src/tools.ts
@@ -16816,7 +16849,7 @@ var llmSummarizeProvider = {
16816
16849
  registry.register(llmSummarizeProvider);
16817
16850
 
16818
16851
  // packages/web-api/src/index.ts
16819
- var VERSION5 = getPackageVersion(dirname12(fileURLToPath6(import.meta.url)));
16852
+ var VERSION5 = getPackageVersion(dirname11(fileURLToPath6(import.meta.url)));
16820
16853
  function getInfoRegistry4() {
16821
16854
  return globalThis.__unipi_info_registry;
16822
16855
  }
@@ -16888,14 +16921,14 @@ function src_default2(pi) {
16888
16921
 
16889
16922
  // packages/utility/src/index.ts
16890
16923
  init_core();
16891
- import { dirname as dirname15 } from "node:path";
16924
+ import { dirname as dirname14 } from "node:path";
16892
16925
  import { fileURLToPath as fileURLToPath7 } from "node:url";
16893
16926
 
16894
16927
  // packages/utility/src/commands.ts
16895
16928
  init_core();
16896
16929
 
16897
16930
  // packages/utility/src/lifecycle/cleanup.ts
16898
- import { existsSync as existsSync16, statSync as statSync7, readdirSync as readdirSync9, unlinkSync as unlinkSync5, rmdirSync } from "node:fs";
16931
+ import { existsSync as existsSync16, statSync as statSync8, readdirSync as readdirSync10, unlinkSync as unlinkSync5, rmdirSync } from "node:fs";
16899
16932
  import { join as join16 } from "node:path";
16900
16933
  import { homedir as homedir12, tmpdir } from "node:os";
16901
16934
  var DEFAULTS = {
@@ -16912,7 +16945,7 @@ function expandHome(path28) {
16912
16945
  }
16913
16946
  function isStale(path28, maxAgeDays) {
16914
16947
  try {
16915
- const stats = statSync7(path28);
16948
+ const stats = statSync8(path28);
16916
16949
  const ageMs = Date.now() - stats.mtime.getTime();
16917
16950
  return ageMs > maxAgeDays * 24 * 60 * 60 * 1e3;
16918
16951
  } catch {
@@ -16924,7 +16957,7 @@ function hasWalLock(dbPath) {
16924
16957
  const shmPath = dbPath + "-shm";
16925
16958
  if (existsSync16(walPath)) {
16926
16959
  try {
16927
- const walStats = statSync7(walPath);
16960
+ const walStats = statSync8(walPath);
16928
16961
  const ageMs = Date.now() - walStats.mtime.getTime();
16929
16962
  return ageMs > 5 * 60 * 1e3;
16930
16963
  } catch {
@@ -16945,14 +16978,14 @@ function cleanDbs(options) {
16945
16978
  const scanDir = (dir) => {
16946
16979
  let entries;
16947
16980
  try {
16948
- entries = readdirSync9(dir);
16981
+ entries = readdirSync10(dir);
16949
16982
  } catch {
16950
16983
  return;
16951
16984
  }
16952
16985
  for (const entry of entries) {
16953
16986
  const fullPath = join16(dir, entry);
16954
16987
  try {
16955
- const stats = statSync7(fullPath);
16988
+ const stats = statSync8(fullPath);
16956
16989
  if (stats.isDirectory()) {
16957
16990
  scanDir(fullPath);
16958
16991
  continue;
@@ -16968,7 +17001,7 @@ function cleanDbs(options) {
16968
17001
  const companion = fullPath + suffix;
16969
17002
  if (existsSync16(companion)) {
16970
17003
  unlinkSync5(companion);
16971
- result.bytesFreed += statSync7(companion).size;
17004
+ result.bytesFreed += statSync8(companion).size;
16972
17005
  }
16973
17006
  }
16974
17007
  result.removed++;
@@ -16997,7 +17030,7 @@ function cleanTemps(options) {
16997
17030
  const tmpDir = tmpdir();
16998
17031
  let entries;
16999
17032
  try {
17000
- entries = readdirSync9(tmpDir);
17033
+ entries = readdirSync10(tmpDir);
17001
17034
  } catch {
17002
17035
  return result;
17003
17036
  }
@@ -17005,7 +17038,7 @@ function cleanTemps(options) {
17005
17038
  if (!patterns.some((p) => p.test(entry))) continue;
17006
17039
  const fullPath = join16(tmpDir, entry);
17007
17040
  try {
17008
- const stats = statSync7(fullPath);
17041
+ const stats = statSync8(fullPath);
17009
17042
  if (!stats.isFile()) continue;
17010
17043
  if (isStale(fullPath, options.tempMaxAgeDays)) {
17011
17044
  result.bytesFreed += stats.size;
@@ -17037,14 +17070,14 @@ function cleanSessions(options) {
17037
17070
  if (!existsSync16(sessionsDir)) return result;
17038
17071
  let entries;
17039
17072
  try {
17040
- entries = readdirSync9(sessionsDir);
17073
+ entries = readdirSync10(sessionsDir);
17041
17074
  } catch {
17042
17075
  return result;
17043
17076
  }
17044
17077
  for (const entry of entries) {
17045
17078
  const fullPath = join16(sessionsDir, entry);
17046
17079
  try {
17047
- const stats = statSync7(fullPath);
17080
+ const stats = statSync8(fullPath);
17048
17081
  if (!stats.isDirectory()) continue;
17049
17082
  if (isStale(fullPath, options.sessionMaxAgeDays)) {
17050
17083
  result.bytesFreed += stats.size;
@@ -17052,10 +17085,10 @@ function cleanSessions(options) {
17052
17085
  if (!options.dryRun) {
17053
17086
  try {
17054
17087
  const removeRecursive = (dir) => {
17055
- const items = readdirSync9(dir);
17088
+ const items = readdirSync10(dir);
17056
17089
  for (const item of items) {
17057
17090
  const itemPath = join16(dir, item);
17058
- const itemStats = statSync7(itemPath);
17091
+ const itemStats = statSync8(itemPath);
17059
17092
  if (itemStats.isDirectory()) {
17060
17093
  removeRecursive(itemPath);
17061
17094
  } else {
@@ -17088,14 +17121,14 @@ function cleanCache(options) {
17088
17121
  if (!existsSync16(cacheDir)) return result;
17089
17122
  let entries;
17090
17123
  try {
17091
- entries = readdirSync9(cacheDir);
17124
+ entries = readdirSync10(cacheDir);
17092
17125
  } catch {
17093
17126
  return result;
17094
17127
  }
17095
17128
  for (const entry of entries) {
17096
17129
  const fullPath = join16(cacheDir, entry);
17097
17130
  try {
17098
- const stats = statSync7(fullPath);
17131
+ const stats = statSync8(fullPath);
17099
17132
  if (!stats.isFile()) continue;
17100
17133
  if (isStale(fullPath, options.tempMaxAgeDays)) {
17101
17134
  result.bytesFreed += stats.size;
@@ -17159,7 +17192,7 @@ function formatCleanupReport(report) {
17159
17192
  }
17160
17193
 
17161
17194
  // packages/utility/src/diagnostics/engine.ts
17162
- import { existsSync as existsSync17, accessSync, constants } from "node:fs";
17195
+ import { existsSync as existsSync17, accessSync as accessSync2, constants as constants2 } from "node:fs";
17163
17196
  import { join as join17 } from "node:path";
17164
17197
  import { homedir as homedir13 } from "node:os";
17165
17198
  function expandHome2(path28) {
@@ -17170,7 +17203,7 @@ function expandHome2(path28) {
17170
17203
  }
17171
17204
  function isReadable(path28) {
17172
17205
  try {
17173
- accessSync(path28, constants.R_OK);
17206
+ accessSync2(path28, constants2.R_OK);
17174
17207
  return true;
17175
17208
  } catch {
17176
17209
  return false;
@@ -17178,7 +17211,7 @@ function isReadable(path28) {
17178
17211
  }
17179
17212
  function isWritable(path28) {
17180
17213
  try {
17181
- accessSync(path28, constants.W_OK);
17214
+ accessSync2(path28, constants2.W_OK);
17182
17215
  return true;
17183
17216
  } catch {
17184
17217
  return false;
@@ -17587,7 +17620,6 @@ var PRESETS = {
17587
17620
  }
17588
17621
  }
17589
17622
  };
17590
- var HARDCODED_FALLBACK = PRESETS.default.colors;
17591
17623
  function getPreset(name) {
17592
17624
  return PRESETS[name] ?? PRESETS.default;
17593
17625
  }
@@ -18242,17 +18274,9 @@ function registerUtilityCommands(pi) {
18242
18274
  }
18243
18275
  return;
18244
18276
  }
18245
- const requestId = `status-${Date.now()}`;
18246
- emitEvent(pi, UNIPI_EVENTS.MODULE_STATUS_REQUEST, { requestId });
18247
- await new Promise((resolve13) => setTimeout(resolve13, 500));
18248
18277
  sendResponse(
18249
18278
  pi,
18250
- `## \u{1F4CA} Module Status
18251
-
18252
- Status request broadcast to all modules.
18253
- Modules that support status reporting will respond via events.
18254
-
18255
- *Request ID: \`${requestId}\`*`
18279
+ "## \u{1F4CA} Module Status\n\nUse `/unipi:info` for the live module and tool dashboard.\nUse `/unipi:doctor` to run configuration and runtime diagnostics."
18256
18280
  );
18257
18281
  }
18258
18282
  });
@@ -19457,7 +19481,7 @@ function registerInfoScreen(pi) {
19457
19481
  }
19458
19482
 
19459
19483
  // packages/utility/src/index.ts
19460
- var VERSION6 = getPackageVersion(dirname15(fileURLToPath7(import.meta.url)));
19484
+ var VERSION6 = getPackageVersion(dirname14(fileURLToPath7(import.meta.url)));
19461
19485
  var firstMessageSeen = false;
19462
19486
  var firstUserText = "";
19463
19487
  var firstInputCtx = null;
@@ -19669,7 +19693,7 @@ function registerUtilityTools(pi, nameBadgeState) {
19669
19693
 
19670
19694
  // packages/ask-user/index.ts
19671
19695
  init_core();
19672
- import { dirname as dirname16 } from "node:path";
19696
+ import { dirname as dirname15 } from "node:path";
19673
19697
  import { fileURLToPath as fileURLToPath8 } from "node:url";
19674
19698
 
19675
19699
  // packages/ask-user/tools.ts
@@ -21054,7 +21078,7 @@ function registerAskUserCommands(pi) {
21054
21078
  }
21055
21079
 
21056
21080
  // packages/ask-user/index.ts
21057
- var VERSION7 = getPackageVersion(dirname16(fileURLToPath8(import.meta.url)));
21081
+ var VERSION7 = getPackageVersion(dirname15(fileURLToPath8(import.meta.url)));
21058
21082
  function ask_user_default(pi) {
21059
21083
  registerAskUserTools(pi);
21060
21084
  registerAskUserCommands(pi);
@@ -21070,7 +21094,7 @@ function ask_user_default(pi) {
21070
21094
 
21071
21095
  // packages/mcp/src/index.ts
21072
21096
  init_core();
21073
- import { dirname as dirname19 } from "node:path";
21097
+ import { dirname as dirname18 } from "node:path";
21074
21098
  import { fileURLToPath as fileURLToPath9 } from "node:url";
21075
21099
 
21076
21100
  // packages/mcp/src/config/manager.ts
@@ -21568,7 +21592,7 @@ var McpClient = class {
21568
21592
  if (this.connected) {
21569
21593
  throw new Error("McpClient is already connected");
21570
21594
  }
21571
- return new Promise((resolve13, reject) => {
21595
+ return new Promise((resolve14, reject) => {
21572
21596
  try {
21573
21597
  const mergedEnv = { ...process.env, ...env };
21574
21598
  this.process = spawn2(command, args, {
@@ -21617,7 +21641,7 @@ Stderr: ${this.stderrBuffer}`
21617
21641
  }).then(() => {
21618
21642
  this.sendNotification("notifications/initialized", {});
21619
21643
  this.connected = true;
21620
- resolve13();
21644
+ resolve14();
21621
21645
  }).catch((err) => {
21622
21646
  this.cleanup();
21623
21647
  reject(
@@ -21661,16 +21685,16 @@ Stderr: ${this.stderrBuffer}`
21661
21685
  this.sendNotification("shutdown", {});
21662
21686
  } catch {
21663
21687
  }
21664
- await new Promise((resolve13) => {
21665
- const timer = setTimeout(() => resolve13(), 500);
21688
+ await new Promise((resolve14) => {
21689
+ const timer = setTimeout(() => resolve14(), 500);
21666
21690
  this.process.on("exit", () => {
21667
21691
  clearTimeout(timer);
21668
- resolve13();
21692
+ resolve14();
21669
21693
  });
21670
21694
  try {
21671
21695
  this.process.kill("SIGTERM");
21672
21696
  } catch {
21673
- resolve13();
21697
+ resolve14();
21674
21698
  }
21675
21699
  });
21676
21700
  if (this.process && !this.process.killed) {
@@ -21700,7 +21724,7 @@ Stderr: ${this.stderrBuffer}`
21700
21724
  }
21701
21725
  }
21702
21726
  sendRequest(method, params) {
21703
- return new Promise((resolve13, reject) => {
21727
+ return new Promise((resolve14, reject) => {
21704
21728
  if (!this.process?.stdin) {
21705
21729
  reject(new Error("MCP server process not available"));
21706
21730
  return;
@@ -21720,7 +21744,7 @@ Stderr: ${this.stderrBuffer}`
21720
21744
  )
21721
21745
  );
21722
21746
  }, this.timeoutMs);
21723
- this.pending.set(id, { resolve: resolve13, reject, timer });
21747
+ this.pending.set(id, { resolve: resolve14, reject, timer });
21724
21748
  const message = JSON.stringify(request) + "\n";
21725
21749
  this.process.stdin.write(message);
21726
21750
  });
@@ -21783,15 +21807,45 @@ Stderr: ${this.stderrBuffer}`
21783
21807
 
21784
21808
  // packages/mcp/src/bridge/translator.ts
21785
21809
  init_core();
21810
+ function compareCodeUnits(left, right) {
21811
+ return left < right ? -1 : left > right ? 1 : 0;
21812
+ }
21813
+ function isObject(value) {
21814
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21815
+ }
21816
+ var LITERAL_VALUE_KEYWORDS = /* @__PURE__ */ new Set(["const", "default", "enum", "examples"]);
21817
+ function canonicalizeValue(value, key, normalizeSchemaKeywords = true) {
21818
+ if (Array.isArray(value)) {
21819
+ if (normalizeSchemaKeywords && key === "required" && value.every((item) => typeof item === "string")) {
21820
+ return [...new Set(value)].sort(compareCodeUnits);
21821
+ }
21822
+ return value.map((item) => canonicalizeValue(item, void 0, normalizeSchemaKeywords));
21823
+ }
21824
+ if (!isObject(value)) return value;
21825
+ const canonical = {};
21826
+ for (const objectKey of Object.keys(value).sort(compareCodeUnits)) {
21827
+ const childNormalizesSchemaKeywords = normalizeSchemaKeywords && !LITERAL_VALUE_KEYWORDS.has(objectKey);
21828
+ canonical[objectKey] = canonicalizeValue(
21829
+ value[objectKey],
21830
+ objectKey,
21831
+ childNormalizesSchemaKeywords
21832
+ );
21833
+ }
21834
+ return canonical;
21835
+ }
21836
+ function canonicalizeJsonSchema(schema) {
21837
+ return canonicalizeValue(schema);
21838
+ }
21786
21839
  function translateMcpTool(mcpTool, serverName, client) {
21787
21840
  const separator = MCP_DEFAULTS.TOOL_NAME_SEPARATOR;
21788
21841
  const toolName = `${serverName}${separator}${mcpTool.name}`;
21789
- const inputSchema = mcpTool.inputSchema ?? {};
21790
- const parameters = {
21842
+ const inputSchema = isObject(mcpTool.inputSchema) ? mcpTool.inputSchema : {};
21843
+ const normalizedSchema = {
21791
21844
  type: "object",
21792
- properties: inputSchema.properties ?? {},
21793
- required: inputSchema.required
21845
+ properties: isObject(inputSchema.properties) ? inputSchema.properties : {},
21846
+ required: Array.isArray(inputSchema.required) ? inputSchema.required : []
21794
21847
  };
21848
+ const parameters = canonicalizeJsonSchema(normalizedSchema);
21795
21849
  const description = [
21796
21850
  mcpTool.description || `MCP tool: ${mcpTool.name}`,
21797
21851
  `[Server: ${serverName}]`
@@ -21849,6 +21903,7 @@ Check server status via /unipi:mcp-settings`
21849
21903
  };
21850
21904
  return {
21851
21905
  name: toolName,
21906
+ label: toolName,
21852
21907
  description,
21853
21908
  parameters,
21854
21909
  execute: execute2
@@ -21861,26 +21916,125 @@ var ServerRegistry = class {
21861
21916
  emitEvent;
21862
21917
  registerTool;
21863
21918
  unregisterTool;
21919
+ canUnregisterTools;
21864
21920
  timeoutMs;
21921
+ createClient;
21865
21922
  constructor(options) {
21866
21923
  this.emitEvent = options.emitEvent;
21867
21924
  this.registerTool = options.registerTool;
21868
21925
  this.unregisterTool = options.unregisterTool;
21926
+ this.canUnregisterTools = options.canUnregisterTools ?? true;
21869
21927
  this.timeoutMs = options.timeoutMs ?? MCP_DEFAULTS.STARTUP_TIMEOUT_MS;
21928
+ this.createClient = options.createClient ?? (() => new McpClient({ timeoutMs: this.timeoutMs }));
21870
21929
  }
21871
21930
  /**
21872
- * Start an MCP server: spawn process, initialize, discover tools, register.
21931
+ * Start one MCP server. Discovery completes before its tools are registered,
21932
+ * and registration always follows final Pi tool-name order.
21873
21933
  */
21874
21934
  async startServer(resolved) {
21875
- const { name, def } = resolved;
21876
- if (this.entries.size >= MCP_DEFAULTS.MAX_SERVERS) {
21935
+ await this.startServers([resolved]);
21936
+ const state = this.getServerState(resolved.name);
21937
+ if (state?.status === "error") {
21938
+ throw new Error(state.error ?? `MCP server "${resolved.name}" failed to start`);
21939
+ }
21940
+ }
21941
+ /**
21942
+ * Start MCP servers behind a discovery barrier.
21943
+ *
21944
+ * Connections and tool discovery run in parallel. Once every server has
21945
+ * either prepared or failed, tools from all successful servers are checked
21946
+ * for duplicate final names, sorted, and only then registered with Pi.
21947
+ * Individual connection failures remain represented by error registry
21948
+ * entries and do not prevent tools from other servers being registered.
21949
+ */
21950
+ async startServers(resolvedServers) {
21951
+ if (resolvedServers.length === 0) return;
21952
+ const names = /* @__PURE__ */ new Set();
21953
+ for (const { name } of resolvedServers) {
21954
+ if (names.has(name)) {
21955
+ throw new Error(`Duplicate MCP server name in startup batch: "${name}"`);
21956
+ }
21957
+ names.add(name);
21958
+ }
21959
+ const replacementCount = [...names].filter((name) => this.entries.has(name)).length;
21960
+ if (this.entries.size - replacementCount + resolvedServers.length > MCP_DEFAULTS.MAX_SERVERS) {
21877
21961
  throw new Error(
21878
21962
  `Maximum number of MCP servers (${MCP_DEFAULTS.MAX_SERVERS}) reached. Stop a server before starting a new one.`
21879
21963
  );
21880
21964
  }
21881
- if (this.entries.has(name)) {
21882
- await this.stopServer(name);
21965
+ await Promise.all(
21966
+ [...names].filter((name) => this.entries.has(name)).map((name) => this.stopServer(name))
21967
+ );
21968
+ const settled = await Promise.allSettled(
21969
+ resolvedServers.map((resolved) => this.prepareServer(resolved))
21970
+ );
21971
+ const prepared = settled.filter((result) => result.status === "fulfilled").map((result) => result.value);
21972
+ if (prepared.length === 0) return;
21973
+ const registrations = prepared.flatMap(({ entry, tools }) => tools.map((tool) => ({ entry, tool }))).sort((left, right) => compareCodeUnits(left.tool.name, right.tool.name));
21974
+ try {
21975
+ this.assertUniqueFinalToolNames(registrations.map(({ tool }) => tool.name));
21976
+ } catch (error) {
21977
+ await this.failPreparedServers(prepared, error);
21978
+ throw error;
21979
+ }
21980
+ const registeredNames = [];
21981
+ try {
21982
+ for (const { tool } of registrations) {
21983
+ this.registerTool(tool);
21984
+ registeredNames.push(tool.name);
21985
+ }
21986
+ } catch (error) {
21987
+ if (this.canUnregisterTools) {
21988
+ for (const toolName of registeredNames.reverse()) {
21989
+ try {
21990
+ this.unregisterTool(toolName);
21991
+ } catch {
21992
+ }
21993
+ }
21994
+ await this.failPreparedServers(prepared, error);
21995
+ } else {
21996
+ const message = `${error instanceof Error ? error.message : String(error)}; some MCP tools remain registered until Pi restarts`;
21997
+ for (const { entry, client, tools } of prepared) {
21998
+ const toolNames = tools.map((tool) => tool.name).filter((name) => registeredNames.includes(name)).sort(compareCodeUnits);
21999
+ entry.toolNames = toolNames;
22000
+ entry.state = {
22001
+ ...entry.state,
22002
+ status: "error",
22003
+ pid: client.pid,
22004
+ toolCount: toolNames.length,
22005
+ error: message
22006
+ };
22007
+ this.emitEvent(UNIPI_EVENTS.MCP_SERVER_ERROR, {
22008
+ name: entry.name,
22009
+ error: message
22010
+ });
22011
+ }
22012
+ }
22013
+ throw error;
21883
22014
  }
22015
+ for (const { entry, client, tools } of [...prepared].sort((left, right) => compareCodeUnits(left.entry.name, right.entry.name))) {
22016
+ const toolNames = tools.map((tool) => tool.name).sort(compareCodeUnits);
22017
+ entry.toolNames = toolNames;
22018
+ entry.state = {
22019
+ ...entry.state,
22020
+ status: "running",
22021
+ pid: client.pid,
22022
+ toolCount: toolNames.length
22023
+ };
22024
+ this.emitEvent(UNIPI_EVENTS.MCP_SERVER_STARTED, {
22025
+ name: entry.name,
22026
+ toolCount: toolNames.length
22027
+ });
22028
+ if (toolNames.length > 0) {
22029
+ this.emitEvent(UNIPI_EVENTS.MCP_TOOLS_REGISTERED, {
22030
+ serverName: entry.name,
22031
+ toolNames
22032
+ });
22033
+ }
22034
+ }
22035
+ }
22036
+ async prepareServer(resolved) {
22037
+ const { name, def } = resolved;
21884
22038
  const state = {
21885
22039
  name,
21886
22040
  status: "starting",
@@ -21895,73 +22049,74 @@ var ServerRegistry = class {
21895
22049
  toolNames: []
21896
22050
  };
21897
22051
  this.entries.set(name, entry);
22052
+ let client = null;
21898
22053
  try {
21899
22054
  const safeCommand = typeof def.command === "string" ? def.command : String(def.command);
21900
22055
  const safeArgs = Array.isArray(def.args) ? def.args : [];
21901
22056
  let safeEnv;
21902
- if (def.env !== void 0 && def.env !== null) {
21903
- if (typeof def.env === "object" && !Array.isArray(def.env)) {
21904
- safeEnv = {};
21905
- for (const [k, v] of Object.entries(def.env)) {
21906
- safeEnv[k] = typeof v === "string" ? v : String(v);
21907
- }
21908
- } else {
21909
- }
21910
- }
21911
- const client = new McpClient({ timeoutMs: this.timeoutMs });
21912
- await client.connect(safeCommand, safeArgs, safeEnv);
21913
- entry.client = client;
21914
- const mcpTools = await client.listTools();
21915
- const toolNames = [];
21916
- for (const mcpTool of mcpTools) {
21917
- const piTool = translateMcpTool(mcpTool, name, client);
21918
- this.registerTool(piTool);
21919
- toolNames.push(piTool.name);
21920
- }
21921
- entry.state = {
21922
- ...state,
21923
- status: "running",
21924
- pid: client.pid,
21925
- toolCount: toolNames.length
21926
- };
21927
- entry.toolNames = toolNames;
21928
- this.emitEvent(UNIPI_EVENTS.MCP_SERVER_STARTED, {
21929
- name,
21930
- toolCount: toolNames.length
21931
- });
21932
- if (toolNames.length > 0) {
21933
- this.emitEvent(UNIPI_EVENTS.MCP_TOOLS_REGISTERED, {
21934
- serverName: name,
21935
- toolNames
21936
- });
21937
- }
21938
- } catch (err) {
21939
- const error = err instanceof Error ? err.message : String(err);
21940
- entry.state = {
21941
- ...state,
21942
- status: "error",
21943
- error
21944
- };
21945
- if (entry.client) {
22057
+ if (def.env !== void 0 && def.env !== null && typeof def.env === "object" && !Array.isArray(def.env)) {
22058
+ safeEnv = {};
22059
+ for (const [key, value] of Object.entries(def.env)) {
22060
+ safeEnv[key] = typeof value === "string" ? value : String(value);
22061
+ }
22062
+ }
22063
+ client = this.createClient();
22064
+ const connectedClient = client;
22065
+ await connectedClient.connect(safeCommand, safeArgs, safeEnv);
22066
+ entry.client = connectedClient;
22067
+ const mcpTools = await connectedClient.listTools();
22068
+ const tools = mcpTools.map((tool) => translateMcpTool(tool, name, connectedClient)).sort((left, right) => compareCodeUnits(left.name, right.name));
22069
+ return { entry, client: connectedClient, tools };
22070
+ } catch (error) {
22071
+ const message = error instanceof Error ? error.message : String(error);
22072
+ entry.state = { ...state, status: "error", error: message };
22073
+ if (client) {
21946
22074
  try {
21947
- await entry.client.disconnect();
22075
+ await client.disconnect();
21948
22076
  } catch {
21949
22077
  }
21950
- entry.client = null;
21951
22078
  }
21952
- this.emitEvent(UNIPI_EVENTS.MCP_SERVER_ERROR, {
21953
- name,
21954
- error
21955
- });
21956
- throw err;
22079
+ entry.client = null;
22080
+ this.emitEvent(UNIPI_EVENTS.MCP_SERVER_ERROR, { name, error: message });
22081
+ throw error;
21957
22082
  }
21958
22083
  }
21959
- /**
21960
- * Stop an MCP server: unregister tools, disconnect client.
21961
- */
22084
+ assertUniqueFinalToolNames(newNames) {
22085
+ const existingNames = this.getActive().flatMap((state) => this.entries.get(state.name)?.toolNames ?? []);
22086
+ const seen = /* @__PURE__ */ new Set();
22087
+ const duplicates = /* @__PURE__ */ new Set();
22088
+ for (const name of [...existingNames, ...newNames]) {
22089
+ if (seen.has(name)) duplicates.add(name);
22090
+ seen.add(name);
22091
+ }
22092
+ if (duplicates.size > 0) {
22093
+ throw new Error(
22094
+ `Duplicate final MCP tool name(s): ${[...duplicates].sort(compareCodeUnits).join(", ")}`
22095
+ );
22096
+ }
22097
+ }
22098
+ async failPreparedServers(prepared, error) {
22099
+ const message = error instanceof Error ? error.message : String(error);
22100
+ await Promise.all(prepared.map(async ({ entry, client }) => {
22101
+ entry.state = { ...entry.state, status: "error", toolCount: 0, error: message };
22102
+ entry.toolNames = [];
22103
+ try {
22104
+ await client.disconnect();
22105
+ } catch {
22106
+ }
22107
+ entry.client = null;
22108
+ this.emitEvent(UNIPI_EVENTS.MCP_SERVER_ERROR, { name: entry.name, error: message });
22109
+ }));
22110
+ }
22111
+ /** Stop an MCP server: unregister tools, disconnect client. */
21962
22112
  async stopServer(name) {
21963
22113
  const entry = this.entries.get(name);
21964
22114
  if (!entry) return;
22115
+ if (entry.toolNames.length > 0 && !this.canUnregisterTools) {
22116
+ throw new Error(
22117
+ "This Pi version cannot remove MCP tools at runtime; restart Pi to change the MCP tool set."
22118
+ );
22119
+ }
21965
22120
  for (const toolName of entry.toolNames) {
21966
22121
  this.unregisterTool(toolName);
21967
22122
  }
@@ -21978,78 +22133,63 @@ var ServerRegistry = class {
21978
22133
  }
21979
22134
  entry.client = null;
21980
22135
  }
21981
- entry.state = {
21982
- ...entry.state,
21983
- status: "stopped",
21984
- toolCount: 0
21985
- };
22136
+ entry.state = { ...entry.state, status: "stopped", toolCount: 0 };
21986
22137
  entry.toolNames = [];
21987
22138
  this.emitEvent(UNIPI_EVENTS.MCP_SERVER_STOPPED, { name });
21988
22139
  }
21989
- /**
21990
- * Restart an MCP server: stop then start.
21991
- */
22140
+ /** Restart an MCP server: stop then start. */
21992
22141
  async restartServer(name) {
21993
22142
  const entry = this.entries.get(name);
21994
- if (!entry) {
21995
- throw new Error(`Server '${name}' not found in registry`);
21996
- }
22143
+ if (!entry) throw new Error(`Server '${name}' not found in registry`);
21997
22144
  const resolved = entry.resolved;
21998
22145
  await this.stopServer(name);
21999
22146
  await this.startServer(resolved);
22000
22147
  }
22001
- /**
22002
- * Stop all running servers.
22003
- */
22148
+ /** Stop all running servers when runtime unregistration is supported. */
22004
22149
  async stopAll() {
22005
22150
  const names = Array.from(this.entries.keys());
22006
22151
  await Promise.allSettled(names.map((name) => this.stopServer(name)));
22007
22152
  }
22008
- /**
22009
- * Get all registered server states.
22010
- */
22153
+ /** Disconnect clients during extension shutdown without claiming tools were removed. */
22154
+ async disconnectAll() {
22155
+ await Promise.allSettled([...this.entries.values()].map(async (entry) => {
22156
+ if (!entry.client) return;
22157
+ try {
22158
+ await entry.client.disconnect();
22159
+ } finally {
22160
+ entry.client = null;
22161
+ }
22162
+ }));
22163
+ }
22164
+ /** Get all registered server states. */
22011
22165
  getAll() {
22012
- return Array.from(this.entries.values()).map((e) => e.state);
22166
+ return Array.from(this.entries.values()).map((entry) => entry.state);
22013
22167
  }
22014
- /**
22015
- * Get states of running servers.
22016
- */
22168
+ /** Get states of running servers. */
22017
22169
  getActive() {
22018
- return this.getAll().filter((s) => s.status === "running");
22170
+ return this.getAll().filter((state) => state.status === "running");
22019
22171
  }
22020
- /**
22021
- * Get states of servers in error state.
22022
- */
22172
+ /** Get states of servers in error state. */
22023
22173
  getFailed() {
22024
- return this.getAll().filter((s) => s.status === "error");
22174
+ return this.getAll().filter((state) => state.status === "error");
22025
22175
  }
22026
- /**
22027
- * Get total number of tools across all active servers.
22028
- */
22176
+ /** Get total number of tools across all active servers. */
22029
22177
  getTotalToolCount() {
22030
- return this.getActive().reduce((sum, s) => sum + s.toolCount, 0);
22178
+ return this.getActive().reduce((sum, state) => sum + state.toolCount, 0);
22031
22179
  }
22032
- /**
22033
- * Get the state of a specific server.
22034
- */
22180
+ /** Get the state of a specific server. */
22035
22181
  getServerState(name) {
22036
22182
  return this.entries.get(name)?.state ?? null;
22037
22183
  }
22038
- /**
22039
- * Get the full registry entry for a server.
22040
- */
22184
+ /** Get the full registry entry for a server. */
22041
22185
  getEntry(name) {
22042
22186
  return this.entries.get(name) ?? null;
22043
22187
  }
22044
- /**
22045
- * Check if a server exists in the registry.
22046
- */
22188
+ /** Check if a server exists in the registry. */
22047
22189
  hasServer(name) {
22048
22190
  return this.entries.has(name);
22049
22191
  }
22050
- /**
22051
- * Get the number of registered servers.
22052
- */
22192
+ /** Get the number of registered servers. */
22053
22193
  get size() {
22054
22194
  return this.entries.size;
22055
22195
  }
@@ -22662,12 +22802,6 @@ function renderMcpSettingsOverlay(params) {
22662
22802
  addedAt: meta.servers[server.name]?.addedAt ?? (/* @__PURE__ */ new Date()).toISOString()
22663
22803
  };
22664
22804
  saveMetadata(configDir, meta);
22665
- if (!newEnabled && registry3) {
22666
- try {
22667
- await registry3.stopServer(server.name);
22668
- } catch {
22669
- }
22670
- }
22671
22805
  refreshServers();
22672
22806
  refresh();
22673
22807
  } catch (err) {
@@ -22814,7 +22948,7 @@ function renderMcpSettingsOverlay(params) {
22814
22948
  }
22815
22949
 
22816
22950
  // packages/mcp/src/index.ts
22817
- var VERSION8 = getPackageVersion(dirname19(fileURLToPath9(import.meta.url)));
22951
+ var VERSION8 = getPackageVersion(dirname18(fileURLToPath9(import.meta.url)));
22818
22952
  var registry2 = null;
22819
22953
  function getInfoRegistry5() {
22820
22954
  return globalThis.__unipi_info_registry;
@@ -22824,20 +22958,18 @@ function getRegistry() {
22824
22958
  }
22825
22959
  function src_default4(pi) {
22826
22960
  pi.on("session_start", async (_event, ctx) => {
22961
+ const toolApi = pi;
22962
+ const registerTool = typeof toolApi.registerTool === "function" ? (tool) => toolApi.registerTool(tool) : typeof toolApi.registerExternalTool === "function" ? (tool) => toolApi.registerExternalTool(tool) : () => {
22963
+ throw new Error("Pi does not expose a supported MCP tool registration API");
22964
+ };
22965
+ const canUnregisterTools = typeof toolApi.unregisterTool === "function" || typeof toolApi.unregisterExternalTool === "function";
22966
+ const unregisterTool = typeof toolApi.unregisterTool === "function" ? (toolName) => toolApi.unregisterTool(toolName) : typeof toolApi.unregisterExternalTool === "function" ? (toolName) => toolApi.unregisterExternalTool(toolName) : () => {
22967
+ };
22827
22968
  registry2 = new ServerRegistry({
22828
22969
  emitEvent: (event, payload) => emitEvent(pi, event, payload),
22829
- registerTool: (tool) => {
22830
- try {
22831
- pi.registerTool?.(tool) ?? pi.registerExternalTool?.(tool);
22832
- } catch {
22833
- }
22834
- },
22835
- unregisterTool: (toolName) => {
22836
- try {
22837
- pi.unregisterTool?.(toolName) ?? pi.unregisterExternalTool?.(toolName);
22838
- } catch {
22839
- }
22840
- }
22970
+ registerTool,
22971
+ unregisterTool,
22972
+ canUnregisterTools
22841
22973
  });
22842
22974
  const cwd = ctx.cwd ?? process.cwd();
22843
22975
  let servers = [];
@@ -22846,13 +22978,10 @@ function src_default4(pi) {
22846
22978
  servers = result.servers;
22847
22979
  } catch (_err) {
22848
22980
  }
22849
- const startPromises = servers.filter((s) => s.enabled).map(async (server) => {
22850
- try {
22851
- await registry2.startServer(server);
22852
- } catch (err) {
22853
- }
22854
- });
22855
- await Promise.allSettled(startPromises);
22981
+ try {
22982
+ await registry2.startServers(servers.filter((server) => server.enabled));
22983
+ } catch (_err) {
22984
+ }
22856
22985
  const infoRegistry2 = getInfoRegistry5();
22857
22986
  if (infoRegistry2 && registry2) {
22858
22987
  const reg = registry2;
@@ -22909,14 +23038,12 @@ function src_default4(pi) {
22909
23038
  `unipi:${MCP_COMMANDS.STATUS}`,
22910
23039
  `unipi:${MCP_COMMANDS.RELOAD}`
22911
23040
  ],
22912
- tools: activeServers.flatMap(
22913
- (s) => registry2?.getEntry(s.name)?.toolNames ?? []
22914
- )
23041
+ tools: activeServers.flatMap((server) => registry2?.getEntry(server.name)?.toolNames ?? []).sort(compareCodeUnits)
22915
23042
  });
22916
23043
  });
22917
23044
  pi.on("session_shutdown", async (_event, _ctx) => {
22918
23045
  if (registry2) {
22919
- await registry2.stopAll();
23046
+ await registry2.disconnectAll();
22920
23047
  registry2 = null;
22921
23048
  }
22922
23049
  });
@@ -23030,38 +23157,16 @@ Updated: ${catalog.lastUpdated}`,
23030
23157
  }
23031
23158
  });
23032
23159
  pi.registerCommand(`unipi:${MCP_COMMANDS.RELOAD}`, {
23033
- description: "Reload all MCP servers (restart with current config)",
23160
+ description: "Explain how to reload MCP servers safely",
23034
23161
  handler: async (_args, ctx) => {
23035
- const reg = getRegistry();
23036
- if (!reg) {
23037
- ctx.ui.notify("MCP extension not initialized", "warning");
23038
- return;
23039
- }
23040
- const all = reg.getAll();
23041
- if (all.length === 0) {
23042
- ctx.ui.notify("No MCP servers configured. Use /unipi:mcp-add to add one.", "info");
23043
- return;
23044
- }
23045
- ctx.ui.notify(`Reloading ${all.length} MCP server(s)...`, "info");
23046
- let restarted = 0;
23047
- let failed = 0;
23048
- for (const state of all) {
23049
- try {
23050
- await reg.restartServer(state.name);
23051
- restarted++;
23052
- } catch (_err) {
23053
- failed++;
23054
- }
23055
- }
23056
- const msg = failed > 0 ? `Reloaded: ${restarted} ok, ${failed} failed` : `Reloaded ${restarted} MCP server(s) successfully`;
23057
- ctx.ui.notify(msg, failed > 0 ? "warning" : "info");
23162
+ ctx.ui.notify("Restart Pi to reload MCP servers and tool schemas safely.", "info");
23058
23163
  }
23059
23164
  });
23060
23165
  }
23061
23166
 
23062
23167
  // packages/notify/index.ts
23063
23168
  init_core();
23064
- import { dirname as dirname22 } from "node:path";
23169
+ import { dirname as dirname21 } from "node:path";
23065
23170
  import { fileURLToPath as fileURLToPath10 } from "node:url";
23066
23171
 
23067
23172
  // packages/notify/tools.ts
@@ -23071,7 +23176,7 @@ import { Type as Type6 } from "typebox";
23071
23176
  // packages/notify/settings.ts
23072
23177
  init_core();
23073
23178
  import { readFileSync as readFileSync19, writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, existsSync as existsSync24 } from "fs";
23074
- import { dirname as dirname20, join as join22 } from "path";
23179
+ import { dirname as dirname19, join as join22 } from "path";
23075
23180
  import { homedir as homedir17 } from "os";
23076
23181
  function resolveConfigPath() {
23077
23182
  const base = NOTIFY_DIRS.CONFIG.replace("~", homedir17());
@@ -23125,7 +23230,7 @@ function loadConfig2() {
23125
23230
  }
23126
23231
  function saveConfig2(config) {
23127
23232
  const configPath = resolveConfigPath();
23128
- const dir = dirname20(configPath);
23233
+ const dir = dirname19(configPath);
23129
23234
  mkdirSync15(dir, { recursive: true });
23130
23235
  writeFileSync17(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23131
23236
  }
@@ -23186,7 +23291,7 @@ init_core();
23186
23291
 
23187
23292
  // packages/notify/ntfy-config.ts
23188
23293
  import { readFileSync as readFileSync20, writeFileSync as writeFileSync18, mkdirSync as mkdirSync16, existsSync as existsSync25 } from "fs";
23189
- import { dirname as dirname21, join as join23 } from "path";
23294
+ import { dirname as dirname20, join as join23 } from "path";
23190
23295
  import { homedir as homedir18 } from "os";
23191
23296
  var DEFAULT_NTFY_CONFIG = {
23192
23297
  enabled: false,
@@ -23226,7 +23331,7 @@ function loadNtfyConfig(cwd) {
23226
23331
  }
23227
23332
  function saveNtfyConfig(scope, cwd, config) {
23228
23333
  const filePath = scope === "project" ? getProjectNtfyPath(cwd) : getGlobalNtfyPath();
23229
- const dir = dirname21(filePath);
23334
+ const dir = dirname20(filePath);
23230
23335
  mkdirSync16(dir, { recursive: true });
23231
23336
  writeFileSync18(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23232
23337
  }
@@ -23266,7 +23371,7 @@ function migrateFromLegacyConfig() {
23266
23371
  token: ntfySection.token,
23267
23372
  priority: ntfySection.priority ?? 3
23268
23373
  };
23269
- const dir = dirname21(globalNtfyPath);
23374
+ const dir = dirname20(globalNtfyPath);
23270
23375
  mkdirSync16(dir, { recursive: true });
23271
23376
  writeFileSync18(
23272
23377
  globalNtfyPath,
@@ -23322,7 +23427,7 @@ async function isWindowFocusedOnWindows() {
23322
23427
  tmpDir = mkdtempSync(join24(tmpdir2(), "pi-focus-"));
23323
23428
  const scriptPath = join24(tmpDir, "check.ps1");
23324
23429
  writeFileSync19(scriptPath, POWERCHECK_SCRIPT, "utf-8");
23325
- const stdout = await new Promise((resolve13, reject) => {
23430
+ const stdout = await new Promise((resolve14, reject) => {
23326
23431
  execFile(
23327
23432
  "powershell.exe",
23328
23433
  [
@@ -23337,7 +23442,7 @@ async function isWindowFocusedOnWindows() {
23337
23442
  { timeout: 5e3, encoding: "utf-8" },
23338
23443
  (err, out) => {
23339
23444
  if (err) reject(err);
23340
- else resolve13(out);
23445
+ else resolve14(out);
23341
23446
  }
23342
23447
  );
23343
23448
  });
@@ -23379,7 +23484,7 @@ async function sendNativeNotification(title, message, options) {
23379
23484
  if (options?.suppressWhenFocused && await isWindowFocused()) {
23380
23485
  throw new SuppressedError2();
23381
23486
  }
23382
- return new Promise((resolve13, reject) => {
23487
+ return new Promise((resolve14, reject) => {
23383
23488
  notifier.notify(
23384
23489
  {
23385
23490
  title,
@@ -23394,7 +23499,7 @@ async function sendNativeNotification(title, message, options) {
23394
23499
  )
23395
23500
  );
23396
23501
  } else {
23397
- resolve13();
23502
+ resolve14();
23398
23503
  }
23399
23504
  }
23400
23505
  );
@@ -23731,7 +23836,7 @@ function getEnabledPlatforms(config, ntfyEnabled) {
23731
23836
  function unregisterEventListeners() {
23732
23837
  unregisterAll();
23733
23838
  }
23734
- async function dispatchNotification(pi, title, message, eventPlatforms, eventType, config, cwd) {
23839
+ async function dispatchNotification(pi, title, message, eventPlatforms, eventType, config, cwd, priority) {
23735
23840
  const ntfyConfig = loadNtfyConfig(cwd);
23736
23841
  const platforms = eventPlatforms.length > 0 ? eventPlatforms : getEnabledPlatforms(config, ntfyConfig.enabled).length > 0 ? getEnabledPlatforms(config, ntfyConfig.enabled) : config.defaultPlatforms;
23737
23842
  const enabledPlatforms = platforms.filter((p) => {
@@ -23744,8 +23849,8 @@ async function dispatchNotification(pi, title, message, eventPlatforms, eventTyp
23744
23849
  const results = await Promise.all(
23745
23850
  enabledPlatforms.map(async (platform) => {
23746
23851
  try {
23747
- await sendToPlatform(platform, title, message, config, cwd);
23748
- return { platform, success: true };
23852
+ const effectivePriority = await sendToPlatform(platform, title, message, config, cwd, priority);
23853
+ return { platform, success: true, ...effectivePriority === void 0 ? {} : { priority: effectivePriority } };
23749
23854
  } catch (err) {
23750
23855
  if (err instanceof SuppressedError2) {
23751
23856
  return { platform, success: true, suppressed: true };
@@ -23770,14 +23875,21 @@ async function dispatchNotification(pi, title, message, eventPlatforms, eventTyp
23770
23875
  });
23771
23876
  return { results, allSuccess };
23772
23877
  }
23773
- async function sendToPlatform(platform, title, message, config, cwd) {
23878
+ function mapNotifyPriority(platform, priority) {
23879
+ const mappings = {
23880
+ gotify: { low: 2, normal: 5, high: 8 },
23881
+ ntfy: { low: 2, normal: 3, high: 5 }
23882
+ };
23883
+ return mappings[platform][priority];
23884
+ }
23885
+ async function sendToPlatform(platform, title, message, config, cwd, priority) {
23774
23886
  switch (platform) {
23775
23887
  case "native":
23776
23888
  await sendNativeNotification(title, message, {
23777
23889
  windowsAppId: config.native.windowsAppId,
23778
23890
  suppressWhenFocused: config.native.suppressWhenFocused
23779
23891
  });
23780
- break;
23892
+ return void 0;
23781
23893
  case "gotify":
23782
23894
  if (!config.gotify.serverUrl || !config.gotify.appToken) {
23783
23895
  throw new Error("Gotify: serverUrl and appToken are required");
@@ -23787,9 +23899,9 @@ async function sendToPlatform(platform, title, message, config, cwd) {
23787
23899
  config.gotify.appToken,
23788
23900
  title,
23789
23901
  message,
23790
- config.gotify.priority
23902
+ priority ? mapNotifyPriority("gotify", priority) : config.gotify.priority
23791
23903
  );
23792
- break;
23904
+ return priority ? mapNotifyPriority("gotify", priority) : config.gotify.priority;
23793
23905
  case "telegram":
23794
23906
  if (!config.telegram.botToken || !config.telegram.chatId) {
23795
23907
  throw new Error("Telegram: botToken and chatId are required");
@@ -23800,10 +23912,10 @@ async function sendToPlatform(platform, title, message, config, cwd) {
23800
23912
  title,
23801
23913
  message
23802
23914
  );
23803
- break;
23915
+ return void 0;
23804
23916
  case "ntfy": {
23805
23917
  const ntfyConfig = loadNtfyConfig(cwd);
23806
- if (!ntfyConfig.enabled) return;
23918
+ if (!ntfyConfig.enabled) return void 0;
23807
23919
  if (!ntfyConfig.serverUrl || !ntfyConfig.topic) {
23808
23920
  throw new Error("ntfy: serverUrl and topic are required");
23809
23921
  }
@@ -23812,10 +23924,10 @@ async function sendToPlatform(platform, title, message, config, cwd) {
23812
23924
  ntfyConfig.topic,
23813
23925
  title,
23814
23926
  message,
23815
- ntfyConfig.priority,
23927
+ priority ? mapNotifyPriority("ntfy", priority) : ntfyConfig.priority,
23816
23928
  ntfyConfig.token
23817
23929
  );
23818
- break;
23930
+ return priority ? mapNotifyPriority("ntfy", priority) : ntfyConfig.priority;
23819
23931
  }
23820
23932
  }
23821
23933
  }
@@ -23944,7 +24056,6 @@ var NotifyUserSchema = Type6.Object({
23944
24056
  priority: Type6.Optional(
23945
24057
  Type6.String({
23946
24058
  enum: ["low", "normal", "high"],
23947
- default: "normal",
23948
24059
  description: "Priority level"
23949
24060
  })
23950
24061
  ),
@@ -23965,23 +24076,31 @@ function registerNotifyTools(pi) {
23965
24076
  const {
23966
24077
  message,
23967
24078
  title,
23968
- priority: _priority,
24079
+ priority,
23969
24080
  platforms
23970
24081
  } = params;
23971
24082
  const config = loadConfig2();
23972
24083
  const notifTitle = title || "Pi Notification";
23973
24084
  const notifPlatforms = platforms || config.defaultPlatforms;
23974
24085
  const cwd = process.cwd();
23975
- dispatchNotification(
24086
+ const dispatchPromise = dispatchNotification(
23976
24087
  pi,
23977
24088
  notifTitle,
23978
24089
  message,
23979
24090
  notifPlatforms,
23980
24091
  "agent_tool",
23981
24092
  config,
23982
- cwd
23983
- ).catch(() => {
24093
+ cwd,
24094
+ priority
24095
+ );
24096
+ void dispatchPromise.catch(() => {
23984
24097
  });
24098
+ const priorityByPlatform = priority ? Object.fromEntries(
24099
+ notifPlatforms.flatMap((platform) => {
24100
+ if (platform === "gotify" || platform === "ntfy") return [[platform, mapNotifyPriority(platform, priority)]];
24101
+ return [];
24102
+ })
24103
+ ) : void 0;
23985
24104
  return {
23986
24105
  content: [
23987
24106
  {
@@ -23989,7 +24108,7 @@ function registerNotifyTools(pi) {
23989
24108
  text: `Notification sending to ${notifPlatforms.length} platform(s): ${notifPlatforms.join(", ")}`
23990
24109
  }
23991
24110
  ],
23992
- details: { platforms: notifPlatforms }
24111
+ details: { platforms: notifPlatforms, priority, priorityByPlatform }
23993
24112
  };
23994
24113
  }
23995
24114
  });
@@ -26116,7 +26235,7 @@ ${results.join("\n")}`, "info");
26116
26235
  }
26117
26236
 
26118
26237
  // packages/notify/index.ts
26119
- var VERSION9 = getPackageVersion(dirname22(fileURLToPath10(import.meta.url)));
26238
+ var VERSION9 = getPackageVersion(dirname21(fileURLToPath10(import.meta.url)));
26120
26239
  function notify_default(pi) {
26121
26240
  registerNotifyTools(pi);
26122
26241
  registerNotifyCommands(pi);
@@ -26145,6 +26264,9 @@ init_core();
26145
26264
  init_core();
26146
26265
  import * as fs14 from "node:fs";
26147
26266
  import * as path13 from "node:path";
26267
+ import {
26268
+ buildSessionContext as buildSessionContext3
26269
+ } from "@earendil-works/pi-coding-agent";
26148
26270
 
26149
26271
  // packages/milestone/milestone.ts
26150
26272
  init_core();
@@ -26290,7 +26412,7 @@ function getProgressSummary(filePath) {
26290
26412
  }
26291
26413
 
26292
26414
  // packages/milestone/hooks.ts
26293
- var sessionStartMs = 0;
26415
+ var MILESTONE_SNAPSHOT_TYPE = "unipi-milestone-snapshot";
26294
26416
  function formatMilestoneContext(filePath) {
26295
26417
  const summary = getProgressSummary(filePath);
26296
26418
  if (summary.totalItems === 0) return null;
@@ -26303,16 +26425,67 @@ function formatMilestoneContext(filePath) {
26303
26425
  focus
26304
26426
  ].filter(Boolean).join("\n");
26305
26427
  }
26428
+ function formatMilestoneSnapshot(workspace, context) {
26429
+ return [
26430
+ "# UniPi Milestone Snapshot",
26431
+ "This snapshot supersedes all prior UniPi milestone snapshots; use only this snapshot for milestone status.",
26432
+ `Workspace: ${workspace}`,
26433
+ `Status: ${context ? "active" : "inactive"}`,
26434
+ context ?? "No milestones are active for this workspace."
26435
+ ].join("\n\n");
26436
+ }
26437
+ function latestEffectiveSnapshot(branch) {
26438
+ const messages = buildSessionContext3(branch).messages;
26439
+ for (let index = messages.length - 1; index >= 0; index--) {
26440
+ const message = messages[index];
26441
+ if (message.role === "custom" && message.customType === MILESTONE_SNAPSHOT_TYPE) {
26442
+ return {
26443
+ content: message.content,
26444
+ details: message.details
26445
+ };
26446
+ }
26447
+ }
26448
+ return void 0;
26449
+ }
26450
+ function latestHistoricalSnapshot(branch) {
26451
+ for (let index = branch.length - 1; index >= 0; index--) {
26452
+ const entry = branch[index];
26453
+ if (entry.type === "custom_message" && entry.customType === MILESTONE_SNAPSHOT_TYPE) {
26454
+ return {
26455
+ content: entry.content,
26456
+ details: entry.details
26457
+ };
26458
+ }
26459
+ }
26460
+ return void 0;
26461
+ }
26462
+ function isActiveSnapshot2(snapshot) {
26463
+ if (typeof snapshot.details?.active === "boolean") return snapshot.details.active;
26464
+ return typeof snapshot.content === "string" && snapshot.content.includes("Status: active");
26465
+ }
26306
26466
  function registerSessionStartHook(pi) {
26307
- pi.on("before_agent_start", (event) => {
26308
- sessionStartMs = Date.now();
26309
- const cwd = process.cwd();
26310
- const milestonesPath = path13.join(cwd, MILESTONE_DIRS.MILESTONES);
26467
+ pi.on("before_agent_start", (_event, ctx) => {
26468
+ const workspace = ctx.cwd;
26469
+ const milestonesPath = path13.join(workspace, MILESTONE_DIRS.MILESTONES);
26311
26470
  const context = formatMilestoneContext(milestonesPath);
26312
- if (!context) return void 0;
26313
- const currentPrompt = event.systemPrompt ?? "";
26471
+ const branch = ctx.sessionManager.getBranch();
26472
+ const latest = latestEffectiveSnapshot(branch);
26473
+ const historical = latestHistoricalSnapshot(branch);
26474
+ if (!context && !latest && !historical) return void 0;
26475
+ const prior = latest ?? historical;
26476
+ if (!context && prior && !isActiveSnapshot2(prior)) return void 0;
26477
+ const content = formatMilestoneSnapshot(workspace, context);
26478
+ if (latest?.content === content) return void 0;
26314
26479
  return {
26315
- systemPrompt: currentPrompt + "\n\n" + context
26480
+ message: {
26481
+ customType: MILESTONE_SNAPSHOT_TYPE,
26482
+ content,
26483
+ display: false,
26484
+ details: {
26485
+ active: context !== null,
26486
+ workspace
26487
+ }
26488
+ }
26316
26489
  };
26317
26490
  });
26318
26491
  }
@@ -26368,14 +26541,16 @@ function scanModifiedDocs(dirs, since) {
26368
26541
  }
26369
26542
  function registerSessionEndHook(pi) {
26370
26543
  const baselineSnapshots = /* @__PURE__ */ new Map();
26371
- pi.on("session_start", () => {
26544
+ let sessionStartMs = 0;
26545
+ let sessionWorkspace = null;
26546
+ pi.on("session_start", (_event, ctx) => {
26372
26547
  sessionStartMs = Date.now();
26548
+ sessionWorkspace = ctx.cwd;
26373
26549
  baselineSnapshots.clear();
26374
- const cwd = process.cwd();
26375
26550
  const scanDirs = [
26376
- path13.join(cwd, ".unipi/docs/specs"),
26377
- path13.join(cwd, ".unipi/docs/plans"),
26378
- path13.join(cwd, ".unipi/docs/quick-work")
26551
+ path13.join(sessionWorkspace, ".unipi/docs/specs"),
26552
+ path13.join(sessionWorkspace, ".unipi/docs/plans"),
26553
+ path13.join(sessionWorkspace, ".unipi/docs/quick-work")
26379
26554
  ];
26380
26555
  for (const dir of scanDirs) {
26381
26556
  if (!fs14.existsSync(dir)) continue;
@@ -26388,17 +26563,14 @@ function registerSessionEndHook(pi) {
26388
26563
  }
26389
26564
  }
26390
26565
  });
26391
- pi.on("input", (event) => {
26392
- return void 0;
26393
- });
26394
- pi.on("session_shutdown", () => {
26395
- const cwd = process.cwd();
26396
- const milestonesPath = path13.join(cwd, MILESTONE_DIRS.MILESTONES);
26566
+ const syncModifiedDocs = () => {
26567
+ if (!sessionWorkspace) return;
26568
+ const milestonesPath = path13.join(sessionWorkspace, MILESTONE_DIRS.MILESTONES);
26397
26569
  if (!fs14.existsSync(milestonesPath)) return;
26398
26570
  const scanDirs = [
26399
- path13.join(cwd, ".unipi/docs/specs"),
26400
- path13.join(cwd, ".unipi/docs/plans"),
26401
- path13.join(cwd, ".unipi/docs/quick-work")
26571
+ path13.join(sessionWorkspace, ".unipi/docs/specs"),
26572
+ path13.join(sessionWorkspace, ".unipi/docs/plans"),
26573
+ path13.join(sessionWorkspace, ".unipi/docs/quick-work")
26402
26574
  ];
26403
26575
  const modifiedFiles = scanModifiedDocs(scanDirs, sessionStartMs);
26404
26576
  for (const filePath of modifiedFiles) {
@@ -26409,15 +26581,17 @@ function registerSessionEndHook(pi) {
26409
26581
  updateItemStatus(milestonesPath, phase, text, true);
26410
26582
  }
26411
26583
  }
26412
- });
26584
+ };
26585
+ pi.events.on(UNIPI_EVENTS.WORKFLOW_END, syncModifiedDocs);
26586
+ pi.on("session_shutdown", syncModifiedDocs);
26413
26587
  }
26414
26588
 
26415
26589
  // packages/milestone/commands.ts
26416
26590
  init_core();
26417
- import { dirname as dirname23, join as join26 } from "node:path";
26591
+ import { dirname as dirname22, join as join26 } from "node:path";
26418
26592
  import { fileURLToPath as fileURLToPath11 } from "node:url";
26419
26593
  import { readFileSync as readFileSync21 } from "node:fs";
26420
- var SKILLS_DIR = join26(dirname23(fileURLToPath11(import.meta.url)), "skills");
26594
+ var SKILLS_DIR = join26(dirname22(fileURLToPath11(import.meta.url)), "skills");
26421
26595
  function loadSkill(skillName) {
26422
26596
  try {
26423
26597
  return readFileSync21(join26(SKILLS_DIR, skillName, "SKILL.md"), "utf-8");
@@ -26644,9 +26818,9 @@ var KanboardServer = class {
26644
26818
  }
26645
26819
  /** Bind server to a port */
26646
26820
  listen(port) {
26647
- return new Promise((resolve13, reject) => {
26821
+ return new Promise((resolve14, reject) => {
26648
26822
  this.server.once("error", reject);
26649
- this.server.listen(port, () => resolve13());
26823
+ this.server.listen(port, () => resolve14());
26650
26824
  });
26651
26825
  }
26652
26826
  /** Handle incoming HTTP request */
@@ -27615,11 +27789,11 @@ function markAutoCompactionError(state, nowMs) {
27615
27789
  }
27616
27790
 
27617
27791
  // packages/compactor/src/session/db.ts
27618
- import { createHash as createHash2 } from "node:crypto";
27792
+ import { createHash as createHash3 } from "node:crypto";
27619
27793
  import { execFileSync } from "node:child_process";
27620
27794
  import { existsSync as existsSync32, mkdirSync as mkdirSync20 } from "node:fs";
27621
27795
  import { homedir as homedir21 } from "node:os";
27622
- import { dirname as dirname26, join as join33 } from "node:path";
27796
+ import { dirname as dirname25, join as join33 } from "node:path";
27623
27797
  function getWorktreeSuffix() {
27624
27798
  const envSuffix = process.env.COMPACTOR_SESSION_SUFFIX;
27625
27799
  if (envSuffix !== void 0) {
@@ -27633,7 +27807,7 @@ function getWorktreeSuffix() {
27633
27807
  { encoding: "utf-8", timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] }
27634
27808
  ).split(/\r?\n/).find((l) => l.startsWith("worktree "))?.replace("worktree ", "")?.trim();
27635
27809
  if (mainWorktree && cwd !== mainWorktree) {
27636
- const suffix = createHash2("sha256").update(cwd).digest("hex").slice(0, 8);
27810
+ const suffix = createHash3("sha256").update(cwd).digest("hex").slice(0, 8);
27637
27811
  return `__${suffix}`;
27638
27812
  }
27639
27813
  } catch {
@@ -27664,7 +27838,7 @@ var SessionDB = class {
27664
27838
  this.dbPath = opts?.dbPath ?? defaultDBPath("session");
27665
27839
  }
27666
27840
  async init() {
27667
- const dir = dirname26(this.dbPath);
27841
+ const dir = dirname25(this.dbPath);
27668
27842
  if (!existsSync32(dir)) mkdirSync20(dir, { recursive: true });
27669
27843
  const sqlite = await getSQLite();
27670
27844
  const Database2 = sqlite.Database ?? sqlite.default?.Database ?? sqlite.default ?? sqlite;
@@ -27775,7 +27949,7 @@ var SessionDB = class {
27775
27949
  }
27776
27950
  insertEvent(sessionId, event, sourceHook = "PostToolUse") {
27777
27951
  if (!this.stmts) return;
27778
- const dataHash = createHash2("sha256").update(event.data).digest("hex").slice(0, 16).toUpperCase();
27952
+ const dataHash = createHash3("sha256").update(event.data).digest("hex").slice(0, 16).toUpperCase();
27779
27953
  const projectDir = String(event.project_dir ?? "").trim();
27780
27954
  const attributionSource = String(event.attribution_source ?? "unknown");
27781
27955
  const rawConfidence = Number(event.attribution_confidence ?? 0);
@@ -27979,8 +28153,49 @@ function extractEventsFromToolResult(result) {
27979
28153
 
27980
28154
  // packages/compactor/src/session/resume-inject.ts
27981
28155
  init_snapshot();
27982
- init_auto_inject();
28156
+
28157
+ // packages/compactor/src/session/auto-inject.ts
28158
+ var MAX_TOKENS2 = 150;
28159
+ function estimateTokens(text) {
28160
+ return Math.ceil(text.length / 4);
28161
+ }
28162
+ function buildAutoInjection(events) {
28163
+ const parts = [];
28164
+ let tokenBudget = MAX_TOKENS2;
28165
+ const roleEvents = events.filter((e) => e.category === "rule");
28166
+ if (roleEvents.length > 0) {
28167
+ const directive = roleEvents[roleEvents.length - 1].data;
28168
+ const directiveText = `[Role Directive]
28169
+ ${directive}`;
28170
+ const tokens = estimateTokens(directiveText);
28171
+ if (tokens <= tokenBudget) {
28172
+ parts.push(directiveText);
28173
+ tokenBudget -= tokens;
28174
+ }
28175
+ }
28176
+ if (tokenBudget > 80) {
28177
+ const intentEvents = events.filter((e) => e.category === "intent");
28178
+ if (intentEvents.length > 0) {
28179
+ const mode = intentEvents[intentEvents.length - 1].data;
28180
+ const modeText = `[Session Mode]
28181
+ ${mode}`;
28182
+ const tokens = estimateTokens(modeText);
28183
+ if (tokens <= tokenBudget) {
28184
+ parts.push(modeText);
28185
+ tokenBudget -= tokens;
28186
+ }
28187
+ }
28188
+ }
28189
+ if (parts.length === 0) return null;
28190
+ const text = parts.join("\n\n");
28191
+ return { text, tokens: estimateTokens(text) };
28192
+ }
28193
+
28194
+ // packages/compactor/src/session/resume-inject.ts
27983
28195
  init_manager();
28196
+ function isSessionContinuityEnabled(config) {
28197
+ return config.sessionContinuity.enabled && config.sessionContinuity.mode !== "off";
28198
+ }
27984
28199
  async function injectResumeSnapshot(db, sessionId, opts) {
27985
28200
  const resume = db.getResume(sessionId);
27986
28201
  if (!resume || resume.consumed) return null;
@@ -28003,11 +28218,22 @@ ${autoInjection}`;
28003
28218
  db.markResumeConsumed(sessionId);
28004
28219
  return fullSnapshot;
28005
28220
  }
28221
+ async function buildResumeContextMessage(db, sessionId) {
28222
+ const snapshot = await injectResumeSnapshot(db, sessionId);
28223
+ if (!snapshot) return void 0;
28224
+ return {
28225
+ message: {
28226
+ customType: "unipi-compactor-resume",
28227
+ content: snapshot,
28228
+ display: false
28229
+ }
28230
+ };
28231
+ }
28006
28232
 
28007
28233
  // packages/compactor/src/executor/executor.ts
28008
28234
  import { spawn as spawn3, execSync, execFileSync as execFileSync2 } from "node:child_process";
28009
28235
  import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync24, rmSync as rmSync3 } from "node:fs";
28010
- import { join as join34, resolve as resolve10 } from "node:path";
28236
+ import { join as join34, resolve as resolve11 } from "node:path";
28011
28237
  import { tmpdir as tmpdir3 } from "node:os";
28012
28238
 
28013
28239
  // packages/compactor/src/executor/runtime.ts
@@ -28099,7 +28325,7 @@ var OS_TMPDIR = (() => {
28099
28325
  process.platform === "darwin" ? ["DARWIN_USER_TEMP_DIR"] : ["-u", "-d"],
28100
28326
  { env: { ...process.env, TMPDIR: void 0 }, encoding: "utf-8" }
28101
28327
  ).trim();
28102
- const dir = process.platform === "darwin" ? result : resolve10(result, "..");
28328
+ const dir = process.platform === "darwin" ? result : resolve11(result, "..");
28103
28329
  if (dir && dir !== process.cwd()) return dir;
28104
28330
  } catch {
28105
28331
  }
@@ -28220,7 +28446,7 @@ var PolyglotExecutor = class {
28220
28446
  async executeFile(opts) {
28221
28447
  const { language, path: path28, timeout = 3e4, background = false } = opts;
28222
28448
  const cmd = buildCommand(this.#runtimes, language, path28);
28223
- const cwd = language === "shell" ? this.#projectRoot : resolve10(path28, "..");
28449
+ const cwd = language === "shell" ? this.#projectRoot : resolve11(path28, "..");
28224
28450
  return this.#spawn(cmd, cwd, "", timeout, background);
28225
28451
  }
28226
28452
  #writeScript(tmpDir, code, language) {
@@ -28243,7 +28469,7 @@ var PolyglotExecutor = class {
28243
28469
  return filePath;
28244
28470
  }
28245
28471
  async #spawn(cmd, cwd, tmpDir, timeout, background) {
28246
- return new Promise((resolve13) => {
28472
+ return new Promise((resolve14) => {
28247
28473
  const proc = spawn3(cmd[0], cmd.slice(1), {
28248
28474
  cwd,
28249
28475
  env: sanitizeEnv(),
@@ -28279,14 +28505,14 @@ var PolyglotExecutor = class {
28279
28505
  }, timeout);
28280
28506
  proc.on("error", (err) => {
28281
28507
  if (timer) clearTimeout(timer);
28282
- resolve13({ stdout, stderr: err.message, exitCode: 1, timedOut: false });
28508
+ resolve14({ stdout, stderr: err.message, exitCode: 1, timedOut: false });
28283
28509
  });
28284
28510
  proc.on("close", (code) => {
28285
28511
  if (timer) clearTimeout(timer);
28286
28512
  if (background && proc.pid) {
28287
28513
  this.#backgroundedPids.add(proc.pid);
28288
28514
  }
28289
- resolve13({
28515
+ resolve14({
28290
28516
  stdout: stdout.slice(0, this.#hardCapBytes),
28291
28517
  stderr: stderr.slice(0, this.#hardCapBytes),
28292
28518
  exitCode: code ?? 0,
@@ -28324,7 +28550,7 @@ init_core();
28324
28550
  init_hooks();
28325
28551
 
28326
28552
  // packages/compactor/src/compaction/search-entries.ts
28327
- import { createHash as createHash4 } from "node:crypto";
28553
+ import { createHash as createHash5 } from "node:crypto";
28328
28554
  function tokenize(text) {
28329
28555
  return text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1);
28330
28556
  }
@@ -28368,7 +28594,7 @@ function searchEntries(blocks, query, opts) {
28368
28594
  kind: b.kind
28369
28595
  }));
28370
28596
  const hashSource = docs.length > 0 ? `${docs.length}:${docs[0].text.slice(0, 80)}:${docs[docs.length - 1].text.slice(-80)}` : "empty";
28371
- const currentHash = createHash4("sha256").update(hashSource).digest("hex");
28597
+ const currentHash = createHash5("sha256").update(hashSource).digest("hex");
28372
28598
  let index;
28373
28599
  let docCount;
28374
28600
  let avgDocLen;
@@ -28811,8 +29037,7 @@ function compactTool() {
28811
29037
  }
28812
29038
 
28813
29039
  // packages/compactor/src/tools/ctx-execute.ts
28814
- async function ctxExecute(input) {
28815
- const executor = new PolyglotExecutor();
29040
+ async function ctxExecute(input, executor = new PolyglotExecutor()) {
28816
29041
  return executor.execute({
28817
29042
  language: input.language,
28818
29043
  code: input.code,
@@ -28822,12 +29047,11 @@ async function ctxExecute(input) {
28822
29047
 
28823
29048
  // packages/compactor/src/tools/ctx-execute-file.ts
28824
29049
  import { readFileSync as readFileSync30 } from "node:fs";
28825
- async function ctxExecuteFile(input) {
29050
+ async function ctxExecuteFile(input, executor = new PolyglotExecutor()) {
28826
29051
  const content = readFileSync30(input.path, "utf-8");
28827
29052
  const code = `const FILE_CONTENT = ${JSON.stringify(content)};
28828
29053
  // User script follows:
28829
29054
  `;
28830
- const executor = new PolyglotExecutor();
28831
29055
  return executor.executeFile({
28832
29056
  language: input.language,
28833
29057
  path: input.path,
@@ -28837,9 +29061,8 @@ async function ctxExecuteFile(input) {
28837
29061
  }
28838
29062
 
28839
29063
  // packages/compactor/src/tools/ctx-batch-execute.ts
28840
- async function ctxBatchExecute(items) {
29064
+ async function ctxBatchExecute(items, executor = new PolyglotExecutor()) {
28841
29065
  const results = [];
28842
- const executor = new PolyglotExecutor();
28843
29066
  for (const item of items) {
28844
29067
  if (item.type === "execute") {
28845
29068
  const result = await executor.execute({
@@ -29024,74 +29247,85 @@ ${lines.join("\n\n")}`,
29024
29247
  deprecationLog2("vcc_recall", "session_recall");
29025
29248
  return recallExec(tcId, p);
29026
29249
  } });
29027
- const sandboxExec = async (_toolCallId, params) => {
29028
- try {
29029
- const c = deps.getCounters?.();
29030
- if (c) {
29031
- c.sandboxRuns++;
29250
+ if (deps.sandbox) {
29251
+ const { executor, allowedLanguages } = deps.sandbox;
29252
+ const assertAllowedLanguage = (language) => {
29253
+ if (!allowedLanguages.includes(language)) {
29254
+ throw new Error(`Language "${language}" is disabled by Compactor sandbox settings. Allowed: ${allowedLanguages.join(", ") || "none"}`);
29032
29255
  }
29033
- deps.sessionDB.incrementSandboxRuns(deps.getSessionId());
29034
- const result = await ctxExecute(params);
29035
- const parts = [];
29036
- if (result.stdout) parts.push(result.stdout);
29037
- if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
29038
- if (result.timedOut) parts.push("[timed out]");
29039
- if (result.exitCode !== 0) parts.push(`[exit code: ${result.exitCode}]`);
29040
- return textResult2(parts.join("\n") || "(no output)", result);
29041
- } catch (err) {
29042
- return textResult2(`Execution error: ${err}`, { error: true });
29043
- }
29044
- };
29045
- pi.registerTool({ name: "sandbox", label: "Sandbox", description: "Run code in a sandboxed environment. Supports 11 languages. Only stdout enters context.", parameters: SandboxParams, execute: sandboxExec });
29046
- pi.registerTool({ name: "ctx_execute", label: "Sandbox", description: "Run code in sandbox. (DEPRECATED: use sandbox instead)", parameters: CtxExecuteParams, async execute(tcId, p) {
29047
- deprecationLog2("ctx_execute", "sandbox");
29048
- return sandboxExec(tcId, p);
29049
- } });
29050
- const sandboxFileExec = async (_toolCallId, params) => {
29051
- try {
29052
- const c = deps.getCounters?.();
29053
- if (c) {
29054
- c.sandboxRuns++;
29256
+ };
29257
+ const sandboxExec = async (_toolCallId, params) => {
29258
+ try {
29259
+ const c = deps.getCounters?.();
29260
+ if (c) {
29261
+ c.sandboxRuns++;
29262
+ }
29263
+ deps.sessionDB.incrementSandboxRuns(deps.getSessionId());
29264
+ assertAllowedLanguage(params.language);
29265
+ const result = await ctxExecute(params, executor);
29266
+ const parts = [];
29267
+ if (result.stdout) parts.push(result.stdout);
29268
+ if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
29269
+ if (result.timedOut) parts.push("[timed out]");
29270
+ if (result.exitCode !== 0) parts.push(`[exit code: ${result.exitCode}]`);
29271
+ return textResult2(parts.join("\n") || "(no output)", result);
29272
+ } catch (err) {
29273
+ return textResult2(`Execution error: ${err}`, { error: true });
29055
29274
  }
29056
- deps.sessionDB.incrementSandboxRuns(deps.getSessionId());
29057
- const result = await ctxExecuteFile(params);
29058
- const parts = [];
29059
- if (result.stdout) parts.push(result.stdout);
29060
- if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
29061
- if (result.timedOut) parts.push("[timed out]");
29062
- return textResult2(parts.join("\n") || "(no output)", result);
29063
- } catch (err) {
29064
- return textResult2(`Execution error: ${err}`, { error: true });
29065
- }
29066
- };
29067
- pi.registerTool({ name: "sandbox_file", label: "Sandbox File", description: "Execute a file in the sandbox. File content is injected as FILE_CONTENT variable.", parameters: SandboxFileParams, execute: sandboxFileExec });
29068
- pi.registerTool({ name: "ctx_execute_file", label: "Sandbox File", description: "Execute file in sandbox. (DEPRECATED: use sandbox_file instead)", parameters: CtxExecuteFileParams, async execute(tcId, p) {
29069
- deprecationLog2("ctx_execute_file", "sandbox_file");
29070
- return sandboxFileExec(tcId, p);
29071
- } });
29072
- const sandboxBatchExec = async (_toolCallId, params) => {
29073
- try {
29074
- const c = deps.getCounters?.();
29075
- if (c) {
29076
- c.sandboxRuns++;
29275
+ };
29276
+ pi.registerTool({ name: "sandbox", label: "Sandbox", description: "Run code in a sandboxed environment. Supports 11 languages. Only stdout enters context.", parameters: SandboxParams, execute: sandboxExec });
29277
+ pi.registerTool({ name: "ctx_execute", label: "Sandbox", description: "Run code in sandbox. (DEPRECATED: use sandbox instead)", parameters: CtxExecuteParams, async execute(tcId, p) {
29278
+ deprecationLog2("ctx_execute", "sandbox");
29279
+ return sandboxExec(tcId, p);
29280
+ } });
29281
+ const sandboxFileExec = async (_toolCallId, params) => {
29282
+ try {
29283
+ const c = deps.getCounters?.();
29284
+ if (c) {
29285
+ c.sandboxRuns++;
29286
+ }
29287
+ deps.sessionDB.incrementSandboxRuns(deps.getSessionId());
29288
+ assertAllowedLanguage(params.language);
29289
+ const result = await ctxExecuteFile(params, executor);
29290
+ const parts = [];
29291
+ if (result.stdout) parts.push(result.stdout);
29292
+ if (result.stderr) parts.push(`[stderr] ${result.stderr}`);
29293
+ if (result.timedOut) parts.push("[timed out]");
29294
+ return textResult2(parts.join("\n") || "(no output)", result);
29295
+ } catch (err) {
29296
+ return textResult2(`Execution error: ${err}`, { error: true });
29077
29297
  }
29078
- deps.sessionDB.incrementSandboxRuns(deps.getSessionId());
29079
- const result = await ctxBatchExecute(params.items);
29080
- const summaries = result.results.map((r, i) => {
29081
- const s = r.result.stdout?.slice(0, 200) || "(no output)";
29082
- return `[${i}] execute \u2192 ${r.result.exitCode === 0 ? "ok" : "fail"}: ${s}`;
29083
- });
29084
- return textResult2(`Batch results (${result.results.length} items):
29298
+ };
29299
+ pi.registerTool({ name: "sandbox_file", label: "Sandbox File", description: "Execute a file in the sandbox. File content is injected as FILE_CONTENT variable.", parameters: SandboxFileParams, execute: sandboxFileExec });
29300
+ pi.registerTool({ name: "ctx_execute_file", label: "Sandbox File", description: "Execute file in sandbox. (DEPRECATED: use sandbox_file instead)", parameters: CtxExecuteFileParams, async execute(tcId, p) {
29301
+ deprecationLog2("ctx_execute_file", "sandbox_file");
29302
+ return sandboxFileExec(tcId, p);
29303
+ } });
29304
+ const sandboxBatchExec = async (_toolCallId, params) => {
29305
+ try {
29306
+ const c = deps.getCounters?.();
29307
+ if (c) {
29308
+ c.sandboxRuns++;
29309
+ }
29310
+ deps.sessionDB.incrementSandboxRuns(deps.getSessionId());
29311
+ for (const item of params.items) assertAllowedLanguage(item.language);
29312
+ const result = await ctxBatchExecute(params.items, executor);
29313
+ const summaries = result.results.map((r, i) => {
29314
+ const s = r.result.stdout?.slice(0, 200) || "(no output)";
29315
+ return `[${i}] execute \u2192 ${r.result.exitCode === 0 ? "ok" : "fail"}: ${s}`;
29316
+ });
29317
+ return textResult2(`Batch results (${result.results.length} items):
29085
29318
  ${summaries.join("\n")}`, result);
29086
- } catch (err) {
29087
- return textResult2(`Batch error: ${err}`, { error: true });
29088
- }
29089
- };
29090
- pi.registerTool({ name: "sandbox_batch", label: "Sandbox Batch", description: "Run multiple code executions atomically as a batch.", parameters: SandboxBatchParams, execute: sandboxBatchExec });
29091
- pi.registerTool({ name: "ctx_batch_execute", label: "Sandbox Batch", description: "Run batch operations. (DEPRECATED: use sandbox_batch instead)", parameters: CtxBatchExecuteParams, async execute(tcId, p) {
29092
- deprecationLog2("ctx_batch_execute", "sandbox_batch");
29093
- return sandboxBatchExec(tcId, p);
29094
- } });
29319
+ } catch (err) {
29320
+ return textResult2(`Batch error: ${err}`, { error: true });
29321
+ }
29322
+ };
29323
+ pi.registerTool({ name: "sandbox_batch", label: "Sandbox Batch", description: "Run multiple code executions atomically as a batch.", parameters: SandboxBatchParams, execute: sandboxBatchExec });
29324
+ pi.registerTool({ name: "ctx_batch_execute", label: "Sandbox Batch", description: "Run batch operations. (DEPRECATED: use sandbox_batch instead)", parameters: CtxBatchExecuteParams, async execute(tcId, p) {
29325
+ deprecationLog2("ctx_batch_execute", "sandbox_batch");
29326
+ return sandboxBatchExec(tcId, p);
29327
+ } });
29328
+ }
29095
29329
  const statsExec = async () => {
29096
29330
  try {
29097
29331
  const result = await ctxStats(deps.sessionDB, deps.getSessionId(), deps.getCounters?.());
@@ -29187,9 +29421,6 @@ function measureResponseBytes(event) {
29187
29421
  function isSandboxTool(name) {
29188
29422
  return name === "bash" || name === "Bash";
29189
29423
  }
29190
- function isIndexTool(_name) {
29191
- return false;
29192
- }
29193
29424
  function compactorExtension(pi) {
29194
29425
  let sessionDB = null;
29195
29426
  let executor = null;
@@ -29207,7 +29438,6 @@ function compactorExtension(pi) {
29207
29438
  const getCounters = () => counters;
29208
29439
  const runtimeStats = {
29209
29440
  bytesReturned: {},
29210
- bytesIndexed: 0,
29211
29441
  bytesSandboxed: 0,
29212
29442
  calls: {},
29213
29443
  sessionStart: Date.now(),
@@ -29225,7 +29455,7 @@ function compactorExtension(pi) {
29225
29455
  } catch {
29226
29456
  sessionDB = null;
29227
29457
  }
29228
- executor = new PolyglotExecutor();
29458
+ executor = null;
29229
29459
  };
29230
29460
  registerCompactionHooks(pi, {
29231
29461
  getSessionDB: () => sessionDB,
@@ -29254,7 +29484,6 @@ function compactorExtension(pi) {
29254
29484
  }
29255
29485
  }
29256
29486
  runtimeStats.bytesReturned = {};
29257
- runtimeStats.bytesIndexed = 0;
29258
29487
  runtimeStats.bytesSandboxed = 0;
29259
29488
  runtimeStats.calls = {};
29260
29489
  runtimeStats.sessionStart = Date.now();
@@ -29263,11 +29492,17 @@ function compactorExtension(pi) {
29263
29492
  autoCompactionState = createAutoCompactionState();
29264
29493
  sessionDB?.ensureSession(fullSessionId, projectDir);
29265
29494
  if (sessionDB) {
29495
+ const sandboxEnabled = config.sandboxExecution.enabled && config.sandboxExecution.mode !== "off";
29496
+ executor = sandboxEnabled ? new PolyglotExecutor({ hardCapBytes: config.sandboxExecution.outputLimit, projectRoot: projectDir }) : null;
29266
29497
  registerCompactorTools(pi, {
29267
29498
  sessionDB,
29268
29499
  getSessionId: () => currentSessionId,
29269
29500
  getBlocks: () => cachedBlocks,
29270
- getCounters
29501
+ getCounters,
29502
+ sandbox: executor ? {
29503
+ executor,
29504
+ allowedLanguages: config.sandboxExecution.allowedLanguages
29505
+ } : void 0
29271
29506
  });
29272
29507
  }
29273
29508
  registerCommands4(pi, getCommandDeps());
@@ -29353,21 +29588,11 @@ function compactorExtension(pi) {
29353
29588
  }
29354
29589
  } catch {
29355
29590
  }
29356
- if (sessionDB) {
29357
- const snapshot = await injectResumeSnapshot(sessionDB, currentSessionId);
29358
- debug("resume_snapshot", { injected: !!snapshot });
29359
- if (snapshot && sessionDB) {
29360
- try {
29361
- const { buildAutoInjection: buildAutoInjection2 } = await Promise.resolve().then(() => (init_auto_inject(), auto_inject_exports));
29362
- const events = sessionDB.getEvents(currentSessionId, { limit: 100 });
29363
- const autoInjection = buildAutoInjection2(events);
29364
- if (autoInjection) {
29365
- debug("auto_injection", { tokens: autoInjection.tokens, length: autoInjection.text.length });
29366
- }
29367
- } catch (err) {
29368
- debug("auto_injection_error", { error: String(err) });
29369
- }
29370
- }
29591
+ const continuityEnabled = isSessionContinuityEnabled(config);
29592
+ if (sessionDB && continuityEnabled) {
29593
+ const resumeContext = await buildResumeContextMessage(sessionDB, currentSessionId);
29594
+ debug("resume_snapshot", { injected: !!resumeContext });
29595
+ return resumeContext;
29371
29596
  }
29372
29597
  });
29373
29598
  pi.on("turn_end", async (_event, ctx) => {
@@ -29423,7 +29648,8 @@ function compactorExtension(pi) {
29423
29648
  }
29424
29649
  });
29425
29650
  pi.on("session_before_compact", async (event, _ctx) => {
29426
- if (sessionDB) {
29651
+ const continuityEnabled = isSessionContinuityEnabled(config);
29652
+ if (sessionDB && continuityEnabled) {
29427
29653
  const sessionId = currentSessionId;
29428
29654
  const events = sessionDB.getEvents(sessionId, { limit: 1e3 });
29429
29655
  const stats = sessionDB.getSessionStats(sessionId);
@@ -29442,12 +29668,18 @@ function compactorExtension(pi) {
29442
29668
  counters.compactions++;
29443
29669
  const compactionEntry = event.compactionEntry;
29444
29670
  const tokensBefore = compactionEntry?.tokensBefore ?? 0;
29671
+ let summarized = 0;
29672
+ let kept = 0;
29673
+ let tokensSaved = 0;
29445
29674
  if (tokensBefore > 0) {
29446
29675
  const charsBefore = tokensBefore * 4;
29447
- const tokensAfter = event.tokensAfter ?? Math.round(tokensBefore * 0.12);
29676
+ const tokensAfter = Math.round(tokensBefore * 0.12);
29677
+ kept = tokensAfter;
29678
+ tokensSaved = tokensBefore - tokensAfter;
29448
29679
  const charsKept = tokensAfter * 4;
29449
29680
  const messagesSummarized = Math.max(1, Math.round(tokensBefore / 500));
29450
- counters.totalTokensCompacted += tokensBefore - tokensAfter;
29681
+ summarized = messagesSummarized;
29682
+ counters.totalTokensCompacted += tokensSaved;
29451
29683
  sessionDB.addCompactionStats(sessionId, charsBefore, charsKept, messagesSummarized);
29452
29684
  } else {
29453
29685
  try {
@@ -29457,12 +29689,23 @@ function compactorExtension(pi) {
29457
29689
  const estTokensAfter = Math.round(estTokensBefore * 0.12);
29458
29690
  const charsBefore = estTokensBefore * 4;
29459
29691
  const charsKept = estTokensAfter * 4;
29460
- counters.totalTokensCompacted += estTokensBefore - estTokensAfter;
29692
+ summarized = eventCount;
29693
+ kept = estTokensAfter;
29694
+ tokensSaved = estTokensBefore - estTokensAfter;
29695
+ counters.totalTokensCompacted += tokensSaved;
29461
29696
  sessionDB.addCompactionStats(sessionId, charsBefore, charsKept, eventCount);
29462
29697
  }
29463
29698
  } catch {
29464
29699
  }
29465
29700
  }
29701
+ const totalEstimated = tokensSaved + kept;
29702
+ emitEvent(pi, UNIPI_EVENTS.COMPACTOR_COMPACTED, {
29703
+ sessionId,
29704
+ summarized,
29705
+ kept,
29706
+ tokensSaved,
29707
+ compressionRatio: kept > 0 ? `${Math.round(totalEstimated / kept)}:1` : "0:1"
29708
+ });
29466
29709
  debug("session_compact", { sessionId, tokensBefore, hasCompactionEntry: !!compactionEntry });
29467
29710
  }
29468
29711
  });
@@ -29554,30 +29797,10 @@ function compactorExtension(pi) {
29554
29797
  if (isSandboxTool(tName)) {
29555
29798
  runtimeStats.bytesSandboxed += responseBytes;
29556
29799
  }
29557
- if (isIndexTool(tName)) {
29558
- runtimeStats.bytesIndexed += responseBytes;
29559
- }
29560
29800
  }
29561
29801
  } catch {
29562
29802
  }
29563
29803
  const toolName = event.toolName ?? "";
29564
- const td = config.toolDisplay;
29565
- const toolConfig = {
29566
- readOutputMode: td?.mode,
29567
- searchOutputMode: td?.mode,
29568
- bashOutputMode: td?.mode,
29569
- previewLines: 20,
29570
- bashCollapsedLines: 5,
29571
- showTruncationHints: true
29572
- };
29573
- try {
29574
- const { applyToolDisplayOverride: applyToolDisplayOverride2 } = await Promise.resolve().then(() => (init_tool_overrides(), tool_overrides_exports));
29575
- const override = applyToolDisplayOverride2(toolName, event, toolConfig);
29576
- if (override !== void 0) {
29577
- return override;
29578
- }
29579
- } catch {
29580
- }
29581
29804
  const diffToolNames = ["edit", "Edit", "write", "Write"];
29582
29805
  if (diffToolNames.includes(toolName)) {
29583
29806
  try {
@@ -30886,14 +31109,6 @@ var FooterRenderer = class _FooterRenderer {
30886
31109
  init_core();
30887
31110
  function subscribeToEvents(pi, registry3) {
30888
31111
  const unsubscribers = [];
30889
- unsubscribers.push(
30890
- pi.events.on(UNIPI_EVENTS.COMPACTOR_STATS_UPDATED, (event) => {
30891
- try {
30892
- registry3.updateData("compactor", event);
30893
- } catch {
30894
- }
30895
- })
30896
- );
30897
31112
  unsubscribers.push(
30898
31113
  pi.events.on(UNIPI_EVENTS.COMPACTOR_COMPACTED, (event) => {
30899
31114
  try {
@@ -32865,7 +33080,7 @@ import { Key as Key6, matchesKey as matchesKey14, truncateToWidth as truncateToW
32865
33080
  // packages/updater/src/readme.ts
32866
33081
  init_core();
32867
33082
  init_core();
32868
- import { dirname as dirname28 } from "node:path";
33083
+ import { dirname as dirname27 } from "node:path";
32869
33084
  import { fileURLToPath as fileURLToPath12 } from "node:url";
32870
33085
  import { existsSync as existsSync39, readFileSync as readFileSync34 } from "fs";
32871
33086
  import { join as join38 } from "path";
@@ -32892,7 +33107,7 @@ var PACKAGE_MAP = {
32892
33107
  updater: MODULES.UPDATER
32893
33108
  };
32894
33109
  function resolveUnipiRoot() {
32895
- const dir = dirname28(fileURLToPath12(import.meta.url));
33110
+ const dir = dirname27(fileURLToPath12(import.meta.url));
32896
33111
  const root = findPackageRoot(dir, "@pi-unipi/unipi");
32897
33112
  return root ?? join38(dir, "..");
32898
33113
  }
@@ -33229,7 +33444,7 @@ import { Key as Key7, matchesKey as matchesKey15, truncateToWidth as truncateToW
33229
33444
  // packages/updater/src/changelog.ts
33230
33445
  init_core();
33231
33446
  import { existsSync as existsSync40, readFileSync as readFileSync36 } from "fs";
33232
- import { dirname as dirname29, join as join39 } from "node:path";
33447
+ import { dirname as dirname28, join as join39 } from "node:path";
33233
33448
  import { fileURLToPath as fileURLToPath13 } from "node:url";
33234
33449
 
33235
33450
  // packages/updater/src/version.ts
@@ -33254,7 +33469,7 @@ function isNewerVersion(latest, current) {
33254
33469
  // packages/updater/src/changelog.ts
33255
33470
  function resolveChangelogPath() {
33256
33471
  try {
33257
- const dir = dirname29(fileURLToPath13(import.meta.url));
33472
+ const dir = dirname28(fileURLToPath13(import.meta.url));
33258
33473
  const root = findPackageRoot(dir, "@pi-unipi/unipi");
33259
33474
  if (root) {
33260
33475
  const packaged = join39(root, "CHANGELOG.md");
@@ -33515,7 +33730,7 @@ function renderChangelogOverlay() {
33515
33730
  // packages/updater/src/settings.ts
33516
33731
  init_core();
33517
33732
  import { readFileSync as readFileSync37, writeFileSync as writeFileSync26, mkdirSync as mkdirSync22, existsSync as existsSync42 } from "fs";
33518
- import { dirname as dirname30, join as join40 } from "path";
33733
+ import { dirname as dirname29, join as join40 } from "path";
33519
33734
  import { homedir as homedir24 } from "os";
33520
33735
  var DEFAULT_CONFIG6 = {
33521
33736
  checkIntervalMs: 36e5,
@@ -33547,7 +33762,7 @@ function loadConfig5() {
33547
33762
  }
33548
33763
  function saveConfig4(config) {
33549
33764
  const configPath = resolveConfigPath2();
33550
- const dir = dirname30(configPath);
33765
+ const dir = dirname29(configPath);
33551
33766
  mkdirSync22(dir, { recursive: true });
33552
33767
  writeFileSync26(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
33553
33768
  }
@@ -33783,7 +33998,7 @@ init_core();
33783
33998
  // packages/updater/src/cache.ts
33784
33999
  init_core();
33785
34000
  import { readFileSync as readFileSync38, writeFileSync as writeFileSync27, mkdirSync as mkdirSync23, existsSync as existsSync43 } from "fs";
33786
- import { dirname as dirname31, join as join41 } from "path";
34001
+ import { dirname as dirname30, join as join41 } from "path";
33787
34002
  import { homedir as homedir25 } from "os";
33788
34003
  function resolveCachePath() {
33789
34004
  const base = UPDATER_DIRS.CACHE.replace("~", homedir25());
@@ -33805,7 +34020,7 @@ function readLastCheck() {
33805
34020
  }
33806
34021
  function writeLastCheck(cache) {
33807
34022
  const cachePath = resolveCachePath();
33808
- const dir = dirname31(cachePath);
34023
+ const dir = dirname30(cachePath);
33809
34024
  mkdirSync23(dir, { recursive: true });
33810
34025
  writeFileSync27(cachePath, JSON.stringify(cache, null, 2) + "\n", "utf-8");
33811
34026
  }
@@ -34215,8 +34430,8 @@ init_core();
34215
34430
  import { Key as Key10 } from "@earendil-works/pi-tui";
34216
34431
 
34217
34432
  // packages/input-shortcuts/src/registers.ts
34218
- import { existsSync as existsSync44, mkdirSync as mkdirSync24, readFileSync as readFileSync39, renameSync as renameSync7, writeFileSync as writeFileSync28 } from "node:fs";
34219
- import { dirname as dirname32, join as join42 } from "node:path";
34433
+ import { existsSync as existsSync44, mkdirSync as mkdirSync24, readFileSync as readFileSync39, renameSync as renameSync8, writeFileSync as writeFileSync28 } from "node:fs";
34434
+ import { dirname as dirname31, join as join42 } from "node:path";
34220
34435
 
34221
34436
  // packages/input-shortcuts/src/types.ts
34222
34437
  var DEFAULT_CONFIG7 = {
@@ -34286,13 +34501,13 @@ var RegisterStore = class {
34286
34501
  /** Atomic write: write to .tmp then rename. */
34287
34502
  save() {
34288
34503
  try {
34289
- const dir = dirname32(this.filePath);
34504
+ const dir = dirname31(this.filePath);
34290
34505
  if (!existsSync44(dir)) {
34291
34506
  mkdirSync24(dir, { recursive: true });
34292
34507
  }
34293
34508
  const tmpPath = this.filePath + ".tmp";
34294
34509
  writeFileSync28(tmpPath, JSON.stringify(this.data, null, 2), "utf-8");
34295
- renameSync7(tmpPath, this.filePath);
34510
+ renameSync8(tmpPath, this.filePath);
34296
34511
  } catch {
34297
34512
  }
34298
34513
  }
@@ -34513,8 +34728,8 @@ var ChordOverlay = class extends Container2 {
34513
34728
 
34514
34729
  // packages/input-shortcuts/src/settings-overlay.ts
34515
34730
  import { SettingsList as SettingsList3 } from "@earendil-works/pi-tui";
34516
- import { existsSync as existsSync45, mkdirSync as mkdirSync25, readFileSync as readFileSync40, renameSync as renameSync8, writeFileSync as writeFileSync29 } from "node:fs";
34517
- import { dirname as dirname33, join as join43 } from "node:path";
34731
+ import { existsSync as existsSync45, mkdirSync as mkdirSync25, readFileSync as readFileSync40, renameSync as renameSync9, writeFileSync as writeFileSync29 } from "node:fs";
34732
+ import { dirname as dirname32, join as join43 } from "node:path";
34518
34733
  var ALT_KEY_OPTIONS = [
34519
34734
  "alt+a",
34520
34735
  "alt+b",
@@ -34563,13 +34778,13 @@ function loadConfig6(baseDir) {
34563
34778
  function saveConfig5(config, baseDir) {
34564
34779
  const filePath = baseDir ? join43(baseDir, CONFIG_FILE) : CONFIG_FILE;
34565
34780
  try {
34566
- const dir = dirname33(filePath);
34781
+ const dir = dirname32(filePath);
34567
34782
  if (!existsSync45(dir)) {
34568
34783
  mkdirSync25(dir, { recursive: true });
34569
34784
  }
34570
34785
  const tmpPath = filePath + ".tmp";
34571
34786
  writeFileSync29(tmpPath, JSON.stringify(config, null, 2), "utf-8");
34572
- renameSync8(tmpPath, filePath);
34787
+ renameSync9(tmpPath, filePath);
34573
34788
  } catch {
34574
34789
  }
34575
34790
  }
@@ -34997,7 +35212,7 @@ import { Type as Type8 } from "typebox";
34997
35212
  // packages/cocoindex/bridge.ts
34998
35213
  init_core();
34999
35214
  import { execFileSync as execFileSync3, spawn as spawn4 } from "node:child_process";
35000
- import { existsSync as existsSync46, mkdirSync as mkdirSync26, readFileSync as readFileSync41, writeFileSync as writeFileSync30, readdirSync as readdirSync13, statSync as statSync8 } from "node:fs";
35215
+ import { existsSync as existsSync46, mkdirSync as mkdirSync26, readFileSync as readFileSync41, writeFileSync as writeFileSync30, readdirSync as readdirSync14, statSync as statSync9 } from "node:fs";
35001
35216
  import { homedir as homedir26 } from "node:os";
35002
35217
  import { join as join44 } from "node:path";
35003
35218
  var DEFAULT_PIPELINE_DIR = ".unipi/cocoindex";
@@ -35073,7 +35288,7 @@ function resolveCocoindexBin() {
35073
35288
  if (existsSync46(uvToolBin)) return uvToolBin;
35074
35289
  const miseRoot = join44(homedir26(), ".local", "share", "mise", "installs", "python");
35075
35290
  try {
35076
- const versions = readdirSync13(miseRoot).sort().reverse();
35291
+ const versions = readdirSync14(miseRoot).sort().reverse();
35077
35292
  for (const ver of versions) {
35078
35293
  const binPath = join44(miseRoot, ver, "bin", "cocoindex");
35079
35294
  if (existsSync46(binPath)) return binPath;
@@ -35136,7 +35351,7 @@ async function indexProject(projectDir) {
35136
35351
  }
35137
35352
  const start = Date.now();
35138
35353
  const cocoindexBin = resolveCocoindexBin();
35139
- return new Promise((resolve13) => {
35354
+ return new Promise((resolve14) => {
35140
35355
  const proc = spawn4(cocoindexBin, ["update", "main.py"], {
35141
35356
  cwd: pipelineDir,
35142
35357
  stdio: ["pipe", "pipe", "pipe"]
@@ -35166,9 +35381,9 @@ async function indexProject(projectDir) {
35166
35381
  const durationMs = Date.now() - start;
35167
35382
  const chunksProcessed = parseChunksProcessed(stdout);
35168
35383
  if (code === 0) {
35169
- resolve13({ success: true, chunksProcessed, durationMs });
35384
+ resolve14({ success: true, chunksProcessed, durationMs });
35170
35385
  } else {
35171
- resolve13({
35386
+ resolve14({
35172
35387
  success: false,
35173
35388
  chunksProcessed,
35174
35389
  durationMs,
@@ -35179,7 +35394,7 @@ async function indexProject(projectDir) {
35179
35394
  proc.on("error", (err) => {
35180
35395
  settled = true;
35181
35396
  clearTimeout(timer);
35182
- resolve13({
35397
+ resolve14({
35183
35398
  success: false,
35184
35399
  chunksProcessed: 0,
35185
35400
  durationMs: Date.now() - start,
@@ -35244,9 +35459,9 @@ async function status(projectDir) {
35244
35459
  const lancedbPath = join44(pipelineDir, ".lancedb");
35245
35460
  if (existsSync46(lancedbPath)) {
35246
35461
  try {
35247
- const stat = statSync8(lancedbPath);
35462
+ const stat = statSync9(lancedbPath);
35248
35463
  lastRun = stat.mtime.toISOString();
35249
- const files = readdirSync13(lancedbPath, { recursive: true });
35464
+ const files = readdirSync14(lancedbPath, { recursive: true });
35250
35465
  docCount = files.filter((f) => f.endsWith(".lance")).length;
35251
35466
  } catch {
35252
35467
  }
@@ -35906,7 +36121,7 @@ function buildUvInstallerFallback(shell) {
35906
36121
  ];
35907
36122
  }
35908
36123
  function runStep(step) {
35909
- return new Promise((resolve13) => {
36124
+ return new Promise((resolve14) => {
35910
36125
  const proc = spawn5(step.command, step.args ?? [], {
35911
36126
  stdio: ["ignore", "pipe", "pipe"],
35912
36127
  env: installerEnv()
@@ -35929,12 +36144,12 @@ Timed out after ${step.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS}ms.`;
35929
36144
  proc.on("close", (code, signal) => {
35930
36145
  settled = true;
35931
36146
  clearTimeout(timeout);
35932
- resolve13({ ok: code === 0, stdout, stderr, exitCode: code, signal });
36147
+ resolve14({ ok: code === 0, stdout, stderr, exitCode: code, signal });
35933
36148
  });
35934
36149
  proc.on("error", (err) => {
35935
36150
  settled = true;
35936
36151
  clearTimeout(timeout);
35937
- resolve13({ ok: false, stdout, stderr: stderr || err.message });
36152
+ resolve14({ ok: false, stdout, stderr: stderr || err.message });
35938
36153
  });
35939
36154
  });
35940
36155
  }
@@ -36136,7 +36351,7 @@ function cocoindexExtension(pi) {
36136
36351
 
36137
36352
  // packages/image/src/index.ts
36138
36353
  init_core();
36139
- import { dirname as dirname35 } from "node:path";
36354
+ import { dirname as dirname34 } from "node:path";
36140
36355
  import { fileURLToPath as fileURLToPath14 } from "node:url";
36141
36356
 
36142
36357
  // packages/image/src/commands.ts
@@ -37890,7 +38105,7 @@ function currentSessionModel(ctx) {
37890
38105
  }
37891
38106
 
37892
38107
  // packages/image/src/index.ts
37893
- var VERSION11 = getPackageVersion(dirname35(fileURLToPath14(import.meta.url)));
38108
+ var VERSION11 = getPackageVersion(dirname34(fileURLToPath14(import.meta.url)));
37894
38109
  function getInfoRegistry6() {
37895
38110
  return globalThis.__unipi_info_registry;
37896
38111
  }