@pi-unipi/unipi 2.4.2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -0
  3. package/docs/prefix-cache-architecture.md +89 -0
  4. package/package.json +24 -22
  5. package/packages/ask-user/package.json +2 -2
  6. package/packages/autocomplete/package.json +1 -1
  7. package/packages/autocomplete/src/constants.ts +2 -0
  8. package/packages/btw/package.json +2 -2
  9. package/packages/cocoindex/README.md +2 -1
  10. package/packages/cocoindex/index.ts +6 -13
  11. package/packages/cocoindex/package.json +4 -3
  12. package/packages/cocoindex/tools.ts +45 -11
  13. package/packages/compactor/README.md +4 -2
  14. package/packages/compactor/package.json +3 -3
  15. package/packages/compactor/src/session/snapshot.ts +3 -2
  16. package/packages/compactor/src/tools/register.ts +6 -2
  17. package/packages/compactor/src/tools/vcc-recall.ts +18 -3
  18. package/packages/core/bounded-output.ts +106 -0
  19. package/packages/core/constants.ts +2 -0
  20. package/packages/core/index.ts +1 -0
  21. package/packages/core/package.json +1 -1
  22. package/packages/core/sandbox.ts +7 -6
  23. package/packages/footer/package.json +2 -2
  24. package/packages/image/package.json +2 -2
  25. package/packages/info-screen/package.json +2 -2
  26. package/packages/input-shortcuts/package.json +2 -2
  27. package/packages/kanboard/package.json +2 -2
  28. package/packages/mcp/README.md +13 -0
  29. package/packages/mcp/package.json +5 -2
  30. package/packages/mcp/src/bridge/registry.ts +244 -137
  31. package/packages/mcp/src/bridge/translator.ts +96 -21
  32. package/packages/mcp/src/index.ts +43 -67
  33. package/packages/mcp/src/tui/settings-overlay.ts +3 -9
  34. package/packages/memory/README.md +15 -11
  35. package/packages/memory/bridge/mempalace_bridge.py +636 -0
  36. package/packages/memory/index.ts +56 -34
  37. package/packages/memory/mempalace.ts +169 -18
  38. package/packages/memory/package.json +3 -3
  39. package/packages/memory/storage.ts +27 -10
  40. package/packages/milestone/README.md +11 -3
  41. package/packages/milestone/hooks.ts +103 -29
  42. package/packages/milestone/index.ts +1 -1
  43. package/packages/milestone/package.json +5 -2
  44. package/packages/notify/package.json +2 -2
  45. package/packages/ralph/index.ts +12 -18
  46. package/packages/ralph/package.json +6 -3
  47. package/packages/ralph/reminder.ts +40 -0
  48. package/packages/ralph/tools.ts +5 -1
  49. package/packages/subagents/README.md +2 -0
  50. package/packages/subagents/package.json +1 -1
  51. package/packages/subagents/src/agent-manager.ts +5 -1
  52. package/packages/subagents/src/agent-runner.ts +2 -2
  53. package/packages/subagents/src/core-compat.ts +73 -0
  54. package/packages/subagents/src/custom-agents.ts +10 -2
  55. package/packages/subagents/src/index.ts +14 -3
  56. package/packages/subagents/src/types.ts +2 -0
  57. package/packages/unipi/bundled.js +1445 -686
  58. package/packages/updater/package.json +2 -2
  59. package/packages/utility/README.md +9 -0
  60. package/packages/utility/package.json +2 -2
  61. package/packages/utility/src/index.ts +48 -0
  62. package/packages/utility/src/lifecycle/cleanup.ts +29 -0
  63. package/packages/utility/src/prefix-cache.ts +263 -0
  64. package/packages/utility/src/types.ts +1 -1
  65. package/packages/web-api/package.json +2 -2
  66. package/packages/workflow/README.md +8 -0
  67. package/packages/workflow/commands.ts +16 -26
  68. package/packages/workflow/index.ts +165 -85
  69. package/packages/workflow/package.json +5 -2
