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 +53 -7
- package/package.json +4 -2
- package/skills/pi-memory-stone/SKILL.md +101 -0
- package/src/commands/index.ts +332 -4
- package/src/config/index.ts +7 -0
- package/src/db/index.ts +44 -6
- package/src/index.ts +34 -51
- package/src/portable/index.ts +217 -0
- package/src/privacy/index.ts +19 -3
- package/src/retrieval/index.ts +12 -4
- package/src/session-state/index.ts +89 -0
- package/src/tools/index.ts +17 -12
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,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-memory-stone",
|
|
3
|
-
"version": "0.1.
|
|
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
|
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
|
|
|
@@ -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
|
+
}
|
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
|
}
|