@hippocampusllms/cli 0.0.0-stage → 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 +132 -2
- package/dist/config.schema.json +73 -0
- package/dist/index.js +1112 -0
- package/dist/memory.schema.json +212 -0
- package/package.json +19 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/adapters/src/index.ts
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
copyFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
mkdirSync
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
var capabilities = {
|
|
14
|
+
mcp: true,
|
|
15
|
+
cli: true,
|
|
16
|
+
taskStartHook: false,
|
|
17
|
+
taskEndHook: false,
|
|
18
|
+
eventCapture: false
|
|
19
|
+
};
|
|
20
|
+
var begin = "# BEGIN HIPPOCAMPUS MANAGED";
|
|
21
|
+
var end = "# END HIPPOCAMPUS MANAGED";
|
|
22
|
+
function connect(host, project, entry, scope, dryRun, remove = false) {
|
|
23
|
+
if (!["claude", "codex"].includes(host))
|
|
24
|
+
throw Error("Supported hosts: claude, codex");
|
|
25
|
+
if (!["project", "user"].includes(scope))
|
|
26
|
+
throw Error("Scope must be project or user");
|
|
27
|
+
if (host === "claude" && scope === "user")
|
|
28
|
+
return {
|
|
29
|
+
host,
|
|
30
|
+
manual: true,
|
|
31
|
+
command: [
|
|
32
|
+
"claude",
|
|
33
|
+
"mcp",
|
|
34
|
+
"add",
|
|
35
|
+
"--transport",
|
|
36
|
+
"stdio",
|
|
37
|
+
"--scope",
|
|
38
|
+
"user",
|
|
39
|
+
"hippocampus",
|
|
40
|
+
"--",
|
|
41
|
+
process.execPath,
|
|
42
|
+
entry,
|
|
43
|
+
"serve",
|
|
44
|
+
"--transport",
|
|
45
|
+
"stdio",
|
|
46
|
+
"--project",
|
|
47
|
+
project
|
|
48
|
+
],
|
|
49
|
+
reason: "Claude user configuration is managed by the Claude CLI. Review this argument array before running it."
|
|
50
|
+
};
|
|
51
|
+
const path = host === "claude" ? join(project, ".mcp.json") : join(
|
|
52
|
+
scope === "user" ? join(homedir(), ".codex") : join(project, ".codex"),
|
|
53
|
+
"config.toml"
|
|
54
|
+
);
|
|
55
|
+
const old = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
56
|
+
const launch = {
|
|
57
|
+
command: process.execPath,
|
|
58
|
+
args: [entry, "serve", "--transport", "stdio", "--project", project]
|
|
59
|
+
};
|
|
60
|
+
let next;
|
|
61
|
+
if (host === "claude") {
|
|
62
|
+
const document = old ? JSON.parse(old) : {};
|
|
63
|
+
document.mcpServers ??= {};
|
|
64
|
+
const existing = document.mcpServers.hippocampus;
|
|
65
|
+
const owner = join(project, ".hippocampus/claude-connection.json");
|
|
66
|
+
const owned = existsSync(owner) ? JSON.parse(readFileSync(owner, "utf8")) : void 0;
|
|
67
|
+
if (existing && JSON.stringify(existing) !== JSON.stringify(owned?.entry))
|
|
68
|
+
throw Error(
|
|
69
|
+
"Existing hippocampus server is not owned by this adapter; preserve it and choose manual setup"
|
|
70
|
+
);
|
|
71
|
+
if (remove) delete document.mcpServers.hippocampus;
|
|
72
|
+
else document.mcpServers.hippocampus = launch;
|
|
73
|
+
next = JSON.stringify(document, null, 2) + "\n";
|
|
74
|
+
if (!dryRun) {
|
|
75
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
76
|
+
if (old && !existsSync(path + ".hippocampus.bak"))
|
|
77
|
+
copyFileSync(path, path + ".hippocampus.bak");
|
|
78
|
+
writeFileSync(path, next);
|
|
79
|
+
writeFileSync(owner, JSON.stringify({ entry: remove ? null : launch }));
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
const start = old.indexOf(begin);
|
|
83
|
+
const stop = old.indexOf(end);
|
|
84
|
+
if (start < 0 !== stop < 0)
|
|
85
|
+
throw Error("Incomplete managed configuration block; restore backup");
|
|
86
|
+
const unowned = start < 0 ? old : old.slice(0, start) + old.slice(stop + end.length);
|
|
87
|
+
if (/\[mcp_servers\.(?:"hippocampus"|hippocampus)\]/.test(unowned))
|
|
88
|
+
throw Error(
|
|
89
|
+
"Existing hippocampus configuration is not managed by this adapter"
|
|
90
|
+
);
|
|
91
|
+
const block = `${begin}
|
|
92
|
+
[mcp_servers.hippocampus]
|
|
93
|
+
command = ${JSON.stringify(launch.command)}
|
|
94
|
+
args = ${JSON.stringify(launch.args)}
|
|
95
|
+
${end}`;
|
|
96
|
+
next = remove ? unowned : unowned.trimEnd() + "\n\n" + block + "\n";
|
|
97
|
+
if (!dryRun) {
|
|
98
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
99
|
+
if (old && !existsSync(path + ".hippocampus.bak"))
|
|
100
|
+
copyFileSync(path, path + ".hippocampus.bak");
|
|
101
|
+
writeFileSync(path, next);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
host,
|
|
106
|
+
scope,
|
|
107
|
+
path,
|
|
108
|
+
dryRun,
|
|
109
|
+
before: old,
|
|
110
|
+
after: next,
|
|
111
|
+
backup: path + ".hippocampus.bak",
|
|
112
|
+
capabilities,
|
|
113
|
+
trigger: "manual or agent-instructed"
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// packages/cli/src/index.ts
|
|
118
|
+
import { parseArgs } from "node:util";
|
|
119
|
+
import {
|
|
120
|
+
existsSync as existsSync3,
|
|
121
|
+
readFileSync as readFileSync3,
|
|
122
|
+
appendFileSync,
|
|
123
|
+
realpathSync as realpathSync2
|
|
124
|
+
} from "node:fs";
|
|
125
|
+
import { resolve as resolve2, join as join3, isAbsolute } from "node:path";
|
|
126
|
+
import { createInterface } from "node:readline/promises";
|
|
127
|
+
|
|
128
|
+
// packages/cli/src/config.ts
|
|
129
|
+
import { homedir as homedir2 } from "node:os";
|
|
130
|
+
import {
|
|
131
|
+
existsSync as existsSync2,
|
|
132
|
+
readFileSync as readFileSync2,
|
|
133
|
+
writeFileSync as writeFileSync2,
|
|
134
|
+
renameSync,
|
|
135
|
+
mkdirSync as mkdirSync2,
|
|
136
|
+
realpathSync
|
|
137
|
+
} from "node:fs";
|
|
138
|
+
import { resolve, dirname as dirname2, join as join2 } from "node:path";
|
|
139
|
+
import { randomUUID } from "node:crypto";
|
|
140
|
+
import { z } from "zod";
|
|
141
|
+
var schema = z.object({
|
|
142
|
+
schemaVersion: z.literal(1),
|
|
143
|
+
projectId: z.string().uuid(),
|
|
144
|
+
storage: z.object({
|
|
145
|
+
provider: z.literal("sqlite"),
|
|
146
|
+
sqlite: z.object({ path: z.string().min(1) }).strict()
|
|
147
|
+
}).strict(),
|
|
148
|
+
retrieval: z.object({
|
|
149
|
+
mode: z.literal("lexical"),
|
|
150
|
+
maxContextTokens: z.number().int().min(100).max(1e4),
|
|
151
|
+
vector: z.object({ provider: z.literal("none") }).strict()
|
|
152
|
+
}).strict(),
|
|
153
|
+
embeddings: z.object({ provider: z.literal("none") }).strict(),
|
|
154
|
+
capture: z.object({ mode: z.literal("explicit") }).strict(),
|
|
155
|
+
privacy: z.object({ telemetry: z.literal(false) }).strict()
|
|
156
|
+
}).strict();
|
|
157
|
+
function defaults() {
|
|
158
|
+
return {
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
projectId: randomUUID(),
|
|
161
|
+
storage: { provider: "sqlite", sqlite: { path: "./memory.sqlite" } },
|
|
162
|
+
retrieval: {
|
|
163
|
+
mode: "lexical",
|
|
164
|
+
maxContextTokens: 1500,
|
|
165
|
+
vector: { provider: "none" }
|
|
166
|
+
},
|
|
167
|
+
embeddings: { provider: "none" },
|
|
168
|
+
capture: { mode: "explicit" },
|
|
169
|
+
privacy: { telemetry: false }
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function root(path) {
|
|
173
|
+
let p2 = realpathSync(resolve(path));
|
|
174
|
+
while (!existsSync2(join2(p2, ".hippocampus/config.json"))) {
|
|
175
|
+
const next = dirname2(p2);
|
|
176
|
+
if (next === p2)
|
|
177
|
+
throw Error("Project not initialized; run hippocampus init");
|
|
178
|
+
p2 = next;
|
|
179
|
+
}
|
|
180
|
+
return p2;
|
|
181
|
+
}
|
|
182
|
+
function configPath(p2) {
|
|
183
|
+
return join2(p2, ".hippocampus/config.json");
|
|
184
|
+
}
|
|
185
|
+
function load(p2) {
|
|
186
|
+
return schema.parse(JSON.parse(readFileSync2(configPath(p2), "utf8")));
|
|
187
|
+
}
|
|
188
|
+
function atomic(path, value) {
|
|
189
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
190
|
+
const temp = path + "." + randomUUID() + ".tmp";
|
|
191
|
+
writeFileSync2(temp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
192
|
+
renameSync(temp, path);
|
|
193
|
+
}
|
|
194
|
+
function database(p2, c) {
|
|
195
|
+
return resolve(dirname2(configPath(p2)), c.storage.sqlite.path);
|
|
196
|
+
}
|
|
197
|
+
function userConfigPath() {
|
|
198
|
+
return join2(
|
|
199
|
+
process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config"),
|
|
200
|
+
"hippocampus",
|
|
201
|
+
"config.json"
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
function merge(a, b) {
|
|
205
|
+
const result = structuredClone(a);
|
|
206
|
+
for (const [key, value] of Object.entries(b)) {
|
|
207
|
+
result[key] = value && typeof value === "object" && !Array.isArray(value) ? merge(result[key] ?? {}, value) : value;
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
function userDefaults() {
|
|
212
|
+
const path = userConfigPath();
|
|
213
|
+
if (!existsSync2(path)) return {};
|
|
214
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
215
|
+
if ("projectId" in raw) throw Error("User defaults cannot contain projectId");
|
|
216
|
+
schema.parse(merge(defaults(), raw));
|
|
217
|
+
return raw;
|
|
218
|
+
}
|
|
219
|
+
function effective(p2) {
|
|
220
|
+
const raw = JSON.parse(readFileSync2(configPath(p2), "utf8"));
|
|
221
|
+
schema.parse(raw);
|
|
222
|
+
const result = merge(merge(defaults(), userDefaults()), raw);
|
|
223
|
+
if (process.env.HIPPOCAMPUS_MAX_CONTEXT_TOKENS)
|
|
224
|
+
result.retrieval.maxContextTokens = Number(
|
|
225
|
+
process.env.HIPPOCAMPUS_MAX_CONTEXT_TOKENS
|
|
226
|
+
);
|
|
227
|
+
return schema.parse(result);
|
|
228
|
+
}
|
|
229
|
+
function saveUser(raw) {
|
|
230
|
+
if ("projectId" in raw) throw Error("User defaults cannot contain projectId");
|
|
231
|
+
schema.parse(merge(defaults(), raw));
|
|
232
|
+
atomic(userConfigPath(), raw);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// packages/core/src/schema.ts
|
|
236
|
+
import { z as z2 } from "zod";
|
|
237
|
+
var evidenceSchema = z2.object({
|
|
238
|
+
type: z2.enum(["commit", "path", "test", "issue", "user", "other"]),
|
|
239
|
+
reference: z2.string().min(1).max(2e3),
|
|
240
|
+
observedAt: z2.string().datetime().optional(),
|
|
241
|
+
contentHash: z2.string().max(128).optional()
|
|
242
|
+
}).strict();
|
|
243
|
+
var relationSchema = z2.object({
|
|
244
|
+
type: z2.enum(["supports", "supersedes", "contradicts", "derived_from"]),
|
|
245
|
+
targetId: z2.string().uuid()
|
|
246
|
+
}).strict();
|
|
247
|
+
var recordSchema = z2.object({
|
|
248
|
+
id: z2.string().uuid(),
|
|
249
|
+
schemaVersion: z2.literal(1),
|
|
250
|
+
revision: z2.number().int().positive(),
|
|
251
|
+
projectId: z2.string().uuid(),
|
|
252
|
+
workspaceId: z2.string().max(200).optional(),
|
|
253
|
+
scope: z2.object({
|
|
254
|
+
level: z2.enum(["project", "component"]),
|
|
255
|
+
component: z2.string().max(200).optional()
|
|
256
|
+
}).strict(),
|
|
257
|
+
applicability: z2.object({
|
|
258
|
+
environments: z2.array(z2.string().max(200)).max(20).optional(),
|
|
259
|
+
workspaces: z2.array(z2.string().max(200)).max(20).optional()
|
|
260
|
+
}).strict(),
|
|
261
|
+
kind: z2.enum([
|
|
262
|
+
"decision",
|
|
263
|
+
"constraint",
|
|
264
|
+
"episode",
|
|
265
|
+
"procedure",
|
|
266
|
+
"correction",
|
|
267
|
+
"task"
|
|
268
|
+
]),
|
|
269
|
+
content: z2.string().min(8).max(8e3),
|
|
270
|
+
rationale: z2.string().max(4e3).optional(),
|
|
271
|
+
cues: z2.array(z2.string().max(200)).max(30),
|
|
272
|
+
entities: z2.array(z2.string().max(200)).max(30),
|
|
273
|
+
evidence: z2.array(evidenceSchema).max(30),
|
|
274
|
+
verification: z2.enum(["hypothesis", "observed", "confirmed"]),
|
|
275
|
+
status: z2.enum(["active", "superseded", "archived", "expired"]),
|
|
276
|
+
createdAt: z2.string().datetime(),
|
|
277
|
+
updatedAt: z2.string().datetime(),
|
|
278
|
+
validFrom: z2.string().datetime().optional(),
|
|
279
|
+
validUntil: z2.string().datetime().optional(),
|
|
280
|
+
lastVerifiedAt: z2.string().datetime().optional(),
|
|
281
|
+
createdBy: z2.object({
|
|
282
|
+
client: z2.string().max(100),
|
|
283
|
+
agentId: z2.string().max(100).optional(),
|
|
284
|
+
sessionId: z2.string().max(100).optional()
|
|
285
|
+
}).strict(),
|
|
286
|
+
pinned: z2.boolean(),
|
|
287
|
+
expiresAt: z2.string().datetime().optional(),
|
|
288
|
+
lastRecalledAt: z2.string().datetime().optional(),
|
|
289
|
+
recallCount: z2.number().int().nonnegative(),
|
|
290
|
+
relations: z2.array(relationSchema).max(30),
|
|
291
|
+
idempotencyKey: z2.string().max(200).optional(),
|
|
292
|
+
utilityFeedback: z2.array(
|
|
293
|
+
z2.object({ useful: z2.boolean(), observedAt: z2.string().datetime() })
|
|
294
|
+
).max(100).optional()
|
|
295
|
+
}).strict().superRefine((m, ctx) => {
|
|
296
|
+
if (m.scope.level === "component" && !m.scope.component)
|
|
297
|
+
ctx.addIssue({
|
|
298
|
+
code: "custom",
|
|
299
|
+
message: "Component scope requires component"
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
var newMemorySchema = z2.object(recordSchema.shape).omit({
|
|
303
|
+
id: true,
|
|
304
|
+
schemaVersion: true,
|
|
305
|
+
revision: true,
|
|
306
|
+
projectId: true,
|
|
307
|
+
createdAt: true,
|
|
308
|
+
updatedAt: true,
|
|
309
|
+
recallCount: true,
|
|
310
|
+
lastRecalledAt: true,
|
|
311
|
+
utilityFeedback: true
|
|
312
|
+
}).partial().required({ kind: true, content: true }).strict();
|
|
313
|
+
|
|
314
|
+
// packages/storage-sqlite/src/index.ts
|
|
315
|
+
import { DatabaseSync } from "node:sqlite";
|
|
316
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
317
|
+
import { dirname as dirname3 } from "node:path";
|
|
318
|
+
|
|
319
|
+
// packages/core/src/model.ts
|
|
320
|
+
import { randomUUID as randomUUID2, createHash } from "node:crypto";
|
|
321
|
+
var AppError = class extends Error {
|
|
322
|
+
constructor(code, message, details) {
|
|
323
|
+
super(message);
|
|
324
|
+
this.code = code;
|
|
325
|
+
this.details = details;
|
|
326
|
+
}
|
|
327
|
+
code;
|
|
328
|
+
details;
|
|
329
|
+
};
|
|
330
|
+
function assert(condition, message) {
|
|
331
|
+
if (!condition) throw new AppError("INVALID_INPUT", message);
|
|
332
|
+
}
|
|
333
|
+
function validateText(text) {
|
|
334
|
+
assert(
|
|
335
|
+
typeof text === "string" && text.trim().length >= 8 && text.length <= 8e3,
|
|
336
|
+
"content must contain 8\u20138000 characters"
|
|
337
|
+
);
|
|
338
|
+
assert(
|
|
339
|
+
!/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|\b(?:sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{20,})\b/i.test(
|
|
340
|
+
text
|
|
341
|
+
),
|
|
342
|
+
"content appears to contain a credential"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
function makeMemory(projectId, rawInput) {
|
|
346
|
+
const input = newMemorySchema.parse(rawInput);
|
|
347
|
+
validateText(input.content);
|
|
348
|
+
assert(
|
|
349
|
+
[
|
|
350
|
+
"decision",
|
|
351
|
+
"constraint",
|
|
352
|
+
"episode",
|
|
353
|
+
"procedure",
|
|
354
|
+
"correction",
|
|
355
|
+
"task"
|
|
356
|
+
].includes(input.kind),
|
|
357
|
+
"invalid kind"
|
|
358
|
+
);
|
|
359
|
+
assert(
|
|
360
|
+
!/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|\b(?:sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{20,})\b/i.test(
|
|
361
|
+
JSON.stringify(input)
|
|
362
|
+
),
|
|
363
|
+
"memory metadata appears to contain a credential"
|
|
364
|
+
);
|
|
365
|
+
assert(
|
|
366
|
+
input.verification !== "confirmed" || Boolean(input.evidence?.length),
|
|
367
|
+
"confirmed memories require evidence"
|
|
368
|
+
);
|
|
369
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
370
|
+
return recordSchema.parse({
|
|
371
|
+
id: randomUUID2(),
|
|
372
|
+
schemaVersion: 1,
|
|
373
|
+
revision: 1,
|
|
374
|
+
projectId,
|
|
375
|
+
scope: input.scope ?? { level: "project" },
|
|
376
|
+
applicability: input.applicability ?? {},
|
|
377
|
+
kind: input.kind,
|
|
378
|
+
content: input.content.trim(),
|
|
379
|
+
rationale: input.rationale,
|
|
380
|
+
cues: input.cues ?? [],
|
|
381
|
+
entities: input.entities ?? [],
|
|
382
|
+
evidence: input.evidence ?? [],
|
|
383
|
+
verification: input.verification ?? "hypothesis",
|
|
384
|
+
status: input.status ?? "active",
|
|
385
|
+
createdAt: now,
|
|
386
|
+
updatedAt: now,
|
|
387
|
+
createdBy: input.createdBy ?? { client: "cli" },
|
|
388
|
+
pinned: input.pinned ?? false,
|
|
389
|
+
expiresAt: input.expiresAt ?? ((input.verification ?? "hypothesis") === "hypothesis" ? new Date(Date.now() + 30 * 864e5).toISOString() : void 0),
|
|
390
|
+
validFrom: input.validFrom,
|
|
391
|
+
validUntil: input.validUntil,
|
|
392
|
+
workspaceId: input.workspaceId,
|
|
393
|
+
lastVerifiedAt: input.lastVerifiedAt,
|
|
394
|
+
recallCount: 0,
|
|
395
|
+
relations: input.relations ?? [],
|
|
396
|
+
idempotencyKey: input.idempotencyKey
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function estimateTokens(value) {
|
|
400
|
+
return Math.ceil(Buffer.byteLength(JSON.stringify(value)) / 3);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// packages/storage-sqlite/src/index.ts
|
|
404
|
+
var SqliteStore = class {
|
|
405
|
+
constructor(path) {
|
|
406
|
+
this.path = path;
|
|
407
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
408
|
+
this.db = new DatabaseSync(path, { timeout: 2500 });
|
|
409
|
+
this.db.exec(
|
|
410
|
+
"PRAGMA foreign_keys=ON; PRAGMA journal_mode=WAL; PRAGMA busy_timeout=2500;"
|
|
411
|
+
);
|
|
412
|
+
this.migrate();
|
|
413
|
+
}
|
|
414
|
+
path;
|
|
415
|
+
name = "sqlite";
|
|
416
|
+
db;
|
|
417
|
+
migrate() {
|
|
418
|
+
const version = this.db.prepare("PRAGMA user_version").get().user_version;
|
|
419
|
+
if (version > 1)
|
|
420
|
+
throw new AppError(
|
|
421
|
+
"INVALID_CONFIG",
|
|
422
|
+
"Database schema is newer than this CLI"
|
|
423
|
+
);
|
|
424
|
+
if (version === 0) {
|
|
425
|
+
this.db.exec(`BEGIN IMMEDIATE;
|
|
426
|
+
CREATE TABLE IF NOT EXISTS memories (id TEXT PRIMARY KEY, project_id TEXT NOT NULL, revision INTEGER NOT NULL,
|
|
427
|
+
status TEXT NOT NULL, idempotency_key TEXT, body TEXT NOT NULL);
|
|
428
|
+
CREATE UNIQUE INDEX IF NOT EXISTS memories_idempotency ON memories(project_id,idempotency_key) WHERE idempotency_key IS NOT NULL;
|
|
429
|
+
CREATE INDEX IF NOT EXISTS memories_project ON memories(project_id,status);
|
|
430
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(id UNINDEXED, project_id UNINDEXED, content, cues, entities);
|
|
431
|
+
PRAGMA user_version=1; COMMIT;`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
transaction(fn) {
|
|
435
|
+
try {
|
|
436
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
437
|
+
const result = fn();
|
|
438
|
+
this.db.exec("COMMIT");
|
|
439
|
+
return result;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
try {
|
|
442
|
+
this.db.exec("ROLLBACK");
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
if (String(error).includes("SQLITE_BUSY"))
|
|
446
|
+
throw new AppError("STORE_BUSY", "SQLite is busy; retry shortly");
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
find(id) {
|
|
451
|
+
const row = this.db.prepare("SELECT body FROM memories WHERE id=?").get(id);
|
|
452
|
+
return row ? recordSchema.parse(JSON.parse(row.body)) : void 0;
|
|
453
|
+
}
|
|
454
|
+
get(projectId, id) {
|
|
455
|
+
const found = this.find(id);
|
|
456
|
+
if (found && found.projectId !== projectId)
|
|
457
|
+
throw new AppError(
|
|
458
|
+
"PROJECT_MISMATCH",
|
|
459
|
+
"Memory belongs to another project"
|
|
460
|
+
);
|
|
461
|
+
return found;
|
|
462
|
+
}
|
|
463
|
+
list(projectId) {
|
|
464
|
+
return this.db.prepare(
|
|
465
|
+
"SELECT body FROM memories WHERE project_id=? ORDER BY rowid DESC LIMIT 1000"
|
|
466
|
+
).all(projectId).map((r) => recordSchema.parse(JSON.parse(r.body)));
|
|
467
|
+
}
|
|
468
|
+
create(memory) {
|
|
469
|
+
return this.transaction(() => {
|
|
470
|
+
if (memory.idempotencyKey) {
|
|
471
|
+
const row = this.db.prepare(
|
|
472
|
+
"SELECT body FROM memories WHERE project_id=? AND idempotency_key=?"
|
|
473
|
+
).get(memory.projectId, memory.idempotencyKey);
|
|
474
|
+
if (row) {
|
|
475
|
+
const prior = recordSchema.parse(JSON.parse(row.body));
|
|
476
|
+
if (prior.kind !== memory.kind || prior.content !== memory.content || JSON.stringify(prior.evidence) !== JSON.stringify(memory.evidence) || JSON.stringify(prior.applicability) !== JSON.stringify(memory.applicability))
|
|
477
|
+
throw new AppError(
|
|
478
|
+
"IDEMPOTENCY_CONFLICT",
|
|
479
|
+
"Idempotency key was used for a different event"
|
|
480
|
+
);
|
|
481
|
+
return prior;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
this.db.prepare(
|
|
485
|
+
"INSERT INTO memories(id,project_id,revision,status,idempotency_key,body) VALUES(?,?,?,?,?,?)"
|
|
486
|
+
).run(
|
|
487
|
+
memory.id,
|
|
488
|
+
memory.projectId,
|
|
489
|
+
memory.revision,
|
|
490
|
+
memory.status,
|
|
491
|
+
memory.idempotencyKey ?? null,
|
|
492
|
+
JSON.stringify(memory)
|
|
493
|
+
);
|
|
494
|
+
this.index(memory);
|
|
495
|
+
return memory;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
index(memory) {
|
|
499
|
+
this.db.prepare("DELETE FROM memory_fts WHERE id=?").run(memory.id);
|
|
500
|
+
this.db.prepare(
|
|
501
|
+
"INSERT INTO memory_fts(id,project_id,content,cues,entities) VALUES(?,?,?,?,?)"
|
|
502
|
+
).run(
|
|
503
|
+
memory.id,
|
|
504
|
+
memory.projectId,
|
|
505
|
+
memory.content,
|
|
506
|
+
memory.cues.join(" "),
|
|
507
|
+
memory.entities.join(" ")
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
update(memory, expectedRevision) {
|
|
511
|
+
return this.transaction(() => {
|
|
512
|
+
const prior = this.get(memory.projectId, memory.id);
|
|
513
|
+
if (!prior) throw new AppError("NOT_FOUND", "Memory not found");
|
|
514
|
+
if (prior.revision !== expectedRevision)
|
|
515
|
+
throw new AppError(
|
|
516
|
+
"REVISION_CONFLICT",
|
|
517
|
+
"Memory changed since it was read",
|
|
518
|
+
{ currentRevision: prior.revision }
|
|
519
|
+
);
|
|
520
|
+
memory.revision = expectedRevision + 1;
|
|
521
|
+
memory.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
522
|
+
this.db.prepare("UPDATE memories SET revision=?,status=?,body=? WHERE id=?").run(memory.revision, memory.status, JSON.stringify(memory), memory.id);
|
|
523
|
+
this.index(memory);
|
|
524
|
+
return memory;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
delete(projectId, id) {
|
|
528
|
+
return this.transaction(() => {
|
|
529
|
+
const current = this.get(projectId, id);
|
|
530
|
+
if (!current) return false;
|
|
531
|
+
const all = this.db.prepare("SELECT body FROM memories WHERE project_id=?").all(projectId).map((row) => recordSchema.parse(JSON.parse(row.body)));
|
|
532
|
+
const removed = /* @__PURE__ */ new Set([id]);
|
|
533
|
+
let changed = true;
|
|
534
|
+
while (changed) {
|
|
535
|
+
changed = false;
|
|
536
|
+
for (const m of all)
|
|
537
|
+
if (!removed.has(m.id) && m.relations.some(
|
|
538
|
+
(r) => r.type === "derived_from" && removed.has(r.targetId)
|
|
539
|
+
)) {
|
|
540
|
+
removed.add(m.id);
|
|
541
|
+
changed = true;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const derived = all.filter((m) => removed.has(m.id) && m.id !== id);
|
|
545
|
+
for (const m of derived) {
|
|
546
|
+
this.db.prepare("DELETE FROM memory_fts WHERE id=?").run(m.id);
|
|
547
|
+
this.db.prepare("DELETE FROM memories WHERE id=?").run(m.id);
|
|
548
|
+
}
|
|
549
|
+
this.db.prepare("DELETE FROM memory_fts WHERE id=?").run(id);
|
|
550
|
+
this.db.prepare("DELETE FROM memories WHERE id=?").run(id);
|
|
551
|
+
for (const m of all.filter((record) => !removed.has(record.id))) {
|
|
552
|
+
const relations = m.relations.filter((r) => !removed.has(r.targetId));
|
|
553
|
+
if (relations.length !== m.relations.length) {
|
|
554
|
+
m.relations = relations;
|
|
555
|
+
m.revision++;
|
|
556
|
+
m.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
557
|
+
this.db.prepare("UPDATE memories SET revision=?,body=? WHERE id=?").run(m.revision, JSON.stringify(m), m.id);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return true;
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
search(projectId, query, limit) {
|
|
564
|
+
const terms = [
|
|
565
|
+
...new Set(query.toLowerCase().match(/[\p{L}\p{N}_]{3,}/gu) ?? [])
|
|
566
|
+
].slice(0, 12);
|
|
567
|
+
if (!terms.length) return [];
|
|
568
|
+
const expression = terms.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
|
|
569
|
+
const rows = this.db.prepare(
|
|
570
|
+
`SELECT m.body FROM memory_fts f JOIN memories m ON m.id=f.id
|
|
571
|
+
WHERE memory_fts MATCH ? AND m.project_id=? ORDER BY bm25(memory_fts) LIMIT ?`
|
|
572
|
+
).all(expression, projectId, Math.min(limit, 100));
|
|
573
|
+
return rows.map((r) => recordSchema.parse(JSON.parse(r.body)));
|
|
574
|
+
}
|
|
575
|
+
close() {
|
|
576
|
+
this.db.close();
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// packages/core/src/service.ts
|
|
581
|
+
var Service = class {
|
|
582
|
+
constructor(store, projectId) {
|
|
583
|
+
this.store = store;
|
|
584
|
+
this.projectId = projectId;
|
|
585
|
+
}
|
|
586
|
+
store;
|
|
587
|
+
projectId;
|
|
588
|
+
remember(input) {
|
|
589
|
+
for (const relation of input.relations ?? [])
|
|
590
|
+
this.inspect(relation.targetId);
|
|
591
|
+
return this.store.create(makeMemory(this.projectId, input));
|
|
592
|
+
}
|
|
593
|
+
inspect(id) {
|
|
594
|
+
const m = this.store.get(this.projectId, id);
|
|
595
|
+
if (!m) throw new AppError("NOT_FOUND", "Memory not found");
|
|
596
|
+
return m;
|
|
597
|
+
}
|
|
598
|
+
revise(id, content, revision, patch = {}) {
|
|
599
|
+
if (!Number.isInteger(revision) || revision < 1)
|
|
600
|
+
throw new AppError(
|
|
601
|
+
"INVALID_INPUT",
|
|
602
|
+
"expectedRevision must be a positive integer"
|
|
603
|
+
);
|
|
604
|
+
if (content !== void 0) validateText(content);
|
|
605
|
+
const m = this.inspect(id);
|
|
606
|
+
return this.store.update(
|
|
607
|
+
recordSchema.parse({
|
|
608
|
+
...m,
|
|
609
|
+
...patch,
|
|
610
|
+
...content !== void 0 ? { content } : {}
|
|
611
|
+
}),
|
|
612
|
+
revision
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
recall(query, budget = 1500, environment, workspace, component) {
|
|
616
|
+
if (!Number.isInteger(budget) || budget < 1 || budget > 1e4)
|
|
617
|
+
throw new AppError(
|
|
618
|
+
"INVALID_INPUT",
|
|
619
|
+
"Budget must be an integer between 1 and 10000"
|
|
620
|
+
);
|
|
621
|
+
const result = {
|
|
622
|
+
schemaVersion: 1,
|
|
623
|
+
estimator: "UTF-8 bytes / 3, conservative estimate",
|
|
624
|
+
memories: []
|
|
625
|
+
};
|
|
626
|
+
if (budget < estimateTokens(result))
|
|
627
|
+
throw new AppError(
|
|
628
|
+
"BUDGET_TOO_SMALL",
|
|
629
|
+
"Budget cannot fit response metadata"
|
|
630
|
+
);
|
|
631
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
632
|
+
for (const m of this.store.search(this.projectId, query, 100)) {
|
|
633
|
+
if (m.scope.level === "component" && m.scope.component !== component)
|
|
634
|
+
continue;
|
|
635
|
+
if (m.workspaceId && m.workspaceId !== workspace) continue;
|
|
636
|
+
if (m.status !== "active" || m.expiresAt && m.expiresAt < now || m.validUntil && m.validUntil < now || m.validFrom && m.validFrom > now)
|
|
637
|
+
continue;
|
|
638
|
+
if (m.applicability.environments?.length && !m.applicability.environments.includes(environment ?? ""))
|
|
639
|
+
continue;
|
|
640
|
+
if (m.applicability.workspaces?.length && !m.applicability.workspaces.includes(workspace ?? ""))
|
|
641
|
+
continue;
|
|
642
|
+
const capsule = {
|
|
643
|
+
id: m.id,
|
|
644
|
+
revision: m.revision,
|
|
645
|
+
content: m.content,
|
|
646
|
+
rationale: m.rationale,
|
|
647
|
+
verification: m.verification,
|
|
648
|
+
evidence: m.evidence,
|
|
649
|
+
relations: m.relations,
|
|
650
|
+
reason: "lexical term match; active and applicable"
|
|
651
|
+
};
|
|
652
|
+
result.memories.push(capsule);
|
|
653
|
+
if (estimateTokens(result) > budget) result.memories.pop();
|
|
654
|
+
}
|
|
655
|
+
return result;
|
|
656
|
+
}
|
|
657
|
+
feedback(id, useful) {
|
|
658
|
+
const m = this.inspect(id);
|
|
659
|
+
return this.store.update(
|
|
660
|
+
{
|
|
661
|
+
...m,
|
|
662
|
+
utilityFeedback: [
|
|
663
|
+
...m.utilityFeedback ?? [],
|
|
664
|
+
{ useful, observedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
665
|
+
].slice(-100)
|
|
666
|
+
},
|
|
667
|
+
m.revision
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
forget(id) {
|
|
671
|
+
return { deleted: this.store.delete(this.projectId, id) };
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// packages/mcp/src/index.ts
|
|
676
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
677
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
678
|
+
import { z as z3 } from "zod";
|
|
679
|
+
async function serve(service, check = () => {
|
|
680
|
+
}) {
|
|
681
|
+
const server = new McpServer({ name: "hippocampus", version: "0.1.0" });
|
|
682
|
+
const register = (name, shape, fn) => server.registerTool(
|
|
683
|
+
"hippocampus_" + name,
|
|
684
|
+
{ inputSchema: shape },
|
|
685
|
+
async (a) => {
|
|
686
|
+
try {
|
|
687
|
+
check();
|
|
688
|
+
const value = fn(a);
|
|
689
|
+
return {
|
|
690
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
691
|
+
structuredContent: value
|
|
692
|
+
};
|
|
693
|
+
} catch (e) {
|
|
694
|
+
return {
|
|
695
|
+
isError: true,
|
|
696
|
+
content: [
|
|
697
|
+
{
|
|
698
|
+
type: "text",
|
|
699
|
+
text: JSON.stringify({
|
|
700
|
+
code: e.code ?? "INVALID_INPUT",
|
|
701
|
+
message: String(e)
|
|
702
|
+
})
|
|
703
|
+
}
|
|
704
|
+
]
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
);
|
|
709
|
+
register(
|
|
710
|
+
"recall",
|
|
711
|
+
{
|
|
712
|
+
query: z3.string().max(2e3),
|
|
713
|
+
budget: z3.number().int().min(100).max(1e4).optional(),
|
|
714
|
+
component: z3.string().optional(),
|
|
715
|
+
environment: z3.string().optional(),
|
|
716
|
+
workspace: z3.string().optional()
|
|
717
|
+
},
|
|
718
|
+
(a) => service.recall(
|
|
719
|
+
a.query,
|
|
720
|
+
a.budget,
|
|
721
|
+
a.environment,
|
|
722
|
+
a.workspace,
|
|
723
|
+
a.component
|
|
724
|
+
)
|
|
725
|
+
);
|
|
726
|
+
register(
|
|
727
|
+
"remember",
|
|
728
|
+
{
|
|
729
|
+
kind: z3.enum([
|
|
730
|
+
"decision",
|
|
731
|
+
"constraint",
|
|
732
|
+
"episode",
|
|
733
|
+
"procedure",
|
|
734
|
+
"correction",
|
|
735
|
+
"task"
|
|
736
|
+
]),
|
|
737
|
+
content: z3.string().max(8e3),
|
|
738
|
+
rationale: z3.string().max(4e3).optional(),
|
|
739
|
+
verification: z3.enum(["hypothesis", "observed", "confirmed"]).optional(),
|
|
740
|
+
evidence: z3.array(evidenceSchema).max(30).optional(),
|
|
741
|
+
relations: z3.array(relationSchema).max(30).optional(),
|
|
742
|
+
applicability: z3.object({
|
|
743
|
+
environments: z3.array(z3.string()).max(20).optional(),
|
|
744
|
+
workspaces: z3.array(z3.string()).max(20).optional()
|
|
745
|
+
}).optional(),
|
|
746
|
+
idempotencyKey: z3.string().max(200).optional()
|
|
747
|
+
},
|
|
748
|
+
(a) => service.remember(a)
|
|
749
|
+
);
|
|
750
|
+
register("inspect", { id: z3.string().uuid() }, (a) => service.inspect(a.id));
|
|
751
|
+
register(
|
|
752
|
+
"revise",
|
|
753
|
+
{
|
|
754
|
+
id: z3.string().uuid(),
|
|
755
|
+
content: z3.string().max(8e3).optional(),
|
|
756
|
+
status: z3.enum(["active", "superseded", "archived", "expired"]).optional(),
|
|
757
|
+
pinned: z3.boolean().optional(),
|
|
758
|
+
expectedRevision: z3.number().int().positive()
|
|
759
|
+
},
|
|
760
|
+
(a) => service.revise(a.id, a.content, a.expectedRevision, {
|
|
761
|
+
...a.status ? { status: a.status } : {},
|
|
762
|
+
...a.pinned !== void 0 ? { pinned: a.pinned } : {}
|
|
763
|
+
})
|
|
764
|
+
);
|
|
765
|
+
register("forget", { id: z3.string().uuid() }, (a) => service.forget(a.id));
|
|
766
|
+
register(
|
|
767
|
+
"feedback",
|
|
768
|
+
{ id: z3.string().uuid(), useful: z3.boolean() },
|
|
769
|
+
(a) => service.feedback(a.id, a.useful)
|
|
770
|
+
);
|
|
771
|
+
let closed = false;
|
|
772
|
+
const closeStore = () => {
|
|
773
|
+
if (!closed) {
|
|
774
|
+
closed = true;
|
|
775
|
+
service.store.close();
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
server.server.onclose = closeStore;
|
|
779
|
+
const close = async () => {
|
|
780
|
+
await server.close();
|
|
781
|
+
closeStore();
|
|
782
|
+
};
|
|
783
|
+
process.once("SIGINT", () => void close());
|
|
784
|
+
process.once("SIGTERM", () => void close());
|
|
785
|
+
await server.connect(new StdioServerTransport());
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// packages/core/src/providers.ts
|
|
789
|
+
var providerRegistry = {
|
|
790
|
+
stores: [
|
|
791
|
+
{ name: "sqlite", implemented: true, canonical: true },
|
|
792
|
+
{ name: "postgresql", implemented: false, canonical: true }
|
|
793
|
+
],
|
|
794
|
+
vectors: [
|
|
795
|
+
{ name: "none", implemented: true },
|
|
796
|
+
{ name: "qdrant", implemented: false },
|
|
797
|
+
{ name: "pgvector", implemented: false }
|
|
798
|
+
],
|
|
799
|
+
embeddings: [{ name: "none", implemented: true }]
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
// packages/cli/src/index.ts
|
|
803
|
+
var { positionals: p, values: v } = parseArgs({
|
|
804
|
+
allowPositionals: true,
|
|
805
|
+
options: {
|
|
806
|
+
component: { type: "string" },
|
|
807
|
+
status: { type: "string" },
|
|
808
|
+
pinned: { type: "string" },
|
|
809
|
+
"expires-at": { type: "string" },
|
|
810
|
+
input: { type: "string" },
|
|
811
|
+
environment: { type: "string" },
|
|
812
|
+
workspace: { type: "string" },
|
|
813
|
+
project: { type: "string" },
|
|
814
|
+
kind: { type: "string" },
|
|
815
|
+
content: { type: "string" },
|
|
816
|
+
source: { type: "string" },
|
|
817
|
+
budget: { type: "string" },
|
|
818
|
+
"expected-revision": { type: "string" },
|
|
819
|
+
transport: { type: "string" },
|
|
820
|
+
scope: { type: "string" },
|
|
821
|
+
json: { type: "boolean" },
|
|
822
|
+
"dry-run": { type: "boolean" }
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
var output = (x) => console.log(JSON.stringify(x, null, 2));
|
|
826
|
+
async function main() {
|
|
827
|
+
const command = p[0];
|
|
828
|
+
let base = resolve2(String(v.project ?? process.cwd()));
|
|
829
|
+
if (command === "init") {
|
|
830
|
+
base = realpathSync2(base);
|
|
831
|
+
if (!existsSync3(configPath(base))) atomic(configPath(base), defaults());
|
|
832
|
+
const c2 = load(base);
|
|
833
|
+
new SqliteStore(database(base, c2)).close();
|
|
834
|
+
const ignore = join3(base, ".gitignore");
|
|
835
|
+
const old = existsSync3(ignore) ? readFileSync3(ignore, "utf8") : "";
|
|
836
|
+
for (const rule of [
|
|
837
|
+
".hippocampus/*.sqlite",
|
|
838
|
+
".hippocampus/*.sqlite-*",
|
|
839
|
+
".hippocampus/local.json"
|
|
840
|
+
])
|
|
841
|
+
if (!old.split("\n").includes(rule))
|
|
842
|
+
appendFileSync(ignore, "\n" + rule + "\n");
|
|
843
|
+
output({ project: base, projectId: c2.projectId });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (!command || command === "help") {
|
|
847
|
+
console.log(
|
|
848
|
+
"hippocampus init | config [show|get|set|validate|test] | remember | recall | memories | inspect | revise | forget | doctor | serve | connect"
|
|
849
|
+
);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (command === "config" && v.scope === "user") {
|
|
853
|
+
const user = userDefaults();
|
|
854
|
+
if (p[1] === "show") {
|
|
855
|
+
output(user);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (p[1] === "validate") {
|
|
859
|
+
output({ valid: true });
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (p[1] === "set") {
|
|
863
|
+
const keys = (p[2] ?? "").split(".");
|
|
864
|
+
if (keys.includes("projectId") || keys.some((k) => ["__proto__", "constructor", "prototype"].includes(k)))
|
|
865
|
+
throw Error("Invalid user field");
|
|
866
|
+
let target = user;
|
|
867
|
+
for (const key of keys.slice(0, -1)) {
|
|
868
|
+
target[key] ??= {};
|
|
869
|
+
target = target[key];
|
|
870
|
+
}
|
|
871
|
+
let value = p[3];
|
|
872
|
+
try {
|
|
873
|
+
value = JSON.parse(value);
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
target[keys.at(-1)] = value;
|
|
877
|
+
saveUser(user);
|
|
878
|
+
output(user);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (p[1] === "get") {
|
|
882
|
+
let value = user;
|
|
883
|
+
for (const key of (p[2] ?? "").split(".")) value = value?.[key];
|
|
884
|
+
if (value === void 0) throw Error("No user override for field");
|
|
885
|
+
output(value);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (!p[1]) {
|
|
889
|
+
if (!process.stdin.isTTY)
|
|
890
|
+
throw Error(
|
|
891
|
+
"Interactive config requires a TTY; use config show/get/set/validate --scope user"
|
|
892
|
+
);
|
|
893
|
+
const rl = createInterface({
|
|
894
|
+
input: process.stdin,
|
|
895
|
+
output: process.stdout
|
|
896
|
+
});
|
|
897
|
+
try {
|
|
898
|
+
console.log(
|
|
899
|
+
"User defaults apply before project configuration. SQLite remains the canonical store."
|
|
900
|
+
);
|
|
901
|
+
const budget = await rl.question("Default context budget [1500]: ");
|
|
902
|
+
const next = {
|
|
903
|
+
...user,
|
|
904
|
+
retrieval: {
|
|
905
|
+
...user.retrieval ?? {},
|
|
906
|
+
maxContextTokens: budget ? Number(budget) : 1500
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
output(next);
|
|
910
|
+
if ((await rl.question("Save? [y/N] ")).toLowerCase() === "y")
|
|
911
|
+
saveUser(next);
|
|
912
|
+
} finally {
|
|
913
|
+
rl.close();
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
throw Error("Use config show/get/set/validate --scope user");
|
|
918
|
+
}
|
|
919
|
+
base = root(base);
|
|
920
|
+
const c = effective(base);
|
|
921
|
+
if (command === "config") {
|
|
922
|
+
if (v.scope && v.scope !== "project")
|
|
923
|
+
throw Error("Unsupported configuration scope");
|
|
924
|
+
if (p[1] === "show") {
|
|
925
|
+
output(c);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (p[1] === "validate") {
|
|
929
|
+
output({ valid: true });
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (p[1] === "get") {
|
|
933
|
+
let x = c;
|
|
934
|
+
for (const key of (p[2] ?? "").split(".")) x = x?.[key];
|
|
935
|
+
if (x === void 0) throw Error("Unknown configuration field");
|
|
936
|
+
output(x);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (p[1] === "set") {
|
|
940
|
+
const candidate = load(base);
|
|
941
|
+
const keys = (p[2] ?? "").split(".");
|
|
942
|
+
if (keys.some((k) => ["__proto__", "constructor", "prototype"].includes(k)))
|
|
943
|
+
throw Error("Invalid field");
|
|
944
|
+
let target = candidate;
|
|
945
|
+
for (const key of keys.slice(0, -1)) {
|
|
946
|
+
if (!target[key]) throw Error("Unknown field");
|
|
947
|
+
target = target[key];
|
|
948
|
+
}
|
|
949
|
+
let value = p[3];
|
|
950
|
+
try {
|
|
951
|
+
value = JSON.parse(value);
|
|
952
|
+
} catch {
|
|
953
|
+
}
|
|
954
|
+
target[keys.at(-1)] = value;
|
|
955
|
+
const next = schema.parse(candidate);
|
|
956
|
+
if (next.projectId !== c.projectId)
|
|
957
|
+
throw Error("Project identity cannot be changed");
|
|
958
|
+
if (database(base, next) !== database(base, c)) {
|
|
959
|
+
const store2 = new SqliteStore(database(base, c));
|
|
960
|
+
const count = store2.list(c.projectId).length;
|
|
961
|
+
store2.close();
|
|
962
|
+
if (count)
|
|
963
|
+
throw Error(
|
|
964
|
+
"Cannot change a nonempty store path; store migration is not implemented"
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
atomic(configPath(base), next);
|
|
968
|
+
output(next);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (!p[1]) {
|
|
972
|
+
if (!process.stdin.isTTY)
|
|
973
|
+
throw Error(
|
|
974
|
+
"Interactive config requires a TTY; use config show/get/set/validate/test"
|
|
975
|
+
);
|
|
976
|
+
const rl = createInterface({
|
|
977
|
+
input: process.stdin,
|
|
978
|
+
output: process.stdout
|
|
979
|
+
});
|
|
980
|
+
try {
|
|
981
|
+
console.log(
|
|
982
|
+
"SQLite is the canonical store; FTS5 is the retrieval index. Vector providers are planned."
|
|
983
|
+
);
|
|
984
|
+
const budget = await rl.question("Context budget [1500]: ");
|
|
985
|
+
const next = schema.parse({
|
|
986
|
+
...c,
|
|
987
|
+
retrieval: {
|
|
988
|
+
...c.retrieval,
|
|
989
|
+
maxContextTokens: budget ? Number(budget) : 1500
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
output(next);
|
|
993
|
+
if ((await rl.question("Save? [y/N] ")).toLowerCase() === "y")
|
|
994
|
+
atomic(configPath(base), next);
|
|
995
|
+
} finally {
|
|
996
|
+
rl.close();
|
|
997
|
+
}
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (command === "connect" || command === "disconnect") {
|
|
1002
|
+
output(
|
|
1003
|
+
connect(
|
|
1004
|
+
p[1] ?? "",
|
|
1005
|
+
base,
|
|
1006
|
+
resolve2(process.argv[1]),
|
|
1007
|
+
v.scope ?? "project",
|
|
1008
|
+
v["dry-run"] ?? false,
|
|
1009
|
+
command === "disconnect"
|
|
1010
|
+
)
|
|
1011
|
+
);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const store = new SqliteStore(database(base, c));
|
|
1015
|
+
const service = new Service(store, c.projectId);
|
|
1016
|
+
if (command === "serve") {
|
|
1017
|
+
if (typeof v.project !== "string" || !isAbsolute(v.project))
|
|
1018
|
+
throw Error("serve requires an absolute --project path");
|
|
1019
|
+
if (v.transport && v.transport !== "stdio")
|
|
1020
|
+
throw Error("Only stdio is supported");
|
|
1021
|
+
const snapshot = JSON.stringify(c);
|
|
1022
|
+
await serve(service, () => {
|
|
1023
|
+
if (JSON.stringify(effective(base)) !== snapshot)
|
|
1024
|
+
throw Error("Configuration changed; restart MCP server");
|
|
1025
|
+
});
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
try {
|
|
1029
|
+
switch (command) {
|
|
1030
|
+
case "config":
|
|
1031
|
+
if (p[1] !== "test") throw Error("Unknown config command");
|
|
1032
|
+
output({ connected: true, provider: "sqlite" });
|
|
1033
|
+
break;
|
|
1034
|
+
case "remember":
|
|
1035
|
+
if (v.input) {
|
|
1036
|
+
const raw = readFileSync3(resolve2(v.input), "utf8");
|
|
1037
|
+
if (Buffer.byteLength(raw) > 32e3)
|
|
1038
|
+
throw Error("Input file exceeds 32KB");
|
|
1039
|
+
output(service.remember(JSON.parse(raw)));
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
output(
|
|
1043
|
+
service.remember({
|
|
1044
|
+
kind: v.kind,
|
|
1045
|
+
content: String(v.content ?? ""),
|
|
1046
|
+
evidence: v.source ? [{ type: "other", reference: String(v.source) }] : []
|
|
1047
|
+
})
|
|
1048
|
+
);
|
|
1049
|
+
break;
|
|
1050
|
+
case "recall":
|
|
1051
|
+
output(
|
|
1052
|
+
service.recall(
|
|
1053
|
+
p[1] ?? "",
|
|
1054
|
+
Number(v.budget ?? c.retrieval.maxContextTokens),
|
|
1055
|
+
v.environment,
|
|
1056
|
+
v.workspace,
|
|
1057
|
+
v.component
|
|
1058
|
+
)
|
|
1059
|
+
);
|
|
1060
|
+
break;
|
|
1061
|
+
case "memories":
|
|
1062
|
+
output(store.list(c.projectId));
|
|
1063
|
+
break;
|
|
1064
|
+
case "inspect":
|
|
1065
|
+
output(service.inspect(p[1] ?? ""));
|
|
1066
|
+
break;
|
|
1067
|
+
case "revise":
|
|
1068
|
+
output(
|
|
1069
|
+
service.revise(
|
|
1070
|
+
p[1] ?? "",
|
|
1071
|
+
v.content,
|
|
1072
|
+
Number(v["expected-revision"]),
|
|
1073
|
+
{
|
|
1074
|
+
...v.status ? { status: v.status } : {},
|
|
1075
|
+
...v.pinned ? { pinned: v.pinned === "true" } : {},
|
|
1076
|
+
...v["expires-at"] ? { expiresAt: v["expires-at"] } : {}
|
|
1077
|
+
}
|
|
1078
|
+
)
|
|
1079
|
+
);
|
|
1080
|
+
break;
|
|
1081
|
+
case "forget":
|
|
1082
|
+
output(service.forget(p[1] ?? ""));
|
|
1083
|
+
break;
|
|
1084
|
+
case "doctor":
|
|
1085
|
+
output({
|
|
1086
|
+
projectId: c.projectId,
|
|
1087
|
+
database: database(base, c),
|
|
1088
|
+
providers: providerRegistry,
|
|
1089
|
+
telemetry: false
|
|
1090
|
+
});
|
|
1091
|
+
break;
|
|
1092
|
+
case "consolidate":
|
|
1093
|
+
if (!v["dry-run"]) throw Error("Only review previews are supported");
|
|
1094
|
+
output({
|
|
1095
|
+
proposals: store.list(c.projectId).filter(
|
|
1096
|
+
(m) => m.expiresAt && m.expiresAt < (/* @__PURE__ */ new Date()).toISOString()
|
|
1097
|
+
).map((m) => ({ id: m.id, action: "review expiry" }))
|
|
1098
|
+
});
|
|
1099
|
+
break;
|
|
1100
|
+
default:
|
|
1101
|
+
throw Error("Unknown command");
|
|
1102
|
+
}
|
|
1103
|
+
} finally {
|
|
1104
|
+
store.close();
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
main().catch((e) => {
|
|
1108
|
+
console.error(
|
|
1109
|
+
JSON.stringify({ code: e.code ?? "INVALID_INPUT", message: e.message })
|
|
1110
|
+
);
|
|
1111
|
+
process.exitCode = e.code === "REVISION_CONFLICT" ? 3 : 1;
|
|
1112
|
+
});
|