@pi-unipi/unipi 2.5.0 → 2.6.1
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.
- package/CHANGELOG.md +43 -0
- package/README.md +2 -0
- package/docs/prefix-cache-architecture.md +89 -0
- package/package.json +22 -21
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +4 -0
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/README.md +2 -1
- package/packages/cocoindex/index.ts +6 -13
- package/packages/cocoindex/package.json +4 -3
- package/packages/cocoindex/tools.ts +45 -11
- package/packages/compactor/README.md +4 -2
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/session/snapshot.ts +3 -2
- package/packages/compactor/src/tools/register.ts +6 -2
- package/packages/compactor/src/tools/vcc-recall.ts +18 -3
- package/packages/core/bounded-output.ts +106 -0
- package/packages/core/constants.ts +3 -0
- package/packages/core/index.ts +1 -0
- package/packages/core/model-cache.ts +18 -12
- package/packages/core/package.json +1 -1
- package/packages/footer/package.json +2 -2
- package/packages/image/package.json +2 -2
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/README.md +4 -0
- package/packages/mcp/package.json +2 -2
- package/packages/mcp/src/bridge/translator.ts +19 -12
- package/packages/memory/index.ts +56 -34
- package/packages/memory/package.json +3 -3
- package/packages/milestone/hooks.ts +1 -1
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +1 -0
- package/packages/notify/commands.ts +61 -3
- package/packages/notify/package.json +2 -2
- package/packages/notify/tui/recap-model-selector.ts +72 -31
- package/packages/notify/tui/settings-overlay.ts +45 -32
- package/packages/ralph/index.ts +12 -18
- package/packages/ralph/package.json +6 -3
- package/packages/ralph/reminder.ts +40 -0
- package/packages/ralph/tools.ts +5 -1
- package/packages/subagents/README.md +2 -0
- package/packages/subagents/package.json +1 -1
- package/packages/subagents/src/agent-manager.ts +5 -1
- package/packages/subagents/src/agent-runner.ts +2 -2
- package/packages/subagents/src/core-compat.ts +73 -0
- package/packages/subagents/src/custom-agents.ts +10 -2
- package/packages/subagents/src/index.ts +14 -3
- package/packages/subagents/src/types.ts +2 -0
- package/packages/unipi/bundled.js +1002 -452
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +9 -0
- package/packages/utility/package.json +2 -2
- package/packages/utility/src/index.ts +48 -0
- package/packages/utility/src/lifecycle/cleanup.ts +29 -0
- package/packages/utility/src/prefix-cache.ts +263 -0
- package/packages/utility/src/types.ts +1 -1
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/index.ts +2 -2
- package/packages/workflow/package.json +2 -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(),
|
|
@@ -295,6 +297,7 @@ export const NOTIFY_COMMANDS = {
|
|
|
295
297
|
SET_NTFY: "notify-set-ntfy",
|
|
296
298
|
TEST: "notify-test",
|
|
297
299
|
RECAP_MODEL: "notify-recap-model",
|
|
300
|
+
NOTIFY_EVENT: "notify-event",
|
|
298
301
|
} as const;
|
|
299
302
|
|
|
300
303
|
/** Notify tool names */
|
package/packages/core/index.ts
CHANGED
|
@@ -9,14 +9,18 @@
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
/** Resolve the model cache directory at call time (respects HOME changes). */
|
|
13
|
+
function cacheDir(): string {
|
|
14
|
+
return path.join(
|
|
15
|
+
process.env.HOME ?? process.env.USERPROFILE ?? "~",
|
|
16
|
+
".unipi/config",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
17
19
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
+
/** Resolve the model cache file path at call time. */
|
|
21
|
+
function cacheFile(): string {
|
|
22
|
+
return path.join(cacheDir(), "models-cache.json");
|
|
23
|
+
}
|
|
20
24
|
|
|
21
25
|
/** A single cached model entry */
|
|
22
26
|
export interface CachedModel {
|
|
@@ -42,8 +46,9 @@ export interface ModelCache {
|
|
|
42
46
|
*/
|
|
43
47
|
export function readModelCache(): CachedModel[] {
|
|
44
48
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
const file = cacheFile();
|
|
50
|
+
if (!fs.existsSync(file)) return [];
|
|
51
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
47
52
|
return Array.isArray(parsed.models) ? parsed.models : [];
|
|
48
53
|
} catch {
|
|
49
54
|
return [];
|
|
@@ -56,14 +61,15 @@ export function readModelCache(): CachedModel[] {
|
|
|
56
61
|
*/
|
|
57
62
|
export function writeModelCache(models: CachedModel[]): void {
|
|
58
63
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
const dir = cacheDir();
|
|
65
|
+
if (!fs.existsSync(dir)) {
|
|
66
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
61
67
|
}
|
|
62
68
|
const cache: ModelCache = {
|
|
63
69
|
updatedAt: new Date().toISOString(),
|
|
64
70
|
models,
|
|
65
71
|
};
|
|
66
|
-
fs.writeFileSync(
|
|
72
|
+
fs.writeFileSync(cacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
|
|
67
73
|
} catch {
|
|
68
74
|
// Best effort — cache is optional
|
|
69
75
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/footer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
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.
|
|
35
|
+
"@pi-unipi/core": "2.6.1"
|
|
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.
|
|
3
|
+
"version": "2.6.1",
|
|
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.
|
|
37
|
+
"@pi-unipi/core": "2.6.1"
|
|
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.
|
|
3
|
+
"version": "2.6.1",
|
|
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.
|
|
36
|
+
"@pi-unipi/core": "2.6.1"
|
|
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.
|
|
3
|
+
"version": "2.6.1",
|
|
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.
|
|
36
|
+
"@pi-unipi/core": "2.6.1"
|
|
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.
|
|
3
|
+
"version": "2.6.1",
|
|
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.
|
|
42
|
+
"@pi-unipi/core": "2.6.1"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
package/packages/mcp/README.md
CHANGED
|
@@ -39,6 +39,10 @@ MCP input properties are cloned and recursively canonicalized before registratio
|
|
|
39
39
|
|
|
40
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
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
|
+
|
|
42
46
|
Example tool calls:
|
|
43
47
|
```
|
|
44
48
|
github__search_code({ query: "authentication middleware" })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
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",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test": "npx tsx --test tests/**/*.test.ts"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@pi-unipi/core": "2.
|
|
33
|
+
"@pi-unipi/core": "2.6.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Naming convention: {serverName}__{toolName}
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { MCP_DEFAULTS } from "@pi-unipi/core";
|
|
8
|
+
import { MCP_DEFAULTS, boundModelOutput } from "@pi-unipi/core";
|
|
9
9
|
import type { McpTool, McpToolResult } from "../types.js";
|
|
10
10
|
import type { McpClient } from "./client.js";
|
|
11
11
|
|
|
@@ -170,21 +170,28 @@ export function translateMcpTool(
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
if (result.isError) {
|
|
174
|
-
const errorText = blocks.map((b) => b.text).join("\n") || "Unknown error";
|
|
175
|
-
return {
|
|
176
|
-
content: [{ type: "text", text: `MCP tool error from ${serverName}: ${errorText}` }],
|
|
177
|
-
details: { error: true, server: serverName, tool: mcpTool.name },
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
173
|
if (blocks.length === 0) {
|
|
182
|
-
blocks.push({ type: "text", text: "(no output)" });
|
|
174
|
+
blocks.push({ type: "text", text: result.isError ? "Unknown error" : "(no output)" });
|
|
183
175
|
}
|
|
184
176
|
|
|
177
|
+
const rawText = blocks.map((block) => block.text).join("\n");
|
|
178
|
+
const wrapper = result.isError ? `MCP tool error from ${serverName}: ` : "";
|
|
179
|
+
const output = boundModelOutput(rawText, {
|
|
180
|
+
maxBytes: Math.max(1024, MCP_DEFAULTS.MAX_MODEL_OUTPUT_BYTES - Buffer.byteLength(wrapper, "utf8")),
|
|
181
|
+
artifactPrefix: `mcp-${serverName}-${mcpTool.name}`,
|
|
182
|
+
});
|
|
183
|
+
const visibleText = `${wrapper}${output.text}`;
|
|
184
|
+
|
|
185
185
|
return {
|
|
186
|
-
content:
|
|
187
|
-
details: {
|
|
186
|
+
content: [{ type: "text", text: visibleText }],
|
|
187
|
+
details: {
|
|
188
|
+
error: result.isError || undefined,
|
|
189
|
+
server: serverName,
|
|
190
|
+
tool: mcpTool.name,
|
|
191
|
+
truncated: output.truncated,
|
|
192
|
+
originalBytes: output.originalBytes,
|
|
193
|
+
artifactPath: output.artifactPath,
|
|
194
|
+
},
|
|
188
195
|
};
|
|
189
196
|
} catch (err) {
|
|
190
197
|
const message = err instanceof Error ? err.message : String(err);
|
package/packages/memory/index.ts
CHANGED
|
@@ -35,6 +35,54 @@ import { isEmbeddingReady, hasModelChanged } from "./settings.js";
|
|
|
35
35
|
/** Package version */
|
|
36
36
|
const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
|
|
37
37
|
|
|
38
|
+
interface MemoryReminderInput {
|
|
39
|
+
projectName: string;
|
|
40
|
+
memories: Array<{ title: string }>;
|
|
41
|
+
canSearch: boolean;
|
|
42
|
+
canStore: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the deterministic, model-visible first-turn memory reminder.
|
|
47
|
+
* Exported so provider-payload regressions can exercise the exact production
|
|
48
|
+
* content without initializing a storage backend.
|
|
49
|
+
*/
|
|
50
|
+
export function buildMemoryRecallReminder(input: MemoryReminderInput): string {
|
|
51
|
+
const lines = [
|
|
52
|
+
"## 🧠 Memory System Active",
|
|
53
|
+
"",
|
|
54
|
+
`You have ${input.memories.length} memories stored for project "${input.projectName}".`,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (input.canSearch && input.memories.length > 0) {
|
|
58
|
+
const titleList = input.memories.slice(0, 20).map((memory) => `- ${memory.title}`).join("\n");
|
|
59
|
+
const extra = input.memories.length > 20
|
|
60
|
+
? `\n... and ${input.memories.length - 20} more`
|
|
61
|
+
: "";
|
|
62
|
+
lines.push(
|
|
63
|
+
"**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
|
|
64
|
+
"",
|
|
65
|
+
"Available memories:",
|
|
66
|
+
titleList + extra,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (input.canStore) {
|
|
71
|
+
lines.push(
|
|
72
|
+
"",
|
|
73
|
+
"**AFTER completing the task**, if you learned something non-obvious,",
|
|
74
|
+
"call `memory_store` to save it for future sessions.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines.push(
|
|
79
|
+
"",
|
|
80
|
+
"Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates.",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
38
86
|
/** Storage instance for current project */
|
|
39
87
|
let projectStorage: MemoryStorage | null = null;
|
|
40
88
|
|
|
@@ -251,44 +299,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
251
299
|
return;
|
|
252
300
|
}
|
|
253
301
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
"",
|
|
257
|
-
`You have ${projectMemories.length} memories stored for project "${projectName}".`,
|
|
258
|
-
];
|
|
259
|
-
|
|
260
|
-
if (canSearch && projectMemories.length > 0) {
|
|
261
|
-
const titleList = projectMemories.slice(0, 20).map(m => `- ${m.title}`).join("\n");
|
|
262
|
-
const extra = projectMemories.length > 20 ? `\n... and ${projectMemories.length - 20} more` : "";
|
|
263
|
-
lines.push(
|
|
264
|
-
"**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
|
|
265
|
-
"",
|
|
266
|
-
"Available memories:",
|
|
267
|
-
titleList + extra,
|
|
268
|
-
);
|
|
269
|
-
} else {
|
|
270
|
-
recallDone = true;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (canStore) {
|
|
274
|
-
lines.push(
|
|
275
|
-
"",
|
|
276
|
-
"**AFTER completing the task**, if you learned something non-obvious,",
|
|
277
|
-
"call `memory_store` to save it for future sessions.",
|
|
278
|
-
);
|
|
279
|
-
} else {
|
|
280
|
-
storeDone = true;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
lines.push(
|
|
284
|
-
"",
|
|
285
|
-
"Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates.",
|
|
286
|
-
);
|
|
302
|
+
if (!canSearch || projectMemories.length === 0) recallDone = true;
|
|
303
|
+
if (!canStore) storeDone = true;
|
|
287
304
|
|
|
288
305
|
return {
|
|
289
306
|
message: {
|
|
290
307
|
customType: "unipi-memory-recall-reminder",
|
|
291
|
-
content:
|
|
308
|
+
content: buildMemoryRecallReminder({
|
|
309
|
+
projectName,
|
|
310
|
+
memories: projectMemories,
|
|
311
|
+
canSearch,
|
|
312
|
+
canStore,
|
|
313
|
+
}),
|
|
292
314
|
display: false,
|
|
293
315
|
},
|
|
294
316
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/memory",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Persistent cross-session memory with MemPalace backend (auto-installed) and SQLite fallback for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"better-sqlite3": "^12.9.0",
|
|
44
44
|
"sqlite-vec": "^0.1.9",
|
|
45
45
|
"js-yaml": "^4.1.0",
|
|
46
|
-
"@pi-unipi/core": "2.
|
|
47
|
-
"@pi-unipi/info-screen": "2.
|
|
46
|
+
"@pi-unipi/core": "2.6.1",
|
|
47
|
+
"@pi-unipi/info-screen": "2.6.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -51,7 +51,7 @@ function formatMilestoneContext(filePath: string): string | null {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Build an append-only snapshot that explicitly invalidates earlier snapshots. */
|
|
54
|
-
function formatMilestoneSnapshot(workspace: string, context: string | null): string {
|
|
54
|
+
export function formatMilestoneSnapshot(workspace: string, context: string | null): string {
|
|
55
55
|
return [
|
|
56
56
|
"# UniPi Milestone Snapshot",
|
|
57
57
|
"This snapshot supersedes all prior UniPi milestone snapshots; use only this snapshot for milestone status.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/milestone",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Lifecycle layer for project-level goals — MILESTONES.md tracking, session hooks, auto-sync",
|
|
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.
|
|
35
|
+
"@pi-unipi/core": "2.6.1"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -13,6 +13,7 @@ Configure once, get alerts everywhere. Per-event platform routing lets you send
|
|
|
13
13
|
| `/unipi:notify-set-tg` | Interactive Telegram bot setup |
|
|
14
14
|
| `/unipi:notify-set-ntfy` | Configure ntfy topic and server |
|
|
15
15
|
| `/unipi:notify-recap-model` | Set model for notification recaps |
|
|
16
|
+
| `/unipi:notify-event` | Toggle a single event without the TUI (`<event> <on\|off>`) — reports the new value; run `/reload` to re-register listeners |
|
|
16
17
|
| `/unipi:notify-test` | Send test notification to all enabled platforms |
|
|
17
18
|
|
|
18
19
|
## Special Triggers
|
|
@@ -12,13 +12,32 @@ import { GotifySetupOverlay } from "./tui/gotify-setup.js";
|
|
|
12
12
|
import { TelegramSetupOverlay } from "./tui/telegram-setup.js";
|
|
13
13
|
import { NtfySetupOverlay } from "./tui/ntfy-setup.js";
|
|
14
14
|
import { RecapModelSelectorOverlay } from "./tui/recap-model-selector.js";
|
|
15
|
-
import {
|
|
15
|
+
import type { CachedModel } from "@pi-unipi/core";
|
|
16
|
+
import { loadConfig, saveConfig } from "./settings.js";
|
|
16
17
|
import { loadNtfyConfig } from "./ntfy-config.js";
|
|
17
18
|
import { sendNativeNotification, SuppressedError } from "./platforms/native.js";
|
|
18
19
|
import { sendGotifyNotification } from "./platforms/gotify.js";
|
|
19
20
|
import { sendTelegramNotification } from "./platforms/telegram.js";
|
|
20
21
|
import { sendNtfyNotification } from "./platforms/ntfy.js";
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Collect models for the recap selector from Pi's live model registry.
|
|
25
|
+
* Returns undefined when the registry is unavailable so the overlay can fall
|
|
26
|
+
* back to the project-wide model cache (issue #27: selector was empty because
|
|
27
|
+
* it only read ~/.unipi/config/models-cache.json, which may not exist even
|
|
28
|
+
* when models are configured in ~/.pi/agent/models.json).
|
|
29
|
+
*/
|
|
30
|
+
function registryModels(ctx: ExtensionContext): CachedModel[] | undefined {
|
|
31
|
+
const registry = ctx.modelRegistry;
|
|
32
|
+
if (!registry) return undefined;
|
|
33
|
+
try {
|
|
34
|
+
const models = registry.getAvailable?.() ?? registry.getAll() ?? [];
|
|
35
|
+
return models.map((m) => ({ provider: m.provider, id: m.id, name: m.name }));
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
/**
|
|
23
42
|
* Register notify commands.
|
|
24
43
|
*/
|
|
@@ -44,7 +63,7 @@ export function registerNotifyCommands(pi: ExtensionAPI): void {
|
|
|
44
63
|
// Open model selector as nested overlay
|
|
45
64
|
ctx.ui.custom(
|
|
46
65
|
(innerTui: any, innerTheme: any, _innerKb: any, innerDone: any) => {
|
|
47
|
-
const selector = new RecapModelSelectorOverlay();
|
|
66
|
+
const selector = new RecapModelSelectorOverlay(registryModels(ctx));
|
|
48
67
|
selector.setTheme(innerTheme);
|
|
49
68
|
selector.onClose = () => innerDone(undefined);
|
|
50
69
|
selector.requestRender = () => innerTui.requestRender();
|
|
@@ -102,9 +121,10 @@ export function registerNotifyCommands(pi: ExtensionAPI): void {
|
|
|
102
121
|
return;
|
|
103
122
|
}
|
|
104
123
|
|
|
124
|
+
const models = registryModels(ctx);
|
|
105
125
|
ctx.ui.custom(
|
|
106
126
|
(tui, theme, _keybindings, done) => {
|
|
107
|
-
const overlay = new RecapModelSelectorOverlay();
|
|
127
|
+
const overlay = new RecapModelSelectorOverlay(models);
|
|
108
128
|
overlay.setTheme(theme);
|
|
109
129
|
overlay.onClose = () => done(undefined);
|
|
110
130
|
overlay.requestRender = () => tui.requestRender();
|
|
@@ -251,6 +271,44 @@ export function registerNotifyCommands(pi: ExtensionAPI): void {
|
|
|
251
271
|
}
|
|
252
272
|
);
|
|
253
273
|
|
|
274
|
+
// /unipi:notify-event <event> <on|off> — Non-TUI event toggle (issue #27 escape
|
|
275
|
+
// hatch for terminals where overlay input is unusable)
|
|
276
|
+
pi.registerCommand(
|
|
277
|
+
`${UNIPI_PREFIX}${NOTIFY_COMMANDS.NOTIFY_EVENT}`,
|
|
278
|
+
{
|
|
279
|
+
description: "Toggle a notify event without the TUI: <event> <on|off>",
|
|
280
|
+
handler: async (args: string, ctx: ExtensionContext) => {
|
|
281
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
282
|
+
const [event, value] = parts;
|
|
283
|
+
|
|
284
|
+
if (parts.length !== 2 || (value !== "on" && value !== "off")) {
|
|
285
|
+
ctx.ui.notify(
|
|
286
|
+
"Usage: /unipi:notify-event <event> <on|off>",
|
|
287
|
+
"warning"
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const config = loadConfig();
|
|
293
|
+
if (!(event in config.events)) {
|
|
294
|
+
const known = Object.keys(config.events).join(", ");
|
|
295
|
+
ctx.ui.notify(
|
|
296
|
+
`Unknown event "${event}". Known events: ${known}`,
|
|
297
|
+
"error"
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
config.events[event].enabled = value === "on";
|
|
303
|
+
saveConfig(config);
|
|
304
|
+
ctx.ui.notify(
|
|
305
|
+
`notify: ${event} is now ${value}. Run /reload to re-register listeners.`,
|
|
306
|
+
"info"
|
|
307
|
+
);
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
|
|
254
312
|
// /unipi:notify-test — Send test notification to all enabled platforms
|
|
255
313
|
pi.registerCommand(
|
|
256
314
|
`${UNIPI_PREFIX}${NOTIFY_COMMANDS.TEST}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/notify",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Cross-platform notification extension for Pi — native OS, Gotify, and Telegram notifications for agent lifecycle events",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
37
|
+
"@pi-unipi/core": "2.6.1",
|
|
38
38
|
"node-notifier": "^10.0.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|