@@ -0,0 +1,106 @@
1
+ import { chmodSync, lstatSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+
6
+ export const DEFAULT_MODEL_OUTPUT_BYTES = 64 * 1024;
7
+ export const MAX_RAW_ARTIFACT_BYTES = 16 * 1024 * 1024;
8
+
9
+ export interface BoundedOutput {
10
+ text: string;
11
+ truncated: boolean;
12
+ originalBytes: number;
13
+ visibleBytes: number;
14
+ artifactPath?: string;
15
+ }
16
+
17
+ export interface BoundOutputOptions {
18
+ maxBytes?: number;
19
+ artifactPrefix?: string;
20
+ artifactDir?: string;
21
+ }
22
+
23
+ function byteSlice(text: string, start: number, end?: number): string {
24
+ return Buffer.from(text, "utf8").subarray(start, end).toString("utf8").replace(/\uFFFD+$/u, "");
25
+ }
26
+
27
+ function secureArtifactDir(customDir?: string): string {
28
+ const dir = customDir ?? join(homedir(), ".unipi", "tool-results");
29
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
30
+ let stat = lstatSync(dir);
31
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
32
+ throw new Error(`Refusing unsafe tool-result directory: ${dir}`);
33
+ }
34
+ if ((stat.mode & 0o077) !== 0) {
35
+ chmodSync(dir, 0o700);
36
+ stat = lstatSync(dir);
37
+ if ((stat.mode & 0o077) !== 0) {
38
+ throw new Error(`Refusing non-private tool-result directory: ${dir}`);
39
+ }
40
+ }
41
+ return dir;
42
+ }
43
+
44
+ function safePrefix(prefix: string): string {
45
+ const safe = prefix.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").slice(0, 48);
46
+ return safe || "tool-result";
47
+ }
48
+
49
+ /**
50
+ * Bound model-visible UTF-8 output and, up to the raw artifact safety cap,
51
+ * preserve the complete text in a private local artifact.
52
+ *
53
+ * Artifacts use random names and mode 0600 beneath a mode-0700 directory. The
54
+ * returned path is intentionally explicit so the agent can retrieve it with
55
+ * the ordinary read tool only when full output is actually needed.
56
+ */
57
+ export function boundModelOutput(text: string, options: BoundOutputOptions = {}): BoundedOutput {
58
+ const maxBytes = Math.max(1024, Math.floor(options.maxBytes ?? DEFAULT_MODEL_OUTPUT_BYTES));
59
+ const originalBytes = Buffer.byteLength(text, "utf8");
60
+ if (originalBytes <= maxBytes) {
61
+ return { text, truncated: false, originalBytes, visibleBytes: originalBytes };
62
+ }
63
+
64
+ let artifactPath: string | undefined;
65
+ let artifactWarning: string | undefined;
66
+ if (originalBytes <= MAX_RAW_ARTIFACT_BYTES) {
67
+ try {
68
+ const dir = secureArtifactDir(options.artifactDir);
69
+ artifactPath = join(dir, `${safePrefix(options.artifactPrefix ?? "tool-result")}-${randomUUID()}.txt`);
70
+ writeFileSync(artifactPath, text, { encoding: "utf8", mode: 0o600, flag: "wx" });
71
+ } catch (error) {
72
+ artifactWarning = `Full-output artifact unavailable: ${error instanceof Error ? error.message : String(error)}`;
73
+ artifactPath = undefined;
74
+ }
75
+ } else {
76
+ artifactWarning = `Full output exceeded the ${MAX_RAW_ARTIFACT_BYTES}-byte local artifact safety cap and was not retained.`;
77
+ }
78
+
79
+ const marker = [
80
+ "",
81
+ "--- output bounded by UniPi ---",
82
+ artifactPath ? `Full output: ${artifactPath}` : artifactWarning!,
83
+ `Original size: ${originalBytes} bytes; model-visible ceiling: ${maxBytes} bytes.`,
84
+ ...(artifactPath ? ["Use the read tool with offset/limit to inspect only the needed region."] : []),
85
+ ].join("\n");
86
+ const omissionReserve = 80;
87
+ const markerBytes = Buffer.byteLength(marker, "utf8");
88
+ const contentBudget = Math.max(1, maxBytes - markerBytes - omissionReserve);
89
+ const headBytes = Math.ceil(contentBudget * 0.75);
90
+ const tailBytes = Math.max(0, contentBudget - headBytes);
91
+ const head = byteSlice(text, 0, headBytes);
92
+ const tail = tailBytes > 0 ? byteSlice(text, originalBytes - tailBytes) : "";
93
+ const omission = `\n… ${Math.max(0, originalBytes - contentBudget)} bytes omitted …\n`;
94
+ let bounded = `${head}${omission}${tail}${marker}`;
95
+ if (Buffer.byteLength(bounded, "utf8") > maxBytes) {
96
+ bounded = byteSlice(bounded, 0, maxBytes);
97
+ }
98
+
99
+ return {
100
+ text: bounded,
101
+ truncated: true,
102
+ originalBytes,
103
+ visibleBytes: Buffer.byteLength(bounded, "utf8"),
104
+ artifactPath,
105
+ };
106
+ }
@@ -168,6 +168,7 @@ export const UTILITY_COMMANDS = {
168
168
  BADGE_TOGGLE: "badge-toggle",
169
169
  BADGE_SETTINGS: "badge-settings",
170
170
  UTIL_SETTINGS: "util-settings",
171
+ PREFIX_CACHE: "prefix-cache",
171
172
  } as const;
