pi-memory-stone 0.1.1 → 0.1.3
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 +53 -7
- package/package.json +7 -1
- package/src/commands/index.ts +326 -4
- package/src/config/index.ts +7 -0
- package/src/db/index.ts +15 -3
- package/src/index.ts +34 -51
- package/src/portable/index.ts +204 -0
- package/src/session-state/index.ts +88 -0
- package/test/indexing.test.ts +0 -97
- package/test/parser.test.ts +0 -261
- package/test/privacy.test.ts +0 -120
- package/test/ranking.test.ts +0 -403
- package/tsconfig.json +0 -13
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ pi-memory-stone package source
|
|
|
48
48
|
│ ├── tools/ # LLM-callable tools
|
|
49
49
|
│ ├── privacy/ # Secret redaction, sensitive path filtering
|
|
50
50
|
│ └── config/ # Project identity, settings
|
|
51
|
-
└── test/ #
|
|
51
|
+
└── test/ # 51 tests across 6 test files
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
## How It Works
|
|
@@ -69,10 +69,12 @@ Every time the agent finishes a response, the extension:
|
|
|
69
69
|
Before each agent turn, the extension:
|
|
70
70
|
|
|
71
71
|
- Builds a focused query from the user's prompt
|
|
72
|
-
-
|
|
72
|
+
- Uses `memory.injectionMode` to choose automatic or manual injection
|
|
73
|
+
- In `auto` mode, searches records via FTS5
|
|
73
74
|
- Ranks by hybrid score: FTS match + same-project boost + recency decay + kind weight + confidence
|
|
74
|
-
- Injects top results (max 5, threshold-limited)
|
|
75
|
-
-
|
|
75
|
+
- Injects top results (max 5, threshold-limited) plus any refs selected with `/memory-inject`
|
|
76
|
+
- In `manual` mode, skips search and injects only refs selected with `/memory-inject`
|
|
77
|
+
- Tracks auto-injected refs to prevent feedback loops
|
|
76
78
|
- Logs every injection for audit via `/memory-last`
|
|
77
79
|
|
|
78
80
|
### 3. Storage Model
|
|
@@ -102,9 +104,18 @@ Before each agent turn, the extension:
|
|
|
102
104
|
| `/memory-status` | `/stone-status` | Show index statistics, record counts by kind, config |
|
|
103
105
|
| `/memory-status --verbose` | | Include per-kind record breakdown |
|
|
104
106
|
| `/memory-search <query>` | `/stone-search` | Search memory for relevant records |
|
|
107
|
+
| `/memory-open <id>` | `/stone-open` | Open a specific memory record by reference ID |
|
|
108
|
+
| `/memory-inject <id> [id ...]` | `/stone-inject` | Manually inject specific refs into future turns this session |
|
|
109
|
+
| `/memory-clear-injected` | `/stone-clear-injected` | Clear manually injected refs for this session |
|
|
110
|
+
| `/memory-mode <auto\|manual>` | `/stone-mode` | Override injection mode for this session |
|
|
105
111
|
| `/memory-last` | `/stone-last` | Show the last memory injection packet |
|
|
106
112
|
| `/memory-forget <id>` | `/stone-forget` | Soft-forget a record (hide from searches) |
|
|
107
113
|
| `/memory-forget <id> --hard` | | Permanently delete (with confirmation) |
|
|
114
|
+
| `/memory-export [path] --format json\|md` | `/stone-export` | Export active records to a portable JSON or Markdown file (`--all` includes inactive records) |
|
|
115
|
+
| `/memory-import <memory-export.json>` | `/stone-import` | Import records from a JSON export, remapping project-scoped records to the current project by default |
|
|
116
|
+
| `/memory-import <memory-export.json> --preserve-project` | | Import records while preserving exported project IDs |
|
|
117
|
+
| `/memory-import <memory-export.json> --global` | | Import all records as global memories |
|
|
118
|
+
| `/memory-backup [path]` | `/stone-backup` | Copy the SQLite memory database to a timestamped backup file |
|
|
108
119
|
| `/memory-on` | | Enable memory injection for this session |
|
|
109
120
|
| `/memory-off` | | Disable memory injection for this session |
|
|
110
121
|
|
|
@@ -154,6 +165,29 @@ Parameters:
|
|
|
154
165
|
hard? Request permanent deletion (requires confirmation)
|
|
155
166
|
```
|
|
156
167
|
|
|
168
|
+
## Portable Export, Import, and Backup
|
|
169
|
+
|
|
170
|
+
Use JSON export/import when you want a portable, reviewable memory transfer between machines:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
/memory-export --format json
|
|
174
|
+
/memory-import memory-export.json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Markdown export is for human review outside SQLite:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
/memory-export memory-export.md --format md
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Use a database backup before pruning or hard deletion:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
/memory-backup
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Import defaults are intentionally practical for machine moves: project-scoped records are remapped to the current project. Use `--preserve-project` to keep exported project IDs, or `--global` to import everything as global memory.
|
|
190
|
+
|
|
157
191
|
## Privacy & Safety
|
|
158
192
|
|
|
159
193
|
### Secret Redaction
|
|
@@ -226,14 +260,16 @@ npm run typecheck
|
|
|
226
260
|
|
|
227
261
|
These scripts are also runnable from a pi package clone installed from git; the required script runners are regular dependencies because pi package installs omit `devDependencies`.
|
|
228
262
|
|
|
229
|
-
|
|
263
|
+
51 tests across 6 test files:
|
|
230
264
|
|
|
231
265
|
| Suite | Tests | Focus |
|
|
232
266
|
|---|---|---|
|
|
233
267
|
| `indexing.test.ts` | 1 | Incremental session indexing |
|
|
234
|
-
| `privacy.test.ts` | 17 | Secret redaction, sensitive path filtering |
|
|
235
268
|
| `parser.test.ts` | 10 | Turn parsing, file activity detection, error extraction |
|
|
269
|
+
| `portable.test.ts` | 3 | JSON/Markdown export, JSON import, SQLite backup |
|
|
270
|
+
| `privacy.test.ts` | 17 | Secret redaction, sensitive path filtering |
|
|
236
271
|
| `ranking.test.ts` | 16 | Hybrid ranking, cross-project filtering, injection formatting |
|
|
272
|
+
| `session-state.test.ts` | 4 | Injection mode config, session ref selection, manual-only injection |
|
|
237
273
|
|
|
238
274
|
## Configuration
|
|
239
275
|
|
|
@@ -246,11 +282,21 @@ Project settings in `.pi/settings.json`:
|
|
|
246
282
|
"maxInjectedRecords": 5,
|
|
247
283
|
"maxInjectedTokens": 1000,
|
|
248
284
|
"scoreThreshold": 0.3,
|
|
249
|
-
"crossProjectEnabled": false
|
|
285
|
+
"crossProjectEnabled": false,
|
|
286
|
+
"injectionMode": "auto"
|
|
250
287
|
}
|
|
251
288
|
}
|
|
252
289
|
```
|
|
253
290
|
|
|
291
|
+
`injectionMode` accepts:
|
|
292
|
+
|
|
293
|
+
| Value | Behavior |
|
|
294
|
+
|---|---|
|
|
295
|
+
| `auto` | Default. Automatically searches relevant memories and also includes refs selected with `/memory-inject`. |
|
|
296
|
+
| `manual` | Disables automatic search-based injection. Only refs selected with `/memory-inject` are injected. |
|
|
297
|
+
|
|
298
|
+
For manual-only memory, keep `enabled: true` and set `injectionMode: "manual"`.
|
|
299
|
+
|
|
254
300
|
## Deferred (Future Slices)
|
|
255
301
|
|
|
256
302
|
- LLM extraction (decisions, preferences, tasks)
|
package/package.json
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-memory-stone",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Global pi extension: preserves and retrieves useful memory across pi sessions",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"keywords": ["pi-package"],
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
7
13
|
"pi": {
|
|
8
14
|
"extensions": ["./src/index.ts"]
|
|
9
15
|
},
|
package/src/commands/index.ts
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Commands: /memory-status, /memory-search, /memory-last
|
|
2
|
+
* Commands: /memory-status, /memory-search, /memory-open, /memory-inject, /memory-mode, /memory-last,
|
|
3
|
+
* /memory-export, /memory-import, /memory-backup
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { getStats, getLastInjection } from "../db/index.js";
|
|
7
|
+
import { getStats, getLastInjection, getRecord } from "../db/index.js";
|
|
7
8
|
import { retrieve } from "../retrieval/index.js";
|
|
8
9
|
import { getProjectId, getConfig } from "../config/index.js";
|
|
10
|
+
import {
|
|
11
|
+
backupMemoryDatabase,
|
|
12
|
+
defaultPortablePath,
|
|
13
|
+
importMemoryJsonFile,
|
|
14
|
+
resolvePortablePath,
|
|
15
|
+
writeMemoryExport,
|
|
16
|
+
type ExportFormat,
|
|
17
|
+
} from "../portable/index.js";
|
|
18
|
+
import {
|
|
19
|
+
INJECTION_MODE_ENTRY,
|
|
20
|
+
MANUAL_INJECTION_ENTRY,
|
|
21
|
+
SESSION_TOGGLE_ENTRY,
|
|
22
|
+
getMemorySessionState,
|
|
23
|
+
isInjectionMode,
|
|
24
|
+
isRecordVisibleInProject,
|
|
25
|
+
parseRefArgs,
|
|
26
|
+
} from "../session-state/index.js";
|
|
9
27
|
|
|
10
28
|
export function registerCommands(pi: ExtensionAPI): void {
|
|
11
29
|
// ── /memory-status ──────────────────────────────────────────────
|
|
@@ -40,6 +58,70 @@ export function registerCommands(pi: ExtensionAPI): void {
|
|
|
40
58
|
},
|
|
41
59
|
});
|
|
42
60
|
|
|
61
|
+
// ── /memory-open ────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
pi.registerCommand("memory-open", {
|
|
64
|
+
description: "Open a specific memory record by reference ID",
|
|
65
|
+
handler: async (args, ctx) => {
|
|
66
|
+
await handleMemoryOpen(args, ctx);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
pi.registerCommand("stone-open", {
|
|
71
|
+
description: "Alias for /memory-open",
|
|
72
|
+
handler: async (args, ctx) => {
|
|
73
|
+
await handleMemoryOpen(args, ctx);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── /memory-inject / /memory-clear-injected ────────────────────
|
|
78
|
+
|
|
79
|
+
pi.registerCommand("memory-inject", {
|
|
80
|
+
description: "Manually inject specific memory refs into future turns",
|
|
81
|
+
handler: async (args, ctx) => {
|
|
82
|
+
await handleMemoryInject(args, ctx, pi);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
pi.registerCommand("stone-inject", {
|
|
87
|
+
description: "Alias for /memory-inject",
|
|
88
|
+
handler: async (args, ctx) => {
|
|
89
|
+
await handleMemoryInject(args, ctx, pi);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
pi.registerCommand("memory-clear-injected", {
|
|
94
|
+
description: "Clear manually injected memory refs for this session",
|
|
95
|
+
handler: async (_args, ctx) => {
|
|
96
|
+
pi.appendEntry(MANUAL_INJECTION_ENTRY, { action: "clear" });
|
|
97
|
+
ctx.ui.notify("Cleared manually injected memory refs for this session", "info");
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
pi.registerCommand("stone-clear-injected", {
|
|
102
|
+
description: "Alias for /memory-clear-injected",
|
|
103
|
+
handler: async (_args, ctx) => {
|
|
104
|
+
pi.appendEntry(MANUAL_INJECTION_ENTRY, { action: "clear" });
|
|
105
|
+
ctx.ui.notify("Cleared manually injected memory refs for this session", "info");
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ── /memory-mode ────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
pi.registerCommand("memory-mode", {
|
|
112
|
+
description: "Set memory injection mode for this session: auto or manual",
|
|
113
|
+
handler: async (args, ctx) => {
|
|
114
|
+
await handleMemoryMode(args, ctx, pi);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
pi.registerCommand("stone-mode", {
|
|
119
|
+
description: "Alias for /memory-mode",
|
|
120
|
+
handler: async (args, ctx) => {
|
|
121
|
+
await handleMemoryMode(args, ctx, pi);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
43
125
|
// ── /memory-last ────────────────────────────────────────────────
|
|
44
126
|
|
|
45
127
|
pi.registerCommand("memory-last", {
|
|
@@ -72,6 +154,50 @@ export function registerCommands(pi: ExtensionAPI): void {
|
|
|
72
154
|
},
|
|
73
155
|
});
|
|
74
156
|
|
|
157
|
+
// ── /memory-export / /memory-import / /memory-backup ───────────
|
|
158
|
+
|
|
159
|
+
pi.registerCommand("memory-export", {
|
|
160
|
+
description: "Export active memory records to JSON or Markdown",
|
|
161
|
+
handler: async (args, ctx) => {
|
|
162
|
+
await handleMemoryExport(args, ctx);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
pi.registerCommand("stone-export", {
|
|
167
|
+
description: "Alias for /memory-export",
|
|
168
|
+
handler: async (args, ctx) => {
|
|
169
|
+
await handleMemoryExport(args, ctx);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
pi.registerCommand("memory-import", {
|
|
174
|
+
description: "Import memory records from a JSON export",
|
|
175
|
+
handler: async (args, ctx) => {
|
|
176
|
+
await handleMemoryImport(args, ctx);
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
pi.registerCommand("stone-import", {
|
|
181
|
+
description: "Alias for /memory-import",
|
|
182
|
+
handler: async (args, ctx) => {
|
|
183
|
+
await handleMemoryImport(args, ctx);
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
pi.registerCommand("memory-backup", {
|
|
188
|
+
description: "Copy the SQLite memory database to a timestamped backup file",
|
|
189
|
+
handler: async (args, ctx) => {
|
|
190
|
+
await handleMemoryBackup(args, ctx);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
pi.registerCommand("stone-backup", {
|
|
195
|
+
description: "Alias for /memory-backup",
|
|
196
|
+
handler: async (args, ctx) => {
|
|
197
|
+
await handleMemoryBackup(args, ctx);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
75
201
|
// ── /memory-on / /memory-off ────────────────────────────────────
|
|
76
202
|
|
|
77
203
|
pi.registerCommand("memory-on", {
|
|
@@ -79,7 +205,7 @@ export function registerCommands(pi: ExtensionAPI): void {
|
|
|
79
205
|
handler: async (_args, ctx) => {
|
|
80
206
|
ctx.ui.notify("Memory injection enabled for this session", "info");
|
|
81
207
|
// Session toggle — stored in extension state
|
|
82
|
-
pi.appendEntry(
|
|
208
|
+
pi.appendEntry(SESSION_TOGGLE_ENTRY, { enabled: true });
|
|
83
209
|
},
|
|
84
210
|
});
|
|
85
211
|
|
|
@@ -87,7 +213,7 @@ export function registerCommands(pi: ExtensionAPI): void {
|
|
|
87
213
|
description: "Disable memory injection for this session",
|
|
88
214
|
handler: async (_args, ctx) => {
|
|
89
215
|
ctx.ui.notify("Memory injection disabled for this session", "info");
|
|
90
|
-
pi.appendEntry(
|
|
216
|
+
pi.appendEntry(SESSION_TOGGLE_ENTRY, { enabled: false });
|
|
91
217
|
},
|
|
92
218
|
});
|
|
93
219
|
}
|
|
@@ -119,7 +245,11 @@ async function handleMemoryStatus(args: string, ctx: ExtensionCommandContext): P
|
|
|
119
245
|
lines.push(` enabled: ${config.enabled}`);
|
|
120
246
|
lines.push(` maxInjectedRecords: ${config.maxInjectedRecords}`);
|
|
121
247
|
lines.push(` maxInjectedTokens: ${config.maxInjectedTokens}`);
|
|
248
|
+
const sessionState = getMemorySessionState(ctx.sessionManager.getBranch());
|
|
122
249
|
lines.push(` crossProjectEnabled: ${config.crossProjectEnabled}`);
|
|
250
|
+
lines.push(` injectionMode: ${config.injectionMode}`);
|
|
251
|
+
lines.push(` effectiveInjectionMode: ${sessionState.injectionMode ?? config.injectionMode}`);
|
|
252
|
+
lines.push(` manuallyInjectedRefs: ${sessionState.manualRefs.length > 0 ? sessionState.manualRefs.join(", ") : "none"}`);
|
|
123
253
|
|
|
124
254
|
if (ctx.hasUI) {
|
|
125
255
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
@@ -169,6 +299,102 @@ async function handleMemorySearch(args: string, ctx: ExtensionCommandContext): P
|
|
|
169
299
|
}
|
|
170
300
|
}
|
|
171
301
|
|
|
302
|
+
async function handleMemoryOpen(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
303
|
+
const refId = args?.trim().split(/\s+/).filter(Boolean)[0];
|
|
304
|
+
|
|
305
|
+
if (!refId) {
|
|
306
|
+
ctx.ui.notify("Usage: /memory-open <ref-id>", "warning");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const record = getRecord(refId);
|
|
311
|
+
if (!record) {
|
|
312
|
+
ctx.ui.notify(`Memory record ${refId} not found.`, "warning");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const currentProjectId = getProjectId(ctx.cwd);
|
|
317
|
+
if (record.status !== "active" || !isRecordVisibleInProject(record, currentProjectId)) {
|
|
318
|
+
ctx.ui.notify(`Memory record ${refId} is not available.`, "warning");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const lines: string[] = [];
|
|
323
|
+
lines.push(`Memory Record: ${record.id}`);
|
|
324
|
+
lines.push(`Kind: ${record.kind}`);
|
|
325
|
+
lines.push(`Scope: ${record.scope}`);
|
|
326
|
+
lines.push(`Project: ${record.project_id ?? "global"}`);
|
|
327
|
+
lines.push(`Created: ${new Date(record.created_at).toISOString()}`);
|
|
328
|
+
lines.push(`Status: ${record.status}`);
|
|
329
|
+
lines.push("");
|
|
330
|
+
lines.push(record.text);
|
|
331
|
+
|
|
332
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function handleMemoryInject(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
336
|
+
const refIds = parseRefArgs(args);
|
|
337
|
+
|
|
338
|
+
if (refIds.length === 0) {
|
|
339
|
+
ctx.ui.notify("Usage: /memory-inject <ref-id> [ref-id ...]", "warning");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const currentProjectId = getProjectId(ctx.cwd);
|
|
344
|
+
const acceptedRefs: string[] = [];
|
|
345
|
+
const rejectedRefs: string[] = [];
|
|
346
|
+
|
|
347
|
+
for (const refId of refIds) {
|
|
348
|
+
const record = getRecord(refId);
|
|
349
|
+
if (record && record.status === "active" && isRecordVisibleInProject(record, currentProjectId)) {
|
|
350
|
+
acceptedRefs.push(refId);
|
|
351
|
+
} else {
|
|
352
|
+
rejectedRefs.push(refId);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (acceptedRefs.length > 0) {
|
|
357
|
+
pi.appendEntry(MANUAL_INJECTION_ENTRY, { action: "add", refs: acceptedRefs });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const lines: string[] = [];
|
|
361
|
+
if (acceptedRefs.length > 0) {
|
|
362
|
+
lines.push(`Manually injected memory refs for this session: ${acceptedRefs.join(", ")}`);
|
|
363
|
+
}
|
|
364
|
+
if (rejectedRefs.length > 0) {
|
|
365
|
+
lines.push(`Unavailable refs skipped: ${rejectedRefs.join(", ")}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
ctx.ui.notify(lines.join("\n") || "No memory refs were injected.", acceptedRefs.length > 0 ? "info" : "warning");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function handleMemoryMode(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
372
|
+
const mode = args?.trim().split(/\s+/).filter(Boolean)[0];
|
|
373
|
+
const config = getConfig(ctx.cwd);
|
|
374
|
+
const sessionState = getMemorySessionState(ctx.sessionManager.getBranch());
|
|
375
|
+
|
|
376
|
+
if (!mode) {
|
|
377
|
+
ctx.ui.notify(
|
|
378
|
+
`Memory injection mode: ${sessionState.injectionMode ?? config.injectionMode}\nUsage: /memory-mode <auto|manual>`,
|
|
379
|
+
"info",
|
|
380
|
+
);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!isInjectionMode(mode)) {
|
|
385
|
+
ctx.ui.notify("Usage: /memory-mode <auto|manual>", "warning");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
pi.appendEntry(INJECTION_MODE_ENTRY, { mode });
|
|
390
|
+
ctx.ui.notify(
|
|
391
|
+
mode === "manual"
|
|
392
|
+
? "Memory injection mode set to manual for this session. Use /memory-inject <ref-id> to choose memories."
|
|
393
|
+
: "Memory injection mode set to auto for this session.",
|
|
394
|
+
"info",
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
172
398
|
async function handleMemoryLast(ctx: ExtensionCommandContext): Promise<void> {
|
|
173
399
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
174
400
|
const last = getLastInjection(sessionId);
|
|
@@ -195,6 +421,66 @@ async function handleMemoryLast(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
195
421
|
}
|
|
196
422
|
}
|
|
197
423
|
|
|
424
|
+
async function handleMemoryExport(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
425
|
+
const parsed = parseCommandArgs(args);
|
|
426
|
+
const formatValue = parsed.options.get("format") ?? (parsed.flags.has("md") ? "md" : "json");
|
|
427
|
+
if (formatValue !== "json" && formatValue !== "md") {
|
|
428
|
+
ctx.ui.notify("Usage: /memory-export [path] [--format json|md] [--all]", "warning");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const format = formatValue as ExportFormat;
|
|
433
|
+
const outputPath = resolvePortablePath(
|
|
434
|
+
ctx.cwd,
|
|
435
|
+
parsed.positionals[0] ?? defaultPortablePath(ctx.cwd, "memory-export", format),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const count = writeMemoryExport(outputPath, format, parsed.flags.has("all"));
|
|
440
|
+
ctx.ui.notify(`Exported ${count} memory records to ${outputPath}`, "info");
|
|
441
|
+
} catch (err) {
|
|
442
|
+
ctx.ui.notify(`Memory export failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function handleMemoryImport(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
447
|
+
const parsed = parseCommandArgs(args);
|
|
448
|
+
const inputPathArg = parsed.positionals[0];
|
|
449
|
+
if (!inputPathArg) {
|
|
450
|
+
ctx.ui.notify("Usage: /memory-import <memory-export.json> [--preserve-project|--global]", "warning");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const inputPath = resolvePortablePath(ctx.cwd, inputPathArg);
|
|
455
|
+
try {
|
|
456
|
+
const result = importMemoryJsonFile(inputPath, {
|
|
457
|
+
projectId: parsed.flags.has("preserve-project") ? undefined : getProjectId(ctx.cwd),
|
|
458
|
+
scopeOverride: parsed.flags.has("global") ? "global" : undefined,
|
|
459
|
+
});
|
|
460
|
+
ctx.ui.notify(
|
|
461
|
+
`Imported ${result.imported} memory records from ${inputPath}${result.skipped ? ` (${result.skipped} skipped)` : ""}`,
|
|
462
|
+
"info",
|
|
463
|
+
);
|
|
464
|
+
} catch (err) {
|
|
465
|
+
ctx.ui.notify(`Memory import failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function handleMemoryBackup(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
470
|
+
const parsed = parseCommandArgs(args);
|
|
471
|
+
const outputPath = resolvePortablePath(
|
|
472
|
+
ctx.cwd,
|
|
473
|
+
parsed.positionals[0] ?? defaultPortablePath(ctx.cwd, "memory-backup", "db"),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
backupMemoryDatabase(outputPath);
|
|
478
|
+
ctx.ui.notify(`Backed up memory database to ${outputPath}`, "info");
|
|
479
|
+
} catch (err) {
|
|
480
|
+
ctx.ui.notify(`Memory backup failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
198
484
|
async function handleMemoryForget(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
199
485
|
const { softForgetRecord, hardDeleteRecord, getRecord } = await import("../db/index.js");
|
|
200
486
|
|
|
@@ -232,3 +518,39 @@ async function handleMemoryForget(args: string, ctx: ExtensionCommandContext): P
|
|
|
232
518
|
}
|
|
233
519
|
}
|
|
234
520
|
}
|
|
521
|
+
|
|
522
|
+
function parseCommandArgs(args: string): {
|
|
523
|
+
flags: Set<string>;
|
|
524
|
+
options: Map<string, string>;
|
|
525
|
+
positionals: string[];
|
|
526
|
+
} {
|
|
527
|
+
const flags = new Set<string>();
|
|
528
|
+
const options = new Map<string, string>();
|
|
529
|
+
const positionals: string[] = [];
|
|
530
|
+
const parts = args?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
531
|
+
|
|
532
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
533
|
+
const part = parts[i];
|
|
534
|
+
if (!part.startsWith("--")) {
|
|
535
|
+
positionals.push(part);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const withoutPrefix = part.slice(2);
|
|
540
|
+
const eqIndex = withoutPrefix.indexOf("=");
|
|
541
|
+
if (eqIndex >= 0) {
|
|
542
|
+
options.set(withoutPrefix.slice(0, eqIndex), withoutPrefix.slice(eqIndex + 1));
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const next = parts[i + 1];
|
|
547
|
+
if (next && !next.startsWith("--") && ["format"].includes(withoutPrefix)) {
|
|
548
|
+
options.set(withoutPrefix, next);
|
|
549
|
+
i += 1;
|
|
550
|
+
} else {
|
|
551
|
+
flags.add(withoutPrefix);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return { flags, options, positionals };
|
|
556
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { execSync } from "node:child_process";
|
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
+
import type { InjectionMode } from "../session-state/index.js";
|
|
11
12
|
|
|
12
13
|
// ─── Project identity ───────────────────────────────────────────────
|
|
13
14
|
|
|
@@ -51,6 +52,8 @@ export interface MemoryConfig {
|
|
|
51
52
|
scoreThreshold: number;
|
|
52
53
|
/** Whether cross-project injection is enabled */
|
|
53
54
|
crossProjectEnabled: boolean;
|
|
55
|
+
/** Search-based automatic injection, or only explicit /memory-inject refs */
|
|
56
|
+
injectionMode: InjectionMode;
|
|
54
57
|
/** Extra ignore patterns */
|
|
55
58
|
ignorePatterns: string[];
|
|
56
59
|
}
|
|
@@ -61,6 +64,7 @@ const DEFAULT_CONFIG: MemoryConfig = {
|
|
|
61
64
|
maxInjectedTokens: 1000,
|
|
62
65
|
scoreThreshold: 0.3,
|
|
63
66
|
crossProjectEnabled: false,
|
|
67
|
+
injectionMode: "auto",
|
|
64
68
|
ignorePatterns: [],
|
|
65
69
|
};
|
|
66
70
|
|
|
@@ -80,6 +84,9 @@ export function getConfig(cwd: string = process.cwd()): MemoryConfig {
|
|
|
80
84
|
const settings = JSON.parse(raw);
|
|
81
85
|
if (settings.memory) {
|
|
82
86
|
const config = { ...DEFAULT_CONFIG, ...settings.memory };
|
|
87
|
+
if (config.injectionMode !== "auto" && config.injectionMode !== "manual") {
|
|
88
|
+
config.injectionMode = DEFAULT_CONFIG.injectionMode;
|
|
89
|
+
}
|
|
83
90
|
_configCache.set(cacheKey, config);
|
|
84
91
|
return config;
|
|
85
92
|
}
|
package/src/db/index.ts
CHANGED
|
@@ -242,11 +242,15 @@ export function upsertRecord(record: {
|
|
|
242
242
|
status?: RecordStatus;
|
|
243
243
|
confidence?: number;
|
|
244
244
|
importance?: number;
|
|
245
|
+
created_at?: number;
|
|
246
|
+
updated_at?: number;
|
|
245
247
|
superseded_by?: string | null;
|
|
246
248
|
derived_from_memory_refs?: string | null;
|
|
247
249
|
}): string {
|
|
248
250
|
const db = getDb();
|
|
249
251
|
const now = Date.now();
|
|
252
|
+
const createdAt = record.created_at ?? now;
|
|
253
|
+
const updatedAt = record.updated_at ?? now;
|
|
250
254
|
const scope = record.scope ?? "project";
|
|
251
255
|
const projectId = scope === "global" ? null : (record.project_id ?? null);
|
|
252
256
|
const redactedText = redactSecrets(record.text);
|
|
@@ -276,7 +280,7 @@ export function upsertRecord(record: {
|
|
|
276
280
|
redactedTags,
|
|
277
281
|
record.confidence ?? 1.0,
|
|
278
282
|
record.importance ?? 0.5,
|
|
279
|
-
|
|
283
|
+
updatedAt,
|
|
280
284
|
record.status ?? existing.status,
|
|
281
285
|
record.superseded_by ?? null,
|
|
282
286
|
record.derived_from_memory_refs ?? null,
|
|
@@ -303,8 +307,8 @@ export function upsertRecord(record: {
|
|
|
303
307
|
record.status ?? "active",
|
|
304
308
|
record.confidence ?? 1.0,
|
|
305
309
|
record.importance ?? 0.5,
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
createdAt,
|
|
311
|
+
updatedAt,
|
|
308
312
|
record.superseded_by ?? null,
|
|
309
313
|
record.derived_from_memory_refs ?? null,
|
|
310
314
|
);
|
|
@@ -331,6 +335,14 @@ export function getRecord(id: string): RecordRow | undefined {
|
|
|
331
335
|
);
|
|
332
336
|
}
|
|
333
337
|
|
|
338
|
+
export function listRecords(options: { includeInactive?: boolean } = {}): RecordRow[] {
|
|
339
|
+
const db = getDb();
|
|
340
|
+
const sql = options.includeInactive
|
|
341
|
+
? "SELECT * FROM records ORDER BY created_at ASC, id ASC"
|
|
342
|
+
: "SELECT * FROM records WHERE status = 'active' ORDER BY created_at ASC, id ASC";
|
|
343
|
+
return db.prepare(sql).all() as unknown as RecordRow[];
|
|
344
|
+
}
|
|
345
|
+
|
|
334
346
|
export function searchRecordsFts(
|
|
335
347
|
query: string,
|
|
336
348
|
limit = 20,
|