pi-memory-stone 0.1.2 → 0.1.4

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 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/ # 44 tests across 4 suites
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
- - Searches records via FTS5
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) as system prompt context
75
- - Tracks injected refs to prevent feedback loops
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
- 44 tests across 4 suites:
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,17 +1,19 @@
1
1
  {
2
2
  "name": "pi-memory-stone",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Global pi extension: preserves and retrieves useful memory across pi sessions",
6
6
  "license": "MIT",
7
7
  "keywords": ["pi-package"],
8
8
  "files": [
9
9
  "src",
10
+ "skills/**/*",
10
11
  "README.md",
11
12
  "LICENSE"
12
13
  ],
13
14
  "pi": {
14
- "extensions": ["./src/index.ts"]
15
+ "extensions": ["./src/index.ts"],
16
+ "skills": ["./skills"]
15
17
  },
16
18
  "scripts": {
17
19
  "test": "NODE_OPTIONS='--experimental-sqlite' tsx --test test/*.test.ts",
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: pi-memory-stone
3
+ description: Persistent cross-session memory for pi via SQLite+FTS5. Remembers past decisions, preferences, turns, and error resolutions across sessions. Use when recalling context from prior sessions, using memory_search/memory_open/memory_remember/memory_forget tools, troubleshooting memory injection, or when user asks about pi-memory-stone, memory stone, session memory, or remembering context.
4
+ ---
5
+
6
+ # Pi Memory Stone
7
+
8
+ pi-memory-stone indexes session turns into a searchable SQLite+FTS5 database (`~/.pi/agent/memory/memory.db`) and auto-injects relevant past memories into the system prompt before each agent turn.
9
+
10
+ **Quick check:** `/memory-status` and `/memory-search <query>`.
11
+
12
+ ## Tools
13
+
14
+ ### `memory_search(query, kind?, scope?, limit?)` — find past context
15
+
16
+ Use **before** answering questions that may have been addressed in past sessions. FTS5-based: include concrete keywords, not abstract concepts. Use `kind` filter for precision: `"decision"`, `"error_resolution"`, `"preference"`, `"task"`, `"turn_summary"`, `"session_summary"`. Use `scope: "global"` for cross-project recall.
17
+
18
+ **Examples:** `memory_search("database schema user authentication")`, `memory_search("Next.js build error", kind: "error_resolution")`, `memory_search("deployment workflow", scope: "global")`.
19
+
20
+ ### `memory_open(ref)` — full record by ID
21
+
22
+ Use when an injection packet or search result references a record ID (e.g., `ref=abc123`). Returns full text and metadata. Only shows records visible in current project.
23
+
24
+ ### `memory_remember(kind, text, scope?, tags?, importance?)` — explicit storage
25
+
26
+ Use **only when the user explicitly asks** to remember something. Defaults to `scope: "project"`. Use `scope: "global"` only when the user says "for all projects". Auto-downgrades to project scope if text appears to contain secrets, hostnames, or internal details.
27
+
28
+ ### `memory_forget(ref, hard?)` — remove stale/wrong memories
29
+
30
+ Soft-forget hides from future searches. Hard delete (`hard: true`) requires user confirmation via the `/memory-forget --hard` command.
31
+
32
+ ## Ranking (craft better queries)
33
+
34
+ Hybrid score: FTS match × kind boost × recency decay × confidence × importance, then sorted descending.
35
+
36
+ | Factor | Boost | Implication |
37
+ |---|---|---|
38
+ | Same-project | ×1.5 | Current project memories rank highest |
39
+ | Kind: decision | ×1.5 | Decisions and errors surface prominently |
40
+ | Kind: error_resolution | ×1.4 | |
41
+ | Kind: preference | ×1.3 | |
42
+ | Recency | Half-life 7 days | Older memories fade; add time hints to queries |
43
+ | Confidence × Importance | 0.5–1.5× | Explicitly remembered items outrank auto-indexed turns |
44
+
45
+ Queries include the user's first ~200 chars + basenames of recently touched files. Include filenames when searching for file-specific context.
46
+
47
+ ## Injection modes
48
+
49
+ | Mode | Behavior |
50
+ |---|---|
51
+ | `auto` (default) | FTS5 search every turn + manual refs. Score threshold: 0.3. Auto-injected refs not re-injected (anti-feedback-loop). |
52
+ | `manual` | Only `/memory-inject` refs. Inject every turn until `/memory-clear-injected`. |
53
+
54
+ Session override (`/memory-mode`) takes precedence over config. Check: `/memory-status`.
55
+
56
+ ## Session state (per-session, persists across turns)
57
+
58
+ - **enabled** — toggle via `/memory-on` / `/memory-off`
59
+ - **injectionMode** — `auto` or `manual`, via `/memory-mode`, falls back to config
60
+ - **manualRefs** — via `/memory-inject <ref>`, cleared by `/memory-clear-injected`
61
+
62
+ ## Commands quick reference
63
+
64
+ | Command | Purpose |
65
+ |---|---|
66
+ | `/memory-status [-v]` | Stats, config, effective mode |
67
+ | `/memory-search <q>` | Search (top 20) |
68
+ | `/memory-open <ref>` | Full record |
69
+ | `/memory-inject <ref>...` | Add manual refs for session |
70
+ | `/memory-clear-injected` | Clear manual refs |
71
+ | `/memory-mode <auto\|manual>` | Override injection mode |
72
+ | `/memory-last` | Last injection packet |
73
+ | `/memory-forget <ref> [--hard]` | Soft/hard delete |
74
+ | `/memory-export [path] [--format json\|md] [--all]` | Portable export |
75
+ | `/memory-import <file> [--preserve-project\|--global]` | Import (default: remap to current project) |
76
+ | `/memory-backup [path]` | Backup SQLite DB |
77
+ | `/memory-on` / `/memory-off` | Enable/disable injection |
78
+
79
+ ## Troubleshooting
80
+
81
+ | Problem | Likely cause | Fix |
82
+ |---|---|---|
83
+ | No memories injected | Score < 0.3 threshold, manual mode, or `/memory-off` | Lower threshold, `/memory-mode auto`, or `/memory-on` |
84
+ | Cross-project memories missing | `crossProjectEnabled: false` in config | Enable in `.pi/settings.json` or `/memory-inject` manually |
85
+ | Search returns nothing | Keywords don't match FTS5, or record soft_forgotten | Use concrete terms; try `/memory-export --all` to find |
86
+ | Global `memory_remember` refused | Text matched as sensitive | The tool auto-downgrades safely; this is expected |
87
+
88
+ ## Config
89
+
90
+ `.pi/settings.json` in project root (all fields optional, shown with defaults):
91
+
92
+ ```json
93
+ { "memory": { "enabled": true, "maxInjectedRecords": 5, "maxInjectedTokens": 1000, "scoreThreshold": 0.3, "crossProjectEnabled": false, "injectionMode": "auto" } }
94
+ ```
95
+
96
+ ## Privacy guarantees
97
+
98
+ - All text redacted before storage: API keys, tokens, passwords, connection strings
99
+ - Sensitive paths never indexed: `.env`, keys, certs, `node_modules`, `.git`, `.ssh`, `.aws`
100
+ - Global scope refused for text containing secret/hostname/internal patterns
101
+ - `memory_open` only shows records visible in current project
@@ -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("memory-stone:session-toggle", { enabled: true });
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("memory-stone:session-toggle", { enabled: false });
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
 
@@ -214,6 +500,12 @@ async function handleMemoryForget(args: string, ctx: ExtensionCommandContext): P
214
500
  continue;
215
501
  }
216
502
 
503
+ const currentProjectId = getProjectId(ctx.cwd);
504
+ if (record.status !== "active" || !isRecordVisibleInProject(record, currentProjectId)) {
505
+ ctx.ui.notify(`Memory record ${refId} is not available.`, "warning");
506
+ continue;
507
+ }
508
+
217
509
  if (hardFlag) {
218
510
  const confirmed = ctx.hasUI
219
511
  ? await ctx.ui.confirm(
@@ -232,3 +524,39 @@ async function handleMemoryForget(args: string, ctx: ExtensionCommandContext): P
232
524
  }
233
525
  }
234
526
  }
