pi-memory-stone 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/package.json +23 -0
- package/src/commands/index.ts +234 -0
- package/src/config/index.ts +108 -0
- package/src/db/index.ts +620 -0
- package/src/db/schema.ts +161 -0
- package/src/index.ts +197 -0
- package/src/indexing/index.ts +207 -0
- package/src/indexing/parser.ts +374 -0
- package/src/privacy/index.ts +167 -0
- package/src/retrieval/index.ts +219 -0
- package/src/tools/index.ts +257 -0
- package/test/indexing.test.ts +97 -0
- package/test/parser.test.ts +261 -0
- package/test/privacy.test.ts +120 -0
- package/test/ranking.test.ts +403 -0
- package/tsconfig.json +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pi-memory-stone
|
|
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,262 @@
|
|
|
1
|
+
# pi-memory-stone
|
|
2
|
+
|
|
3
|
+
A global pi extension that preserves and retrieves useful memory across pi sessions. Raw pi session JSONL files remain the source of truth; the extension builds a searchable, privacy-safe SQLite+FTS5 index with backreferences to exact session entries.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
**MVP vertical slice** — deterministic indexing, FTS5 search, conservative injection, commands, tools, and tests are complete. LLM extraction, embeddings, and historical backfill are deferred.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
Install globally as a pi package:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi install git:github.com/nikolasp/pi-memory-stone
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or install manually in pi's global extension directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/nikolasp/pi-memory-stone ~/.pi/agent/extensions/pi-memory-stone
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then restart pi or run `/reload`, and verify it is active:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
/memory-status
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> Scope note: `pi install -l ...` / `pi install --local ...` writes to the current project's `.pi/settings.json` and only loads there. For all projects, run `pi install git:github.com/nikolasp/pi-memory-stone` without `--local` (user settings) or use `~/.pi/agent/extensions/`.
|
|
30
|
+
|
|
31
|
+
## Architecture
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
~/.pi/agent/memory/memory.db # SQLite + FTS5 (WAL mode)
|
|
35
|
+
~/.pi/agent/extensions/pi-memory-stone/
|
|
36
|
+
├── src/
|
|
37
|
+
│ ├── index.ts # Entry point: hooks, lifecycle
|
|
38
|
+
│ ├── db/ # SQLite connection, migrations, CRUD
|
|
39
|
+
│ ├── indexing/ # Deterministic JSONL parser, agent_end handler
|
|
40
|
+
│ ├── retrieval/ # FTS search, hybrid ranking, injection builder
|
|
41
|
+
│ ├── commands/ # /memory-* slash commands
|
|
42
|
+
│ ├── tools/ # LLM-callable tools
|
|
43
|
+
│ ├── privacy/ # Secret redaction, sensitive path filtering
|
|
44
|
+
│ └── config/ # Project identity, settings
|
|
45
|
+
└── test/ # 44 tests across 4 suites
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## How It Works
|
|
49
|
+
|
|
50
|
+
### 1. Indexing (agent_end)
|
|
51
|
+
|
|
52
|
+
Every time the agent finishes a response, the extension:
|
|
53
|
+
|
|
54
|
+
- Reads new session entries since the last indexed position
|
|
55
|
+
- Parses turns deterministically (no LLM): extracts user prompts, assistant responses, tool calls, errors
|
|
56
|
+
- Redacts secrets (API keys, tokens, passwords, private keys) before storage
|
|
57
|
+
- Skips sensitive files (`.env`, keys, certs, `node_modules`, `.git`)
|
|
58
|
+
- Stores structured `turn_summary` and `error_resolution` records in SQLite
|
|
59
|
+
- Maintains `index_state` per session file so indexing is incremental
|
|
60
|
+
|
|
61
|
+
### 2. Retrieval (before_agent_start)
|
|
62
|
+
|
|
63
|
+
Before each agent turn, the extension:
|
|
64
|
+
|
|
65
|
+
- Builds a focused query from the user's prompt
|
|
66
|
+
- Searches records via FTS5
|
|
67
|
+
- Ranks by hybrid score: FTS match + same-project boost + recency decay + kind weight + confidence
|
|
68
|
+
- Injects top results (max 5, threshold-limited) as system prompt context
|
|
69
|
+
- Tracks injected refs to prevent feedback loops
|
|
70
|
+
- Logs every injection for audit via `/memory-last`
|
|
71
|
+
|
|
72
|
+
### 3. Storage Model
|
|
73
|
+
|
|
74
|
+
**Records:**
|
|
75
|
+
| Kind | Description |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `turn_summary` | Concatenated user prompt + assistant response + tools used |
|
|
78
|
+
| `error_resolution` | Tool errors with context |
|
|
79
|
+
| `decision` | Explicitly remembered decisions (LLM/tool) |
|
|
80
|
+
| `preference` | User preferences (LLM/tool) |
|
|
81
|
+
| `task` | Tracked tasks (LLM/tool) |
|
|
82
|
+
| `session_summary` | Session-level summary (future) |
|
|
83
|
+
|
|
84
|
+
**Scopes:**
|
|
85
|
+
| Scope | Visibility |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `project` | Visible within the same project (git repo root) |
|
|
88
|
+
| `global` | Visible across all projects (explicit opt-in only) |
|
|
89
|
+
|
|
90
|
+
**Statuses:** `active`, `soft_forgotten`, `hard_forgotten`, `superseded`
|
|
91
|
+
|
|
92
|
+
## Commands
|
|
93
|
+
|
|
94
|
+
| Command | Alias | Description |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `/memory-status` | `/stone-status` | Show index statistics, record counts by kind, config |
|
|
97
|
+
| `/memory-status --verbose` | | Include per-kind record breakdown |
|
|
98
|
+
| `/memory-search <query>` | `/stone-search` | Search memory for relevant records |
|
|
99
|
+
| `/memory-last` | `/stone-last` | Show the last memory injection packet |
|
|
100
|
+
| `/memory-forget <id>` | `/stone-forget` | Soft-forget a record (hide from searches) |
|
|
101
|
+
| `/memory-forget <id> --hard` | | Permanently delete (with confirmation) |
|
|
102
|
+
| `/memory-on` | | Enable memory injection for this session |
|
|
103
|
+
| `/memory-off` | | Disable memory injection for this session |
|
|
104
|
+
|
|
105
|
+
## Tools
|
|
106
|
+
|
|
107
|
+
### `memory_search`
|
|
108
|
+
|
|
109
|
+
Search memory stone for relevant records. Use before making decisions to recall past context.
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Parameters:
|
|
113
|
+
query Search query text
|
|
114
|
+
kind? Filter: decision | preference | task | error_resolution | turn_summary | session_summary
|
|
115
|
+
scope? Filter: project | global
|
|
116
|
+
limit? Max results (default 5)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `memory_open`
|
|
120
|
+
|
|
121
|
+
Open a specific memory record by its reference ID.
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
Parameters:
|
|
125
|
+
ref Memory record reference ID (from search results or injection packets)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `memory_remember`
|
|
129
|
+
|
|
130
|
+
Explicitly store a memory record. Only use when the user explicitly asks to remember something.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
Parameters:
|
|
134
|
+
kind Record kind: decision | preference | task | error_resolution | turn_summary | session_summary
|
|
135
|
+
text Memory text to store
|
|
136
|
+
scope? project (default) | global
|
|
137
|
+
tags? Comma-separated tags
|
|
138
|
+
importance? 0-1 (default 0.5)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `memory_forget`
|
|
142
|
+
|
|
143
|
+
Soft-forget a memory record by its reference ID. Hard deletion requires explicit user confirmation via command.
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
Parameters:
|
|
147
|
+
ref Memory record reference ID
|
|
148
|
+
hard? Request permanent deletion (requires confirmation)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Privacy & Safety
|
|
152
|
+
|
|
153
|
+
### Secret Redaction
|
|
154
|
+
|
|
155
|
+
All text is redacted before storage. Patterns covered:
|
|
156
|
+
|
|
157
|
+
- OpenAI API keys (`sk-...`)
|
|
158
|
+
- GitHub tokens (`ghp_...`, `ghs_...`)
|
|
159
|
+
- AWS access keys (`AKIA...`)
|
|
160
|
+
- JWT tokens
|
|
161
|
+
- Generic API keys and secrets in `key=value` assignments
|
|
162
|
+
- Private keys (PEM format)
|
|
163
|
+
- Password/secrets in assignments
|
|
164
|
+
- Connection strings (credentials removed)
|
|
165
|
+
|
|
166
|
+
### Path Filtering
|
|
167
|
+
|
|
168
|
+
Files at these paths are never indexed:
|
|
169
|
+
|
|
170
|
+
- `.env*`, `.envrc`
|
|
171
|
+
- Keys and certificates (`.pem`, `.key`, `.crt`)
|
|
172
|
+
- SSH keys (`id_rsa`, `id_ed25519`, `.ssh/`)
|
|
173
|
+
- AWS credentials (`~/.aws/`)
|
|
174
|
+
- GPG keys (`~/.gnupg/`)
|
|
175
|
+
- Dependency dirs (`node_modules`, `.git`, `dist`, `build`, `.next`, etc.)
|
|
176
|
+
|
|
177
|
+
### Cross-Project Safety
|
|
178
|
+
|
|
179
|
+
- Records default to project scope
|
|
180
|
+
- Cross-project memory requires explicit global flag
|
|
181
|
+
- Global promotion refuses sensitive patterns (paths, secrets, hostnames, implementation details)
|
|
182
|
+
- `memory_open` never sends raw excerpts to LLM automatically
|
|
183
|
+
|
|
184
|
+
## Ranking
|
|
185
|
+
|
|
186
|
+
Results are ranked by a hybrid score:
|
|
187
|
+
|
|
188
|
+
| Factor | Effect |
|
|
189
|
+
|---|---|
|
|
190
|
+
| FTS5 match quality | Base score (normalized reciprocal rank) |
|
|
191
|
+
| Same project | ×1.5 boost |
|
|
192
|
+
| Global scope | ×1.2 boost |
|
|
193
|
+
| Kind weight | Decision ×1.5, Preference ×1.3, Error ×1.4 |
|
|
194
|
+
| Recency | Exponential decay, half-life 7 days |
|
|
195
|
+
| Confidence | Direct multiplier |
|
|
196
|
+
| Importance | 0.5–1.5x multiplier |
|
|
197
|
+
|
|
198
|
+
## DB Schema
|
|
199
|
+
|
|
200
|
+
```sql
|
|
201
|
+
sessions — Indexed session metadata
|
|
202
|
+
records — Structured memory records (with FTS5 index)
|
|
203
|
+
record_fts — Full-text search index (contentless)
|
|
204
|
+
file_activity — File read/write/edit/bash tracking
|
|
205
|
+
injections — Audit log of memory injections
|
|
206
|
+
index_state — Per-session indexing progress
|
|
207
|
+
jobs — Background job queue
|
|
208
|
+
schema_migrations— Versioned migration tracking
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Storage: `~/.pi/agent/memory/memory.db` (SQLite, WAL mode, busy timeout 5s).
|
|
212
|
+
|
|
213
|
+
## Tests
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
cd ~/.pi/agent/extensions/pi-memory-stone
|
|
217
|
+
npm test
|
|
218
|
+
npm run typecheck
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
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`.
|
|
222
|
+
|
|
223
|
+
44 tests across 4 suites:
|
|
224
|
+
|
|
225
|
+
| Suite | Tests | Focus |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| `indexing.test.ts` | 1 | Incremental session indexing |
|
|
228
|
+
| `privacy.test.ts` | 17 | Secret redaction, sensitive path filtering |
|
|
229
|
+
| `parser.test.ts` | 10 | Turn parsing, file activity detection, error extraction |
|
|
230
|
+
| `ranking.test.ts` | 16 | Hybrid ranking, cross-project filtering, injection formatting |
|
|
231
|
+
|
|
232
|
+
## Configuration
|
|
233
|
+
|
|
234
|
+
Project settings in `.pi/settings.json`:
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"memory": {
|
|
239
|
+
"enabled": true,
|
|
240
|
+
"maxInjectedRecords": 5,
|
|
241
|
+
"maxInjectedTokens": 1000,
|
|
242
|
+
"scoreThreshold": 0.3,
|
|
243
|
+
"crossProjectEnabled": false
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Deferred (Future Slices)
|
|
249
|
+
|
|
250
|
+
- LLM extraction (decisions, preferences, tasks)
|
|
251
|
+
- Historical backfill (`/memory-backfill`)
|
|
252
|
+
- Embedding-based semantic search
|
|
253
|
+
- `/memory-edit`, `/memory-supersede`
|
|
254
|
+
- Rich TUI memory browser
|
|
255
|
+
- Multi-machine sync
|
|
256
|
+
- Daemon-mode background worker for LLM extraction
|
|
257
|
+
- `/memory-prune-missing`
|
|
258
|
+
- `.pi/memoryignore` and `~/.pi/agent/memoryignore`
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
MIT — see [LICENSE](./LICENSE) for details.
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-memory-stone",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Global pi extension: preserves and retrieves useful memory across pi sessions",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"pi": {
|
|
8
|
+
"extensions": ["./src/index.ts"]
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "NODE_OPTIONS='--experimental-sqlite' tsx --test test/*.test.ts",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"tsx": "^4.22.3",
|
|
16
|
+
"typescript": "^5.9.3"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@earendil-works/pi-ai": "*",
|
|
20
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
21
|
+
"typebox": "*"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands: /memory-status, /memory-search, /memory-last
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { getStats, getLastInjection } from "../db/index.js";
|
|
7
|
+
import { retrieve } from "../retrieval/index.js";
|
|
8
|
+
import { getProjectId, getConfig } from "../config/index.js";
|
|
9
|
+
|
|
10
|
+
export function registerCommands(pi: ExtensionAPI): void {
|
|
11
|
+
// ── /memory-status ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
pi.registerCommand("memory-status", {
|
|
14
|
+
description: "Show memory stone index statistics",
|
|
15
|
+
handler: async (args, ctx) => {
|
|
16
|
+
await handleMemoryStatus(args, ctx);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
pi.registerCommand("stone-status", {
|
|
21
|
+
description: "Alias for /memory-status",
|
|
22
|
+
handler: async (args, ctx) => {
|
|
23
|
+
await handleMemoryStatus(args, ctx);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ── /memory-search ──────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
pi.registerCommand("memory-search", {
|
|
30
|
+
description: "Search memory stone for relevant records",
|
|
31
|
+
handler: async (args, ctx) => {
|
|
32
|
+
await handleMemorySearch(args, ctx);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
pi.registerCommand("stone-search", {
|
|
37
|
+
description: "Alias for /memory-search",
|
|
38
|
+
handler: async (args, ctx) => {
|
|
39
|
+
await handleMemorySearch(args, ctx);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── /memory-last ────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
pi.registerCommand("memory-last", {
|
|
46
|
+
description: "Show the last memory injection packet",
|
|
47
|
+
handler: async (_args, ctx) => {
|
|
48
|
+
await handleMemoryLast(ctx);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
pi.registerCommand("stone-last", {
|
|
53
|
+
description: "Alias for /memory-last",
|
|
54
|
+
handler: async (_args, ctx) => {
|
|
55
|
+
await handleMemoryLast(ctx);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── /memory-forget ──────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
pi.registerCommand("memory-forget", {
|
|
62
|
+
description: "Soft-forget a memory record by ID. Use --hard for permanent deletion.",
|
|
63
|
+
handler: async (args, ctx) => {
|
|
64
|
+
await handleMemoryForget(args, ctx);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
pi.registerCommand("stone-forget", {
|
|
69
|
+
description: "Alias for /memory-forget",
|
|
70
|
+
handler: async (args, ctx) => {
|
|
71
|
+
await handleMemoryForget(args, ctx);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── /memory-on / /memory-off ────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
pi.registerCommand("memory-on", {
|
|
78
|
+
description: "Enable memory injection for this session",
|
|
79
|
+
handler: async (_args, ctx) => {
|
|
80
|
+
ctx.ui.notify("Memory injection enabled for this session", "info");
|
|
81
|
+
// Session toggle — stored in extension state
|
|
82
|
+
pi.appendEntry("memory-stone:session-toggle", { enabled: true });
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
pi.registerCommand("memory-off", {
|
|
87
|
+
description: "Disable memory injection for this session",
|
|
88
|
+
handler: async (_args, ctx) => {
|
|
89
|
+
ctx.ui.notify("Memory injection disabled for this session", "info");
|
|
90
|
+
pi.appendEntry("memory-stone:session-toggle", { enabled: false });
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Handler implementations ────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
async function handleMemoryStatus(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
98
|
+
const verbose = args.includes("--verbose") || args.includes("-v");
|
|
99
|
+
const stats = getStats();
|
|
100
|
+
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
lines.push("📊 Memory Stone Status");
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push(` Total active records: ${stats.totalRecords}`);
|
|
105
|
+
lines.push(` Indexed sessions: ${stats.totalSessions}`);
|
|
106
|
+
lines.push(` File activity entries: ${stats.totalFileActivity}`);
|
|
107
|
+
lines.push("");
|
|
108
|
+
|
|
109
|
+
if (verbose && Object.keys(stats.recordsByKind).length > 0) {
|
|
110
|
+
lines.push(" Records by kind:");
|
|
111
|
+
for (const [kind, count] of Object.entries(stats.recordsByKind)) {
|
|
112
|
+
lines.push(` ${kind}: ${count}`);
|
|
113
|
+
}
|
|
114
|
+
lines.push("");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const config = getConfig(ctx.cwd);
|
|
118
|
+
lines.push(" Config:");
|
|
119
|
+
lines.push(` enabled: ${config.enabled}`);
|
|
120
|
+
lines.push(` maxInjectedRecords: ${config.maxInjectedRecords}`);
|
|
121
|
+
lines.push(` maxInjectedTokens: ${config.maxInjectedTokens}`);
|
|
122
|
+
lines.push(` crossProjectEnabled: ${config.crossProjectEnabled}`);
|
|
123
|
+
|
|
124
|
+
if (ctx.hasUI) {
|
|
125
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function handleMemorySearch(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
130
|
+
const query = args?.trim();
|
|
131
|
+
|
|
132
|
+
if (!query) {
|
|
133
|
+
ctx.ui.notify("Usage: /memory-search <query>", "warning");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const projectId = getProjectId(ctx.cwd);
|
|
138
|
+
const config = getConfig(ctx.cwd);
|
|
139
|
+
|
|
140
|
+
const results = retrieve(query, projectId, [], {
|
|
141
|
+
limit: 20,
|
|
142
|
+
crossProjectEnabled: config.crossProjectEnabled,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (results.length === 0) {
|
|
146
|
+
ctx.ui.notify("No matching memories found.", "info");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const lines: string[] = [];
|
|
151
|
+
lines.push(`🔍 Memory search results for: "${query}"`);
|
|
152
|
+
lines.push("");
|
|
153
|
+
|
|
154
|
+
for (const [i, r] of results.entries()) {
|
|
155
|
+
const age = Date.now() - r.record.created_at;
|
|
156
|
+
const ageStr = age < 3600000
|
|
157
|
+
? `${Math.floor(age / 60000)}m ago`
|
|
158
|
+
: age < 86400000
|
|
159
|
+
? `${Math.floor(age / 3600000)}h ago`
|
|
160
|
+
: `${Math.floor(age / 86400000)}d ago`;
|
|
161
|
+
|
|
162
|
+
lines.push(` ${i + 1}. [${r.record.kind}] ${r.record.id} (${ageStr}, score: ${r.score.toFixed(2)})`);
|
|
163
|
+
lines.push(` ${r.record.text.slice(0, 120)}`);
|
|
164
|
+
lines.push("");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (ctx.hasUI) {
|
|
168
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function handleMemoryLast(ctx: ExtensionCommandContext): Promise<void> {
|
|
173
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
174
|
+
const last = getLastInjection(sessionId);
|
|
175
|
+
|
|
176
|
+
if (!last) {
|
|
177
|
+
ctx.ui.notify("No memory injections in this session yet.", "info");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const lines: string[] = [];
|
|
182
|
+
lines.push("📋 Last Memory Injection");
|
|
183
|
+
lines.push("");
|
|
184
|
+
|
|
185
|
+
if (last.packet) {
|
|
186
|
+
lines.push(last.packet);
|
|
187
|
+
} else {
|
|
188
|
+
lines.push(` Injected refs: ${last.injected_refs ?? "none"}`);
|
|
189
|
+
lines.push(` Reasons: ${last.reasons ?? "none"}`);
|
|
190
|
+
lines.push(` Time: ${new Date(last.created_at).toISOString()}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (ctx.hasUI) {
|
|
194
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function handleMemoryForget(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
199
|
+
const { softForgetRecord, hardDeleteRecord, getRecord } = await import("../db/index.js");
|
|
200
|
+
|
|
201
|
+
const parts = args?.trim().split(/\s+/) ?? [];
|
|
202
|
+
const hardFlag = parts.includes("--hard");
|
|
203
|
+
const refIds = parts.filter((p) => !p.startsWith("--"));
|
|
204
|
+
|
|
205
|
+
if (refIds.length === 0) {
|
|
206
|
+
ctx.ui.notify("Usage: /memory-forget <ref-id> [--hard]", "warning");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const refId of refIds) {
|
|
211
|
+
const record = getRecord(refId);
|
|
212
|
+
if (!record) {
|
|
213
|
+
ctx.ui.notify(`Record ${refId} not found.`, "warning");
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (hardFlag) {
|
|
218
|
+
const confirmed = ctx.hasUI
|
|
219
|
+
? await ctx.ui.confirm(
|
|
220
|
+
"Permanent deletion",
|
|
221
|
+
`Permanently delete memory record?\n\nKind: ${record.kind}\nText: ${record.text.slice(0, 100)}`,
|
|
222
|
+
)
|
|
223
|
+
: false;
|
|
224
|
+
|
|
225
|
+
if (confirmed) {
|
|
226
|
+
hardDeleteRecord(refId);
|
|
227
|
+
ctx.ui.notify(`Permanently deleted record ${refId}`, "info");
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
softForgetRecord(refId);
|
|
231
|
+
ctx.ui.notify(`Soft-forgotten record ${refId}`, "info");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration module.
|
|
3
|
+
* Project identity defaults to git repo root.
|
|
4
|
+
* Reads .pi/settings.json for memory.* config overrides.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
|
|
12
|
+
// ─── Project identity ───────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
let _gitRootCache: Map<string, string | null> = new Map();
|
|
15
|
+
|
|
16
|
+
export function getProjectId(cwd: string): string | null {
|
|
17
|
+
// Use git repo root as project identity
|
|
18
|
+
if (_gitRootCache.has(cwd)) {
|
|
19
|
+
return _gitRootCache.get(cwd) ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
27
|
+
}).trim();
|
|
28
|
+
_gitRootCache.set(cwd, root);
|
|
29
|
+
return root;
|
|
30
|
+
} catch {
|
|
31
|
+
// Not a git repo; use cwd as fallback
|
|
32
|
+
_gitRootCache.set(cwd, cwd);
|
|
33
|
+
return cwd;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clearProjectCache(): void {
|
|
38
|
+
_gitRootCache.clear();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Config ─────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export interface MemoryConfig {
|
|
44
|
+
/** Whether memory injection is enabled */
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
/** Max records to inject per turn */
|
|
47
|
+
maxInjectedRecords: number;
|
|
48
|
+
/** Max tokens for injected packet */
|
|
49
|
+
maxInjectedTokens: number;
|
|
50
|
+
/** Minimum score threshold for injection */
|
|
51
|
+
scoreThreshold: number;
|
|
52
|
+
/** Whether cross-project injection is enabled */
|
|
53
|
+
crossProjectEnabled: boolean;
|
|
54
|
+
/** Extra ignore patterns */
|
|
55
|
+
ignorePatterns: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DEFAULT_CONFIG: MemoryConfig = {
|
|
59
|
+
enabled: true,
|
|
60
|
+
maxInjectedRecords: 5,
|
|
61
|
+
maxInjectedTokens: 1000,
|
|
62
|
+
scoreThreshold: 0.3,
|
|
63
|
+
crossProjectEnabled: false,
|
|
64
|
+
ignorePatterns: [],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const _configCache: Map<string, MemoryConfig> = new Map();
|
|
68
|
+
|
|
69
|
+
export function getConfig(cwd: string = process.cwd()): MemoryConfig {
|
|
70
|
+
const cacheKey = cwd;
|
|
71
|
+
const cached = _configCache.get(cacheKey);
|
|
72
|
+
if (cached) return cached;
|
|
73
|
+
|
|
74
|
+
// Try loading from project .pi/settings.json
|
|
75
|
+
const projectSettings = join(cwd, ".pi", "settings.json");
|
|
76
|
+
|
|
77
|
+
if (existsSync(projectSettings)) {
|
|
78
|
+
try {
|
|
79
|
+
const raw = readFileSync(projectSettings, "utf8");
|
|
80
|
+
const settings = JSON.parse(raw);
|
|
81
|
+
if (settings.memory) {
|
|
82
|
+
const config = { ...DEFAULT_CONFIG, ...settings.memory };
|
|
83
|
+
_configCache.set(cacheKey, config);
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Invalid JSON, use defaults
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const config = { ...DEFAULT_CONFIG };
|
|
92
|
+
_configCache.set(cacheKey, config);
|
|
93
|
+
return config;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function reloadConfig(): void {
|
|
97
|
+
_configCache.clear();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Environment ────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export function getHomeDir(): string {
|
|
103
|
+
return homedir() || "/tmp";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getMemoryDir(): string {
|
|
107
|
+
return `${getHomeDir()}/.pi/agent/memory`;
|
|
108
|
+
}
|