172
173
 
173
174
  /** Utility tool names */
@@ -232,6 +233,7 @@ export const MCP_DEFAULTS = {
232
233
  STARTUP_TIMEOUT_MS: 10000,
233
234
  MAX_SERVERS: 20,
234
235
  TOOL_NAME_SEPARATOR: "__",
236
+ MAX_MODEL_OUTPUT_BYTES: 64 * 1024,
235
237
  } as const;
236
238
 
237
239
  /** Compactor sentinel — when passed as customInstructions to ctx.compact(),
@@ -10,3 +10,4 @@ export * from "./sandbox.js";
10
10
  export * from "./utils.js";
11
11
  export * from "./model-cache.js";
12
12
  export * from "./tui-width.js";
13
+ export * from "./bounded-output.js";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -2,7 +2,8 @@
2
2
  * @unipi/core — Sandbox module
3
3
  *
4
4
  * Defines tool access levels for workflow commands.
5
- * Used with pi.setActiveTools() to enforce restrictions.
5
+ * Workflow enforces blocked names at tool_call time so provider tool schemas
6
+ * and ordering remain stable; filtering helpers remain available to other callers.
6
7
  */
7
8
 
8
9
  import { WORKFLOW_COMMANDS } from "./constants.js";
@@ -13,9 +14,9 @@ export type SandboxLevel = "read_only" | "brainstorm" | "write_unipi" | "review"
13
14
  /**
14
15
  * Built-in workflow tools used when no active tool list is supplied.
15
16
  *
16
- * Workflow commands should normally call filterToolsForLevel() with the current
17
- * active tool list. That keeps safe extension tools (memory, ask-user, web,
18
- * notify, etc.) available while removing only tools that violate the sandbox.
17
+ * Legacy fallback list used by callers that need an explicit filtered tool set.
18
+ * The workflow package itself keeps Pi's active tools unchanged and enforces
19
+ * BLOCKED_TOOLS at tool_call time.
19
20
  */
