pi-mnemoteca 0.3.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/index.ts +366 -0
  4. package/package.json +43 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 gandazgul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # pi-mnemoteca
2
+
3
+ Pi extension for **local persistent memory** using
4
+ [Mnemoteca](https://github.com/gandazgul/mnemoteca). It gives your AI coding
5
+ agent memory that persists across sessions. It is offline and does not use cloud
6
+ APIs.
7
+
8
+ ## Prerequisites
9
+
10
+ Install the `mnemoteca` binary first:
11
+
12
+ ```bash
13
+ curl -fsSL https://raw.githubusercontent.com/gandazgul/mnemoteca/main/install.sh | sh
14
+ mnemoteca setup
15
+ ```
16
+
17
+ See the [Mnemoteca README](https://github.com/gandazgul/mnemoteca#installation)
18
+ for detailed setup instructions. On first use, Mnemoteca downloads its ML models,
19
+ approximately 500 MB one time.
20
+
21
+ Make sure the `mnemoteca` binary is in your `PATH`.
22
+
23
+ ## Installation
24
+
25
+ Install from npm:
26
+
27
+ ```bash
28
+ pi install npm:pi-mnemoteca
29
+ ```
30
+
31
+ Install from a local checkout during development:
32
+
33
+ ```bash
34
+ pi install ./pi-mnemoteca
35
+ ```
36
+
37
+ ## Upgrade from pi-mnemosyne
38
+
39
+ If you already used the old Pi extension, stop Pi agents before you change
40
+ packages.
41
+
42
+ 1. Migrate CLI data first if needed. Use the
43
+ [Mnemoteca migration guide](https://github.com/gandazgul/mnemoteca/blob/main/docs/migrate-from-mnemosyne.md).
44
+ 2. Install the new extension at the same scope where the old extension was
45
+ installed:
46
+ ```bash
47
+ pi install npm:pi-mnemoteca
48
+ ```
49
+ 3. Verify that Pi loads the new extension and that memory tools work:
50
+ ```bash
51
+ pi list
52
+ ```
53
+ Store and recall a harmless test memory if needed.
54
+ 4. Remove the old package at the matching scope:
55
+ ```bash
56
+ pi remove npm:pi-mnemosyne
57
+ ```
58
+ 5. Restart Pi agents.
59
+
60
+ If your old installation was project-local, run the install and remove commands
61
+ from that project. If it was user-level, use the same user-level Pi context. Do
62
+ not keep `pi-mnemoteca` and `pi-mnemosyne` active together for normal use.
63
+
64
+ Windows users must finish this replacement before restarting Pi agents. There is
65
+ no Windows `mnemosyne` compatibility shim, alias, copied executable, or renamed
66
+ executable.
67
+
68
+ ## Memory tools
69
+
70
+ The agent-facing tool names stay stable. They describe memory capabilities, not
71
+ product branding.
72
+
73
+ | Tool | Purpose |
74
+ | --- | --- |
75
+ | `memory_recall` | Search project memory. |
76
+ | `memory_recall_global` | Search global memory. |
77
+ | `memory_store` | Store a project memory. Set `core=true` to tag it as core. |
78
+ | `memory_store_global` | Store a global memory. Set `core=true` to tag it as core. |
79
+ | `memory_delete` | Delete a memory by the numeric document ID shown in recall or list output. |
80
+
81
+ Project memory uses a collection name derived from the project directory name.
82
+ If that name is empty or `global`, the extension uses `default`.
83
+
84
+ The project collection is initialized on `session_start`. The global collection
85
+ is created on first use of `mnemoteca add -g` or the equivalent global store
86
+ tool.
87
+
88
+ ## Session behavior
89
+
90
+ On session start, the extension:
91
+
92
+ 1. Stores the project working directory.
93
+ 2. Checks for `.mnemoteca-debug`.
94
+ 3. Initializes the project collection with `mnemoteca init`.
95
+ 4. Fetches project and global core memories.
96
+ 5. Caches the core-memory block.
97
+
98
+ Before each agent start, including after compaction, the extension appends the
99
+ cached core-memory block and memory-use guidance to the system prompt. Project
100
+ core memories appear before global core memories. If one core-memory query
101
+ fails, the other can still appear.
102
+
103
+ A non-core store does not invalidate the core cache. A core store and any delete
104
+ operation invalidate it, because the core-tag state can change.
105
+
106
+ ## Debug files
107
+
108
+ Create `.mnemoteca-debug` in the project directory before session start to enable
109
+ debug output. The extension writes:
110
+
111
+ - `.mnemoteca-debug.log`
112
+ - `.mnemoteca-debug-prompt.txt`
113
+
114
+ Debug writes are best effort and do not stop agent execution.
115
+
116
+ ## Commands taught to the agent
117
+
118
+ - Use `mnemoteca search -f plain [query]` and `mnemoteca search -g -f plain [query]` to search relevant memories.
119
+ - After significant decisions, use `mnemoteca add "memory content"` to save a concise fact. Use `mnemoteca add -g "memory content"` for cross-project preferences.
120
+ - Delete contradicted memories with `mnemoteca delete [memory id]` after storing the updated memory.
121
+ - Mark critical, always-relevant context as core with `-t core`. You can use repeated tags, such as `mnemoteca add "database is sqlite" -t core -t tech-stack`.
122
+
123
+ ## How it works
124
+
125
+ ```text
126
+ session_start
127
+ ├─ save cwd and derive project collection
128
+ ├─ mnemoteca init
129
+ └─ fetch project and global core memories
130
+
131
+ before_agent_start
132
+ └─ append cached core memories and guidance
133
+
134
+ memory tools
135
+ ├─ mnemoteca search
136
+ ├─ mnemoteca add [-t core]
137
+ └─ mnemoteca delete
138
+ ```
139
+
140
+ The extension calls the `mnemoteca` executable with argument arrays. It does not
141
+ own data storage, select databases, or run migrations.
package/index.ts ADDED
@@ -0,0 +1,366 @@
1
+ /**
2
+ * pi-mnemoteca — Local persistent memory for the pi AI agent.
3
+ *
4
+ * Gives the agent memory that persists across sessions using Mnemoteca,
5
+ * a local document store with hybrid search (BM25 + vector similarity).
6
+ * All ML inference runs locally via ONNX Runtime. No cloud APIs required.
7
+ *
8
+ * Features:
9
+ * - Core memories (tagged `core`) injected into the system prompt at session start
10
+ * - memory_recall / memory_recall_global tools for on-demand search
11
+ * - memory_store / memory_store_global tools with optional `core` tagging
12
+ * - memory_delete tool for removing outdated memories
13
+ * - Cache invalidation: core memory cache refreshed only when dirty
14
+ */
15
+
16
+ import * as path from "node:path";
17
+ import * as fs from "node:fs";
18
+ import type {ExtensionAPI} from "@mariozechner/pi-coding-agent";
19
+ import {Type} from "@sinclair/typebox";
20
+
21
+ export default function mnemotecaExtension(pi: ExtensionAPI): void {
22
+ let projectName = "";
23
+ let projectCwd = "";
24
+
25
+ // ── Debug support ────────────────────────────────────────────────
26
+ let debugEnabled = false;
27
+ const debugLog: string[] = [];
28
+
29
+ function debug(message: string): void {
30
+ const timestamp = new Date().toISOString();
31
+ const line = `[${timestamp}] ${message}`;
32
+ debugLog.push(line);
33
+ if (debugEnabled) {
34
+ try {
35
+ const debugPath = path.join(projectCwd || ".", ".mnemoteca-debug.log");
36
+ fs.appendFileSync(debugPath, line + "\n", "utf-8");
37
+ } catch { /* best effort */ }
38
+ }
39
+ }
40
+
41
+ function writeDebugPrompt(fullPrompt: string): void {
42
+ if (!debugEnabled) return;
43
+ try {
44
+ const debugInfo = [
45
+ "=== Mnemoteca Debug Info ===",
46
+ `Timestamp: ${new Date().toISOString()}`,
47
+ `Project Name: ${projectName}`,
48
+ `Project CWD: ${projectCwd}`,
49
+ `Cache Valid: ${cacheValid}`,
50
+ `Cached Core Block Length: ${cachedCoreBlock.length}`,
51
+ `Debug Log (last 50 entries):`,
52
+ ...debugLog.slice(-50),
53
+ "",
54
+ "=== System Prompt ===",
55
+ fullPrompt,
56
+ ].join("\n");
57
+ fs.writeFileSync(
58
+ path.join(projectCwd, ".mnemoteca-debug-prompt.txt"),
59
+ debugInfo,
60
+ "utf-8",
61
+ );
62
+ } catch { /* best effort */ }
63
+ }
64
+
65
+ // ── Core memory cache ─────────────────────────────────────────────
66
+ let cachedCoreBlock = "";
67
+ let cacheValid = false;
68
+
69
+ // ── Helper: execute Mnemoteca CLI ─────────────────────────────────
70
+
71
+ async function mnemoteca(...args: string[]): Promise<string> {
72
+ debug(`exec: mnemoteca ${args.join(" ")}`);
73
+ try {
74
+ const result = await pi.exec("mnemoteca", args, { cwd: projectCwd });
75
+ debug(`exec result: code=${result.code} stdout=${result.stdout.length}bytes stderr=${result.stderr.length}bytes`);
76
+
77
+ if (result.code !== 0) {
78
+ const errMsg = result.stderr.trim() || `mnemoteca ${args[0]} failed (exit ${result.code})`;
79
+ // Exit code 127 = command not found in shell
80
+ if (result.code === 127 || errMsg.includes("not found") || errMsg.includes("ENOENT") || errMsg.includes("No such file")) {
81
+ debug(`ERROR: mnemoteca binary not found`);
82
+ return "Error: mnemoteca binary not found. Install it: https://github.com/gandazgul/mnemoteca#quick-start";
83
+ }
84
+ debug(`ERROR: ${errMsg}`);
85
+ throw new Error(errMsg);
86
+ }
87
+
88
+ // Mnemoteca writes output to stderr, use whichever has content
89
+ return result.stdout || result.stderr;
90
+ } catch (e: unknown) {
91
+ const msg = e instanceof Error ? e.message : String(e);
92
+ debug(`CATCH: ${msg}`);
93
+ if (
94
+ msg.includes("not found") ||
95
+ msg.includes("ENOENT") ||
96
+ msg.includes("No such file")
97
+ ) {
98
+ return "Error: mnemoteca binary not found. Install it: https://github.com/gandazgul/mnemoteca#quick-start";
99
+ }
100
+ throw e;
101
+ }
102
+ }
103
+
104
+ // ── Helper: fetch and format core memories ────────────────────────
105
+
106
+ async function fetchCoreMemories(): Promise<string> {
107
+ debug(`fetchCoreMemories: projectName=${projectName}, cwd=${projectCwd}`);
108
+ const sections: string[] = [];
109
+
110
+ // Fetch project core memories
111
+ try {
112
+ const localCore = await mnemoteca(
113
+ "list", "--name", projectName, "--tag", "core", "--format", "plain",
114
+ );
115
+ const trimmed = localCore.trim();
116
+ debug(`project core: ${trimmed.length} chars, starts with: ${JSON.stringify(trimmed.substring(0, 80))}`);
117
+ if (trimmed && !trimmed.startsWith("No documents")) {
118
+ sections.push(`Project Core Memories (${projectName}):\n\n${trimmed}`);
119
+ }
120
+ } catch (e) {
121
+ debug(`project core error: ${e instanceof Error ? e.message : String(e)}`);
122
+ }
123
+
124
+ // Fetch global core memories
125
+ try {
126
+ const globalCore = await mnemoteca(
127
+ "list", "--global", "--tag", "core", "--format", "plain",
128
+ );
129
+ const trimmed = globalCore.trim();
130
+ debug(`global core: ${trimmed.length} chars, starts with: ${JSON.stringify(trimmed.substring(0, 80))}`);
131
+ if (trimmed && !trimmed.startsWith("No documents")) {
132
+ sections.push(`Global Core Memories:\n\n${trimmed}`);
133
+ }
134
+ } catch (e) {
135
+ debug(`global core error: ${e instanceof Error ? e.message : String(e)}`);
136
+ }
137
+
138
+ const memoriesBlock = sections.length > 0
139
+ ? `\n\n${sections.join("\n\n")}`
140
+ : "";
141
+
142
+ return `\n\n${memoriesBlock}
143
+
144
+ When to use memory:
145
+ - Search memory when past context would help answer the user's request.
146
+ - Store concise summaries of important decisions, preferences, and patterns.
147
+ - Delete outdated memories when new decisions contradict them. Recall results include a document ID in square brackets (e.g. [42]). Pass that numeric ID to memory_delete to remove the memory.
148
+ - Use **core** for facts that should always be in context (project architecture, key conventions, user preferences).
149
+ - Use **global** variants for cross-project preferences (coding style, tool choices).
150
+ - At the end of a session, store any relevant memories for future sessions.`;
151
+ }
152
+
153
+ // ── Helper: refresh cache if needed ───────────────────────────────
154
+
155
+ async function ensureCacheValid(): Promise<void> {
156
+ if (cacheValid) return;
157
+ cachedCoreBlock = await fetchCoreMemories();
158
+ cacheValid = true;
159
+ }
160
+
161
+ function invalidateCache(): void {
162
+ cacheValid = false;
163
+ }
164
+
165
+ // ── Session start: init collection + load core memories ──────────
166
+
167
+ pi.on("session_start", async (_event, ctx) => {
168
+ projectCwd = ctx.cwd;
169
+
170
+ // Check for debug flag file
171
+ try {
172
+ fs.accessSync(path.join(projectCwd, ".mnemoteca-debug"));
173
+ debugEnabled = true;
174
+ // Clear previous debug log file
175
+ try { fs.writeFileSync(path.join(projectCwd, ".mnemoteca-debug.log"), "", "utf-8"); } catch { /* ok */ }
176
+ } catch {
177
+ debugEnabled = false;
178
+ }
179
+
180
+ debug(`session_start: cwd=${projectCwd}, debugEnabled=${debugEnabled}`);
181
+
182
+ // Resolve project name from cwd basename
183
+ const rawName = path.basename(projectCwd);
184
+ projectName = rawName === "global" ? "default" : (rawName || "default");
185
+
186
+ debug(`project: name=${projectName}`);
187
+
188
+ // Auto-init the project collection (idempotent)
189
+ try {
190
+ await mnemoteca("init", "--name", projectName);
191
+ } catch (e) {
192
+ debug(`init error: ${e instanceof Error ? e.message : String(e)}`);
193
+ }
194
+
195
+ // Pre-fetch core memories
196
+ invalidateCache();
197
+ await ensureCacheValid();
198
+ debug(`session_start complete: cacheValid=${cacheValid}, coreBlockLength=${cachedCoreBlock.length}`);
199
+ });
200
+
201
+ // ── Before agent start: inject core memories into system prompt ──
202
+
203
+ pi.on("before_agent_start", async (event) => {
204
+ await ensureCacheValid();
205
+
206
+ const fullPrompt = event.systemPrompt + cachedCoreBlock;
207
+
208
+ debug(`before_agent_start: systemPrompt=${event.systemPrompt.length}chars, coreBlock=${cachedCoreBlock.length}chars, total=${fullPrompt.length}chars`);
209
+ writeDebugPrompt(fullPrompt);
210
+
211
+ return {
212
+ systemPrompt: fullPrompt,
213
+ };
214
+ });
215
+
216
+ // ── Tools ─────────────────────────────────────────────────────────
217
+
218
+ pi.registerTool({
219
+ name: "memory_recall",
220
+ label: "Memory Recall",
221
+ description:
222
+ "Search project memory for relevant context, past decisions, and preferences. Use this at the start of conversations and whenever past context would help.",
223
+ promptSnippet: "Search project memory for past context and decisions",
224
+ parameters: Type.Object({
225
+ query: Type.String({ description: "Semantic search query" }),
226
+ }),
227
+
228
+ async execute(_toolCallId, params) {
229
+ // Quote the query to prevent SQLite FTS errors with hyphens and special characters
230
+ const safeQuery = `"${params.query.replaceAll('"', '""')}"`;
231
+ const result = await mnemoteca(
232
+ "search", "--name", projectName, "--format", "plain", safeQuery,
233
+ );
234
+ return {
235
+ content: [{ type: "text", text: result.trim() || "No memories found." }],
236
+ details: undefined,
237
+ };
238
+ },
239
+ });
240
+
241
+ pi.registerTool({
242
+ name: "memory_recall_global",
243
+ label: "Memory Recall Global",
244
+ description:
245
+ "Search global memory for cross-project preferences, decisions and patterns.",
246
+ promptSnippet: "Search global memory for cross-project preferences",
247
+ parameters: Type.Object({
248
+ query: Type.String({ description: "Semantic search query" }),
249
+ }),
250
+
251
+ async execute(_toolCallId, params) {
252
+ const safeQuery = `"${params.query.replaceAll('"', '""')}"`;
253
+ const result = await mnemoteca(
254
+ "search", "--global", "--format", "plain", safeQuery,
255
+ );
256
+ return {
257
+ content: [{ type: "text", text: result.trim() || "No global memories found." }],
258
+ details: undefined,
259
+ };
260
+ },
261
+ });
262
+
263
+ pi.registerTool({
264
+ name: "memory_store",
265
+ label: "Memory Store",
266
+ description:
267
+ "Store a project memory: a decision, preference, or important context. One concise concept per memory. Set core=true for critical context that should always be available in every session (use sparingly).",
268
+ promptSnippet: "Store a project-scoped memory (decision, preference, context)",
269
+ promptGuidelines: [
270
+ "Use memory_store to save important decisions, preferences, and context for future sessions.",
271
+ "Set core=true only for critical, always-relevant context (like project architecture or key conventions). Core memories are injected into every prompt, so keep them lean.",
272
+ ],
273
+ parameters: Type.Object({
274
+ content: Type.String({ description: "Concise memory to store" }),
275
+ core: Type.Optional(Type.Boolean({
276
+ description: "If true, this memory is always injected into context (like AGENTS.md). Use sparingly.",
277
+ })),
278
+ }),
279
+
280
+ async execute(_toolCallId, params) {
281
+ const args = ["add", "--name", projectName];
282
+ if (params.core) {
283
+ args.push("--tag", "core");
284
+ }
285
+ args.push(params.content);
286
+
287
+ const result = await mnemoteca(...args);
288
+
289
+ if (params.core) {
290
+ invalidateCache();
291
+ }
292
+
293
+ return {
294
+ content: [{ type: "text", text: result.trim() }],
295
+ details: undefined,
296
+ };
297
+ },
298
+ });
299
+
300
+ pi.registerTool({
301
+ name: "memory_store_global",
302
+ label: "Memory Store Global",
303
+ description:
304
+ "Store a cross-project memory: personal preferences, coding style, tool choices. Set core=true for critical cross-project context that should always be available.",
305
+ promptSnippet: "Store a cross-project memory (coding style, tool choices)",
306
+ parameters: Type.Object({
307
+ content: Type.String({ description: "Global memory to store" }),
308
+ core: Type.Optional(Type.Boolean({
309
+ description: "If true, this memory is always injected into context. Use sparingly.",
310
+ })),
311
+ }),
312
+
313
+ async execute(_toolCallId, params) {
314
+ // Ensure the global collection exists
315
+ try {
316
+ await mnemoteca("init", "--global");
317
+ } catch {
318
+ // Already exists — fine
319
+ }
320
+
321
+ const args = ["add", "--global"];
322
+ if (params.core) {
323
+ args.push("--tag", "core");
324
+ }
325
+ args.push(params.content);
326
+
327
+ const result = await mnemoteca(...args);
328
+
329
+ if (params.core) {
330
+ invalidateCache();
331
+ }
332
+
333
+ return {
334
+ content: [{ type: "text", text: result.trim() }],
335
+ details: undefined,
336
+ };
337
+ },
338
+ });
339
+
340
+ pi.registerTool({
341
+ name: "memory_delete",
342
+ label: "Memory Delete",
343
+ description:
344
+ "Delete an outdated or incorrect memory by its document ID (shown in [brackets] in recall/list results).",
345
+ promptSnippet: "Delete an outdated memory by document ID",
346
+ promptGuidelines: [
347
+ "Use memory_delete to remove outdated, incorrect, or superseded memories.",
348
+ "First call memory_recall to find the memory. The document ID is shown in [brackets] (e.g. [42]) in recall and list results. Pass that numeric ID to memory_delete.",
349
+ ],
350
+ parameters: Type.Object({
351
+ id: Type.Number({ description: "Document ID to delete" }),
352
+ }),
353
+
354
+ async execute(_toolCallId, params) {
355
+ const result = await mnemoteca("delete", String(params.id));
356
+
357
+ // Invalidate cache since we don't know if the deleted memory was core
358
+ invalidateCache();
359
+
360
+ return {
361
+ content: [{ type: "text", text: result.trim() }],
362
+ details: undefined,
363
+ };
364
+ },
365
+ });
366
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "pi-mnemoteca",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "Pi extension for local persistent memory using Mnemoteca — offline semantic search, no cloud required",
6
+ "author": "gandazgul",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/gandazgul/pi-mnemoteca.git"
11
+ },
12
+ "keywords": [
13
+ "pi-package",
14
+ "mnemoteca",
15
+ "memory",
16
+ "local",
17
+ "offline",
18
+ "semantic-search",
19
+ "ai",
20
+ "coding-agent"
21
+ ],
22
+ "pi": {
23
+ "extensions": [
24
+ "./"
25
+ ]
26
+ },
27
+ "files": [
28
+ "index.ts",
29
+ "README.md"
30
+ ],
31
+ "peerDependencies": {
32
+ "@mariozechner/pi-coding-agent": ">=0.53.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^25.5.2",
36
+ "typescript": "^5.9.3",
37
+ "tsx": "^4.20.6"
38
+ },
39
+ "scripts": {
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "tsc --noEmit -p tsconfig.test.json && tsx index.test.ts"
42
+ }
43
+ }