@pi-unipi/unipi 2.4.2 → 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 (40) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +23 -22
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +1 -1
  5. package/packages/btw/package.json +2 -2
  6. package/packages/cocoindex/package.json +2 -2
  7. package/packages/compactor/package.json +3 -3
  8. package/packages/core/package.json +1 -1
  9. package/packages/core/sandbox.ts +7 -6
  10. package/packages/footer/package.json +2 -2
  11. package/packages/image/package.json +2 -2
  12. package/packages/info-screen/package.json +2 -2
  13. package/packages/input-shortcuts/package.json +2 -2
  14. package/packages/kanboard/package.json +2 -2
  15. package/packages/mcp/README.md +9 -0
  16. package/packages/mcp/package.json +5 -2
  17. package/packages/mcp/src/bridge/registry.ts +244 -137
  18. package/packages/mcp/src/bridge/translator.ts +77 -9
  19. package/packages/mcp/src/index.ts +43 -67
  20. package/packages/mcp/src/tui/settings-overlay.ts +3 -9
  21. package/packages/memory/README.md +15 -11
  22. package/packages/memory/bridge/mempalace_bridge.py +636 -0
  23. package/packages/memory/mempalace.ts +169 -18
  24. package/packages/memory/package.json +3 -3
  25. package/packages/memory/storage.ts +27 -10
  26. package/packages/milestone/README.md +11 -3
  27. package/packages/milestone/hooks.ts +103 -29
  28. package/packages/milestone/index.ts +1 -1
  29. package/packages/milestone/package.json +5 -2
  30. package/packages/notify/package.json +2 -2
  31. package/packages/ralph/package.json +3 -3
  32. package/packages/subagents/package.json +1 -1
  33. package/packages/unipi/bundled.js +693 -408
  34. package/packages/updater/package.json +2 -2
  35. package/packages/utility/package.json +2 -2
  36. package/packages/web-api/package.json +2 -2
  37. package/packages/workflow/README.md +8 -0
  38. package/packages/workflow/commands.ts +16 -26
  39. package/packages/workflow/index.ts +165 -85
  40. package/packages/workflow/package.json +5 -2
@@ -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");
1035
+ }
1036
+ function readMigrationState(flagPath = MIGRATED_FLAG) {
1037
+ try {
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;
1041
+ } catch {
1042
+ return null;
1043
+ }
1044
+ }
1045
+ function isMigrated(sourceFingerprint = getMemorySourceFingerprint(), flagPath = MIGRATED_FLAG) {
1046
+ return readMigrationState(flagPath)?.sourceFingerprint === sourceFingerprint;
978
1047
  }
