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
|
@@ -0,0 +1,261 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
});
|