pi-mempalace 0.1.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.
- package/README.md +303 -0
- package/extensions/pi-mempalace/index.ts +1045 -0
- package/extensions/pi-mempalace/memory_store.ts +1531 -0
- package/package.json +45 -0
- package/skills/memory-setup/SKILL.md +97 -0
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-mempalace — Persistent Agent Memory Extension
|
|
3
|
+
*
|
|
4
|
+
* Raw verbatim storage of conversation exchanges with semantic search.
|
|
5
|
+
* Never lose context again.
|
|
6
|
+
*
|
|
7
|
+
* Provides:
|
|
8
|
+
* - `memory_search` tool — semantic search across all stored memories
|
|
9
|
+
* - `memory_save` tool — manually save a specific piece of information
|
|
10
|
+
* - `memory_recall` tool — retrieve memories for a project/topic (L2)
|
|
11
|
+
* - `memory_status` tool — show memory store overview
|
|
12
|
+
* - Auto-capture of conversation exchanges on session shutdown/compact
|
|
13
|
+
* - Wake-up context injection (L0 identity + L1 top memories) into system prompt
|
|
14
|
+
* - Status widget showing memory count
|
|
15
|
+
* - `/memory` command for quick operations
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
ExtensionAPI,
|
|
20
|
+
ExtensionContext,
|
|
21
|
+
} from "@mariozechner/pi-coding-agent";
|
|
22
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
23
|
+
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
24
|
+
import type { MemoryStats } from "./memory_store.js";
|
|
25
|
+
import { Type } from "@sinclair/typebox";
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
|
|
29
|
+
import { MemoryStore } from "./memory_store.js";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Constants
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const MEMORY_DIR = path.join(
|
|
36
|
+
process.env.HOME || process.env.USERPROFILE || "~",
|
|
37
|
+
".pi",
|
|
38
|
+
"agent",
|
|
39
|
+
"memory"
|
|
40
|
+
);
|
|
41
|
+
const CONFIG_PATH = path.join(MEMORY_DIR, "config.json");
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Types
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
interface MemoryConfig {
|
|
48
|
+
/** Auto-capture conversation exchanges */
|
|
49
|
+
autoCapture: boolean;
|
|
50
|
+
/** Inject wake-up context into system prompt */
|
|
51
|
+
wakeUpEnabled: boolean;
|
|
52
|
+
/** Maximum tokens for wake-up context */
|
|
53
|
+
wakeUpMaxTokens: number;
|
|
54
|
+
/** Default project name (auto-detected from cwd if not set) */
|
|
55
|
+
defaultProject: string | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface MemoryRuntime {
|
|
59
|
+
/** Current configuration */
|
|
60
|
+
config: MemoryConfig;
|
|
61
|
+
/** Total memories in store (cached) */
|
|
62
|
+
totalMemories: number;
|
|
63
|
+
/** Per-project counts (cached) */
|
|
64
|
+
projects: Record<string, number>;
|
|
65
|
+
/** Whether the backend is available */
|
|
66
|
+
backendAvailable: boolean;
|
|
67
|
+
/** Cached wake-up text (refreshed on session_start) */
|
|
68
|
+
wakeUpText: string | null;
|
|
69
|
+
/** Current project context */
|
|
70
|
+
currentProject: string;
|
|
71
|
+
/** Whether memory mode is enabled */
|
|
72
|
+
enabled: boolean;
|
|
73
|
+
/** The memory store instance */
|
|
74
|
+
store: MemoryStore;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Defaults
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
function defaultConfig(): MemoryConfig {
|
|
82
|
+
return {
|
|
83
|
+
autoCapture: true,
|
|
84
|
+
wakeUpEnabled: true,
|
|
85
|
+
wakeUpMaxTokens: 800,
|
|
86
|
+
defaultProject: null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createRuntime(): MemoryRuntime {
|
|
91
|
+
return {
|
|
92
|
+
config: defaultConfig(),
|
|
93
|
+
totalMemories: 0,
|
|
94
|
+
projects: {},
|
|
95
|
+
backendAvailable: false,
|
|
96
|
+
wakeUpText: null,
|
|
97
|
+
currentProject: "general",
|
|
98
|
+
enabled: true,
|
|
99
|
+
store: new MemoryStore(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Helpers
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
function loadConfig(): MemoryConfig {
|
|
108
|
+
const defaults = defaultConfig();
|
|
109
|
+
try {
|
|
110
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
111
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
112
|
+
return { ...defaults, ...raw };
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Use defaults
|
|
116
|
+
}
|
|
117
|
+
return defaults;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function saveConfig(config: MemoryConfig): void {
|
|
121
|
+
fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
122
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function detectProject(cwd: string): string {
|
|
126
|
+
const gitDir = path.join(cwd, ".git");
|
|
127
|
+
if (fs.existsSync(gitDir)) {
|
|
128
|
+
return path.basename(cwd);
|
|
129
|
+
}
|
|
130
|
+
return path.basename(cwd) || "general";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract text content from a message content block array.
|
|
135
|
+
*/
|
|
136
|
+
function extractTextFromContent(content: unknown): string {
|
|
137
|
+
if (!Array.isArray(content)) return "";
|
|
138
|
+
const textParts: string[] = [];
|
|
139
|
+
for (const block of content) {
|
|
140
|
+
if (typeof block === "object" && block !== null) {
|
|
141
|
+
const b = block as Record<string, unknown>;
|
|
142
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
143
|
+
textParts.push(b.text);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return textParts.join("\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Runtime store (per-session)
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
function createRuntimeStore() {
|
|
155
|
+
const runtimes = new Map<string, MemoryRuntime>();
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
ensure(sessionKey: string): MemoryRuntime {
|
|
159
|
+
let runtime = runtimes.get(sessionKey);
|
|
160
|
+
if (!runtime) {
|
|
161
|
+
runtime = createRuntime();
|
|
162
|
+
runtimes.set(sessionKey, runtime);
|
|
163
|
+
}
|
|
164
|
+
return runtime;
|
|
165
|
+
},
|
|
166
|
+
clear(sessionKey: string): void {
|
|
167
|
+
runtimes.delete(sessionKey);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Shared tool helpers
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
function textResult(text: string, details: Record<string, unknown> | null = null) {
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text" as const, text }],
|
|
179
|
+
details,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderTextResult(result: any) {
|
|
184
|
+
const t = result.content[0];
|
|
185
|
+
return new Text(t?.type === "text" ? t.text : "", 0, 0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Stats overlay
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
function barChart(
|
|
193
|
+
items: [string, number][],
|
|
194
|
+
maxBarWidth: number,
|
|
195
|
+
theme: { fg: (color: string, text: string) => string },
|
|
196
|
+
): string[] {
|
|
197
|
+
if (items.length === 0) return [" (none)"];
|
|
198
|
+
const maxVal = Math.max(...items.map(([, v]) => v));
|
|
199
|
+
const maxLabel = Math.max(...items.map(([k]) => k.length));
|
|
200
|
+
return items.map(([label, count]) => {
|
|
201
|
+
const barLen = maxVal > 0 ? Math.round((count / maxVal) * maxBarWidth) : 0;
|
|
202
|
+
const bar = theme.fg("accent", "█".repeat(barLen)) + "░".repeat(maxBarWidth - barLen);
|
|
203
|
+
const paddedLabel = label.padEnd(maxLabel);
|
|
204
|
+
return ` ${theme.fg("text", paddedLabel)} ${bar} ${theme.fg("dim", String(count))}`;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sparkline(timeline: Record<string, number>, days: number): string {
|
|
209
|
+
const sparks = " ▁▂▃▄▅▆▇█";
|
|
210
|
+
const now = new Date();
|
|
211
|
+
const values: number[] = [];
|
|
212
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
213
|
+
const d = new Date(now);
|
|
214
|
+
d.setDate(d.getDate() - i);
|
|
215
|
+
const key = d.toISOString().slice(0, 10);
|
|
216
|
+
values.push(timeline[key] || 0);
|
|
217
|
+
}
|
|
218
|
+
const max = Math.max(...values, 1);
|
|
219
|
+
return values.map((v) => sparks[Math.round((v / max) * (sparks.length - 1))]).join("");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function formatDate(iso: string | null): string {
|
|
223
|
+
if (!iso) return "—";
|
|
224
|
+
const d = new Date(iso);
|
|
225
|
+
return d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function daysBetween(a: string, b: string): number {
|
|
229
|
+
return Math.round((new Date(b).getTime() - new Date(a).getTime()) / 86400000);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function showStatsOverlay(
|
|
233
|
+
ctx: ExtensionContext,
|
|
234
|
+
stats: MemoryStats,
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
237
|
+
const container = new Container();
|
|
238
|
+
|
|
239
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
240
|
+
container.addChild(new Text(theme.fg("accent", " 🧠 Memory Stats"), 0, 0));
|
|
241
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("border", s)));
|
|
242
|
+
|
|
243
|
+
if (stats.total === 0) {
|
|
244
|
+
container.addChild(new Text(theme.fg("dim", " No memories stored yet."), 0, 0));
|
|
245
|
+
} else {
|
|
246
|
+
// Overview
|
|
247
|
+
const span = stats.oldest && stats.newest
|
|
248
|
+
? `${daysBetween(stats.oldest, stats.newest)}d span`
|
|
249
|
+
: "";
|
|
250
|
+
const lines = [
|
|
251
|
+
` ${theme.fg("text", "Total")} ${theme.fg("accent", String(stats.total))} memories`,
|
|
252
|
+
` ${theme.fg("text", "Storage")} ${theme.fg("accent", `${stats.storageSizeKb} KB`)}`,
|
|
253
|
+
` ${theme.fg("text", "Sessions")} ${theme.fg("accent", String(stats.sessions))}`,
|
|
254
|
+
` ${theme.fg("text", "Avg length")} ${theme.fg("accent", `${stats.avgContentLength}`)} chars`,
|
|
255
|
+
` ${theme.fg("text", "First memory")} ${theme.fg("dim", formatDate(stats.oldest))}`,
|
|
256
|
+
` ${theme.fg("text", "Last memory")} ${theme.fg("dim", formatDate(stats.newest))}${span ? ` (${theme.fg("dim", span)})` : ""}`,
|
|
257
|
+
];
|
|
258
|
+
container.addChild(new Text(lines.join("\n"), 0, 0));
|
|
259
|
+
|
|
260
|
+
// Activity sparkline (last 28 days)
|
|
261
|
+
container.addChild(new Spacer(1));
|
|
262
|
+
const spark = sparkline(stats.timeline, 28);
|
|
263
|
+
container.addChild(new Text(
|
|
264
|
+
` ${theme.fg("text", "Activity (28d)")} ${theme.fg("accent", spark)}`,
|
|
265
|
+
0, 0,
|
|
266
|
+
));
|
|
267
|
+
|
|
268
|
+
// Projects bar chart
|
|
269
|
+
const projectEntries = Object.entries(stats.projects)
|
|
270
|
+
.sort(([, a], [, b]) => b - a);
|
|
271
|
+
if (projectEntries.length > 0) {
|
|
272
|
+
container.addChild(new Spacer(1));
|
|
273
|
+
container.addChild(new Text(theme.fg("text", " Projects"), 0, 0));
|
|
274
|
+
const projectBars = barChart(projectEntries.slice(0, 8), 20, theme);
|
|
275
|
+
container.addChild(new Text(projectBars.join("\n"), 0, 0));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Topics bar chart
|
|
279
|
+
const topicEntries = Object.entries(stats.topics)
|
|
280
|
+
.filter(([t]) => t !== "general")
|
|
281
|
+
.sort(([, a], [, b]) => b - a);
|
|
282
|
+
if (topicEntries.length > 0) {
|
|
283
|
+
container.addChild(new Spacer(1));
|
|
284
|
+
container.addChild(new Text(theme.fg("text", " Topics"), 0, 0));
|
|
285
|
+
const topicBars = barChart(topicEntries.slice(0, 8), 20, theme);
|
|
286
|
+
container.addChild(new Text(topicBars.join("\n"), 0, 0));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Sources breakdown
|
|
290
|
+
const sourceEntries = Object.entries(stats.sources)
|
|
291
|
+
.sort(([, a], [, b]) => b - a);
|
|
292
|
+
if (sourceEntries.length > 0) {
|
|
293
|
+
container.addChild(new Spacer(1));
|
|
294
|
+
container.addChild(new Text(theme.fg("text", " Sources"), 0, 0));
|
|
295
|
+
const sourceBars = barChart(sourceEntries, 20, theme);
|
|
296
|
+
container.addChild(new Text(sourceBars.join("\n"), 0, 0));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("border", s)));
|
|
301
|
+
container.addChild(new Text(theme.fg("dim", " press any key to close"), 0, 0));
|
|
302
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
render: (w: number) => container.render(w),
|
|
306
|
+
invalidate: () => container.invalidate(),
|
|
307
|
+
handleInput: () => done(undefined),
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Extension
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
export default function memoryExtension(pi: ExtensionAPI) {
|
|
317
|
+
const runtimeStore = createRuntimeStore();
|
|
318
|
+
const getSessionKey = (ctx: ExtensionContext) => ctx.sessionManager.getSessionId();
|
|
319
|
+
const getRuntime = (ctx: ExtensionContext): MemoryRuntime =>
|
|
320
|
+
runtimeStore.ensure(getSessionKey(ctx));
|
|
321
|
+
|
|
322
|
+
// -----------------------------------------------------------------------
|
|
323
|
+
// State reconstruction
|
|
324
|
+
// -----------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
const reconstructState = async (ctx: ExtensionContext) => {
|
|
327
|
+
const runtime = getRuntime(ctx);
|
|
328
|
+
runtime.config = loadConfig();
|
|
329
|
+
runtime.currentProject = runtime.config.defaultProject || detectProject(ctx.cwd);
|
|
330
|
+
runtime.enabled = true;
|
|
331
|
+
|
|
332
|
+
// Load the memory store from disk
|
|
333
|
+
try {
|
|
334
|
+
runtime.store.load();
|
|
335
|
+
runtime.backendAvailable = true;
|
|
336
|
+
const status = runtime.store.status();
|
|
337
|
+
runtime.totalMemories = status.total_memories;
|
|
338
|
+
runtime.projects = status.projects;
|
|
339
|
+
} catch {
|
|
340
|
+
runtime.backendAvailable = false;
|
|
341
|
+
runtime.totalMemories = 0;
|
|
342
|
+
runtime.projects = {};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Pre-generate wake-up text (no embedding needed — just reads from memory)
|
|
346
|
+
if (runtime.config.wakeUpEnabled && runtime.backendAvailable) {
|
|
347
|
+
try {
|
|
348
|
+
const wakeup = runtime.store.wakeup({
|
|
349
|
+
project: runtime.currentProject,
|
|
350
|
+
max_tokens: runtime.config.wakeUpMaxTokens,
|
|
351
|
+
});
|
|
352
|
+
runtime.wakeUpText = wakeup.text || null;
|
|
353
|
+
} catch {
|
|
354
|
+
runtime.wakeUpText = null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// -----------------------------------------------------------------------
|
|
360
|
+
// Lifecycle hooks
|
|
361
|
+
// -----------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
pi.on("session_start", async (_e, ctx) => {
|
|
364
|
+
await reconstructState(ctx);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
pi.on("session_tree", async (_e, ctx) => {
|
|
368
|
+
await reconstructState(ctx);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
pi.on("session_shutdown", async (_e, ctx) => {
|
|
372
|
+
runtimeStore.clear(getSessionKey(ctx));
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Auto-capture: after each agent turn, extract and store the exchange
|
|
376
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
377
|
+
const runtime = getRuntime(ctx);
|
|
378
|
+
if (!runtime.enabled || !runtime.config.autoCapture) return;
|
|
379
|
+
if (!runtime.backendAvailable) return;
|
|
380
|
+
|
|
381
|
+
if (event.message?.role !== "assistant") return;
|
|
382
|
+
|
|
383
|
+
const msg = event.message as unknown as Record<string, unknown>;
|
|
384
|
+
const assistantText = extractTextFromContent(msg.content);
|
|
385
|
+
if (!assistantText || assistantText.length < 20) return;
|
|
386
|
+
|
|
387
|
+
// Find the preceding user message from session history
|
|
388
|
+
const branch = ctx.sessionManager.getBranch();
|
|
389
|
+
let userText = "";
|
|
390
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
391
|
+
const entry = branch[i];
|
|
392
|
+
if (entry.type === "message" && entry.message.role === "user") {
|
|
393
|
+
const userMsg = entry.message as unknown as Record<string, unknown>;
|
|
394
|
+
if (typeof userMsg.content === "string") {
|
|
395
|
+
userText = userMsg.content;
|
|
396
|
+
} else if (Array.isArray(userMsg.content)) {
|
|
397
|
+
userText = extractTextFromContent(userMsg.content);
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!userText || userText.length < 10) return;
|
|
404
|
+
|
|
405
|
+
// Build exchange content
|
|
406
|
+
const exchange = `> ${userText}\n\n${assistantText}`;
|
|
407
|
+
const content = exchange.length > 2000
|
|
408
|
+
? exchange.slice(0, 2000) + "\n[truncated]"
|
|
409
|
+
: exchange;
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
const result = await runtime.store.store({
|
|
413
|
+
content,
|
|
414
|
+
project: runtime.currentProject,
|
|
415
|
+
topic: "general",
|
|
416
|
+
source: "auto-capture",
|
|
417
|
+
timestamp: new Date().toISOString(),
|
|
418
|
+
session_id: getSessionKey(ctx),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (result.status === "stored") {
|
|
422
|
+
runtime.totalMemories++;
|
|
423
|
+
runtime.projects[runtime.currentProject] =
|
|
424
|
+
(runtime.projects[runtime.currentProject] || 0) + 1;
|
|
425
|
+
}
|
|
426
|
+
} catch {
|
|
427
|
+
// Silently fail — don't interrupt the session
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Inject wake-up context into system prompt
|
|
432
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
433
|
+
const runtime = getRuntime(ctx);
|
|
434
|
+
if (!runtime.enabled || !runtime.config.wakeUpEnabled) return;
|
|
435
|
+
if (!runtime.wakeUpText) return;
|
|
436
|
+
|
|
437
|
+
const extra =
|
|
438
|
+
"\n\n## Agent Memory (ACTIVE)\n" +
|
|
439
|
+
"You have persistent memory across sessions. Previous conversations and decisions are stored and searchable.\n" +
|
|
440
|
+
"Use `memory_search` to find past context. Use `memory_save` to explicitly remember something important.\n" +
|
|
441
|
+
"Use `memory_recall` to browse memories for a specific project or topic.\n" +
|
|
442
|
+
"Use `memory_graph` to discover cross-project connections via shared topics.\n" +
|
|
443
|
+
"Use `knowledge_add` to record structured facts. Use `knowledge_query` to query them.\n\n" +
|
|
444
|
+
runtime.wakeUpText;
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
systemPrompt: event.systemPrompt + extra,
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// -----------------------------------------------------------------------
|
|
452
|
+
// Tools
|
|
453
|
+
// -----------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
// --- memory_search ---
|
|
456
|
+
pi.registerTool({
|
|
457
|
+
name: "memory_search",
|
|
458
|
+
label: "Memory Search",
|
|
459
|
+
description:
|
|
460
|
+
"Search stored memories using semantic similarity. Finds past conversations, decisions, and context across all sessions.",
|
|
461
|
+
promptSnippet: "memory_search(query, project?, topic?, n_results?) — semantic search across stored memories",
|
|
462
|
+
promptGuidelines: [
|
|
463
|
+
"Use when the user asks about past decisions, previous conversations, or 'what did we decide about X'",
|
|
464
|
+
"Filter by project to narrow results to a specific codebase",
|
|
465
|
+
"Returns ranked results with similarity scores — higher is more relevant",
|
|
466
|
+
],
|
|
467
|
+
parameters: Type.Object({
|
|
468
|
+
query: Type.String({ description: "What to search for (natural language)" }),
|
|
469
|
+
project: Type.Optional(
|
|
470
|
+
Type.String({ description: "Filter to a specific project" })
|
|
471
|
+
),
|
|
472
|
+
topic: Type.Optional(
|
|
473
|
+
Type.String({ description: "Filter to a specific topic" })
|
|
474
|
+
),
|
|
475
|
+
n_results: Type.Optional(
|
|
476
|
+
Type.Number({ description: "Number of results (default: 5, max: 20)" })
|
|
477
|
+
),
|
|
478
|
+
}),
|
|
479
|
+
|
|
480
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
481
|
+
const runtime = getRuntime(ctx);
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const result = await runtime.store.search(params.query, {
|
|
485
|
+
project: params.project,
|
|
486
|
+
topic: params.topic,
|
|
487
|
+
n_results: params.n_results,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (result.results.length === 0) {
|
|
491
|
+
return textResult(`No memories found for: "${params.query}"`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let text = `Found ${result.results.length} memories for "${params.query}":\n\n`;
|
|
495
|
+
for (const hit of result.results) {
|
|
496
|
+
const sim = (hit.similarity * 100).toFixed(1);
|
|
497
|
+
text += `[${hit.project}/${hit.topic}] (${sim}% match, ${hit.timestamp})\n`;
|
|
498
|
+
text += `${hit.text}\n\n---\n\n`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return textResult(text, { query: params.query, hitCount: result.results.length });
|
|
502
|
+
} catch (e: unknown) {
|
|
503
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
504
|
+
return textResult(`Memory search failed: ${msg}`);
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
renderResult: renderTextResult,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// --- memory_save ---
|
|
512
|
+
pi.registerTool({
|
|
513
|
+
name: "memory_save",
|
|
514
|
+
label: "Memory Save",
|
|
515
|
+
description:
|
|
516
|
+
"Explicitly save a piece of information to persistent memory. Use for important decisions, facts, or context you want to remember across sessions.",
|
|
517
|
+
promptSnippet: "memory_save(content, project?, topic?) — save to persistent memory",
|
|
518
|
+
promptGuidelines: [
|
|
519
|
+
"Use when the user says 'remember this' or when an important decision is made",
|
|
520
|
+
"Include enough context in the content for it to be useful later",
|
|
521
|
+
"Set project and topic for better organization and retrieval",
|
|
522
|
+
],
|
|
523
|
+
parameters: Type.Object({
|
|
524
|
+
content: Type.String({
|
|
525
|
+
description: "The information to remember (include context)",
|
|
526
|
+
}),
|
|
527
|
+
project: Type.Optional(
|
|
528
|
+
Type.String({ description: "Project this belongs to" })
|
|
529
|
+
),
|
|
530
|
+
topic: Type.Optional(
|
|
531
|
+
Type.String({ description: "Topic category (e.g., 'auth', 'database', 'architecture')" })
|
|
532
|
+
),
|
|
533
|
+
importance: Type.Optional(
|
|
534
|
+
Type.Number({ description: "Importance weight 0.0-1.0 (default: 0.8 for manual saves). Higher = more likely to appear in session wake-up context." })
|
|
535
|
+
),
|
|
536
|
+
}),
|
|
537
|
+
|
|
538
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
539
|
+
const runtime = getRuntime(ctx);
|
|
540
|
+
const project = params.project || runtime.currentProject;
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const result = await runtime.store.store({
|
|
544
|
+
content: params.content,
|
|
545
|
+
project,
|
|
546
|
+
topic: params.topic || "general",
|
|
547
|
+
source: "manual-save",
|
|
548
|
+
importance: params.importance ?? 0.8, // Manual saves rank higher than auto-captures (0.5)
|
|
549
|
+
timestamp: new Date().toISOString(),
|
|
550
|
+
session_id: getSessionKey(ctx),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
if (result.status === "duplicate") {
|
|
554
|
+
return textResult(`This memory already exists (${result.id}).`, { status: "duplicate", id: result.id });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Update cached counts
|
|
558
|
+
runtime.totalMemories++;
|
|
559
|
+
runtime.projects[project] = (runtime.projects[project] || 0) + 1;
|
|
560
|
+
|
|
561
|
+
return textResult(
|
|
562
|
+
`✅ Saved to memory (${result.id}) in ${project}/${params.topic || "general"}`,
|
|
563
|
+
{ status: "stored", id: result.id, project },
|
|
564
|
+
);
|
|
565
|
+
} catch (e: unknown) {
|
|
566
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
567
|
+
return textResult(`Failed to save memory: ${msg}`);
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
renderResult: renderTextResult,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// --- memory_recall ---
|
|
575
|
+
pi.registerTool({
|
|
576
|
+
name: "memory_recall",
|
|
577
|
+
label: "Memory Recall",
|
|
578
|
+
description:
|
|
579
|
+
"Browse memories for a specific project or topic. Returns recent/important memories filtered by metadata. Use for getting context about a project or topic area.",
|
|
580
|
+
promptSnippet: "memory_recall(project?, topic?, n_results?) — browse memories by project/topic",
|
|
581
|
+
promptGuidelines: [
|
|
582
|
+
"Use when you need context about a specific project or topic area",
|
|
583
|
+
"Good for 'what have we been working on in project X' type questions",
|
|
584
|
+
"Complements memory_search — recall browses by metadata, search uses semantic similarity",
|
|
585
|
+
],
|
|
586
|
+
parameters: Type.Object({
|
|
587
|
+
project: Type.Optional(
|
|
588
|
+
Type.String({ description: "Filter to a specific project" })
|
|
589
|
+
),
|
|
590
|
+
topic: Type.Optional(
|
|
591
|
+
Type.String({ description: "Filter to a specific topic" })
|
|
592
|
+
),
|
|
593
|
+
n_results: Type.Optional(
|
|
594
|
+
Type.Number({ description: "Number of results (default: 10, max: 50)" })
|
|
595
|
+
),
|
|
596
|
+
}),
|
|
597
|
+
|
|
598
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
599
|
+
const runtime = getRuntime(ctx);
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
const result = runtime.store.recall({
|
|
603
|
+
project: params.project,
|
|
604
|
+
topic: params.topic,
|
|
605
|
+
n_results: params.n_results,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
if (result.results.length === 0) {
|
|
609
|
+
const label = [params.project, params.topic].filter(Boolean).join("/") || "all";
|
|
610
|
+
return textResult(`No memories found for: ${label}`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let text = `${result.results.length} memories`;
|
|
614
|
+
if (params.project) text += ` for project "${params.project}"`;
|
|
615
|
+
if (params.topic) text += ` in topic "${params.topic}"`;
|
|
616
|
+
text += ":\n\n";
|
|
617
|
+
|
|
618
|
+
for (const item of result.results) {
|
|
619
|
+
text += `[${item.project}/${item.topic}] (${item.timestamp})\n`;
|
|
620
|
+
text += `${item.text}\n\n---\n\n`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return textResult(text, { count: result.results.length });
|
|
624
|
+
} catch (e: unknown) {
|
|
625
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
626
|
+
return textResult(`Memory recall failed: ${msg}`);
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
|
|
630
|
+
renderResult: renderTextResult,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// --- memory_status ---
|
|
634
|
+
pi.registerTool({
|
|
635
|
+
name: "memory_status",
|
|
636
|
+
label: "Memory Status",
|
|
637
|
+
description:
|
|
638
|
+
"Show the current state of the memory store: total memories, per-project counts, storage size, and configuration.",
|
|
639
|
+
promptSnippet: "memory_status() — show memory store overview",
|
|
640
|
+
parameters: Type.Object({}),
|
|
641
|
+
|
|
642
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
643
|
+
const runtime = getRuntime(ctx);
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const result = runtime.store.status();
|
|
647
|
+
|
|
648
|
+
// Update cached state
|
|
649
|
+
runtime.totalMemories = result.total_memories;
|
|
650
|
+
runtime.projects = result.projects;
|
|
651
|
+
|
|
652
|
+
let text = "## Memory Status\n\n";
|
|
653
|
+
text += `- **Total memories**: ${result.total_memories}\n`;
|
|
654
|
+
text += `- **Identity**: ${result.identity_exists ? "✅ configured" : "❌ not configured"}\n`;
|
|
655
|
+
text += `- **Storage**: ${result.storage_size_kb} KB\n`;
|
|
656
|
+
text += `- **Current project**: ${runtime.currentProject}\n`;
|
|
657
|
+
text += `- **Auto-capture**: ${runtime.config.autoCapture ? "on" : "off"}\n`;
|
|
658
|
+
text += `- **Wake-up**: ${runtime.config.wakeUpEnabled ? "on" : "off"}\n`;
|
|
659
|
+
text += `- **Backend**: pure TypeScript (in-process)\n\n`;
|
|
660
|
+
|
|
661
|
+
if (result.projects && Object.keys(result.projects).length > 0) {
|
|
662
|
+
text += "### Projects\n";
|
|
663
|
+
for (const [proj, count] of Object.entries(result.projects)) {
|
|
664
|
+
text += `- ${proj}: ${count} memories\n`;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Knowledge graph stats
|
|
669
|
+
try {
|
|
670
|
+
const kgStats = runtime.store.knowledgeStats();
|
|
671
|
+
if (kgStats.entityCount > 0) {
|
|
672
|
+
text += `\n### Knowledge Graph\n`;
|
|
673
|
+
text += `- Entities: ${kgStats.entityCount}\n`;
|
|
674
|
+
text += `- Facts: ${kgStats.tripleCount} (${kgStats.activeTriples} active)\n`;
|
|
675
|
+
}
|
|
676
|
+
} catch { /* KG not available yet */ }
|
|
677
|
+
|
|
678
|
+
// Palace graph tunnels
|
|
679
|
+
try {
|
|
680
|
+
const graph = runtime.store.getPalaceGraph();
|
|
681
|
+
if (graph.edges.length > 0) {
|
|
682
|
+
text += `\n### Palace Tunnels\n`;
|
|
683
|
+
text += `- ${graph.edges.length} tunnel(s) connecting projects\n`;
|
|
684
|
+
}
|
|
685
|
+
} catch { /* Graph not available yet */ }
|
|
686
|
+
|
|
687
|
+
return textResult(text, { totalMemories: result.total_memories, projects: result.projects });
|
|
688
|
+
} catch (e: unknown) {
|
|
689
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
690
|
+
return textResult(`Memory status unavailable: ${msg}\n\nRun /skill:memory-setup to configure.`);
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
|
|
694
|
+
renderResult: renderTextResult,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// --- memory_graph ---
|
|
698
|
+
pi.registerTool({
|
|
699
|
+
name: "memory_graph",
|
|
700
|
+
label: "Memory Graph",
|
|
701
|
+
description: "Show the palace graph: projects as nodes, shared topics as tunnel connections between them. Reveals cross-project relationships.",
|
|
702
|
+
promptSnippet: "memory_graph() — view cross-project connections via shared topics",
|
|
703
|
+
promptGuidelines: [
|
|
704
|
+
"Use when the user asks about connections between projects",
|
|
705
|
+
"Shows which topics create 'tunnels' between different projects",
|
|
706
|
+
"Helps discover hidden relationships in the memory palace",
|
|
707
|
+
],
|
|
708
|
+
parameters: Type.Object({}),
|
|
709
|
+
|
|
710
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
711
|
+
const runtime = getRuntime(ctx);
|
|
712
|
+
try {
|
|
713
|
+
const graph = runtime.store.getPalaceGraph();
|
|
714
|
+
|
|
715
|
+
if (graph.nodes.length === 0) {
|
|
716
|
+
return textResult("No projects in the palace yet.");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
let text = "## \uD83C\uDFF0 Palace Graph\n\n";
|
|
720
|
+
|
|
721
|
+
// Nodes (projects)
|
|
722
|
+
text += "### Wings (Projects)\n";
|
|
723
|
+
for (const node of graph.nodes.sort((a, b) => b.memoryCount - a.memoryCount)) {
|
|
724
|
+
const topics = node.topics.length > 0 ? ` — rooms: ${node.topics.join(", ")}` : "";
|
|
725
|
+
text += `- **${node.name}** (${node.memoryCount} memories)${topics}\n`;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Edges (tunnels)
|
|
729
|
+
if (graph.edges.length > 0) {
|
|
730
|
+
text += "\n### Tunnels (Cross-Project Connections)\n";
|
|
731
|
+
for (const edge of graph.edges.sort((a, b) => b.strength - a.strength)) {
|
|
732
|
+
text += `- \uD83D\uDD17 **${edge.projectA}** \u2194 **${edge.projectB}** via topic "${edge.topic}" (${edge.strength} shared memories)\n`;
|
|
733
|
+
}
|
|
734
|
+
} else {
|
|
735
|
+
text += "\n*No tunnels yet — topics are project-unique. Shared topics across projects create tunnel connections.*\n";
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return textResult(text, { nodeCount: graph.nodes.length, edgeCount: graph.edges.length });
|
|
739
|
+
} catch (e: unknown) {
|
|
740
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
741
|
+
return textResult(`Palace graph failed: ${msg}`);
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
renderResult: renderTextResult,
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// --- memory_tunnel ---
|
|
748
|
+
pi.registerTool({
|
|
749
|
+
name: "memory_tunnel",
|
|
750
|
+
label: "Memory Tunnel",
|
|
751
|
+
description: "Traverse a tunnel between two projects via a shared topic. Returns memories from both projects for that topic.",
|
|
752
|
+
promptSnippet: "memory_tunnel(topic, project_a, project_b, n_results?) — traverse cross-project connection",
|
|
753
|
+
promptGuidelines: [
|
|
754
|
+
"Use after memory_graph reveals a tunnel connection",
|
|
755
|
+
"Shows how the same topic is discussed in different project contexts",
|
|
756
|
+
"Good for finding cross-cutting concerns like auth, database, or architecture patterns",
|
|
757
|
+
],
|
|
758
|
+
parameters: Type.Object({
|
|
759
|
+
topic: Type.String({ description: "The shared topic to traverse" }),
|
|
760
|
+
project_a: Type.String({ description: "First project" }),
|
|
761
|
+
project_b: Type.String({ description: "Second project" }),
|
|
762
|
+
n_results: Type.Optional(Type.Number({ description: "Results per project (default: 10)" })),
|
|
763
|
+
}),
|
|
764
|
+
|
|
765
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
766
|
+
const runtime = getRuntime(ctx);
|
|
767
|
+
try {
|
|
768
|
+
const results = runtime.store.traverseTunnel(
|
|
769
|
+
params.topic, params.project_a, params.project_b, params.n_results
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
if (results.length === 0) {
|
|
773
|
+
return textResult(`No memories found in tunnel: ${params.project_a} \u2194 ${params.project_b} via "${params.topic}"`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
let text = `## \uD83D\uDD17 Tunnel: ${params.project_a} \u2194 ${params.project_b} via "${params.topic}"\n\n`;
|
|
777
|
+
for (const item of results) {
|
|
778
|
+
text += `[${item.project}/${item.topic}] (${item.timestamp})\n`;
|
|
779
|
+
text += `${item.text}\n\n---\n\n`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return textResult(text, { count: results.length });
|
|
783
|
+
} catch (e: unknown) {
|
|
784
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
785
|
+
return textResult(`Tunnel traversal failed: ${msg}`);
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
renderResult: renderTextResult,
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// --- knowledge_add ---
|
|
792
|
+
pi.registerTool({
|
|
793
|
+
name: "knowledge_add",
|
|
794
|
+
label: "Knowledge Add",
|
|
795
|
+
description: "Add a structured fact (triple) to the knowledge graph. Facts are temporal — they can have start and end dates. Example: 'myapp uses PostgreSQL since 2025-01-01'.",
|
|
796
|
+
promptSnippet: "knowledge_add(subject, predicate, object, valid_from?, valid_to?, project?) — add a structured fact",
|
|
797
|
+
promptGuidelines: [
|
|
798
|
+
"Use when the user states a fact, makes a decision, or establishes a relationship",
|
|
799
|
+
"Subject and object are entities (people, tools, projects), predicate is the relationship",
|
|
800
|
+
"Set valid_from/valid_to for time-bounded facts (e.g., 'used React until 2025-06')",
|
|
801
|
+
"Common predicates: uses, depends_on, decided, prefers, created_by, replaces, implements",
|
|
802
|
+
],
|
|
803
|
+
parameters: Type.Object({
|
|
804
|
+
subject: Type.String({ description: "The subject entity (e.g., 'myapp', 'Alice')" }),
|
|
805
|
+
predicate: Type.String({ description: "The relationship (e.g., 'uses', 'depends_on', 'decided')" }),
|
|
806
|
+
object: Type.String({ description: "The object entity (e.g., 'PostgreSQL', 'React')" }),
|
|
807
|
+
valid_from: Type.Optional(Type.String({ description: "When this fact became true (ISO date)" })),
|
|
808
|
+
valid_to: Type.Optional(Type.String({ description: "When this fact stopped being true (ISO date, null if still true)" })),
|
|
809
|
+
project: Type.Optional(Type.String({ description: "Project context for this fact" })),
|
|
810
|
+
}),
|
|
811
|
+
|
|
812
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
813
|
+
const runtime = getRuntime(ctx);
|
|
814
|
+
const project = params.project || runtime.currentProject;
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
const result = runtime.store.addTriple({
|
|
818
|
+
subject: params.subject,
|
|
819
|
+
predicate: params.predicate,
|
|
820
|
+
object: params.object,
|
|
821
|
+
valid_from: params.valid_from,
|
|
822
|
+
valid_to: params.valid_to,
|
|
823
|
+
project,
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const timeInfo = params.valid_from ? ` (since ${params.valid_from})` : "";
|
|
827
|
+
return textResult(
|
|
828
|
+
`\u2705 Added fact: **${params.subject}** ${params.predicate} **${params.object}**${timeInfo}`,
|
|
829
|
+
{ status: result.status, id: result.id }
|
|
830
|
+
);
|
|
831
|
+
} catch (e: unknown) {
|
|
832
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
833
|
+
return textResult(`Failed to add fact: ${msg}`);
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
renderResult: renderTextResult,
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// --- knowledge_query ---
|
|
840
|
+
pi.registerTool({
|
|
841
|
+
name: "knowledge_query",
|
|
842
|
+
label: "Knowledge Query",
|
|
843
|
+
description: "Query the knowledge graph for facts about an entity. Supports temporal queries — ask 'what was true about X in January 2025'.",
|
|
844
|
+
promptSnippet: "knowledge_query(entity, at_time?, project?) — query facts about an entity",
|
|
845
|
+
promptGuidelines: [
|
|
846
|
+
"Use when the user asks about relationships, decisions, or facts about an entity",
|
|
847
|
+
"Set at_time to query historical state (e.g., 'what database did we use in 2024?')",
|
|
848
|
+
"Returns all known facts — both as subject and object of relationships",
|
|
849
|
+
],
|
|
850
|
+
parameters: Type.Object({
|
|
851
|
+
entity: Type.String({ description: "The entity to query (e.g., 'myapp', 'PostgreSQL')" }),
|
|
852
|
+
at_time: Type.Optional(Type.String({ description: "Query facts valid at this time (ISO date)" })),
|
|
853
|
+
project: Type.Optional(Type.String({ description: "Filter to a specific project" })),
|
|
854
|
+
}),
|
|
855
|
+
|
|
856
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
857
|
+
const runtime = getRuntime(ctx);
|
|
858
|
+
|
|
859
|
+
try {
|
|
860
|
+
const result = runtime.store.queryEntity(params.entity, {
|
|
861
|
+
at_time: params.at_time,
|
|
862
|
+
project: params.project,
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
if (!result.entity) {
|
|
866
|
+
return textResult(`No entity found: "${params.entity}"`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
let text = `## \uD83E\uDDE9 ${result.entity.name} (${result.entity.type})\n\n`;
|
|
870
|
+
|
|
871
|
+
if (result.facts.length === 0) {
|
|
872
|
+
text += "No facts recorded.\n";
|
|
873
|
+
} else {
|
|
874
|
+
for (const fact of result.facts) {
|
|
875
|
+
const timeRange = [
|
|
876
|
+
fact.valid_from ? `from ${fact.valid_from}` : null,
|
|
877
|
+
fact.valid_to ? `until ${fact.valid_to}` : null,
|
|
878
|
+
].filter(Boolean).join(" ");
|
|
879
|
+
const time = timeRange ? ` (${timeRange})` : "";
|
|
880
|
+
const confidence = fact.confidence < 1.0 ? ` [${(fact.confidence * 100).toFixed(0)}% confidence]` : "";
|
|
881
|
+
text += `- **${fact.subject}** ${fact.predicate} **${fact.object}**${time}${confidence}\n`;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return textResult(text, { entity: result.entity.name, factCount: result.facts.length });
|
|
886
|
+
} catch (e: unknown) {
|
|
887
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
888
|
+
return textResult(`Knowledge query failed: ${msg}`);
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
renderResult: renderTextResult,
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
// --- knowledge_status ---
|
|
895
|
+
pi.registerTool({
|
|
896
|
+
name: "knowledge_status",
|
|
897
|
+
label: "Knowledge Status",
|
|
898
|
+
description: "Show knowledge graph statistics: entities, facts, predicates, and entity types.",
|
|
899
|
+
promptSnippet: "knowledge_status() — overview of the knowledge graph",
|
|
900
|
+
parameters: Type.Object({}),
|
|
901
|
+
|
|
902
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
903
|
+
const runtime = getRuntime(ctx);
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
const stats = runtime.store.knowledgeStats();
|
|
907
|
+
|
|
908
|
+
let text = "## \uD83E\uDDE9 Knowledge Graph\n\n";
|
|
909
|
+
text += `- **Entities**: ${stats.entityCount}\n`;
|
|
910
|
+
text += `- **Total facts**: ${stats.tripleCount}\n`;
|
|
911
|
+
text += `- **Active facts**: ${stats.activeTriples} (no end date)\n\n`;
|
|
912
|
+
|
|
913
|
+
if (Object.keys(stats.entityTypes).length > 0) {
|
|
914
|
+
text += "### Entity Types\n";
|
|
915
|
+
for (const [type, count] of Object.entries(stats.entityTypes)) {
|
|
916
|
+
text += `- ${type}: ${count}\n`;
|
|
917
|
+
}
|
|
918
|
+
text += "\n";
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (Object.keys(stats.predicates).length > 0) {
|
|
922
|
+
text += "### Top Predicates\n";
|
|
923
|
+
for (const [pred, count] of Object.entries(stats.predicates)) {
|
|
924
|
+
text += `- ${pred}: ${count}\n`;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return textResult(text, stats);
|
|
929
|
+
} catch (e: unknown) {
|
|
930
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
931
|
+
return textResult(`Knowledge status failed: ${msg}`);
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
renderResult: renderTextResult,
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
// -----------------------------------------------------------------------
|
|
938
|
+
// Commands
|
|
939
|
+
// -----------------------------------------------------------------------
|
|
940
|
+
|
|
941
|
+
pi.registerCommand("memory", {
|
|
942
|
+
description: "Memory management: status, search, project, graph, knowledge, on/off",
|
|
943
|
+
handler: async (args, ctx) => {
|
|
944
|
+
const runtime = getRuntime(ctx);
|
|
945
|
+
const parts = (args || "").trim().split(/\s+/);
|
|
946
|
+
const subcmd = parts[0] || "status";
|
|
947
|
+
|
|
948
|
+
switch (subcmd) {
|
|
949
|
+
case "status": {
|
|
950
|
+
try {
|
|
951
|
+
const result = runtime.store.status();
|
|
952
|
+
ctx.ui.notify(
|
|
953
|
+
`🧠 ${result.total_memories} memories | ${Object.keys(result.projects).length} projects | ${result.storage_size_kb} KB`,
|
|
954
|
+
"info"
|
|
955
|
+
);
|
|
956
|
+
} catch (e: unknown) {
|
|
957
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
958
|
+
ctx.ui.notify(`Memory error: ${msg}`, "error");
|
|
959
|
+
}
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
case "project": {
|
|
964
|
+
const projectName = parts.slice(1).join(" ") || detectProject(ctx.cwd);
|
|
965
|
+
runtime.currentProject = projectName;
|
|
966
|
+
runtime.config.defaultProject = projectName;
|
|
967
|
+
saveConfig(runtime.config);
|
|
968
|
+
ctx.ui.notify(`Project set to: ${projectName}`, "info");
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
case "on": {
|
|
973
|
+
runtime.enabled = true;
|
|
974
|
+
runtime.config.autoCapture = true;
|
|
975
|
+
runtime.config.wakeUpEnabled = true;
|
|
976
|
+
saveConfig(runtime.config);
|
|
977
|
+
ctx.ui.notify("Memory enabled", "info");
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
case "off": {
|
|
982
|
+
runtime.enabled = false;
|
|
983
|
+
runtime.config.autoCapture = false;
|
|
984
|
+
runtime.config.wakeUpEnabled = false;
|
|
985
|
+
saveConfig(runtime.config);
|
|
986
|
+
ctx.ui.notify("Memory disabled", "info");
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
case "stats": {
|
|
991
|
+
if (!runtime.backendAvailable) {
|
|
992
|
+
ctx.ui.notify("Memory backend not available", "error");
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
const stats = runtime.store.computeStats();
|
|
997
|
+
await showStatsOverlay(ctx, stats);
|
|
998
|
+
} catch (e: unknown) {
|
|
999
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1000
|
+
ctx.ui.notify(`Stats error: ${msg}`, "error");
|
|
1001
|
+
}
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
case "search": {
|
|
1006
|
+
const query = parts.slice(1).join(" ");
|
|
1007
|
+
if (!query) {
|
|
1008
|
+
ctx.ui.notify("Usage: /memory search <query>", "warning");
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
pi.sendUserMessage(`Search my memory for: ${query}`);
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
case "graph": {
|
|
1016
|
+
pi.sendUserMessage("Show the palace graph with cross-project connections");
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
case "knowledge": {
|
|
1021
|
+
const entity = parts.slice(1).join(" ");
|
|
1022
|
+
if (!entity) {
|
|
1023
|
+
ctx.ui.notify("Usage: /memory knowledge <entity>", "warning");
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
pi.sendUserMessage(`Query knowledge graph for: ${entity}`);
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
default: {
|
|
1031
|
+
ctx.ui.notify(
|
|
1032
|
+
"Usage: /memory [status|stats|project <name>|search <query>|graph|knowledge <entity>|on|off]",
|
|
1033
|
+
"info"
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
getArgumentCompletions: (prefix) => {
|
|
1039
|
+
const commands = ["status", "stats", "project", "search", "graph", "knowledge", "on", "off"];
|
|
1040
|
+
return commands
|
|
1041
|
+
.filter((c) => c.startsWith(prefix))
|
|
1042
|
+
.map((c) => ({ label: c, value: c, type: "text" as const }));
|
|
1043
|
+
},
|
|
1044
|
+
});
|
|
1045
|
+
}
|