527
+
528
+ function parseCommandArgs(args: string): {
529
+ flags: Set<string>;
530
+ options: Map<string, string>;
531
+ positionals: string[];
532
+ } {
533
+ const flags = new Set<string>();
534
+ const options = new Map<string, string>();
535
+ const positionals: string[] = [];
536
+ const parts = args?.trim().split(/\s+/).filter(Boolean) ?? [];
537
+
538
+ for (let i = 0; i < parts.length; i += 1) {
539
+ const part = parts[i];
540
+ if (!part.startsWith("--")) {
541
+ positionals.push(part);
542
+ continue;
543
+ }
544
+
545
+ const withoutPrefix = part.slice(2);
546
+ const eqIndex = withoutPrefix.indexOf("=");
547
+ if (eqIndex >= 0) {
548
+ options.set(withoutPrefix.slice(0, eqIndex), withoutPrefix.slice(eqIndex + 1));
549
+ continue;
550
+ }
551
+
552
+ const next = parts[i + 1];
553
+ if (next && !next.startsWith("--") && ["format"].includes(withoutPrefix)) {
554
+ options.set(withoutPrefix, next);
555
+ i += 1;
556
+ } else {
557
+ flags.add(withoutPrefix);
558
+ }
559
+ }
560
+
561
+ return { flags, options, positionals };
562
+ }
@@ -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
  }