pi-memory-stone 0.1.0 → 0.1.2
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 +14 -8
- package/package.json +7 -1
- 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
|
@@ -8,31 +8,37 @@ A global pi extension that preserves and retrieves useful memory across pi sessi
|
|
|
8
8
|
|
|
9
9
|
## Quick Start
|
|
10
10
|
|
|
11
|
-
Install globally
|
|
11
|
+
Install globally from npm:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
pi install
|
|
14
|
+
pi install npm:pi-memory-stone
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Package page: https://www.npmjs.com/package/pi-memory-stone
|
|
18
|
+
|
|
19
|
+
Then restart pi or run `/reload`, and verify it is active:
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
|
|
22
|
+
/memory-status
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
Alternative installs:
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
|
-
|
|
28
|
+
# Install directly from GitHub
|
|
29
|
+
pi install git:github.com/nikolasp/pi-memory-stone
|
|
30
|
+
|
|
31
|
+
# Manual global extension checkout
|
|
32
|
+
git clone https://github.com/nikolasp/pi-memory-stone ~/.pi/agent/extensions/pi-memory-stone
|
|
27
33
|
```
|
|
28
34
|
|
|
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
|
|
35
|
+
> 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 npm:pi-memory-stone` without `--local` (user settings) or use `~/.pi/agent/extensions/`.
|
|
30
36
|
|
|
31
37
|
## Architecture
|
|
32
38
|
|
|
33
39
|
```
|
|
34
40
|
~/.pi/agent/memory/memory.db # SQLite + FTS5 (WAL mode)
|
|
35
|
-
|
|
41
|
+
pi-memory-stone package source
|
|
36
42
|
├── src/
|
|
37
43
|
│ ├── index.ts # Entry point: hooks, lifecycle
|
|
38
44
|
│ ├── db/ # SQLite connection, migrations, CRUD
|
package/package.json
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-memory-stone",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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/test/indexing.test.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for incremental session indexing.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, beforeEach, after } from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { existsSync, unlinkSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { indexSessionOnAgentEnd } from "../src/indexing/index.js";
|
|
11
|
-
import { closeDb, getDb, getDbPath } from "../src/db/index.js";
|
|
12
|
-
|
|
13
|
-
const testMemoryDir = mkdtempSync(join(tmpdir(), "pi-memory-stone-indexing-"));
|
|
14
|
-
process.env.PI_MEMORY_STONE_DB_PATH = join(testMemoryDir, "memory.db");
|
|
15
|
-
|
|
16
|
-
function cleanDb() {
|
|
17
|
-
const dbPath = getDbPath();
|
|
18
|
-
closeDb();
|
|
19
|
-
for (const f of [dbPath, dbPath + "-wal", dbPath + "-shm"]) {
|
|
20
|
-
try { if (existsSync(f)) unlinkSync(f); } catch {}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function makeUserEntry(id: string, text: string) {
|
|
25
|
-
return {
|
|
26
|
-
type: "message",
|
|
27
|
-
id,
|
|
28
|
-
parentId: null,
|
|
29
|
-
message: {
|
|
30
|
-
role: "user",
|
|
31
|
-
content: text,
|
|
32
|
-
timestamp: Date.now(),
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function makeAssistantEntry(id: string, text: string) {
|
|
38
|
-
return {
|
|
39
|
-
type: "message",
|
|
40
|
-
id,
|
|
41
|
-
parentId: null,
|
|
42
|
-
message: {
|
|
43
|
-
role: "assistant",
|
|
44
|
-
content: [{ type: "text", text }],
|
|
45
|
-
timestamp: Date.now(),
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function makeContext(sessionFile: string, branch: unknown[]) {
|
|
51
|
-
return {
|
|
52
|
-
cwd: testMemoryDir,
|
|
53
|
-
sessionManager: {
|
|
54
|
-
getSessionFile: () => sessionFile,
|
|
55
|
-
getSessionId: () => "session-1",
|
|
56
|
-
getBranch: () => branch,
|
|
57
|
-
getLeafId: () => (branch.at(-1) as { id?: string } | undefined)?.id,
|
|
58
|
-
},
|
|
59
|
-
} as any;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
describe("indexSessionOnAgentEnd", () => {
|
|
63
|
-
beforeEach(() => cleanDb());
|
|
64
|
-
|
|
65
|
-
after(() => {
|
|
66
|
-
cleanDb();
|
|
67
|
-
rmSync(testMemoryDir, { recursive: true, force: true });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("indexes only entries after the last indexed entry when timestamps are absent", async () => {
|
|
71
|
-
const sessionFile = join(testMemoryDir, "session.jsonl");
|
|
72
|
-
writeFileSync(sessionFile, "");
|
|
73
|
-
|
|
74
|
-
const firstBranch = [
|
|
75
|
-
makeUserEntry("001", "First question"),
|
|
76
|
-
makeAssistantEntry("002", "First answer"),
|
|
77
|
-
];
|
|
78
|
-
const first = await indexSessionOnAgentEnd(makeContext(sessionFile, firstBranch), {});
|
|
79
|
-
assert.equal(first.errors.length, 0);
|
|
80
|
-
|
|
81
|
-
const fullBranch = [
|
|
82
|
-
...firstBranch,
|
|
83
|
-
makeUserEntry("003", "Second question"),
|
|
84
|
-
makeAssistantEntry("004", "Second answer"),
|
|
85
|
-
];
|
|
86
|
-
const second = await indexSessionOnAgentEnd(makeContext(sessionFile, fullBranch), {});
|
|
87
|
-
assert.equal(second.errors.length, 0);
|
|
88
|
-
|
|
89
|
-
const rows = getDb()
|
|
90
|
-
.prepare("SELECT text FROM records WHERE kind = 'turn_summary' ORDER BY created_at")
|
|
91
|
-
.all() as Array<{ text: string }>;
|
|
92
|
-
|
|
93
|
-
assert.equal(rows.length, 2);
|
|
94
|
-
assert.ok(rows.some((r) => r.text.includes("First question")));
|
|
95
|
-
assert.ok(rows.some((r) => r.text.includes("Second question")));
|
|
96
|
-
});
|
|
97
|
-
});
|
package/test/parser.test.ts
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the session parser module.
|
|
3
|
-
* Run with: node --experimental-sqlite --test test/parser.test.ts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { parseEntries, turnsToRecords } from "../src/indexing/parser.js";
|
|
9
|
-
|
|
10
|
-
// ─── Helpers ────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
function makeUserEntry(id: string, text: string) {
|
|
13
|
-
return {
|
|
14
|
-
type: "message",
|
|
15
|
-
id,
|
|
16
|
-
parentId: id === "001" ? null : String(Number(id) - 1).padStart(3, "0"),
|
|
17
|
-
timestamp: new Date().toISOString(),
|
|
18
|
-
message: {
|
|
19
|
-
role: "user",
|
|
20
|
-
content: text,
|
|
21
|
-
timestamp: Date.now(),
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function makeAssistantEntry(id: string, text: string, toolCalls: Array<{ id: string; name: string; arguments: Record<string, unknown> }> = []) {
|
|
27
|
-
const content: Array<Record<string, unknown>> = [];
|
|
28
|
-
if (text) {
|
|
29
|
-
content.push({ type: "text", text });
|
|
30
|
-
}
|
|
31
|
-
for (const tc of toolCalls) {
|
|
32
|
-
content.push({ type: "toolCall", ...tc });
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
type: "message",
|
|
36
|
-
id,
|
|
37
|
-
parentId: String(Number(id) - 1).padStart(3, "0"),
|
|
38
|
-
timestamp: new Date().toISOString(),
|
|
39
|
-
message: {
|
|
40
|
-
role: "assistant",
|
|
41
|
-
content,
|
|
42
|
-
provider: "anthropic",
|
|
43
|
-
model: "claude-sonnet-4-5",
|
|
44
|
-
usage: { totalTokens: 100 },
|
|
45
|
-
stopReason: "stop",
|
|
46
|
-
timestamp: Date.now(),
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function makeToolResultEntry(id: string, toolCallId: string, toolName: string, text: string, isError = false) {
|
|
52
|
-
return {
|
|
53
|
-
type: "message",
|
|
54
|
-
id,
|
|
55
|
-
parentId: String(Number(id) - 1).padStart(3, "0"),
|
|
56
|
-
timestamp: new Date().toISOString(),
|
|
57
|
-
message: {
|
|
58
|
-
role: "toolResult",
|
|
59
|
-
toolCallId,
|
|
60
|
-
toolName,
|
|
61
|
-
content: [{ type: "text", text }],
|
|
62
|
-
isError,
|
|
63
|
-
timestamp: Date.now(),
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ─── Tests ──────────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
describe("parseEntries", () => {
|
|
71
|
-
it("parses a simple user-assistant turn", () => {
|
|
72
|
-
const entries = [
|
|
73
|
-
makeUserEntry("001", "Hello, can you help me?"),
|
|
74
|
-
makeAssistantEntry("002", "Of course! How can I assist?"),
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
const { turns } = parseEntries(entries as any);
|
|
78
|
-
assert.equal(turns.length, 1);
|
|
79
|
-
assert.equal(turns[0].userEntryId, "001");
|
|
80
|
-
assert.ok(turns[0].userPrompt.includes("Hello"));
|
|
81
|
-
assert.ok(turns[0].assistantText.includes("Of course"));
|
|
82
|
-
assert.equal(turns[0].toolCalls.length, 0);
|
|
83
|
-
assert.equal(turns[0].errors.length, 0);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("parses a turn with tool calls", () => {
|
|
87
|
-
const entries = [
|
|
88
|
-
makeUserEntry("001", "Read package.json"),
|
|
89
|
-
makeAssistantEntry("002", "Let me read that file.", [
|
|
90
|
-
{ id: "call_1", name: "read", arguments: { path: "package.json" } },
|
|
91
|
-
]),
|
|
92
|
-
makeToolResultEntry("003", "call_1", "read", '{"name": "my-package"}'),
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
const { turns } = parseEntries(entries as any);
|
|
96
|
-
assert.equal(turns.length, 1);
|
|
97
|
-
assert.equal(turns[0].toolCalls.length, 1);
|
|
98
|
-
assert.equal(turns[0].toolCalls[0].toolName, "read");
|
|
99
|
-
assert.deepEqual(turns[0].toolCalls[0].args, { path: "package.json" });
|
|
100
|
-
assert.equal(turns[0].toolCalls[0].resultText, '{"name": "my-package"}');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("preserves assistant tool calls without tool results", () => {
|
|
104
|
-
const entries = [
|
|
105
|
-
makeUserEntry("001", "Read package.json"),
|
|
106
|
-
makeAssistantEntry("002", "Let me read that file.", [
|
|
107
|
-
{ id: "call_1", name: "read", arguments: { path: "package.json" } },
|
|
108
|
-
]),
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
const { turns } = parseEntries(entries as any);
|
|
112
|
-
assert.equal(turns.length, 1);
|
|
113
|
-
assert.equal(turns[0].toolCalls.length, 1);
|
|
114
|
-
assert.equal(turns[0].toolCalls[0].toolCallId, "call_1");
|
|
115
|
-
assert.equal(turns[0].toolCalls[0].toolName, "read");
|
|
116
|
-
assert.deepEqual(turns[0].toolCalls[0].args, { path: "package.json" });
|
|
117
|
-
assert.equal(turns[0].toolCalls[0].resultText, "");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("parses multiple turns", () => {
|
|
121
|
-
const entries = [
|
|
122
|
-
makeUserEntry("001", "Question 1"),
|
|
123
|
-
makeAssistantEntry("002", "Answer 1"),
|
|
124
|
-
makeUserEntry("003", "Question 2"),
|
|
125
|
-
makeAssistantEntry("004", "Answer 2"),
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
const { turns } = parseEntries(entries as any);
|
|
129
|
-
assert.equal(turns.length, 2);
|
|
130
|
-
assert.equal(turns[0].userEntryId, "001");
|
|
131
|
-
assert.equal(turns[1].userEntryId, "003");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("detects errors in tool results", () => {
|
|
135
|
-
const entries = [
|
|
136
|
-
makeUserEntry("001", "Run a command"),
|
|
137
|
-
makeAssistantEntry("002", "Running...", [
|
|
138
|
-
{ id: "call_1", name: "bash", arguments: { command: "invalid" } },
|
|
139
|
-
]),
|
|
140
|
-
makeToolResultEntry("003", "call_1", "bash", "Command not found", true),
|
|
141
|
-
];
|
|
142
|
-
|
|
143
|
-
const { turns } = parseEntries(entries as any);
|
|
144
|
-
assert.equal(turns[0].errors.length, 1);
|
|
145
|
-
assert.equal(turns[0].errors[0].toolName, "bash");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("detects file activities from tool calls", () => {
|
|
149
|
-
const entries = [
|
|
150
|
-
makeUserEntry("001", "Check package.json"),
|
|
151
|
-
makeAssistantEntry("002", "Let me check.", [
|
|
152
|
-
{ id: "call_1", name: "read", arguments: { path: "src/index.ts" } },
|
|
153
|
-
{ id: "call_2", name: "edit", arguments: { path: "src/utils.ts" } },
|
|
154
|
-
]),
|
|
155
|
-
makeToolResultEntry("003", "call_1", "read", "content..."),
|
|
156
|
-
makeToolResultEntry("004", "call_2", "edit", "edited"),
|
|
157
|
-
];
|
|
158
|
-
|
|
159
|
-
const { fileActivities } = parseEntries(entries as any);
|
|
160
|
-
|
|
161
|
-
const reads = fileActivities.filter((f) => f.action === "read");
|
|
162
|
-
const edits = fileActivities.filter((f) => f.action === "edit");
|
|
163
|
-
|
|
164
|
-
assert.equal(reads.length, 1);
|
|
165
|
-
assert.equal(edits.length, 1);
|
|
166
|
-
assert.equal(reads[0].path, "src/index.ts");
|
|
167
|
-
assert.equal(edits[0].path, "src/utils.ts");
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("skips sensitive file paths", () => {
|
|
171
|
-
const entries = [
|
|
172
|
-
makeUserEntry("001", "Check env"),
|
|
173
|
-
makeAssistantEntry("002", "Checking...", [
|
|
174
|
-
{ id: "call_1", name: "read", arguments: { path: ".env" } },
|
|
175
|
-
{ id: "call_2", name: "read", arguments: { path: "src/app.ts" } },
|
|
176
|
-
]),
|
|
177
|
-
makeToolResultEntry("003", "call_1", "read", "SECRET=abc"),
|
|
178
|
-
makeToolResultEntry("004", "call_2", "read", "code..."),
|
|
179
|
-
];
|
|
180
|
-
|
|
181
|
-
const { fileActivities } = parseEntries(entries as any);
|
|
182
|
-
const envActivity = fileActivities.filter((f) => f.path.includes(".env"));
|
|
183
|
-
const normalActivity = fileActivities.filter((f) => f.path === "src/app.ts");
|
|
184
|
-
|
|
185
|
-
assert.equal(envActivity.length, 0, "Should skip .env files");
|
|
186
|
-
assert.equal(normalActivity.length, 1, "Should keep normal files once");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("handles compactions", () => {
|
|
190
|
-
const entries = [
|
|
191
|
-
makeUserEntry("001", "Test"),
|
|
192
|
-
makeAssistantEntry("002", "Response"),
|
|
193
|
-
{
|
|
194
|
-
type: "compaction",
|
|
195
|
-
id: "003",
|
|
196
|
-
parentId: "002",
|
|
197
|
-
timestamp: new Date().toISOString(),
|
|
198
|
-
summary: "Previous context summarized",
|
|
199
|
-
firstKeptEntryId: "001",
|
|
200
|
-
tokensBefore: 5000,
|
|
201
|
-
},
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
const { compactions } = parseEntries(entries as any);
|
|
205
|
-
assert.equal(compactions.length, 1);
|
|
206
|
-
assert.ok(compactions[0].summary.includes("Previous"));
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe("turnsToRecords", () => {
|
|
211
|
-
it("generates turn_summary records", () => {
|
|
212
|
-
const turns = [
|
|
213
|
-
{
|
|
214
|
-
userEntryId: "001",
|
|
215
|
-
userPrompt: "Fix the authentication bug",
|
|
216
|
-
assistantEntryIds: ["002"],
|
|
217
|
-
assistantText: "I found the issue in auth.ts and fixed it.",
|
|
218
|
-
toolCalls: [
|
|
219
|
-
{
|
|
220
|
-
entryId: "003",
|
|
221
|
-
toolName: "read",
|
|
222
|
-
args: { path: "src/auth.ts" },
|
|
223
|
-
resultText: "code...",
|
|
224
|
-
isError: false,
|
|
225
|
-
},
|
|
226
|
-
],
|
|
227
|
-
errors: [],
|
|
228
|
-
},
|
|
229
|
-
];
|
|
230
|
-
|
|
231
|
-
const records = turnsToRecords(turns, "/home/project", "session-1", "/path/session.jsonl");
|
|
232
|
-
assert.ok(records.length >= 1);
|
|
233
|
-
assert.equal(records[0].kind, "turn_summary");
|
|
234
|
-
assert.ok(records[0].text.includes("Fix the authentication"));
|
|
235
|
-
assert.ok(records[0].text.includes("auth.ts"));
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("generates error_resolution records for errors", () => {
|
|
239
|
-
const turns = [
|
|
240
|
-
{
|
|
241
|
-
userEntryId: "001",
|
|
242
|
-
userPrompt: "Run deployment",
|
|
243
|
-
assistantEntryIds: ["002"],
|
|
244
|
-
assistantText: "Running deploy...",
|
|
245
|
-
toolCalls: [],
|
|
246
|
-
errors: [
|
|
247
|
-
{
|
|
248
|
-
entryId: "003",
|
|
249
|
-
toolName: "bash",
|
|
250
|
-
message: "Permission denied: cannot write to /etc",
|
|
251
|
-
},
|
|
252
|
-
],
|
|
253
|
-
},
|
|
254
|
-
];
|
|
255
|
-
|
|
256
|
-
const records = turnsToRecords(turns, "/home/project", "session-1", "/path/session.jsonl");
|
|
257
|
-
const errorRecords = records.filter((r) => r.kind === "error_resolution");
|
|
258
|
-
assert.equal(errorRecords.length, 1);
|
|
259
|
-
assert.ok(errorRecords[0].text.includes("Permission denied"));
|
|
260
|
-
});
|
|
261
|
-
});
|
package/test/privacy.test.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the privacy/redaction module.
|
|
3
|
-
* Run with: node --experimental-sqlite --test test/privacy.test.ts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { redactSecrets, isSensitivePath, shouldIgnoreFile } from "../src/privacy/index.js";
|
|
9
|
-
|
|
10
|
-
describe("redactSecrets", () => {
|
|
11
|
-
it("redacts OpenAI API keys", () => {
|
|
12
|
-
const input = "My key is sk-proj-abc123def456ghi789jkl012mno345pqr678stu";
|
|
13
|
-
const result = redactSecrets(input);
|
|
14
|
-
assert.ok(result.includes("[REDACTED:openai-key]"));
|
|
15
|
-
assert.ok(!result.includes("sk-proj-"));
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("redacts GitHub tokens", () => {
|
|
19
|
-
const input = "export GITHUB_TOKEN=ghp_abc123def456ghi789jkl012mno345pqr678";
|
|
20
|
-
const result = redactSecrets(input);
|
|
21
|
-
assert.ok(result.includes("[REDACTED:github-token]"));
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("redacts JWT tokens", () => {
|
|
25
|
-
const input = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
|
|
26
|
-
const result = redactSecrets(input);
|
|
27
|
-
assert.ok(result.includes("[REDACTED:jwt]"));
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("redacts passwords in assignments", () => {
|
|
31
|
-
const input = 'password="superSecret123" and pwd="admin123"';
|
|
32
|
-
const result = redactSecrets(input);
|
|
33
|
-
assert.ok(result.includes("[REDACTED:password]"));
|
|
34
|
-
assert.ok(!result.includes("superSecret123"));
|
|
35
|
-
assert.ok(!result.includes("admin123"));
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("redacts unquoted password, token, and secret assignments", () => {
|
|
39
|
-
const input = "password=superSecret123 pwd=admin123 TOKEN=abcdef0123456789 SECRET=shhDontKeepMe";
|
|
40
|
-
const result = redactSecrets(input);
|
|
41
|
-
assert.ok(result.includes("[REDACTED:password]"));
|
|
42
|
-
assert.ok(result.includes("[REDACTED:token]"));
|
|
43
|
-
assert.ok(result.includes("[REDACTED:secret]"));
|
|
44
|
-
assert.ok(!result.includes("superSecret123"));
|
|
45
|
-
assert.ok(!result.includes("abcdef0123456789"));
|
|
46
|
-
assert.ok(!result.includes("shhDontKeepMe"));
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("redacts connection strings", () => {
|
|
50
|
-
const input = "DATABASE_URL=mongodb://user:pass@localhost:27017/db";
|
|
51
|
-
const result = redactSecrets(input);
|
|
52
|
-
assert.ok(result.includes("[REDACTED]@"));
|
|
53
|
-
assert.ok(!result.includes("user:pass@"));
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("redacts private keys", () => {
|
|
57
|
-
const input = `-----BEGIN PRIVATE KEY-----
|
|
58
|
-
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
|
|
59
|
-
-----END PRIVATE KEY-----`;
|
|
60
|
-
const result = redactSecrets(input);
|
|
61
|
-
assert.ok(result.includes("[REDACTED:private-key]"));
|
|
62
|
-
assert.ok(!result.includes("MIIEvQ"));
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("preserves normal text", () => {
|
|
66
|
-
const input = "Just some normal conversation about code";
|
|
67
|
-
const result = redactSecrets(input);
|
|
68
|
-
assert.equal(result, input);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("isSensitivePath", () => {
|
|
73
|
-
it("flags .env files", () => {
|
|
74
|
-
assert.ok(isSensitivePath(".env"));
|
|
75
|
-
assert.ok(isSensitivePath(".env.local"));
|
|
76
|
-
assert.ok(isSensitivePath("path/to/.env.production"));
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("flags key/cert files", () => {
|
|
80
|
-
assert.ok(isSensitivePath("cert.pem"));
|
|
81
|
-
assert.ok(isSensitivePath("private.key"));
|
|
82
|
-
assert.ok(isSensitivePath("path/to/server.crt"));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("flags SSH keys", () => {
|
|
86
|
-
assert.ok(isSensitivePath("~/.ssh/id_rsa"));
|
|
87
|
-
assert.ok(isSensitivePath("/home/user/.ssh/id_ed25519"));
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("flags AWS credentials", () => {
|
|
91
|
-
assert.ok(isSensitivePath("~/.aws/credentials"));
|
|
92
|
-
assert.ok(isSensitivePath("~/.aws/config"));
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("flags node_modules", () => {
|
|
96
|
-
assert.ok(isSensitivePath("node_modules/foo/bar.ts"));
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("flags .git directory", () => {
|
|
100
|
-
assert.ok(isSensitivePath(".git/config"));
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("allows normal source files", () => {
|
|
104
|
-
assert.ok(!isSensitivePath("src/index.ts"));
|
|
105
|
-
assert.ok(!isSensitivePath("components/Button.tsx"));
|
|
106
|
-
assert.ok(!isSensitivePath("README.md"));
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("shouldIgnoreFile", () => {
|
|
111
|
-
it("returns true for sensitive files", () => {
|
|
112
|
-
assert.ok(shouldIgnoreFile(".env"));
|
|
113
|
-
assert.ok(shouldIgnoreFile("secrets/config.json"));
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("returns false for normal files", () => {
|
|
117
|
-
assert.ok(!shouldIgnoreFile("src/utils.ts"));
|
|
118
|
-
assert.ok(!shouldIgnoreFile("package.json"));
|
|
119
|
-
});
|
|
120
|
-
});
|
package/test/ranking.test.ts
DELETED
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the retrieval/ranking module.
|
|
3
|
-
* Run with: NODE_OPTIONS="--experimental-sqlite" tsx --test test/ranking.test.ts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, before, after } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { existsSync, unlinkSync, mkdtempSync, rmSync } from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import {
|
|
12
|
-
buildSearchQuery,
|
|
13
|
-
rankAndFilter,
|
|
14
|
-
buildInjectionPacket,
|
|
15
|
-
formatInjectionForLlm,
|
|
16
|
-
} from "../src/retrieval/index.js";
|
|
17
|
-
import {
|
|
18
|
-
getDb,
|
|
19
|
-
closeDb,
|
|
20
|
-
upsertRecord,
|
|
21
|
-
getDbPath,
|
|
22
|
-
softForgetRecord,
|
|
23
|
-
getRecord,
|
|
24
|
-
hardDeleteRecord,
|
|
25
|
-
insertInjection,
|
|
26
|
-
getLastInjection,
|
|
27
|
-
} from "../src/db/index.js";
|
|
28
|
-
import type { RecordRow } from "../src/db/index.js";
|
|
29
|
-
import type { RecordKind, RecordScope } from "../src/db/schema.js";
|
|
30
|
-
|
|
31
|
-
// ─── DB cleanup ─────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
const testMemoryDir = mkdtempSync(join(tmpdir(), "pi-memory-stone-ranking-"));
|
|
34
|
-
process.env.PI_MEMORY_STONE_DB_PATH = join(testMemoryDir, "memory.db");
|
|
35
|
-
|
|
36
|
-
function cleanDb() {
|
|
37
|
-
const dbPath = getDbPath();
|
|
38
|
-
closeDb();
|
|
39
|
-
// Small delay to ensure file handles are released
|
|
40
|
-
const files = [dbPath, dbPath + "-wal", dbPath + "-shm"];
|
|
41
|
-
for (const f of files) {
|
|
42
|
-
try { if (existsSync(f)) unlinkSync(f); } catch {}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ─── Seed data helper ───────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
function seedTestRecords() {
|
|
49
|
-
const now = Date.now();
|
|
50
|
-
const records: Array<{
|
|
51
|
-
kind: RecordKind;
|
|
52
|
-
scope: RecordScope;
|
|
53
|
-
project_id: string | null;
|
|
54
|
-
text: string;
|
|
55
|
-
tags?: string;
|
|
56
|
-
created_at: number;
|
|
57
|
-
}> = [
|
|
58
|
-
{
|
|
59
|
-
kind: "decision",
|
|
60
|
-
scope: "project",
|
|
61
|
-
project_id: "/home/test-project",
|
|
62
|
-
text: "Decided to use TypeScript for the frontend. Reasoning: better type safety and tooling support.",
|
|
63
|
-
tags: "typescript,frontend,decision",
|
|
64
|
-
created_at: now - 1000,
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
kind: "preference",
|
|
68
|
-
scope: "project",
|
|
69
|
-
project_id: "/home/test-project",
|
|
70
|
-
text: "User prefers 2-space indentation for all TypeScript files.",
|
|
71
|
-
tags: "formatting,preference",
|
|
72
|
-
created_at: now - 2000,
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
kind: "error_resolution",
|
|
76
|
-
scope: "project",
|
|
77
|
-
project_id: "/home/test-project",
|
|
78
|
-
text: "Tool: bash\nError: Module not found: @earendil-works/pi-coding-agent\nResolution: Run npm install in the extension directory.",
|
|
79
|
-
tags: "error,resolve",
|
|
80
|
-
created_at: now - 5000,
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
kind: "task",
|
|
84
|
-
scope: "project",
|
|
85
|
-
project_id: "/home/test-project",
|
|
86
|
-
text: "TODO: Add unit tests for the authentication module.",
|
|
87
|
-
tags: "task,todo",
|
|
88
|
-
created_at: now - 10000,
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
kind: "preference",
|
|
92
|
-
scope: "global",
|
|
93
|
-
project_id: null,
|
|
94
|
-
text: "User always wants answers in concise bullet-point format.",
|
|
95
|
-
tags: "formatting,global",
|
|
96
|
-
created_at: now - 3000,
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
kind: "decision",
|
|
100
|
-
scope: "project",
|
|
101
|
-
project_id: "/home/other-project",
|
|
102
|
-
text: "Decided to use PostgreSQL instead of MySQL for the other project.",
|
|
103
|
-
tags: "database,decision",
|
|
104
|
-
created_at: now - 6000,
|
|
105
|
-
},
|
|
106
|
-
];
|
|
107
|
-
|
|
108
|
-
for (const r of records) {
|
|
109
|
-
upsertRecord({
|
|
110
|
-
kind: r.kind,
|
|
111
|
-
scope: r.scope,
|
|
112
|
-
project_id: r.project_id,
|
|
113
|
-
text: r.text,
|
|
114
|
-
tags: r.tags,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ─── Tests ──────────────────────────────────────────────────────────
|
|
120
|
-
|
|
121
|
-
describe("buildSearchQuery", () => {
|
|
122
|
-
it("uses user prompt text", () => {
|
|
123
|
-
const query = buildSearchQuery("How should I format TypeScript files?");
|
|
124
|
-
assert.ok(query.includes("How should I format"));
|
|
125
|
-
assert.ok(query.includes("TypeScript files"));
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("includes recent file basenames", () => {
|
|
129
|
-
const query = buildSearchQuery("Fix auth bug", [
|
|
130
|
-
"src/auth/login.ts",
|
|
131
|
-
"src/auth/signup.ts",
|
|
132
|
-
]);
|
|
133
|
-
assert.ok(query.includes("login.ts"));
|
|
134
|
-
assert.ok(query.includes("signup.ts"));
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("truncates long prompts", () => {
|
|
138
|
-
const longPrompt = "A".repeat(500);
|
|
139
|
-
const query = buildSearchQuery(longPrompt);
|
|
140
|
-
assert.ok(query.length <= 250);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe("rankAndFilter", () => {
|
|
145
|
-
before(() => {
|
|
146
|
-
cleanDb();
|
|
147
|
-
seedTestRecords();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
after(() => {
|
|
151
|
-
cleanDb();
|
|
152
|
-
rmSync(testMemoryDir, { recursive: true, force: true });
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("boosts same-project records", () => {
|
|
156
|
-
const db = getDb();
|
|
157
|
-
const records = db
|
|
158
|
-
.prepare(
|
|
159
|
-
`SELECT r.*, 0 as rank FROM records r
|
|
160
|
-
WHERE r.text LIKE '%TypeScript%' AND r.status = 'active'`,
|
|
161
|
-
)
|
|
162
|
-
.all() as unknown as (RecordRow & { rank: number })[];
|
|
163
|
-
|
|
164
|
-
const ranked = rankAndFilter(records, "/home/test-project", false);
|
|
165
|
-
|
|
166
|
-
const sameProject = ranked.filter(
|
|
167
|
-
(r) => r.record.project_id === "/home/test-project",
|
|
168
|
-
);
|
|
169
|
-
assert.ok(sameProject.length > 0);
|
|
170
|
-
assert.ok(sameProject.some((r) => r.reasons.includes("same-project")));
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("filters out cross-project records when crossProjectEnabled is false", () => {
|
|
174
|
-
const db = getDb();
|
|
175
|
-
const records = db
|
|
176
|
-
.prepare(
|
|
177
|
-
`SELECT r.*, 0 as rank FROM records r
|
|
178
|
-
WHERE r.text LIKE '%PostgreSQL%' AND r.status = 'active'`,
|
|
179
|
-
)
|
|
180
|
-
.all() as unknown as (RecordRow & { rank: number })[];
|
|
181
|
-
|
|
182
|
-
const ranked = rankAndFilter(records, "/home/test-project", false);
|
|
183
|
-
|
|
184
|
-
const otherProject = ranked.filter(
|
|
185
|
-
(r) => r.record.project_id === "/home/other-project",
|
|
186
|
-
);
|
|
187
|
-
assert.equal(otherProject.length, 0);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("filters out global-scope records from other projects when cross-project is disabled", () => {
|
|
191
|
-
upsertRecord({
|
|
192
|
-
kind: "preference",
|
|
193
|
-
scope: "global",
|
|
194
|
-
project_id: "/home/other-project",
|
|
195
|
-
text: "Other project global preference about bullet lists",
|
|
196
|
-
tags: "global,other-project",
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const db = getDb();
|
|
200
|
-
const records = db
|
|
201
|
-
.prepare(
|
|
202
|
-
`SELECT r.*, 0 as rank FROM records r
|
|
203
|
-
WHERE r.text LIKE '%Other project global preference%'`,
|
|
204
|
-
)
|
|
205
|
-
.all() as unknown as (RecordRow & { rank: number })[];
|
|
206
|
-
|
|
207
|
-
const ranked = rankAndFilter(records, "/home/test-project", false);
|
|
208
|
-
assert.equal(ranked.length, 0);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("includes global-scope records from other projects when cross-project", () => {
|
|
212
|
-
const db = getDb();
|
|
213
|
-
const records = db
|
|
214
|
-
.prepare(
|
|
215
|
-
`SELECT r.*, 0 as rank FROM records r
|
|
216
|
-
WHERE r.text LIKE '%bullet%' AND r.status = 'active'`,
|
|
217
|
-
)
|
|
218
|
-
.all() as unknown as (RecordRow & { rank: number })[];
|
|
219
|
-
|
|
220
|
-
const ranked = rankAndFilter(records, "/home/test-project", true);
|
|
221
|
-
|
|
222
|
-
const globalRecords = ranked.filter((r) => r.record.scope === "global");
|
|
223
|
-
assert.ok(globalRecords.length > 0);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("preserves soft-forgotten status on duplicate upsert", () => {
|
|
227
|
-
const id = upsertRecord({
|
|
228
|
-
kind: "decision",
|
|
229
|
-
scope: "project",
|
|
230
|
-
project_id: "/home/test-project",
|
|
231
|
-
text: "Remember not to resurrect this duplicate memory",
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
softForgetRecord(id);
|
|
235
|
-
upsertRecord({
|
|
236
|
-
kind: "decision",
|
|
237
|
-
scope: "project",
|
|
238
|
-
project_id: "/home/test-project",
|
|
239
|
-
text: "Remember not to resurrect this duplicate memory",
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
assert.equal(getRecord(id)?.status, "soft_forgotten");
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it("keeps duplicate text separate across projects", () => {
|
|
246
|
-
const id1 = upsertRecord({
|
|
247
|
-
kind: "decision",
|
|
248
|
-
scope: "project",
|
|
249
|
-
project_id: "/home/test-project",
|
|
250
|
-
text: "Use the same deployment checklist everywhere",
|
|
251
|
-
});
|
|
252
|
-
const id2 = upsertRecord({
|
|
253
|
-
kind: "decision",
|
|
254
|
-
scope: "project",
|
|
255
|
-
project_id: "/home/other-project",
|
|
256
|
-
text: "Use the same deployment checklist everywhere",
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
assert.notEqual(id1, id2);
|
|
260
|
-
assert.equal(getRecord(id1)?.project_id, "/home/test-project");
|
|
261
|
-
assert.equal(getRecord(id2)?.project_id, "/home/other-project");
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("redacts secrets at the record storage boundary", () => {
|
|
265
|
-
const id = upsertRecord({
|
|
266
|
-
kind: "preference",
|
|
267
|
-
scope: "project",
|
|
268
|
-
project_id: "/home/test-project",
|
|
269
|
-
text: "Store password=superSecret123 for later",
|
|
270
|
-
tags: "token=abcdef0123456789",
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const record = getRecord(id);
|
|
274
|
-
assert.ok(record);
|
|
275
|
-
assert.ok(record.text.includes("[REDACTED:password]"));
|
|
276
|
-
assert.ok(!record.text.includes("superSecret123"));
|
|
277
|
-
assert.ok(record.tags?.includes("[REDACTED:token]"));
|
|
278
|
-
assert.ok(!record.tags?.includes("abcdef0123456789"));
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("hard deletion removes injection audit rows that reference the record", () => {
|
|
282
|
-
const id = upsertRecord({
|
|
283
|
-
kind: "decision",
|
|
284
|
-
scope: "project",
|
|
285
|
-
project_id: "/home/test-project",
|
|
286
|
-
text: "Delete this audit-visible memory",
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
insertInjection({
|
|
290
|
-
session_id: "session-with-deleted-memory",
|
|
291
|
-
injected_refs: id,
|
|
292
|
-
packet: `Memory text for ${id}: Delete this audit-visible memory`,
|
|
293
|
-
});
|
|
294
|
-
assert.ok(getLastInjection("session-with-deleted-memory"));
|
|
295
|
-
|
|
296
|
-
hardDeleteRecord(id);
|
|
297
|
-
assert.equal(getLastInjection("session-with-deleted-memory"), undefined);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it("filters out non-active records", () => {
|
|
301
|
-
// Create a soft-forgotten record
|
|
302
|
-
upsertRecord({
|
|
303
|
-
kind: "decision",
|
|
304
|
-
scope: "project",
|
|
305
|
-
project_id: "/home/test-project",
|
|
306
|
-
text: "Forgotten decision about architecture",
|
|
307
|
-
status: "soft_forgotten",
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
const db = getDb();
|
|
311
|
-
const records = db
|
|
312
|
-
.prepare(
|
|
313
|
-
`SELECT r.*, 0 as rank FROM records r
|
|
314
|
-
WHERE r.text LIKE '%Forgotten decision%'`,
|
|
315
|
-
)
|
|
316
|
-
.all() as unknown as (RecordRow & { rank: number })[];
|
|
317
|
-
|
|
318
|
-
const ranked = rankAndFilter(records, "/home/test-project", false);
|
|
319
|
-
assert.equal(ranked.length, 0);
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
describe("buildInjectionPacket", () => {
|
|
324
|
-
it("builds a structured injection packet", () => {
|
|
325
|
-
const results = [
|
|
326
|
-
{
|
|
327
|
-
record: {
|
|
328
|
-
id: "abc123",
|
|
329
|
-
kind: "decision" as RecordKind,
|
|
330
|
-
text: "Decided to use TypeScript",
|
|
331
|
-
project_id: "/home/test-project",
|
|
332
|
-
created_at: Date.now() - 1000,
|
|
333
|
-
} as RecordRow,
|
|
334
|
-
score: 0.95,
|
|
335
|
-
reasons: ["same-project"],
|
|
336
|
-
},
|
|
337
|
-
];
|
|
338
|
-
|
|
339
|
-
const packet = buildInjectionPacket(results);
|
|
340
|
-
assert.ok(packet.header.includes("Memory: loaded"));
|
|
341
|
-
assert.equal(packet.items.length, 1);
|
|
342
|
-
assert.equal(packet.items[0].ref, "abc123");
|
|
343
|
-
assert.ok(packet.items[0].text.includes("TypeScript"));
|
|
344
|
-
assert.ok(packet.footer.includes("memory-forget"));
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it("adds stale hints for old records", () => {
|
|
348
|
-
const thirtyDaysAgo = Date.now() - 31 * 24 * 60 * 60 * 1000;
|
|
349
|
-
const results = [
|
|
350
|
-
{
|
|
351
|
-
record: {
|
|
352
|
-
id: "old123",
|
|
353
|
-
kind: "decision" as RecordKind,
|
|
354
|
-
text: "Old decision",
|
|
355
|
-
project_id: "/home/test-project",
|
|
356
|
-
created_at: thirtyDaysAgo,
|
|
357
|
-
} as RecordRow,
|
|
358
|
-
score: 0.5,
|
|
359
|
-
reasons: [],
|
|
360
|
-
},
|
|
361
|
-
];
|
|
362
|
-
|
|
363
|
-
const packet = buildInjectionPacket(results);
|
|
364
|
-
assert.ok(packet.items[0].staleHint);
|
|
365
|
-
assert.ok(packet.items[0].staleHint!.includes("stale"));
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
describe("formatInjectionForLlm", () => {
|
|
370
|
-
it("formats packet for LLM consumption", () => {
|
|
371
|
-
const packet = {
|
|
372
|
-
header: "Memory: loaded 2 items",
|
|
373
|
-
items: [
|
|
374
|
-
{ ref: "abc", kind: "decision", text: "Use TypeScript" },
|
|
375
|
-
{ ref: "def", kind: "preference", text: "2-space indent" },
|
|
376
|
-
],
|
|
377
|
-
footer: "Use /memory-forget to remove",
|
|
378
|
-
recordCount: 2,
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const formatted = formatInjectionForLlm(packet, 1000);
|
|
382
|
-
assert.ok(formatted.includes("Memory: loaded"));
|
|
383
|
-
assert.ok(formatted.includes("[decision ref=abc]"));
|
|
384
|
-
assert.ok(formatted.includes("[preference ref=def]"));
|
|
385
|
-
assert.ok(formatted.includes("/memory-forget"));
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("respects token budget", () => {
|
|
389
|
-
const longText = "x".repeat(5000);
|
|
390
|
-
const packet = {
|
|
391
|
-
header: "Header",
|
|
392
|
-
items: [{ ref: "abc", kind: "decision", text: longText }],
|
|
393
|
-
footer: "Footer",
|
|
394
|
-
recordCount: 1,
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
const formatted = formatInjectionForLlm(packet, 10);
|
|
398
|
-
assert.ok(
|
|
399
|
-
formatted.length < 200,
|
|
400
|
-
`Expected <200 chars but got ${formatted.length}`,
|
|
401
|
-
);
|
|
402
|
-
});
|
|
403
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"skipLibCheck": true,
|
|
8
|
-
"noEmit": true,
|
|
9
|
-
"types": ["node"],
|
|
10
|
-
"allowImportingTsExtensions": false
|
|
11
|
-
},
|
|
12
|
-
"include": ["src/**/*.ts", "test/**/*.ts"]
|
|
13
|
-
}
|