979
- function markMigrated() {
1048
+ function markMigrated(sourceFingerprint, result, flagPath = MIGRATED_FLAG) {
1049
+ if (result.failed !== 0 || result.verified !== result.discovered) return false;
980
1050
  try {
981
- fs4.mkdirSync(path4.dirname(MIGRATED_FLAG), { recursive: true });
982
- fs4.writeFileSync(MIGRATED_FLAG, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
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;
983
1062
  } catch {
1063
+ return false;
984
1064
  }
985
1065
  }
986
- function runBridge(install, palace, cmd, args = {}) {
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
  }
@@ -1597,7 +1688,7 @@ ${record.content}
1597
1688
  const filePath = path5.join(this.scopeDir, file);
1598
1689
  const record = parseMemoryFile(filePath);
1599
1690
  if (!record) continue;
1600
- const id = record.title.toLowerCase().replace(/[^a-z0-9]+/g, "_");
1691
+ const id = record.id || record.title.toLowerCase().replace(/[^a-z0-9]+/g, "_");
1601
1692
  if (existingIds.has(id)) continue;
1602
1693
  try {
1603
1694
  record.id = id;
@@ -2239,8 +2330,8 @@ var init_embedding = __esm({
2239
2330
  });
2240
2331
 
2241
2332
  // packages/info-screen/usage-parser.ts
2242
- import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync4, existsSync as existsSync9, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8, renameSync as renameSync2, openSync, readSync, closeSync } from "node:fs";
2243
- import { join as join9, dirname as dirname10 } from "node:path";
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";
2244
2335
  import { homedir as homedir5 } from "node:os";
2245
2336
  function getCachePath() {
2246
2337
  const base = process.env.UNIPI_DIR || join9(homedir5(), ".unipi");
@@ -2262,10 +2353,10 @@ function readCache() {
2262
2353
  function writeCache(cache) {
2263
2354
  try {
2264
2355
  const path28 = getCachePath();
2265
- mkdirSync7(dirname10(path28), { recursive: true });
2356
+ mkdirSync7(dirname9(path28), { recursive: true });
2266
2357
  const tmp = `${path28}.${process.pid}.tmp`;
2267
2358
  writeFileSync8(tmp, JSON.stringify(cache), "utf-8");
2268
- renameSync2(tmp, path28);
2359
+ renameSync3(tmp, path28);
2269
2360
  } catch {
2270
2361
  }
2271
2362
  }
@@ -2361,7 +2452,7 @@ function extractRecords(filePath, modelIndex, models) {
2361
2452
  }
2362
2453
  function collectSessionFiles(dir, files) {
2363
2454
  try {
2364
- const entries = readdirSync4(dir, { withFileTypes: true });
2455
+ const entries = readdirSync5(dir, { withFileTypes: true });
2365
2456
  for (const entry of entries) {
2366
2457
  const entryPath = join9(dir, entry.name);
2367
2458
  if (entry.isDirectory()) {
@@ -2375,7 +2466,7 @@ function collectSessionFiles(dir, files) {
2375
2466
  }
2376
2467
  async function parseUsageStatsAsync() {
2377
2468
  const yielder = async () => {
2378
- await new Promise((resolve13) => setImmediate(resolve13));
2469
+ await new Promise((resolve14) => setImmediate(resolve14));
2379
2470
  };
2380
2471
  const { stats, pending } = collectStats(yielder);
2381
2472
  if (pending) await pending;
@@ -2411,7 +2502,7 @@ function collectStats(yielder) {
2411
2502
  let mtimeMs = 0;
2412
2503
  let size = 0;
2413
2504
  try {
2414
- const st = statSync4(filePath);
2505
+ const st = statSync5(filePath);
2415
2506
  mtimeMs = st.mtimeMs;
2416
2507
  size = st.size;
2417
2508
  } catch {
@@ -2592,19 +2683,19 @@ __export(env_exports, {
2592
2683
  formatEnvironmentInfo: () => formatEnvironmentInfo,
2593
2684
  getEnvironmentInfo: () => getEnvironmentInfo
2594
2685
  });
2595
- import { existsSync as existsSync18, readdirSync as readdirSync10 } from "node:fs";
2596
- 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";
2597
2688
  import { homedir as homedir14 } from "node:os";
2598
2689
  function getEnvironmentInfo() {
2599
2690
  const unipiModules = [];
2600
2691
  const configPaths = [];
2601
2692
  const extensionPaths = [];
2602
2693
  try {
2603
- const nodeModules = resolve5(process.cwd(), "node_modules");
2694
+ const nodeModules = resolve6(process.cwd(), "node_modules");
2604
2695
  if (existsSync18(nodeModules)) {
2605
2696
  const scopePath = join18(nodeModules, "@pi-unipi");
2606
2697
  if (existsSync18(scopePath)) {
2607
- for (const entry of readdirSync10(scopePath)) {
2698
+ for (const entry of readdirSync11(scopePath)) {
2608
2699
  if (entry.startsWith(".")) continue;
2609
2700
  const pkgPath = join18(scopePath, entry, "package.json");
2610
2701
  if (existsSync18(pkgPath)) {
@@ -2616,7 +2707,7 @@ function getEnvironmentInfo() {
2616
2707
  } catch {
2617
2708
  }
2618
2709
  const globalConfig = join18(homedir14(), ".unipi", "config");
2619
- const projectConfig = resolve5(process.cwd(), ".unipi", "config");
2710
+ const projectConfig = resolve6(process.cwd(), ".unipi", "config");
2620
2711
  if (existsSync18(globalConfig)) {
2621
2712
  configPaths.push(globalConfig);
2622
2713
  }
@@ -3863,7 +3954,7 @@ var init_schema = __esm({
3863
3954
  // packages/compactor/src/config/manager.ts
3864
3955
  import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync29, writeFileSync as writeFileSync23 } from "node:fs";
3865
3956
  import { homedir as homedir20 } from "node:os";
3866
- import { dirname as dirname25, join as join32 } from "node:path";
3957
+ import { dirname as dirname24, join as join32 } from "node:path";
3867
3958
  function projectConfigPath(cwd) {
3868
3959
  return join32(cwd, ".unipi", "config", "compactor.json");
3869
3960
  }
@@ -3900,7 +3991,7 @@ function loadConfig4(cwd) {
3900
3991
  function saveConfig3(config, opts) {
3901
3992
  try {
3902
3993
  const targetPath = opts?.perProject && opts?.cwd ? projectConfigPath(opts.cwd) : COMPACTOR_CONFIG_PATH;
3903
- const dir = dirname25(targetPath);
3994
+ const dir = dirname24(targetPath);
3904
3995
  if (!existsSync31(dir)) mkdirSync19(dir, { recursive: true });
3905
3996
  writeFileSync23(targetPath, `${JSON.stringify(config, null, 2)}
3906
3997
  `);
@@ -3935,7 +4026,7 @@ function migrateConfig(partial) {
3935
4026
  }
3936
4027
  function scaffoldConfig() {
3937
4028
  try {
3938
- const dir = dirname25(COMPACTOR_CONFIG_PATH);
4029
+ const dir = dirname24(COMPACTOR_CONFIG_PATH);
3939
4030
  if (!existsSync31(dir)) mkdirSync19(dir, { recursive: true });
3940
4031
  if (!existsSync31(COMPACTOR_CONFIG_PATH)) {
3941
4032
  writeFileSync23(COMPACTOR_CONFIG_PATH, `${JSON.stringify(DEFAULT_COMPACTOR_CONFIG, null, 2)}
@@ -5452,9 +5543,9 @@ var init_snapshot = __esm({
5452
5543
  });
5453
5544
 
5454
5545
  // packages/compactor/src/config/presets.ts
5455
- import { createHash as createHash3 } from "node:crypto";
5546
+ import { createHash as createHash4 } from "node:crypto";
5456
5547
  function presetHash(config) {
5457
- return createHash3("sha256").update(JSON.stringify(config)).digest("hex");
5548
+ return createHash4("sha256").update(JSON.stringify(config)).digest("hex");
5458
5549
  }
5459
5550
  function configsEqual(a, b) {
5460
5551
  const aHash = presetHash(a);
@@ -6309,7 +6400,7 @@ __export(evaluator_exports, {
6309
6400
  splitChainedCommands: () => splitChainedCommands
6310
6401
  });
6311
6402
  import { realpathSync as realpathSync2, existsSync as existsSync37, readFileSync as readFileSync32 } from "node:fs";
6312
- import { resolve as resolve11, join as join36 } from "node:path";
6403
+ import { resolve as resolve12, join as join36 } from "node:path";
6313
6404
  import { homedir as homedir22 } from "node:os";
6314
6405
  function loadProjectPermissions(cwd, basePolicy) {
6315
6406
  const settingsPath = join36(cwd, ".pi", "settings.json");
@@ -6381,7 +6472,7 @@ function splitChainedCommands(command) {
6381
6472
  return commands;
6382
6473
  }
6383
6474
  function evaluateFilePath(filePath, policy, cwd = process.cwd()) {
6384
- const resolved = resolve11(cwd, filePath);
6475
+ const resolved = resolve12(cwd, filePath);
6385
6476
  try {
6386
6477
  const real = realpathSync2(resolved);
6387
6478
  const home = homedir22();
@@ -6551,6 +6642,9 @@ var init_thinking_label = __esm({
6551
6642
  init_core();
6552
6643
  import { dirname as dirname2 } from "node:path";
6553
6644
  import { fileURLToPath } from "node:url";
6645
+ import {
6646
+ buildSessionContext
6647
+ } from "@earendil-works/pi-coding-agent";
6554
6648
 
6555
6649
  // packages/workflow/commands.ts
6556
6650
  init_core();
@@ -6824,15 +6918,10 @@ function registerWorkflowCommands(pi, options) {
6824
6918
  fullCommand: `/${fullCommand}`,
6825
6919
  args: args?.trim() ?? ""
6826
6920
  };
6827
- if (!options.startWorkflow(workflowEvent)) {
6921
+ if (!options.activateSandbox(workflowEvent)) {
6828
6922
  if (ctx.hasUI) ctx.ui.notify("Another UniPi workflow is still active", "warning");
6829
6923
  return;
6830
6924
  }
6831
- const currentTools = options.getActiveTools();
6832
- options.saveTools(currentTools);
6833
- const sandboxTools = getToolsForCommand(cmd.name, currentTools);
6834
- const sandboxLevel = getSandboxLevel(cmd.name);
6835
- options.setActiveTools([...sandboxTools], sandboxLevel);
6836
6925
  let skillContent = "";
6837
6926
  try {
6838
6927
  const skillPath = join3(
@@ -6861,7 +6950,12 @@ Arguments: ${args.trim()}`;
6861
6950
  ${skillContent}
6862
6951
  </skill_content>`;
6863
6952
  }
6864
- pi.sendUserMessage(message, { deliverAs: "followUp" });
6953
+ try {
6954
+ pi.sendUserMessage(message, { deliverAs: "followUp" });
6955
+ } catch (error) {
6956
+ options.abortWorkflow();
6957
+ throw error;
6958
+ }
6865
6959
  if (ctx.hasUI) {
6866
6960
  ctx.ui.notify(`Running /${fullCommand}`, "info");
6867
6961
  const ralphStatus = options.isRalphDetected() ? "\u2713 rl" : "\u25CB rl";
@@ -6929,90 +7023,147 @@ var WorkflowLifecycle = class {
6929
7023
 
6930
7024
  // packages/workflow/index.ts
6931
7025
  var VERSION = getPackageVersion(dirname2(fileURLToPath(import.meta.url)));
6932
- var ralphDetected = false;
6933
- var savedTools = null;
6934
- var sandboxActive = false;
6935
- var currentSandboxLevel = null;
6936
- var currentSandboxTools = null;
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
+ }
6937
7100
  function workflow_default(pi) {
6938
7101
  const workflowLifecycle = new WorkflowLifecycle();
7102
+ let ralphDetected = false;
7103
+ let sandboxCommand = null;
6939
7104
  registerWorkflowCommands(pi, {
6940
7105
  isRalphDetected: () => ralphDetected,
6941
- getActiveTools: () => pi.getActiveTools(),
6942
- setActiveTools: (tools, level) => {
6943
- pi.setActiveTools(tools);
6944
- sandboxActive = true;
6945
- currentSandboxLevel = level;
6946
- currentSandboxTools = tools;
6947
- },
6948
- saveTools: (tools) => {
6949
- savedTools = tools;
6950
- },
6951
- startWorkflow: (event) => {
7106
+ activateSandbox: (event) => {
6952
7107
  if (!workflowLifecycle.start(event)) return false;
7108
+ sandboxCommand = event.command;
6953
7109
  emitEvent(pi, UNIPI_EVENTS.WORKFLOW_START, event);
6954
7110
  return true;
7111
+ },
7112
+ abortWorkflow: () => {
7113
+ sandboxCommand = null;
7114
+ workflowLifecycle.reset();
6955
7115
  }
6956
7116
  });
6957
7117
  pi.on("tool_call", async (event, _ctx) => {
6958
- if (!sandboxActive || !currentSandboxLevel) return;
6959
- const allowed = currentSandboxTools ?? [];
6960
- 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);
6961
7122
  return {
6962
7123
  block: true,
6963
- 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(", ")}`
6964
7125
  };
6965
7126
  }
6966
7127
  });
6967
- pi.on("before_agent_start", async (event, _ctx) => {
6968
- if (!sandboxActive || !currentSandboxLevel) return;
6969
- const allowed = currentSandboxTools ?? pi.getActiveTools();
6970
- const blocked = getBlockedToolsForLevel(currentSandboxLevel);
6971
- const blockedLine = blocked.length > 0 ? `
6972
- Blocked tools: ${blocked.join(", ")} \u2014 removed from your tool list.` : "\nNo tools were blocked by this sandbox.";
6973
- const base = `
6974
-
6975
- <sandbox>
6976
- Sandbox mode: ${currentSandboxLevel}.
6977
- Available tools: ${allowed.join(", ")}.${blockedLine}`;
6978
- if (currentSandboxLevel === "brainstorm") {
6979
- return {
6980
- systemPrompt: event.systemPrompt + base + `
6981
- The write tool is available but restricted to .unipi/docs/specs/ only.
6982
- bash is available ONLY for specific setup use case (e.g., git init, mkdir). Do NOT use bash for reading files or listing directories \u2014 use grep, find, ls instead.
6983
- Do NOT attempt to call blocked tools. Do NOT output tool call XML for them.
6984
- If the user requests an action that requires a blocked tool, respond that you do not have access.
6985
- </sandbox>`
6986
- };
6987
- }
6988
- if (currentSandboxLevel === "write_unipi") {
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;
6989
7136
  return {
6990
- systemPrompt: event.systemPrompt + base + `
6991
- Write tool is restricted to .unipi/docs/ only (specs and plans).
6992
- Use grep, find, ls for file discovery \u2014 do NOT guess filenames.
6993
- bash is blocked \u2014 use read, write, edit, grep, find, ls only.
6994
- Do NOT attempt to call blocked tools. Do NOT output tool call XML for them.
6995
- If the user requests an action that requires a blocked tool, respond that you do not have access.
6996
- </sandbox>`
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
+ }
6997
7147
  };
6998
7148
  }
7149
+ const prior = latest ?? historical;
7150
+ if (!prior || !isActiveSnapshot(prior)) return void 0;
6999
7151
  return {
7000
- systemPrompt: event.systemPrompt + base + `
7001
- Do NOT attempt to call blocked tools. Do NOT output tool call XML for them.
7002
- If the user requires an action that requires a blocked tool, respond that you do not have access.
7003
- </sandbox>`
7152
+ message: {
7153
+ customType: WORKFLOW_SANDBOX_SNAPSHOT_TYPE,
7154
+ content: formatInactiveSandboxSnapshot(),
7155
+ display: false,
7156
+ details: {
7157
+ active: false
7158
+ }
7159
+ }
7004
7160
  };
7005
7161
  });
7006
7162
  pi.on("agent_end", async (event, _ctx) => {
7007
- if (sandboxActive && savedTools) {
7008
- pi.setActiveTools(savedTools);
7009
- savedTools = null;
7010
- sandboxActive = false;
7011
- currentSandboxLevel = null;
7012
- currentSandboxTools = null;
7013
- }
7014
7163
  const completedWorkflow = workflowLifecycle.complete(event.messages);
7015
- if (completedWorkflow) emitEvent(pi, UNIPI_EVENTS.WORKFLOW_END, completedWorkflow);
7164
+ if (!completedWorkflow) return;
7165
+ sandboxCommand = null;
7166
+ emitEvent(pi, UNIPI_EVENTS.WORKFLOW_END, completedWorkflow);
7016
7167
  });
7017
7168
  pi.on("session_start", async (_event, ctx) => {
7018
7169
  initUnipiDirs();
@@ -7025,7 +7176,7 @@ If the user requires an action that requires a blocked tool, respond that you do
7025
7176
  if (!ralphDetected) {
7026
7177
  try {
7027
7178
  const allTools = pi.getAllTools();
7028
- ralphDetected = allTools.some((t) => t.name === "ralph_start");
7179
+ ralphDetected = allTools.some((tool) => tool.name === "ralph_start");
7029
7180
  } catch {
7030
7181
  }
7031
7182
  }
@@ -7036,14 +7187,11 @@ If the user requires an action that requires a blocked tool, respond that you do
7036
7187
  });
7037
7188
  pi.events.on(UNIPI_EVENTS.MODULE_READY, (data) => {
7038
7189
  const event = data;
7039
- if (event?.name === MODULES.RALPH) {
7040
- ralphDetected = true;
7041
- }
7190
+ if (event?.name === MODULES.RALPH) ralphDetected = true;
7042
7191
  });
7043
7192
  pi.on("session_shutdown", async () => {
7044
7193
  ralphDetected = false;
7045
- savedTools = null;
7046
- sandboxActive = false;
7194
+ sandboxCommand = null;
7047
7195
  workflowLifecycle.reset();
7048
7196
  });
7049
7197
  }
@@ -7922,7 +8070,7 @@ function handleNuke(rest, ctx, pi) {
7922
8070
  // packages/memory/index.ts
7923
8071
  init_core();
7924
8072
  init_storage();
7925
- import { dirname as dirname8 } from "node:path";
8073
+ import { dirname as dirname7 } from "node:path";
7926
8074
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7927
8075
 
7928
8076
  // packages/memory/tools.ts
@@ -8630,7 +8778,7 @@ init_settings();
8630
8778
  function getInfoRegistry2() {
8631
8779
  return globalThis.__unipi_info_registry;
8632
8780
  }
8633
- var VERSION3 = getPackageVersion(dirname8(fileURLToPath4(import.meta.url)));
8781
+ var VERSION3 = getPackageVersion(dirname7(fileURLToPath4(import.meta.url)));
8634
8782
  var projectStorage = null;
8635
8783
  var orphanSyncPending = false;
8636
8784
  function ensureOrphanSync(storage) {
@@ -8852,12 +9000,12 @@ function memory_default(pi) {
8852
9000
 
8853
9001
  // packages/info-screen/index.ts
8854
9002
  init_core();
8855
- import { dirname as dirname11 } from "node:path";
9003
+ import { dirname as dirname10 } from "node:path";
8856
9004
  import { fileURLToPath as fileURLToPath5 } from "node:url";
8857
9005
 
8858
9006
  // packages/info-screen/config.ts
8859
9007
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync6 } from "node:fs";
8860
- import { join as join8, dirname as dirname9 } from "node:path";
9008
+ import { join as join8, dirname as dirname8 } from "node:path";
8861
9009
  import { homedir as homedir4 } from "node:os";
8862
9010
 
8863
9011
  // packages/info-screen/types.ts
@@ -8886,7 +9034,7 @@ function readSettingsFile() {
8886
9034
  }
8887
9035
  }
8888
9036
  function writeSettingsFile(data) {
8889
- const dir = dirname9(SETTINGS_PATH);
9037
+ const dir = dirname8(SETTINGS_PATH);
8890
9038
  if (!existsSync8(dir)) {
8891
9039
  mkdirSync6(dir, { recursive: true });
8892
9040
  }
@@ -9187,7 +9335,7 @@ if (!globalThis.__unipi_info_registry) {
9187
9335
 
9188
9336
  // packages/info-screen/core-groups.ts
9189
9337
  init_core();
9190
- 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";
9191
9339
  import { join as join10 } from "node:path";
9192
9340
  import { homedir as homedir6 } from "node:os";
9193
9341
  init_usage_parser();
@@ -9248,7 +9396,7 @@ function discoverExtensions() {
9248
9396
  for (const dir of extensionDirs) {
9249
9397
  if (!existsSync10(dir)) continue;
9250
9398
  try {
9251
- const entries = readdirSync5(dir, { withFileTypes: true });
9399
+ const entries = readdirSync6(dir, { withFileTypes: true });
9252
9400
  for (const entry of entries) {
9253
9401
  const name = entry.name;
9254
9402
  if (counted.has(name)) continue;
@@ -9322,7 +9470,7 @@ function discoverSkills() {
9322
9470
  for (const dir of skillDirs) {
9323
9471
  if (!existsSync10(dir)) continue;
9324
9472
  try {
9325
- const entries = readdirSync5(dir, { withFileTypes: true });
9473
+ const entries = readdirSync6(dir, { withFileTypes: true });
9326
9474
  for (const entry of entries) {
9327
9475
  if (!entry.isDirectory()) continue;
9328
9476
  const name = entry.name;
@@ -10433,7 +10581,7 @@ var SettingsOverlay = class _SettingsOverlay {
10433
10581
  };
10434
10582
 
10435
10583
  // packages/info-screen/index.ts
10436
- var VERSION4 = getPackageVersion(dirname11(fileURLToPath5(import.meta.url)));
10584
+ var VERSION4 = getPackageVersion(dirname10(fileURLToPath5(import.meta.url)));
10437
10585
  function info_screen_default(pi) {
10438
10586
  setPiApi(pi);
10439
10587
  registerCoreGroups();
@@ -10576,7 +10724,7 @@ function info_screen_default(pi) {
10576
10724
  import { defineTool } from "@earendil-works/pi-coding-agent";
10577
10725
  import { Text } from "@earendil-works/pi-tui";
10578
10726
  import { Type as Type3 } from "typebox";
10579
- import { existsSync as existsSync13, readdirSync as readdirSync7 } from "node:fs";
10727
+ import { existsSync as existsSync13, readdirSync as readdirSync8 } from "node:fs";
10580
10728
  import { join as join13 } from "node:path";
10581
10729
  import { homedir as homedir9 } from "node:os";
10582
10730
 
@@ -10815,14 +10963,14 @@ ${agentPrompt}`;
10815
10963
  init_model_resolver();
10816
10964
 
10817
10965
  // packages/subagents/src/custom-agents.ts
10818
- 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";
10819
10967
  import { join as join11 } from "node:path";
10820
10968
  import { homedir as homedir7 } from "node:os";
10821
10969
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
10822
10970
  function backupCorrupted(filePath) {
10823
10971
  const backupPath = filePath + ".bak";
10824
10972
  try {
10825
- renameSync3(filePath, backupPath);
10973
+ renameSync4(filePath, backupPath);
10826
10974
  } catch {
10827
10975
  }
10828
10976
  }
@@ -10871,7 +11019,7 @@ function loadCustomAgents(cwd) {
10871
11019
  const agents = /* @__PURE__ */ new Map();
10872
11020
  const globalDir = getGlobalAgentsDir();
10873
11021
  if (existsSync11(globalDir)) {
10874
- const files = readdirSync6(globalDir).filter((f) => f.endsWith(".md"));
11022
+ const files = readdirSync7(globalDir).filter((f) => f.endsWith(".md"));
10875
11023
  for (const file of files) {
10876
11024
  const agent = loadAgentFromFile(join11(globalDir, file), "global");
10877
11025
  if (agent) {
@@ -10881,7 +11029,7 @@ function loadCustomAgents(cwd) {
10881
11029
  }
10882
11030
  const projectDir = getProjectAgentsDir(cwd);
10883
11031
  if (existsSync11(projectDir)) {
10884
- const files = readdirSync6(projectDir).filter((f) => f.endsWith(".md"));
11032
+ const files = readdirSync7(projectDir).filter((f) => f.endsWith(".md"));
10885
11033
  for (const file of files) {
10886
11034
  const agent = loadAgentFromFile(join11(projectDir, file), "project");
10887
11035
  if (agent) {
@@ -10908,17 +11056,17 @@ var FileLock = class {
10908
11056
  */
10909
11057
  async acquire(filePath, agentId) {
10910
11058
  while (this.locks.has(filePath)) {
10911
- await new Promise((resolve13) => {
11059
+ await new Promise((resolve14) => {
10912
11060
  const queue = this.queues.get(filePath) ?? [];
10913
- queue.push(resolve13);
11061
+ queue.push(resolve14);
10914
11062
  this.queues.set(filePath, queue);
10915
11063
  });
10916
11064
  }
10917
11065
  let releaseFn;
10918
- const promise = new Promise((resolve13) => {
11066
+ const promise = new Promise((resolve14) => {
10919
11067
  releaseFn = () => {
10920
11068
  this.locks.delete(filePath);
10921
- resolve13();
11069
+ resolve14();
10922
11070
  const queue = this.queues.get(filePath);
10923
11071
  if (queue && queue.length > 0) {
10924
11072
  const next = queue.shift();
@@ -11236,7 +11384,7 @@ var AgentManager = class {
11236
11384
  };
11237
11385
 
11238
11386
  // packages/subagents/src/config.ts
11239
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync9 } from "node:fs";
11387
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync11, renameSync as renameSync5, writeFileSync as writeFileSync9 } from "node:fs";
11240
11388
  import { join as join12 } from "node:path";
11241
11389
  import { homedir as homedir8 } from "node:os";
11242
11390
  var DEFAULT_CONFIG2 = {
@@ -11261,7 +11409,7 @@ function ensureDirExists(dirPath) {
11261
11409
  function writeConfigAtomic(filePath, config) {
11262
11410
  const tmpPath = filePath + ".tmp";
11263
11411
  writeFileSync9(tmpPath, JSON.stringify(config, null, 2), "utf-8");
11264
- renameSync4(tmpPath, filePath);
11412
+ renameSync5(tmpPath, filePath);
11265
11413
  }
11266
11414
  function loadConfigFromPath(filePath) {
11267
11415
  if (!existsSync12(filePath)) return null;
@@ -11277,7 +11425,7 @@ function loadConfigFromPath(filePath) {
11277
11425
  function repairCorrupted(filePath) {
11278
11426
  const backupPath = filePath + ".bak";
11279
11427
  try {
11280
- renameSync4(filePath, backupPath);
11428
+ renameSync5(filePath, backupPath);
11281
11429
  } catch {
11282
11430
  }
11283
11431
  writeConfigAtomic(filePath, DEFAULT_CONFIG2);
@@ -12096,7 +12244,7 @@ function src_default(pi) {
12096
12244
  for (const dir of [globalAgentsDir, workspaceAgentsDir]) {
12097
12245
  try {
12098
12246
  if (existsSync13(dir)) {
12099
- for (const file of readdirSync7(dir)) {
12247
+ for (const file of readdirSync8(dir)) {
12100
12248
  if (file.endsWith(".md") && !customTypes.includes(file.replace(".md", ""))) {
12101
12249
  customTypes.push(file.replace(".md", ""));
12102
12250
  }
@@ -12542,7 +12690,7 @@ Description: ${record.description}
12542
12690
  // packages/btw/extensions/btw.ts
12543
12691
  init_core();
12544
12692
  import {
12545
- buildSessionContext,
12693
+ buildSessionContext as buildSessionContext2,
12546
12694
  createAgentSession as createAgentSession2,
12547
12695
  createExtensionRuntime,
12548
12696
  SessionManager as SessionManager2
@@ -12628,7 +12776,7 @@ function buildBtwSeedState(ctx, thread, mode) {
12628
12776
  if (mode === "contextual") {
12629
12777
  try {
12630
12778
  messages.push(
12631
- ...buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages.filter(
12779
+ ...buildSessionContext2(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages.filter(
12632
12780
  (message) => !isVisibleBtwMessage(message)
12633
12781
  )
12634
12782
  );
@@ -14031,7 +14179,7 @@ ${summary}`;
14031
14179
 
14032
14180
  // packages/web-api/src/index.ts
14033
14181
  init_core();
14034
- import { dirname as dirname12 } from "node:path";
14182
+ import { dirname as dirname11 } from "node:path";
14035
14183
  import { fileURLToPath as fileURLToPath6 } from "node:url";
14036
14184
 
14037
14185
  // packages/web-api/src/tools.ts
@@ -16701,7 +16849,7 @@ var llmSummarizeProvider = {
16701
16849
  registry.register(llmSummarizeProvider);
16702
16850
 
16703
16851
  // packages/web-api/src/index.ts
16704
- var VERSION5 = getPackageVersion(dirname12(fileURLToPath6(import.meta.url)));
16852
+ var VERSION5 = getPackageVersion(dirname11(fileURLToPath6(import.meta.url)));
16705
16853
  function getInfoRegistry4() {
16706
16854
  return globalThis.__unipi_info_registry;
16707
16855
  }
@@ -16773,14 +16921,14 @@ function src_default2(pi) {
16773
16921
 
16774
16922
  // packages/utility/src/index.ts
16775
16923
  init_core();
16776
- import { dirname as dirname15 } from "node:path";
16924
+ import { dirname as dirname14 } from "node:path";
16777
16925
  import { fileURLToPath as fileURLToPath7 } from "node:url";
16778
16926
 
16779
16927
  // packages/utility/src/commands.ts
16780
16928
  init_core();
16781
16929
 
16782
16930
  // packages/utility/src/lifecycle/cleanup.ts
16783
- 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";
16784
16932
  import { join as join16 } from "node:path";
16785
16933
  import { homedir as homedir12, tmpdir } from "node:os";
16786
16934
  var DEFAULTS = {
@@ -16797,7 +16945,7 @@ function expandHome(path28) {
16797
16945
  }
16798
16946
  function isStale(path28, maxAgeDays) {
16799
16947
  try {
16800
- const stats = statSync7(path28);
16948
+ const stats = statSync8(path28);
16801
16949
  const ageMs = Date.now() - stats.mtime.getTime();
16802
16950
  return ageMs > maxAgeDays * 24 * 60 * 60 * 1e3;
16803
16951
  } catch {
@@ -16809,7 +16957,7 @@ function hasWalLock(dbPath) {
16809
16957
  const shmPath = dbPath + "-shm";
16810
16958
  if (existsSync16(walPath)) {
16811
16959
  try {
16812
- const walStats = statSync7(walPath);
16960
+ const walStats = statSync8(walPath);
16813
16961
  const ageMs = Date.now() - walStats.mtime.getTime();
16814
16962
  return ageMs > 5 * 60 * 1e3;
16815
16963
  } catch {
@@ -16830,14 +16978,14 @@ function cleanDbs(options) {
16830
16978
  const scanDir = (dir) => {
16831
16979
  let entries;
16832
16980
  try {
16833
- entries = readdirSync9(dir);
16981
+ entries = readdirSync10(dir);
16834
16982
  } catch {
16835
16983
  return;
16836
16984
  }
16837
16985
  for (const entry of entries) {
16838
16986
  const fullPath = join16(dir, entry);
16839
16987
  try {
16840
- const stats = statSync7(fullPath);
16988
+ const stats = statSync8(fullPath);
16841
16989
  if (stats.isDirectory()) {
16842
16990
  scanDir(fullPath);
16843
16991
  continue;
@@ -16853,7 +17001,7 @@ function cleanDbs(options) {
16853
17001
  const companion = fullPath + suffix;
16854
17002
  if (existsSync16(companion)) {
16855
17003
  unlinkSync5(companion);
16856
- result.bytesFreed += statSync7(companion).size;
17004
+ result.bytesFreed += statSync8(companion).size;
16857
17005
  }
16858
17006
  }
16859
17007
  result.removed++;
@@ -16882,7 +17030,7 @@ function cleanTemps(options) {
16882
17030
  const tmpDir = tmpdir();
16883
17031
  let entries;
16884
17032
  try {
16885
- entries = readdirSync9(tmpDir);
17033
+ entries = readdirSync10(tmpDir);
16886
17034
  } catch {
16887
17035
  return result;
16888
17036
  }
@@ -16890,7 +17038,7 @@ function cleanTemps(options) {
16890
17038
  if (!patterns.some((p) => p.test(entry))) continue;
16891
17039
  const fullPath = join16(tmpDir, entry);
16892
17040
  try {
16893
- const stats = statSync7(fullPath);
17041
+ const stats = statSync8(fullPath);
16894
17042
  if (!stats.isFile()) continue;
16895
17043
  if (isStale(fullPath, options.tempMaxAgeDays)) {
16896
17044
  result.bytesFreed += stats.size;
@@ -16922,14 +17070,14 @@ function cleanSessions(options) {
16922
17070
  if (!existsSync16(sessionsDir)) return result;
16923
17071
  let entries;
16924
17072
  try {
16925
- entries = readdirSync9(sessionsDir);
17073
+ entries = readdirSync10(sessionsDir);
16926
17074
  } catch {
16927
17075
  return result;
16928
17076
  }
16929
17077
  for (const entry of entries) {
16930
17078
  const fullPath = join16(sessionsDir, entry);
16931
17079
  try {
16932
- const stats = statSync7(fullPath);
17080
+ const stats = statSync8(fullPath);
16933
17081
  if (!stats.isDirectory()) continue;
16934
17082
  if (isStale(fullPath, options.sessionMaxAgeDays)) {
16935
17083
  result.bytesFreed += stats.size;
@@ -16937,10 +17085,10 @@ function cleanSessions(options) {
16937
17085
  if (!options.dryRun) {
16938
17086
  try {
16939
17087
  const removeRecursive = (dir) => {
16940
- const items = readdirSync9(dir);
17088
+ const items = readdirSync10(dir);
16941
17089
  for (const item of items) {
16942
17090
  const itemPath = join16(dir, item);
16943
- const itemStats = statSync7(itemPath);
17091
+ const itemStats = statSync8(itemPath);
16944
17092
  if (itemStats.isDirectory()) {
16945
17093
  removeRecursive(itemPath);
16946
17094
  } else {
@@ -16973,14 +17121,14 @@ function cleanCache(options) {
16973
17121
  if (!existsSync16(cacheDir)) return result;
16974
17122
  let entries;
16975
17123
  try {
16976
- entries = readdirSync9(cacheDir);
17124
+ entries = readdirSync10(cacheDir);
16977
17125
  } catch {
16978
17126
  return result;
16979
17127
  }
16980
17128
  for (const entry of entries) {
16981
17129
  const fullPath = join16(cacheDir, entry);
16982
17130
  try {
16983
- const stats = statSync7(fullPath);
17131
+ const stats = statSync8(fullPath);
16984
17132
  if (!stats.isFile()) continue;
16985
17133
  if (isStale(fullPath, options.tempMaxAgeDays)) {
16986
17134
  result.bytesFreed += stats.size;
@@ -17044,7 +17192,7 @@ function formatCleanupReport(report) {
17044
17192
  }
17045
17193
 
17046
17194
  // packages/utility/src/diagnostics/engine.ts
17047
- import { existsSync as existsSync17, accessSync, constants } from "node:fs";
17195
+ import { existsSync as existsSync17, accessSync as accessSync2, constants as constants2 } from "node:fs";
17048
17196
  import { join as join17 } from "node:path";
17049
17197
  import { homedir as homedir13 } from "node:os";
17050
17198
  function expandHome2(path28) {
@@ -17055,7 +17203,7 @@ function expandHome2(path28) {
17055
17203
  }
17056
17204
  function isReadable(path28) {
17057
17205
  try {
17058
- accessSync(path28, constants.R_OK);
17206
+ accessSync2(path28, constants2.R_OK);
17059
17207
  return true;
17060
17208
  } catch {
17061
17209
  return false;
@@ -17063,7 +17211,7 @@ function isReadable(path28) {
17063
17211
  }
17064
17212
  function isWritable(path28) {
17065
17213
  try {
17066
- accessSync(path28, constants.W_OK);
17214
+ accessSync2(path28, constants2.W_OK);
17067
17215
  return true;
17068
17216
  } catch {
17069
17217
  return false;
@@ -19333,7 +19481,7 @@ function registerInfoScreen(pi) {
19333
19481
  }
19334
19482
 
19335
19483
  // packages/utility/src/index.ts
19336
- var VERSION6 = getPackageVersion(dirname15(fileURLToPath7(import.meta.url)));
19484
+ var VERSION6 = getPackageVersion(dirname14(fileURLToPath7(import.meta.url)));
19337
19485
  var firstMessageSeen = false;
19338
19486
  var firstUserText = "";
19339
19487
  var firstInputCtx = null;
@@ -19545,7 +19693,7 @@ function registerUtilityTools(pi, nameBadgeState) {
19545
19693
 
19546
19694
  // packages/ask-user/index.ts
19547
19695
  init_core();
19548
- import { dirname as dirname16 } from "node:path";
19696
+ import { dirname as dirname15 } from "node:path";
19549
19697
  import { fileURLToPath as fileURLToPath8 } from "node:url";
19550
19698
 
19551
19699
  // packages/ask-user/tools.ts
@@ -20930,7 +21078,7 @@ function registerAskUserCommands(pi) {
20930
21078
  }
20931
21079
 
20932
21080
  // packages/ask-user/index.ts
20933
- var VERSION7 = getPackageVersion(dirname16(fileURLToPath8(import.meta.url)));
21081
+ var VERSION7 = getPackageVersion(dirname15(fileURLToPath8(import.meta.url)));
20934
21082
  function ask_user_default(pi) {
20935
21083
  registerAskUserTools(pi);
20936
21084
  registerAskUserCommands(pi);
@@ -20946,7 +21094,7 @@ function ask_user_default(pi) {
20946
21094
 
20947
21095
  // packages/mcp/src/index.ts
20948
21096
  init_core();
20949
- import { dirname as dirname19 } from "node:path";
21097
+ import { dirname as dirname18 } from "node:path";
20950
21098
  import { fileURLToPath as fileURLToPath9 } from "node:url";
20951
21099
 
20952
21100
  // packages/mcp/src/config/manager.ts
@@ -21444,7 +21592,7 @@ var McpClient = class {
21444
21592
  if (this.connected) {
21445
21593
  throw new Error("McpClient is already connected");
21446
21594
  }
21447
- return new Promise((resolve13, reject) => {
21595
+ return new Promise((resolve14, reject) => {
21448
21596
  try {
21449
21597
  const mergedEnv = { ...process.env, ...env };
21450
21598
  this.process = spawn2(command, args, {
@@ -21493,7 +21641,7 @@ Stderr: ${this.stderrBuffer}`
21493
21641
  }).then(() => {
21494
21642
  this.sendNotification("notifications/initialized", {});
21495
21643
  this.connected = true;
21496
- resolve13();
21644
+ resolve14();
21497
21645
  }).catch((err) => {
21498
21646
  this.cleanup();
21499
21647
  reject(
@@ -21537,16 +21685,16 @@ Stderr: ${this.stderrBuffer}`
21537
21685
  this.sendNotification("shutdown", {});
21538
21686
  } catch {
21539
21687
  }
21540
- await new Promise((resolve13) => {
21541
- const timer = setTimeout(() => resolve13(), 500);
21688
+ await new Promise((resolve14) => {
21689
+ const timer = setTimeout(() => resolve14(), 500);
21542
21690
  this.process.on("exit", () => {
21543
21691
  clearTimeout(timer);
21544
- resolve13();
21692
+ resolve14();
21545
21693
  });
21546
21694
  try {
21547
21695
  this.process.kill("SIGTERM");
21548
21696
  } catch {
21549
- resolve13();
21697
+ resolve14();
21550
21698
  }
21551
21699
  });
21552
21700
  if (this.process && !this.process.killed) {
@@ -21576,7 +21724,7 @@ Stderr: ${this.stderrBuffer}`
21576
21724
  }
21577
21725
  }
21578
21726
  sendRequest(method, params) {
21579
- return new Promise((resolve13, reject) => {
21727
+ return new Promise((resolve14, reject) => {
21580
21728
  if (!this.process?.stdin) {
21581
21729
  reject(new Error("MCP server process not available"));
21582
21730
  return;
@@ -21596,7 +21744,7 @@ Stderr: ${this.stderrBuffer}`
21596
21744
  )
21597
21745
  );
21598
21746
  }, this.timeoutMs);
21599
- this.pending.set(id, { resolve: resolve13, reject, timer });
21747
+ this.pending.set(id, { resolve: resolve14, reject, timer });
21600
21748
  const message = JSON.stringify(request) + "\n";
21601
21749
  this.process.stdin.write(message);
21602
21750
  });
@@ -21659,15 +21807,45 @@ Stderr: ${this.stderrBuffer}`
21659
21807
 
21660
21808
  // packages/mcp/src/bridge/translator.ts
21661
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
+ }
21662
21839
  function translateMcpTool(mcpTool, serverName, client) {
21663
21840
  const separator = MCP_DEFAULTS.TOOL_NAME_SEPARATOR;
21664
21841
  const toolName = `${serverName}${separator}${mcpTool.name}`;
21665
- const inputSchema = mcpTool.inputSchema ?? {};
21666
- const parameters = {
21842
+ const inputSchema = isObject(mcpTool.inputSchema) ? mcpTool.inputSchema : {};
21843
+ const normalizedSchema = {
21667
21844
  type: "object",
21668
- properties: inputSchema.properties ?? {},
21669
- required: inputSchema.required
21845
+ properties: isObject(inputSchema.properties) ? inputSchema.properties : {},
21846
+ required: Array.isArray(inputSchema.required) ? inputSchema.required : []
21670
21847
  };
21848
+ const parameters = canonicalizeJsonSchema(normalizedSchema);
21671
21849
  const description = [
21672
21850
  mcpTool.description || `MCP tool: ${mcpTool.name}`,
21673
21851
  `[Server: ${serverName}]`
@@ -21725,6 +21903,7 @@ Check server status via /unipi:mcp-settings`
21725
21903
  };
21726
21904
  return {
21727
21905
  name: toolName,
21906
+ label: toolName,
21728
21907
  description,
21729
21908
  parameters,
21730
21909
  execute: execute2
@@ -21737,26 +21916,125 @@ var ServerRegistry = class {
21737
21916
  emitEvent;
21738
21917
  registerTool;
21739
21918
  unregisterTool;
21919
+ canUnregisterTools;
21740
21920
  timeoutMs;
21921
+ createClient;
21741
21922
  constructor(options) {
21742
21923
  this.emitEvent = options.emitEvent;
21743
21924
  this.registerTool = options.registerTool;
21744
21925
  this.unregisterTool = options.unregisterTool;
21926
+ this.canUnregisterTools = options.canUnregisterTools ?? true;
21745
21927
  this.timeoutMs = options.timeoutMs ?? MCP_DEFAULTS.STARTUP_TIMEOUT_MS;
21928
+ this.createClient = options.createClient ?? (() => new McpClient({ timeoutMs: this.timeoutMs }));
21746
21929
  }
21747
21930
  /**
21748
- * 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.
21749
21933
  */
21750
21934
  async startServer(resolved) {
21751
- const { name, def } = resolved;
21752
- 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) {
21753
21961
  throw new Error(
21754
21962
  `Maximum number of MCP servers (${MCP_DEFAULTS.MAX_SERVERS}) reached. Stop a server before starting a new one.`
21755
21963
  );
21756
21964
  }
21757
- if (this.entries.has(name)) {
21758
- 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;
21759
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;
21760
22038
  const state = {
21761
22039
  name,
21762
22040
  status: "starting",
@@ -21771,73 +22049,74 @@ var ServerRegistry = class {
21771
22049
  toolNames: []
21772
22050
  };
21773
22051
  this.entries.set(name, entry);
22052
+ let client = null;
21774
22053
  try {
21775
22054
  const safeCommand = typeof def.command === "string" ? def.command : String(def.command);
21776
22055
  const safeArgs = Array.isArray(def.args) ? def.args : [];
21777
22056
  let safeEnv;
21778
- if (def.env !== void 0 && def.env !== null) {
21779
- if (typeof def.env === "object" && !Array.isArray(def.env)) {
21780
- safeEnv = {};
21781
- for (const [k, v] of Object.entries(def.env)) {
21782
- safeEnv[k] = typeof v === "string" ? v : String(v);
21783
- }
21784
- } else {
21785
- }
21786
- }
21787
- const client = new McpClient({ timeoutMs: this.timeoutMs });
21788
- await client.connect(safeCommand, safeArgs, safeEnv);
21789
- entry.client = client;
21790
- const mcpTools = await client.listTools();
21791
- const toolNames = [];
21792
- for (const mcpTool of mcpTools) {
21793
- const piTool = translateMcpTool(mcpTool, name, client);
21794
- this.registerTool(piTool);
21795
- toolNames.push(piTool.name);
21796
- }
21797
- entry.state = {
21798
- ...state,
21799
- status: "running",
21800
- pid: client.pid,
21801
- toolCount: toolNames.length
21802
- };
21803
- entry.toolNames = toolNames;
21804
- this.emitEvent(UNIPI_EVENTS.MCP_SERVER_STARTED, {
21805
- name,
21806
- toolCount: toolNames.length
21807
- });
21808
- if (toolNames.length > 0) {
21809
- this.emitEvent(UNIPI_EVENTS.MCP_TOOLS_REGISTERED, {
21810
- serverName: name,
21811
- toolNames
21812
- });
21813
- }
21814
- } catch (err) {
21815
- const error = err instanceof Error ? err.message : String(err);
21816
- entry.state = {
21817
- ...state,
21818
- status: "error",
21819
- error
21820
- };
21821
- if (entry.client) {
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) {
21822
22074
  try {
21823
- await entry.client.disconnect();
22075
+ await client.disconnect();
21824
22076
  } catch {
21825
22077
  }
21826
- entry.client = null;
21827
22078
  }
21828
- this.emitEvent(UNIPI_EVENTS.MCP_SERVER_ERROR, {
21829
- name,
21830
- error
21831
- });
21832
- throw err;
22079
+ entry.client = null;
22080
+ this.emitEvent(UNIPI_EVENTS.MCP_SERVER_ERROR, { name, error: message });
22081
+ throw error;
21833
22082
  }
21834
22083
  }
21835
- /**
21836
- * Stop an MCP server: unregister tools, disconnect client.
21837
- */
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. */
21838
22112
  async stopServer(name) {
21839
22113
  const entry = this.entries.get(name);
21840
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
+ }
21841
22120
  for (const toolName of entry.toolNames) {
21842
22121
  this.unregisterTool(toolName);
21843
22122
  }
@@ -21854,78 +22133,63 @@ var ServerRegistry = class {
21854
22133
  }
21855
22134
  entry.client = null;
21856
22135
  }
21857
- entry.state = {
21858
- ...entry.state,
21859
- status: "stopped",
21860
- toolCount: 0
21861
- };
22136
+ entry.state = { ...entry.state, status: "stopped", toolCount: 0 };
21862
22137
  entry.toolNames = [];
21863
22138
  this.emitEvent(UNIPI_EVENTS.MCP_SERVER_STOPPED, { name });
21864
22139
  }
21865
- /**
21866
- * Restart an MCP server: stop then start.
21867
- */
22140
+ /** Restart an MCP server: stop then start. */
21868
22141
  async restartServer(name) {
21869
22142
  const entry = this.entries.get(name);
21870
- if (!entry) {
21871
- throw new Error(`Server '${name}' not found in registry`);
21872
- }
22143
+ if (!entry) throw new Error(`Server '${name}' not found in registry`);
21873
22144
  const resolved = entry.resolved;
21874
22145
  await this.stopServer(name);
21875
22146
  await this.startServer(resolved);
21876
22147
  }
21877
- /**
21878
- * Stop all running servers.
21879
- */
22148
+ /** Stop all running servers when runtime unregistration is supported. */
21880
22149
  async stopAll() {
21881
22150
  const names = Array.from(this.entries.keys());
21882
22151
  await Promise.allSettled(names.map((name) => this.stopServer(name)));
21883
22152
  }
21884
- /**
21885
- * Get all registered server states.
21886
- */
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. */
21887
22165
  getAll() {
21888
- return Array.from(this.entries.values()).map((e) => e.state);
22166
+ return Array.from(this.entries.values()).map((entry) => entry.state);
21889
22167
  }
21890
- /**
21891
- * Get states of running servers.
21892
- */
22168
+ /** Get states of running servers. */
21893
22169
  getActive() {
21894
- return this.getAll().filter((s) => s.status === "running");
22170
+ return this.getAll().filter((state) => state.status === "running");
21895
22171
  }
21896
- /**
21897
- * Get states of servers in error state.
21898
- */
22172
+ /** Get states of servers in error state. */
21899
22173
  getFailed() {
21900
- return this.getAll().filter((s) => s.status === "error");
22174
+ return this.getAll().filter((state) => state.status === "error");
21901
22175
  }
21902
- /**
21903
- * Get total number of tools across all active servers.
21904
- */
22176
+ /** Get total number of tools across all active servers. */
21905
22177
  getTotalToolCount() {
21906
- return this.getActive().reduce((sum, s) => sum + s.toolCount, 0);
22178
+ return this.getActive().reduce((sum, state) => sum + state.toolCount, 0);
21907
22179
  }
21908
- /**
21909
- * Get the state of a specific server.
21910
- */
22180
+ /** Get the state of a specific server. */
21911
22181
  getServerState(name) {
21912
22182
  return this.entries.get(name)?.state ?? null;
21913
22183
  }
21914
- /**
21915
- * Get the full registry entry for a server.
21916
- */
22184
+ /** Get the full registry entry for a server. */
21917
22185
  getEntry(name) {
21918
22186
  return this.entries.get(name) ?? null;
21919
22187
  }
21920
- /**
21921
- * Check if a server exists in the registry.
21922
- */
22188
+ /** Check if a server exists in the registry. */
21923
22189
  hasServer(name) {
21924
22190
  return this.entries.has(name);
21925
22191
  }
21926
- /**
21927
- * Get the number of registered servers.
21928
- */
22192
+ /** Get the number of registered servers. */
21929
22193
  get size() {
21930
22194
  return this.entries.size;
21931
22195
  }
@@ -22538,12 +22802,6 @@ function renderMcpSettingsOverlay(params) {
22538
22802
  addedAt: meta.servers[server.name]?.addedAt ?? (/* @__PURE__ */ new Date()).toISOString()
22539
22803
  };
22540
22804
  saveMetadata(configDir, meta);
22541
- if (!newEnabled && registry3) {
22542
- try {
22543
- await registry3.stopServer(server.name);
22544
- } catch {
22545
- }
22546
- }
22547
22805
  refreshServers();
22548
22806
  refresh();
22549
22807
  } catch (err) {
@@ -22690,7 +22948,7 @@ function renderMcpSettingsOverlay(params) {
22690
22948
  }
22691
22949
 
22692
22950
  // packages/mcp/src/index.ts
22693
- var VERSION8 = getPackageVersion(dirname19(fileURLToPath9(import.meta.url)));
22951
+ var VERSION8 = getPackageVersion(dirname18(fileURLToPath9(import.meta.url)));
22694
22952
  var registry2 = null;
22695
22953
  function getInfoRegistry5() {
22696
22954
  return globalThis.__unipi_info_registry;
@@ -22700,20 +22958,18 @@ function getRegistry() {
22700
22958
  }
22701
22959
  function src_default4(pi) {
22702
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
+ };
22703
22968
  registry2 = new ServerRegistry({
22704
22969
  emitEvent: (event, payload) => emitEvent(pi, event, payload),
22705
- registerTool: (tool) => {
22706
- try {
22707
- pi.registerTool?.(tool) ?? pi.registerExternalTool?.(tool);
22708
- } catch {
22709
- }
22710
- },
22711
- unregisterTool: (toolName) => {
22712
- try {
22713
- pi.unregisterTool?.(toolName) ?? pi.unregisterExternalTool?.(toolName);
22714
- } catch {
22715
- }
22716
- }
22970
+ registerTool,
22971
+ unregisterTool,
22972
+ canUnregisterTools
22717
22973
  });
22718
22974
  const cwd = ctx.cwd ?? process.cwd();
22719
22975
  let servers = [];
@@ -22722,13 +22978,10 @@ function src_default4(pi) {
22722
22978
  servers = result.servers;
22723
22979
  } catch (_err) {
22724
22980
  }
22725
- const startPromises = servers.filter((s) => s.enabled).map(async (server) => {
22726
- try {
22727
- await registry2.startServer(server);
22728
- } catch (err) {
22729
- }
22730
- });
22731
- await Promise.allSettled(startPromises);
22981
+ try {
22982
+ await registry2.startServers(servers.filter((server) => server.enabled));
22983
+ } catch (_err) {
22984
+ }
22732
22985
  const infoRegistry2 = getInfoRegistry5();
22733
22986
  if (infoRegistry2 && registry2) {
22734
22987
  const reg = registry2;
@@ -22785,14 +23038,12 @@ function src_default4(pi) {
22785
23038
  `unipi:${MCP_COMMANDS.STATUS}`,
22786
23039
  `unipi:${MCP_COMMANDS.RELOAD}`
22787
23040
  ],
22788
- tools: activeServers.flatMap(
22789
- (s) => registry2?.getEntry(s.name)?.toolNames ?? []
22790
- )
23041
+ tools: activeServers.flatMap((server) => registry2?.getEntry(server.name)?.toolNames ?? []).sort(compareCodeUnits)
22791
23042
  });
22792
23043
  });
22793
23044
  pi.on("session_shutdown", async (_event, _ctx) => {
22794
23045
  if (registry2) {
22795
- await registry2.stopAll();
23046
+ await registry2.disconnectAll();
22796
23047
  registry2 = null;
22797
23048
  }
22798
23049
  });
@@ -22906,38 +23157,16 @@ Updated: ${catalog.lastUpdated}`,
22906
23157
  }
22907
23158
  });
22908
23159
  pi.registerCommand(`unipi:${MCP_COMMANDS.RELOAD}`, {
22909
- description: "Reload all MCP servers (restart with current config)",
23160
+ description: "Explain how to reload MCP servers safely",
22910
23161
  handler: async (_args, ctx) => {
22911
- const reg = getRegistry();
22912
- if (!reg) {
22913
- ctx.ui.notify("MCP extension not initialized", "warning");
22914
- return;
22915
- }
22916
- const all = reg.getAll();
22917
- if (all.length === 0) {
22918
- ctx.ui.notify("No MCP servers configured. Use /unipi:mcp-add to add one.", "info");
22919
- return;
22920
- }
22921
- ctx.ui.notify(`Reloading ${all.length} MCP server(s)...`, "info");
22922
- let restarted = 0;
22923
- let failed = 0;
22924
- for (const state of all) {
22925
- try {
22926
- await reg.restartServer(state.name);
22927
- restarted++;
22928
- } catch (_err) {
22929
- failed++;
22930
- }
22931
- }
22932
- const msg = failed > 0 ? `Reloaded: ${restarted} ok, ${failed} failed` : `Reloaded ${restarted} MCP server(s) successfully`;
22933
- ctx.ui.notify(msg, failed > 0 ? "warning" : "info");
23162
+ ctx.ui.notify("Restart Pi to reload MCP servers and tool schemas safely.", "info");
22934
23163
  }
22935
23164
  });
22936
23165
  }
22937
23166
 
22938
23167
  // packages/notify/index.ts
22939
23168
  init_core();
22940
- import { dirname as dirname22 } from "node:path";
23169
+ import { dirname as dirname21 } from "node:path";
22941
23170
  import { fileURLToPath as fileURLToPath10 } from "node:url";
22942
23171
 
22943
23172
  // packages/notify/tools.ts
@@ -22947,7 +23176,7 @@ import { Type as Type6 } from "typebox";
22947
23176
  // packages/notify/settings.ts
22948
23177
  init_core();
22949
23178
  import { readFileSync as readFileSync19, writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, existsSync as existsSync24 } from "fs";
22950
- import { dirname as dirname20, join as join22 } from "path";
23179
+ import { dirname as dirname19, join as join22 } from "path";
22951
23180
  import { homedir as homedir17 } from "os";
22952
23181
  function resolveConfigPath() {
22953
23182
  const base = NOTIFY_DIRS.CONFIG.replace("~", homedir17());
@@ -23001,7 +23230,7 @@ function loadConfig2() {
23001
23230
  }
23002
23231
  function saveConfig2(config) {
23003
23232
  const configPath = resolveConfigPath();
23004
- const dir = dirname20(configPath);
23233
+ const dir = dirname19(configPath);
23005
23234
  mkdirSync15(dir, { recursive: true });
23006
23235
  writeFileSync17(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23007
23236
  }
@@ -23062,7 +23291,7 @@ init_core();
23062
23291
 
23063
23292
  // packages/notify/ntfy-config.ts
23064
23293
  import { readFileSync as readFileSync20, writeFileSync as writeFileSync18, mkdirSync as mkdirSync16, existsSync as existsSync25 } from "fs";
23065
- import { dirname as dirname21, join as join23 } from "path";
23294
+ import { dirname as dirname20, join as join23 } from "path";
23066
23295
  import { homedir as homedir18 } from "os";
23067
23296
  var DEFAULT_NTFY_CONFIG = {
23068
23297
  enabled: false,
@@ -23102,7 +23331,7 @@ function loadNtfyConfig(cwd) {
23102
23331
  }
23103
23332
  function saveNtfyConfig(scope, cwd, config) {
23104
23333
  const filePath = scope === "project" ? getProjectNtfyPath(cwd) : getGlobalNtfyPath();
23105
- const dir = dirname21(filePath);
23334
+ const dir = dirname20(filePath);
23106
23335
  mkdirSync16(dir, { recursive: true });
23107
23336
  writeFileSync18(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
23108
23337
  }
@@ -23142,7 +23371,7 @@ function migrateFromLegacyConfig() {
23142
23371
  token: ntfySection.token,
23143
23372
  priority: ntfySection.priority ?? 3
23144
23373
  };
23145
- const dir = dirname21(globalNtfyPath);
23374
+ const dir = dirname20(globalNtfyPath);
23146
23375
  mkdirSync16(dir, { recursive: true });
23147
23376
  writeFileSync18(
23148
23377
  globalNtfyPath,
@@ -23198,7 +23427,7 @@ async function isWindowFocusedOnWindows() {
23198
23427
  tmpDir = mkdtempSync(join24(tmpdir2(), "pi-focus-"));
23199
23428
  const scriptPath = join24(tmpDir, "check.ps1");
23200
23429
  writeFileSync19(scriptPath, POWERCHECK_SCRIPT, "utf-8");
23201
- const stdout = await new Promise((resolve13, reject) => {
23430
+ const stdout = await new Promise((resolve14, reject) => {
23202
23431
  execFile(
23203
23432
  "powershell.exe",
23204
23433
  [
@@ -23213,7 +23442,7 @@ async function isWindowFocusedOnWindows() {
23213
23442
  { timeout: 5e3, encoding: "utf-8" },
23214
23443
  (err, out) => {
23215
23444
  if (err) reject(err);
23216
- else resolve13(out);
23445
+ else resolve14(out);
23217
23446
  }
23218
23447
  );
23219
23448
  });
@@ -23255,7 +23484,7 @@ async function sendNativeNotification(title, message, options) {
23255
23484
  if (options?.suppressWhenFocused && await isWindowFocused()) {
23256
23485
  throw new SuppressedError2();
23257
23486
  }
23258
- return new Promise((resolve13, reject) => {
23487
+ return new Promise((resolve14, reject) => {
23259
23488
  notifier.notify(
23260
23489
  {
23261
23490
  title,
@@ -23270,7 +23499,7 @@ async function sendNativeNotification(title, message, options) {
23270
23499
  )
23271
23500
  );
23272
23501
  } else {
23273
- resolve13();
23502
+ resolve14();
23274
23503
  }
23275
23504
  }
23276
23505
  );
@@ -26006,7 +26235,7 @@ ${results.join("\n")}`, "info");
26006
26235
  }
26007
26236
 
26008
26237
  // packages/notify/index.ts
26009
- var VERSION9 = getPackageVersion(dirname22(fileURLToPath10(import.meta.url)));
26238
+ var VERSION9 = getPackageVersion(dirname21(fileURLToPath10(import.meta.url)));
26010
26239
  function notify_default(pi) {
26011
26240
  registerNotifyTools(pi);
26012
26241
  registerNotifyCommands(pi);
@@ -26035,6 +26264,9 @@ init_core();
26035
26264
  init_core();
26036
26265
  import * as fs14 from "node:fs";
26037
26266
  import * as path13 from "node:path";
26267
+ import {
26268
+ buildSessionContext as buildSessionContext3
26269
+ } from "@earendil-works/pi-coding-agent";
26038
26270
 
26039
26271
  // packages/milestone/milestone.ts
26040
26272
  init_core();
@@ -26180,7 +26412,7 @@ function getProgressSummary(filePath) {
26180
26412
  }
26181
26413
 
26182
26414
  // packages/milestone/hooks.ts
26183
- var sessionStartMs = 0;
26415
+ var MILESTONE_SNAPSHOT_TYPE = "unipi-milestone-snapshot";
26184
26416
  function formatMilestoneContext(filePath) {
26185
26417
  const summary = getProgressSummary(filePath);
26186
26418
  if (summary.totalItems === 0) return null;
@@ -26193,16 +26425,67 @@ function formatMilestoneContext(filePath) {
26193
26425
  focus
26194
26426
  ].filter(Boolean).join("\n");
26195
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
+ }
26196
26466
  function registerSessionStartHook(pi) {
26197
- pi.on("before_agent_start", (event) => {
26198
- sessionStartMs = Date.now();
26199
- const cwd = process.cwd();
26200
- const milestonesPath = path13.join(cwd, MILESTONE_DIRS.MILESTONES);
26467
+ pi.on("before_agent_start", (_event, ctx) => {
26468
+ const workspace = ctx.cwd;
26469
+ const milestonesPath = path13.join(workspace, MILESTONE_DIRS.MILESTONES);
26201
26470
  const context = formatMilestoneContext(milestonesPath);
26202
- if (!context) return void 0;
26203
- 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;
26204
26479
  return {
26205
- 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
+ }
26206
26489
  };
26207
26490
  });
26208
26491
  }
@@ -26258,14 +26541,16 @@ function scanModifiedDocs(dirs, since) {
26258
26541
  }
26259
26542
  function registerSessionEndHook(pi) {
26260
26543
  const baselineSnapshots = /* @__PURE__ */ new Map();
26261
- pi.on("session_start", () => {
26544
+ let sessionStartMs = 0;
26545
+ let sessionWorkspace = null;
26546
+ pi.on("session_start", (_event, ctx) => {
26262
26547
  sessionStartMs = Date.now();
26548
+ sessionWorkspace = ctx.cwd;
26263
26549
  baselineSnapshots.clear();
26264
- const cwd = process.cwd();
26265
26550
  const scanDirs = [
26266
- path13.join(cwd, ".unipi/docs/specs"),
26267
- path13.join(cwd, ".unipi/docs/plans"),
26268
- 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")
26269
26554
  ];
26270
26555
  for (const dir of scanDirs) {
26271
26556
  if (!fs14.existsSync(dir)) continue;
@@ -26279,13 +26564,13 @@ function registerSessionEndHook(pi) {
26279
26564
  }
26280
26565
  });
26281
26566
  const syncModifiedDocs = () => {
26282
- const cwd = process.cwd();
26283
- const milestonesPath = path13.join(cwd, MILESTONE_DIRS.MILESTONES);
26567
+ if (!sessionWorkspace) return;
26568
+ const milestonesPath = path13.join(sessionWorkspace, MILESTONE_DIRS.MILESTONES);
26284
26569
  if (!fs14.existsSync(milestonesPath)) return;
26285
26570
  const scanDirs = [
26286
- path13.join(cwd, ".unipi/docs/specs"),
26287
- path13.join(cwd, ".unipi/docs/plans"),
26288
- 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")
26289
26574
  ];
26290
26575
  const modifiedFiles = scanModifiedDocs(scanDirs, sessionStartMs);
26291
26576
  for (const filePath of modifiedFiles) {
@@ -26303,10 +26588,10 @@ function registerSessionEndHook(pi) {
26303
26588
 
26304
26589
  // packages/milestone/commands.ts
26305
26590
  init_core();
26306
- import { dirname as dirname23, join as join26 } from "node:path";
26591
+ import { dirname as dirname22, join as join26 } from "node:path";
26307
26592
  import { fileURLToPath as fileURLToPath11 } from "node:url";
26308
26593
  import { readFileSync as readFileSync21 } from "node:fs";
26309
- var SKILLS_DIR = join26(dirname23(fileURLToPath11(import.meta.url)), "skills");
26594
+ var SKILLS_DIR = join26(dirname22(fileURLToPath11(import.meta.url)), "skills");
26310
26595
  function loadSkill(skillName) {
26311
26596
  try {
26312
26597
  return readFileSync21(join26(SKILLS_DIR, skillName, "SKILL.md"), "utf-8");
@@ -26533,9 +26818,9 @@ var KanboardServer = class {
26533
26818
  }
26534
26819
  /** Bind server to a port */
26535
26820
  listen(port) {
26536
- return new Promise((resolve13, reject) => {
26821
+ return new Promise((resolve14, reject) => {
26537
26822
  this.server.once("error", reject);
26538
- this.server.listen(port, () => resolve13());
26823
+ this.server.listen(port, () => resolve14());
26539
26824
  });
26540
26825
  }
26541
26826
  /** Handle incoming HTTP request */
@@ -27504,11 +27789,11 @@ function markAutoCompactionError(state, nowMs) {
27504
27789
  }
27505
27790
 
27506
27791
  // packages/compactor/src/session/db.ts
27507
- import { createHash as createHash2 } from "node:crypto";
27792
+ import { createHash as createHash3 } from "node:crypto";
27508
27793
  import { execFileSync } from "node:child_process";
27509
27794
  import { existsSync as existsSync32, mkdirSync as mkdirSync20 } from "node:fs";
27510
27795
  import { homedir as homedir21 } from "node:os";
27511
- import { dirname as dirname26, join as join33 } from "node:path";
27796
+ import { dirname as dirname25, join as join33 } from "node:path";
27512
27797
  function getWorktreeSuffix() {
27513
27798
  const envSuffix = process.env.COMPACTOR_SESSION_SUFFIX;
27514
27799
  if (envSuffix !== void 0) {
@@ -27522,7 +27807,7 @@ function getWorktreeSuffix() {
27522
27807
  { encoding: "utf-8", timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] }
27523
27808
  ).split(/\r?\n/).find((l) => l.startsWith("worktree "))?.replace("worktree ", "")?.trim();
27524
27809
  if (mainWorktree && cwd !== mainWorktree) {
27525
- const suffix = createHash2("sha256").update(cwd).digest("hex").slice(0, 8);
27810
+ const suffix = createHash3("sha256").update(cwd).digest("hex").slice(0, 8);
27526
27811
  return `__${suffix}`;
27527
27812
  }
27528
27813
  } catch {
@@ -27553,7 +27838,7 @@ var SessionDB = class {
27553
27838
  this.dbPath = opts?.dbPath ?? defaultDBPath("session");
27554
27839
  }
27555
27840
  async init() {
27556
- const dir = dirname26(this.dbPath);
27841
+ const dir = dirname25(this.dbPath);
27557
27842
  if (!existsSync32(dir)) mkdirSync20(dir, { recursive: true });
27558
27843
  const sqlite = await getSQLite();
27559
27844
  const Database2 = sqlite.Database ?? sqlite.default?.Database ?? sqlite.default ?? sqlite;
@@ -27664,7 +27949,7 @@ var SessionDB = class {
27664
27949
  }
27665
27950
  insertEvent(sessionId, event, sourceHook = "PostToolUse") {
27666
27951
  if (!this.stmts) return;
27667
- 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();
27668
27953
  const projectDir = String(event.project_dir ?? "").trim();
27669
27954
  const attributionSource = String(event.attribution_source ?? "unknown");
27670
27955
  const rawConfidence = Number(event.attribution_confidence ?? 0);
@@ -27948,7 +28233,7 @@ async function buildResumeContextMessage(db, sessionId) {
27948
28233
  // packages/compactor/src/executor/executor.ts
27949
28234
  import { spawn as spawn3, execSync, execFileSync as execFileSync2 } from "node:child_process";
27950
28235
  import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync24, rmSync as rmSync3 } from "node:fs";
27951
- import { join as join34, resolve as resolve10 } from "node:path";
28236
+ import { join as join34, resolve as resolve11 } from "node:path";
27952
28237
  import { tmpdir as tmpdir3 } from "node:os";
27953
28238
 
27954
28239
  // packages/compactor/src/executor/runtime.ts
@@ -28040,7 +28325,7 @@ var OS_TMPDIR = (() => {
28040
28325
  process.platform === "darwin" ? ["DARWIN_USER_TEMP_DIR"] : ["-u", "-d"],
28041
28326
  { env: { ...process.env, TMPDIR: void 0 }, encoding: "utf-8" }
28042
28327
  ).trim();
28043
- const dir = process.platform === "darwin" ? result : resolve10(result, "..");
28328
+ const dir = process.platform === "darwin" ? result : resolve11(result, "..");
28044
28329
  if (dir && dir !== process.cwd()) return dir;
28045
28330
  } catch {
28046
28331
  }
@@ -28161,7 +28446,7 @@ var PolyglotExecutor = class {
28161
28446
  async executeFile(opts) {
28162
28447
  const { language, path: path28, timeout = 3e4, background = false } = opts;
28163
28448
  const cmd = buildCommand(this.#runtimes, language, path28);
28164
- const cwd = language === "shell" ? this.#projectRoot : resolve10(path28, "..");
28449
+ const cwd = language === "shell" ? this.#projectRoot : resolve11(path28, "..");
28165
28450
  return this.#spawn(cmd, cwd, "", timeout, background);
28166
28451
  }
28167
28452
  #writeScript(tmpDir, code, language) {
@@ -28184,7 +28469,7 @@ var PolyglotExecutor = class {
28184
28469
  return filePath;
28185
28470
  }
28186
28471
  async #spawn(cmd, cwd, tmpDir, timeout, background) {
28187
- return new Promise((resolve13) => {
28472
+ return new Promise((resolve14) => {
28188
28473
  const proc = spawn3(cmd[0], cmd.slice(1), {
28189
28474
  cwd,
28190
28475
  env: sanitizeEnv(),
@@ -28220,14 +28505,14 @@ var PolyglotExecutor = class {
28220
28505
  }, timeout);
28221
28506
  proc.on("error", (err) => {
28222
28507
  if (timer) clearTimeout(timer);
28223
- resolve13({ stdout, stderr: err.message, exitCode: 1, timedOut: false });
28508
+ resolve14({ stdout, stderr: err.message, exitCode: 1, timedOut: false });
28224
28509
  });
28225
28510
  proc.on("close", (code) => {
28226
28511
  if (timer) clearTimeout(timer);
28227
28512
  if (background && proc.pid) {
28228
28513
  this.#backgroundedPids.add(proc.pid);
28229
28514
  }
28230
- resolve13({
28515
+ resolve14({
28231
28516
  stdout: stdout.slice(0, this.#hardCapBytes),
28232
28517
  stderr: stderr.slice(0, this.#hardCapBytes),
28233
28518
  exitCode: code ?? 0,
@@ -28265,7 +28550,7 @@ init_core();
28265
28550
  init_hooks();
28266
28551
 
28267
28552
  // packages/compactor/src/compaction/search-entries.ts
28268
- import { createHash as createHash4 } from "node:crypto";
28553
+ import { createHash as createHash5 } from "node:crypto";
28269
28554
  function tokenize(text) {
28270
28555
  return text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1);
28271
28556
  }
@@ -28309,7 +28594,7 @@ function searchEntries(blocks, query, opts) {
28309
28594
  kind: b.kind
28310
28595
  }));
28311
28596
  const hashSource = docs.length > 0 ? `${docs.length}:${docs[0].text.slice(0, 80)}:${docs[docs.length - 1].text.slice(-80)}` : "empty";
28312
- const currentHash = createHash4("sha256").update(hashSource).digest("hex");
28597
+ const currentHash = createHash5("sha256").update(hashSource).digest("hex");
28313
28598
  let index;
28314
28599
  let docCount;
28315
28600
  let avgDocLen;
@@ -32795,7 +33080,7 @@ import { Key as Key6, matchesKey as matchesKey14, truncateToWidth as truncateToW
32795
33080
  // packages/updater/src/readme.ts
32796
33081
  init_core();
32797
33082
  init_core();
32798
- import { dirname as dirname28 } from "node:path";
33083
+ import { dirname as dirname27 } from "node:path";
32799
33084
  import { fileURLToPath as fileURLToPath12 } from "node:url";
32800
33085
  import { existsSync as existsSync39, readFileSync as readFileSync34 } from "fs";
32801
33086
  import { join as join38 } from "path";
@@ -32822,7 +33107,7 @@ var PACKAGE_MAP = {
32822
33107
  updater: MODULES.UPDATER
32823
33108
  };
32824
33109
  function resolveUnipiRoot() {
32825
- const dir = dirname28(fileURLToPath12(import.meta.url));
33110
+ const dir = dirname27(fileURLToPath12(import.meta.url));
32826
33111
  const root = findPackageRoot(dir, "@pi-unipi/unipi");
32827
33112
  return root ?? join38(dir, "..");
32828
33113
  }
@@ -33159,7 +33444,7 @@ import { Key as Key7, matchesKey as matchesKey15, truncateToWidth as truncateToW
33159
33444
  // packages/updater/src/changelog.ts
33160
33445
  init_core();
33161
33446
  import { existsSync as existsSync40, readFileSync as readFileSync36 } from "fs";
33162
- import { dirname as dirname29, join as join39 } from "node:path";
33447
+ import { dirname as dirname28, join as join39 } from "node:path";
33163
33448
  import { fileURLToPath as fileURLToPath13 } from "node:url";
33164
33449
 
33165
33450
  // packages/updater/src/version.ts
@@ -33184,7 +33469,7 @@ function isNewerVersion(latest, current) {
33184
33469
  // packages/updater/src/changelog.ts
33185
33470
  function resolveChangelogPath() {
33186
33471
  try {
33187
- const dir = dirname29(fileURLToPath13(import.meta.url));
33472
+ const dir = dirname28(fileURLToPath13(import.meta.url));
33188
33473
  const root = findPackageRoot(dir, "@pi-unipi/unipi");
33189
33474
  if (root) {
33190
33475
  const packaged = join39(root, "CHANGELOG.md");
@@ -33445,7 +33730,7 @@ function renderChangelogOverlay() {
33445
33730
  // packages/updater/src/settings.ts
33446
33731
  init_core();
33447
33732
  import { readFileSync as readFileSync37, writeFileSync as writeFileSync26, mkdirSync as mkdirSync22, existsSync as existsSync42 } from "fs";
33448
- import { dirname as dirname30, join as join40 } from "path";
33733
+ import { dirname as dirname29, join as join40 } from "path";
33449
33734
  import { homedir as homedir24 } from "os";
33450
33735
  var DEFAULT_CONFIG6 = {
33451
33736
  checkIntervalMs: 36e5,
@@ -33477,7 +33762,7 @@ function loadConfig5() {
33477
33762
  }
33478
33763
  function saveConfig4(config) {
33479
33764
  const configPath = resolveConfigPath2();
33480
- const dir = dirname30(configPath);
33765
+ const dir = dirname29(configPath);
33481
33766
  mkdirSync22(dir, { recursive: true });
33482
33767
  writeFileSync26(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
33483
33768
  }
@@ -33713,7 +33998,7 @@ init_core();
33713
33998
  // packages/updater/src/cache.ts
33714
33999
  init_core();
33715
34000
  import { readFileSync as readFileSync38, writeFileSync as writeFileSync27, mkdirSync as mkdirSync23, existsSync as existsSync43 } from "fs";
33716
- import { dirname as dirname31, join as join41 } from "path";
34001
+ import { dirname as dirname30, join as join41 } from "path";
33717
34002
  import { homedir as homedir25 } from "os";
33718
34003
  function resolveCachePath() {
33719
34004
  const base = UPDATER_DIRS.CACHE.replace("~", homedir25());
@@ -33735,7 +34020,7 @@ function readLastCheck() {
33735
34020
  }
33736
34021
  function writeLastCheck(cache) {
33737
34022
  const cachePath = resolveCachePath();
33738
- const dir = dirname31(cachePath);
34023
+ const dir = dirname30(cachePath);
33739
34024
  mkdirSync23(dir, { recursive: true });
33740
34025
  writeFileSync27(cachePath, JSON.stringify(cache, null, 2) + "\n", "utf-8");
33741
34026
  }
@@ -34145,8 +34430,8 @@ init_core();
34145
34430
  import { Key as Key10 } from "@earendil-works/pi-tui";
34146
34431
 
34147
34432
  // packages/input-shortcuts/src/registers.ts
34148
- import { existsSync as existsSync44, mkdirSync as mkdirSync24, readFileSync as readFileSync39, renameSync as renameSync7, writeFileSync as writeFileSync28 } from "node:fs";
34149
- import { dirname as dirname32, join as join42 } from "node:path";
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";
34150
34435
 
34151
34436
  // packages/input-shortcuts/src/types.ts
34152
34437
  var DEFAULT_CONFIG7 = {
@@ -34216,13 +34501,13 @@ var RegisterStore = class {
34216
34501
  /** Atomic write: write to .tmp then rename. */
34217
34502
  save() {
34218
34503
  try {
34219
- const dir = dirname32(this.filePath);
34504
+ const dir = dirname31(this.filePath);
34220
34505
  if (!existsSync44(dir)) {
34221
34506
  mkdirSync24(dir, { recursive: true });
34222
34507
  }
34223
34508
  const tmpPath = this.filePath + ".tmp";
34224
34509
  writeFileSync28(tmpPath, JSON.stringify(this.data, null, 2), "utf-8");
34225
- renameSync7(tmpPath, this.filePath);
34510
+ renameSync8(tmpPath, this.filePath);
34226
34511
  } catch {
34227
34512
  }
34228
34513
  }
@@ -34443,8 +34728,8 @@ var ChordOverlay = class extends Container2 {
34443
34728
 
34444
34729
  // packages/input-shortcuts/src/settings-overlay.ts
34445
34730
  import { SettingsList as SettingsList3 } from "@earendil-works/pi-tui";
34446
- import { existsSync as existsSync45, mkdirSync as mkdirSync25, readFileSync as readFileSync40, renameSync as renameSync8, writeFileSync as writeFileSync29 } from "node:fs";
34447
- import { dirname as dirname33, join as join43 } from "node:path";
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";
34448
34733
  var ALT_KEY_OPTIONS = [
34449
34734
  "alt+a",
34450
34735
  "alt+b",
@@ -34493,13 +34778,13 @@ function loadConfig6(baseDir) {
34493
34778
  function saveConfig5(config, baseDir) {
34494
34779
  const filePath = baseDir ? join43(baseDir, CONFIG_FILE) : CONFIG_FILE;
34495
34780
  try {
34496
- const dir = dirname33(filePath);
34781
+ const dir = dirname32(filePath);
34497
34782
  if (!existsSync45(dir)) {
34498
34783
  mkdirSync25(dir, { recursive: true });
34499
34784
  }
34500
34785
  const tmpPath = filePath + ".tmp";
34501
34786
  writeFileSync29(tmpPath, JSON.stringify(config, null, 2), "utf-8");
34502
- renameSync8(tmpPath, filePath);
34787
+ renameSync9(tmpPath, filePath);
34503
34788
  } catch {
34504
34789
  }
34505
34790
  }
@@ -34927,7 +35212,7 @@ import { Type as Type8 } from "typebox";
34927
35212
  // packages/cocoindex/bridge.ts
34928
35213
  init_core();
34929
35214
  import { execFileSync as execFileSync3, spawn as spawn4 } from "node:child_process";
34930
- import { existsSync as existsSync46, mkdirSync as mkdirSync26, readFileSync as readFileSync41, writeFileSync as writeFileSync30, readdirSync as readdirSync13, statSync as statSync8 } from "node:fs";
35215
+ import { existsSync as existsSync46, mkdirSync as mkdirSync26, readFileSync as readFileSync41, writeFileSync as writeFileSync30, readdirSync as readdirSync14, statSync as statSync9 } from "node:fs";
34931
35216
  import { homedir as homedir26 } from "node:os";
34932
35217
  import { join as join44 } from "node:path";
34933
35218
  var DEFAULT_PIPELINE_DIR = ".unipi/cocoindex";
@@ -35003,7 +35288,7 @@ function resolveCocoindexBin() {
35003
35288
  if (existsSync46(uvToolBin)) return uvToolBin;
35004
35289
  const miseRoot = join44(homedir26(), ".local", "share", "mise", "installs", "python");
35005
35290
  try {
35006
- const versions = readdirSync13(miseRoot).sort().reverse();
35291
+ const versions = readdirSync14(miseRoot).sort().reverse();
35007
35292
  for (const ver of versions) {
35008
35293
  const binPath = join44(miseRoot, ver, "bin", "cocoindex");
35009
35294
  if (existsSync46(binPath)) return binPath;
@@ -35066,7 +35351,7 @@ async function indexProject(projectDir) {
35066
35351
  }
35067
35352
  const start = Date.now();
35068
35353
  const cocoindexBin = resolveCocoindexBin();
35069
- return new Promise((resolve13) => {
35354
+ return new Promise((resolve14) => {
35070
35355
  const proc = spawn4(cocoindexBin, ["update", "main.py"], {
35071
35356
  cwd: pipelineDir,
35072
35357
  stdio: ["pipe", "pipe", "pipe"]
@@ -35096,9 +35381,9 @@ async function indexProject(projectDir) {
35096
35381
  const durationMs = Date.now() - start;
35097
35382
  const chunksProcessed = parseChunksProcessed(stdout);
35098
35383
  if (code === 0) {
35099
- resolve13({ success: true, chunksProcessed, durationMs });
35384
+ resolve14({ success: true, chunksProcessed, durationMs });
35100
35385
  } else {
35101
- resolve13({
35386
+ resolve14({
35102
35387
  success: false,
35103
35388
  chunksProcessed,
35104
35389
  durationMs,
@@ -35109,7 +35394,7 @@ async function indexProject(projectDir) {
35109
35394
  proc.on("error", (err) => {
35110
35395
  settled = true;
35111
35396
  clearTimeout(timer);
35112
- resolve13({
35397
+ resolve14({
35113
35398
  success: false,
35114
35399
  chunksProcessed: 0,
35115
35400
  durationMs: Date.now() - start,
@@ -35174,9 +35459,9 @@ async function status(projectDir) {
35174
35459
  const lancedbPath = join44(pipelineDir, ".lancedb");
35175
35460
  if (existsSync46(lancedbPath)) {
35176
35461
  try {
35177
- const stat = statSync8(lancedbPath);
35462
+ const stat = statSync9(lancedbPath);
35178
35463
  lastRun = stat.mtime.toISOString();
35179
- const files = readdirSync13(lancedbPath, { recursive: true });
35464
+ const files = readdirSync14(lancedbPath, { recursive: true });
35180
35465
  docCount = files.filter((f) => f.endsWith(".lance")).length;
35181
35466
  } catch {
35182
35467
  }
@@ -35836,7 +36121,7 @@ function buildUvInstallerFallback(shell) {
35836
36121
  ];
35837
36122
  }
35838
36123
  function runStep(step) {
35839
- return new Promise((resolve13) => {
36124
+ return new Promise((resolve14) => {
35840
36125
  const proc = spawn5(step.command, step.args ?? [], {
35841
36126
  stdio: ["ignore", "pipe", "pipe"],
35842
36127
  env: installerEnv()
@@ -35859,12 +36144,12 @@ Timed out after ${step.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS}ms.`;
35859
36144
  proc.on("close", (code, signal) => {
35860
36145
  settled = true;
35861
36146
  clearTimeout(timeout);
35862
- resolve13({ ok: code === 0, stdout, stderr, exitCode: code, signal });
36147
+ resolve14({ ok: code === 0, stdout, stderr, exitCode: code, signal });
35863
36148
  });
35864
36149
  proc.on("error", (err) => {
35865
36150
  settled = true;
35866
36151
  clearTimeout(timeout);
35867
- resolve13({ ok: false, stdout, stderr: stderr || err.message });
36152
+ resolve14({ ok: false, stdout, stderr: stderr || err.message });
35868
36153
  });
35869
36154
  });
35870
36155
  }
@@ -36066,7 +36351,7 @@ function cocoindexExtension(pi) {
36066
36351
 
36067
36352
  // packages/image/src/index.ts
36068
36353
  init_core();
36069
- import { dirname as dirname35 } from "node:path";
36354
+ import { dirname as dirname34 } from "node:path";
36070
36355
  import { fileURLToPath as fileURLToPath14 } from "node:url";
36071
36356
 
36072
36357
  // packages/image/src/commands.ts
@@ -37820,7 +38105,7 @@ function currentSessionModel(ctx) {
37820
38105
  }
37821
38106
 
37822
38107
  // packages/image/src/index.ts
37823
- var VERSION11 = getPackageVersion(dirname35(fileURLToPath14(import.meta.url)));
38108
+ var VERSION11 = getPackageVersion(dirname34(fileURLToPath14(import.meta.url)));
37824
38109
  function getInfoRegistry6() {
37825
38110
  return globalThis.__unipi_info_registry;
37826
38111
  }