20
21
  const FALLBACK_TOOLS: Record<SandboxLevel, readonly string[]> = {
21
22
  /** Only read-only file tools — no bash, no write, no edit */
@@ -78,8 +79,8 @@ export function getSandboxLevel(commandName: string): SandboxLevel {
78
79
  /**
79
80
  * Get fallback tools for a sandbox level.
80
81
  *
81
- * Prefer filterToolsForLevel(level, activeTools) when applying a sandbox so
82
- * extension tools are preserved unless explicitly blocked.
82
+ * Prefer isToolAllowed() for cache-stable call-time enforcement. Filtering is
83
+ * retained for compatibility with callers that intentionally alter tool sets.
83
84
  */
84
85
  export function getToolsForLevel(level: SandboxLevel): readonly string[] {
85
86
  return FALLBACK_TOOLS[level];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.4.1"
35
+ "@pi-unipi/core": "2.6.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/image",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Image generation and image recognition tools for the Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.4.1"
37
+ "@pi-unipi/core": "2.6.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-ai": "^0.80.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "2.4.1"
36
+ "@pi-unipi/core": "2.6.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/input-shortcuts",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "2.4.1"
36
+ "@pi-unipi/core": "2.6.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Visualization layer for unipi workflow — HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@pi-unipi/core": "2.4.1"
42
+ "@pi-unipi/core": "2.6.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -12,6 +12,7 @@ The add command opens a split-pane overlay: server browser on the left, JSON con
12
12
  | `/unipi:mcp-settings` | Interactive settings with enable/disable/edit |
13
13
  | `/unipi:mcp-sync` | Force sync server catalog from GitHub |
14
14
  | `/unipi:mcp-status` | Text summary of all configured servers |
15
+ | `/unipi:mcp-reload` | Remind you to restart Pi so tool schemas reload as a clean cache epoch |
15
16
 
16
17
  ### Setup Flow
17
18
 
@@ -30,6 +31,18 @@ MCP registers with the info-screen dashboard, showing server count, active serve
30
31
 
31
32
  MCP tools are registered dynamically based on configured servers. Once a server is added and Pi restarts, its tools become available to the agent.
32
33
 
34
+ ### Deterministic Definitions and Cache Behavior
35
+
36
+ At session startup, enabled servers connect and discover tools in parallel. Registration waits for all discoveries to settle, then registers the successful combined tool set in canonical `{serverName}__{toolName}` order. Duplicate final names are rejected explicitly instead of allowing one definition to overwrite another.
37
+
38
+ MCP input properties are cloned and recursively canonicalized before registration: schema object keys use locale-independent UTF-16 code-unit order, valid schema `required` string arrays are sorted and deduplicated, a missing top-level `required` becomes `[]`, and literal-value arrays keep their source order. Each tool also receives a stable label matching its final Pi name. These stable definitions and registration order prevent equivalent MCP configurations from changing the serialized tool list between runs, improving provider prompt-cache reuse. A server that fails discovery is excluded from the combined set; a registration error fails startup for that prepared set and is not reported as successful.
39
+
40
+ Pi 0.80 cannot remove dynamically registered tools. Enabling, disabling, deleting, or changing MCP servers is therefore applied on the next Pi restart rather than mutating the tool list mid-session. This prevents stale schemas and makes the restart an explicit cache-epoch boundary.
41
+
42
+ ### Bounded Results
43
+
44
+ MCP text results are model-visible up to a hard 64 KiB ceiling. A larger result keeps a bounded head/tail preview and, when the raw result is at most 16 MiB, writes the complete text to a private mode-0600 artifact under `~/.unipi/tool-results/`. Existing result directories are tightened to mode 0700. The returned result includes the path and directs the agent to use `read` with offset/limit. Results above the raw safety cap or filesystem write failures still return a bounded preview with an explicit non-retention warning. MCP image bytes are not written by this text bridge; image blocks remain represented by MIME metadata as before.
45
+
33
46
  Example tool calls:
34
47
  ```
35
48
  github__search_code({ query: "authentication middleware" })
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/mcp",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "MCP server management extension for Pi coding agent — browse, add, configure, and use MCP servers",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -26,8 +26,11 @@
26
26
  "skills/**/*",
27
27
  "README.md"
28
28
  ],
29
+ "scripts": {
30
+ "test": "npx tsx --test tests/**/*.test.ts"
31
+ },
29
32
  "dependencies": {
30
- "@pi-unipi/core": "2.4.1"
33
+ "@pi-unipi/core": "2.6.0"
31
34
  },
32
35
  "peerDependencies": {
33
36
  "@earendil-works/pi-coding-agent": "^0.80.0",