agent-journal 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/index.js +1952 -0
  4. package/package.json +65 -0
package/dist/index.js ADDED
@@ -0,0 +1,1952 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ // src/db/connection.ts
8
+ import fs from "node:fs";
9
+ import path2 from "node:path";
10
+ import Database from "better-sqlite3";
11
+ import * as sqliteVec from "sqlite-vec";
12
+
13
+ // src/db/migrations.ts
14
+ var VERSION = 1;
15
+ function runMigrations(db) {
16
+ const current = db.pragma("user_version", { simple: true });
17
+ if (current >= VERSION) {
18
+ return;
19
+ }
20
+ db.transaction(() => {
21
+ db.exec(`
22
+ CREATE TABLE IF NOT EXISTS project (
23
+ id TEXT PRIMARY KEY,
24
+ resolution_key TEXT NOT NULL UNIQUE,
25
+ display_name TEXT,
26
+ config TEXT,
27
+ created_at INTEGER NOT NULL
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS entity (
31
+ id TEXT PRIMARY KEY,
32
+ project_id TEXT NOT NULL REFERENCES project(id),
33
+ type TEXT NOT NULL,
34
+ title TEXT NOT NULL,
35
+ summary TEXT,
36
+ tags TEXT,
37
+ created_at INTEGER NOT NULL,
38
+ last_updated_at INTEGER NOT NULL,
39
+ last_accessed_at INTEGER,
40
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','invalid')),
41
+ invalidation_note TEXT,
42
+ invalidated_at INTEGER,
43
+ superseded_by TEXT REFERENCES entity(id)
44
+ );
45
+ CREATE INDEX IF NOT EXISTS idx_entity_project ON entity(project_id, status);
46
+
47
+ CREATE TABLE IF NOT EXISTS statement (
48
+ id TEXT PRIMARY KEY,
49
+ project_id TEXT NOT NULL REFERENCES project(id),
50
+ entity_id TEXT NOT NULL REFERENCES entity(id),
51
+ edge_id TEXT REFERENCES relationship(id),
52
+ claim TEXT NOT NULL,
53
+ confidence_level TEXT NOT NULL CHECK (confidence_level IN ('low','medium','high','verified')),
54
+ confidence_reason TEXT NOT NULL,
55
+ derivation_method TEXT NOT NULL CHECK (derivation_method IN
56
+ ('direct-observation','command-output','user-assertion','inference','external-doc')),
57
+ citations TEXT,
58
+ created_at INTEGER NOT NULL,
59
+ last_accessed_at INTEGER,
60
+ valid_from INTEGER,
61
+ valid_to INTEGER,
62
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','invalid')),
63
+ invalidation_note TEXT,
64
+ invalidated_at INTEGER,
65
+ superseded_by TEXT REFERENCES statement(id)
66
+ );
67
+ CREATE INDEX IF NOT EXISTS idx_stmt_entity ON statement(entity_id, status);
68
+ CREATE INDEX IF NOT EXISTS idx_stmt_project ON statement(project_id, status);
69
+ CREATE INDEX IF NOT EXISTS idx_stmt_superby ON statement(superseded_by);
70
+ CREATE INDEX IF NOT EXISTS idx_stmt_created ON statement(project_id, created_at);
71
+
72
+ CREATE TABLE IF NOT EXISTS relationship (
73
+ id TEXT PRIMARY KEY,
74
+ project_id TEXT NOT NULL REFERENCES project(id),
75
+ from_entity TEXT NOT NULL REFERENCES entity(id),
76
+ to_entity TEXT NOT NULL REFERENCES entity(id),
77
+ type TEXT NOT NULL,
78
+ confidence_level TEXT NOT NULL CHECK (confidence_level IN ('low','medium','high','verified')),
79
+ confidence_reason TEXT NOT NULL,
80
+ derivation_method TEXT NOT NULL,
81
+ citations TEXT,
82
+ created_at INTEGER NOT NULL,
83
+ last_accessed_at INTEGER,
84
+ valid_from INTEGER,
85
+ valid_to INTEGER,
86
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','invalid')),
87
+ invalidation_note TEXT,
88
+ invalidated_at INTEGER,
89
+ superseded_by TEXT REFERENCES relationship(id)
90
+ );
91
+
92
+ CREATE TABLE IF NOT EXISTS journal_entry (
93
+ id TEXT PRIMARY KEY,
94
+ project_id TEXT NOT NULL REFERENCES project(id),
95
+ created_at INTEGER NOT NULL,
96
+ commands TEXT,
97
+ proven TEXT,
98
+ disproven TEXT,
99
+ narrative TEXT,
100
+ is_stub INTEGER NOT NULL DEFAULT 0,
101
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','invalid')),
102
+ superseded_by TEXT REFERENCES journal_entry(id),
103
+ invalidated_at INTEGER
104
+ );
105
+ CREATE INDEX IF NOT EXISTS idx_jrnl_project ON journal_entry(project_id, created_at);
106
+
107
+ CREATE TABLE IF NOT EXISTS journal_link (
108
+ journal_id TEXT NOT NULL REFERENCES journal_entry(id),
109
+ target_type TEXT NOT NULL CHECK (target_type IN ('entity','statement','relationship','journal_entry')),
110
+ target_id TEXT NOT NULL,
111
+ role TEXT NOT NULL CHECK (role IN ('created','changed','proven','disproven','deleted')),
112
+ PRIMARY KEY (journal_id, target_type, target_id, role)
113
+ );
114
+ CREATE INDEX IF NOT EXISTS idx_jlink_target ON journal_link(target_type, target_id);
115
+
116
+ CREATE TABLE IF NOT EXISTS embedding (
117
+ owner_type TEXT NOT NULL CHECK (owner_type IN ('statement','journal_entry')),
118
+ owner_id TEXT NOT NULL,
119
+ vec_rowid INTEGER NOT NULL,
120
+ model_id TEXT NOT NULL,
121
+ dim INTEGER NOT NULL,
122
+ content_hash TEXT NOT NULL,
123
+ PRIMARY KEY (owner_type, owner_id)
124
+ );
125
+ CREATE INDEX IF NOT EXISTS idx_emb_vecrow ON embedding(vec_rowid);
126
+
127
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_index USING vec0(embedding float[384]);
128
+
129
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_statements USING fts5(
130
+ statement_id UNINDEXED, claim, entity_title
131
+ );
132
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_journal USING fts5(
133
+ journal_id UNINDEXED, narrative, commands
134
+ );
135
+ `);
136
+ db.pragma(`user_version = ${VERSION}`);
137
+ })();
138
+ }
139
+
140
+ // src/domain/paths.ts
141
+ import crypto from "node:crypto";
142
+ import os from "node:os";
143
+ import path from "node:path";
144
+ var CONFIG_DIR_NAME = "agent-memory";
145
+ function configRoot() {
146
+ const base = process.env.XDG_CONFIG_DIR ?? process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
147
+ return path.join(base, CONFIG_DIR_NAME);
148
+ }
149
+ function projectConfigPathForKey(key) {
150
+ const hash = crypto.createHash("sha256").update(key).digest("hex");
151
+ return path.join(configRoot(), `project_${hash}.json`);
152
+ }
153
+ function modelCacheDir() {
154
+ return configRoot();
155
+ }
156
+
157
+ // src/db/connection.ts
158
+ function defaultDbPath() {
159
+ return process.env.AGENT_JOURNAL_DB ?? path2.join(configRoot(), "memory.db");
160
+ }
161
+ function openDb(file) {
162
+ fs.mkdirSync(path2.dirname(file), { recursive: true });
163
+ const isNew = !fs.existsSync(file);
164
+ const db = new Database(file);
165
+ sqliteVec.load(db);
166
+ if (isNew) {
167
+ db.pragma("auto_vacuum = INCREMENTAL");
168
+ }
169
+ db.pragma("journal_mode = WAL");
170
+ db.pragma("busy_timeout = 5000");
171
+ db.pragma("foreign_keys = ON");
172
+ db.pragma("synchronous = NORMAL");
173
+ runMigrations(db);
174
+ return db;
175
+ }
176
+
177
+ // src/domain/embeddings.ts
178
+ import crypto2 from "node:crypto";
179
+ import fs2 from "node:fs";
180
+ import path3 from "node:path";
181
+ import { env, pipeline } from "@huggingface/transformers";
182
+ var HUB_MODEL_ID = "Xenova/bge-small-en-v1.5";
183
+ var STORED_MODEL_ID = "bge-small-en-v1.5";
184
+ var DIM = 384;
185
+ var QUERY_PREFIX = "Represent this sentence for searching relevant passages: ";
186
+ var CORRUPT_MODEL_PATTERNS = [
187
+ /protobuf parsing failed/i,
188
+ /load model from .* failed/i,
189
+ /failed to load model/i,
190
+ /deserialize tensor/i,
191
+ /invalid model/i,
192
+ /unexpected end/i
193
+ ];
194
+ function isCorruptModelError(err) {
195
+ const message = err instanceof Error ? err.message : String(err);
196
+ return CORRUPT_MODEL_PATTERNS.some((pattern) => pattern.test(message));
197
+ }
198
+ var TransformersEmbeddings = class {
199
+ loadPromise = null;
200
+ cacheDir;
201
+ constructor() {
202
+ this.cacheDir = modelCacheDir();
203
+ fs2.mkdirSync(this.cacheDir, { recursive: true });
204
+ env.cacheDir = this.cacheDir;
205
+ env.allowLocalModels = false;
206
+ }
207
+ warmup() {
208
+ void this.embedDocument("warmup").catch((err) => {
209
+ console.error("agent-journal embedding warmup failed:", err);
210
+ });
211
+ }
212
+ async ready() {
213
+ await this.load();
214
+ }
215
+ async embedQuery(text) {
216
+ return this.embed(`${QUERY_PREFIX}${text}`);
217
+ }
218
+ async embedDocument(text) {
219
+ return this.embed(text);
220
+ }
221
+ modelId() {
222
+ return STORED_MODEL_ID;
223
+ }
224
+ dim() {
225
+ return DIM;
226
+ }
227
+ contentHash(documentText) {
228
+ return crypto2.createHash("sha256").update(`${STORED_MODEL_ID}
229
+ ${documentText}`).digest("hex");
230
+ }
231
+ load() {
232
+ if (!this.loadPromise) {
233
+ const attempt = this.loadWithRecovery();
234
+ attempt.catch(() => {
235
+ if (this.loadPromise === attempt) this.loadPromise = null;
236
+ });
237
+ this.loadPromise = attempt;
238
+ }
239
+ return this.loadPromise;
240
+ }
241
+ async loadWithRecovery() {
242
+ try {
243
+ return await this.loadPipeline();
244
+ } catch (err) {
245
+ if (!isCorruptModelError(err)) throw err;
246
+ console.error("agent-journal: cached model appears corrupt; re-downloading.", err);
247
+ this.purgeModelCache();
248
+ return await this.loadPipeline();
249
+ }
250
+ }
251
+ loadPipeline() {
252
+ const loadPipeline = pipeline;
253
+ return loadPipeline("feature-extraction", HUB_MODEL_ID, { dtype: "q8" });
254
+ }
255
+ purgeModelCache() {
256
+ const modelDir = path3.join(this.cacheDir, ...HUB_MODEL_ID.split("/"));
257
+ fs2.rmSync(modelDir, { recursive: true, force: true });
258
+ }
259
+ async embed(text) {
260
+ const extractor = await this.load();
261
+ const output = await extractor(text, { pooling: "cls", normalize: true });
262
+ if (output.data.length !== DIM) {
263
+ throw new Error(`Embedding model returned ${output.data.length} dimensions, expected ${DIM}`);
264
+ }
265
+ return output.data;
266
+ }
267
+ };
268
+
269
+ // src/domain/project.ts
270
+ import fs3 from "node:fs";
271
+ import path4 from "node:path";
272
+ import { execFileSync } from "node:child_process";
273
+
274
+ // src/domain/ids.ts
275
+ import { ulid } from "ulid";
276
+ var PREFIX = {
277
+ project: "proj",
278
+ entity: "ent",
279
+ statement: "stmt",
280
+ relationship: "rel",
281
+ journal: "jrnl",
282
+ embedding: "emb"
283
+ };
284
+ var PREFIX_TO_KIND = new Map(
285
+ Object.entries(PREFIX).map(([kind, prefix]) => [prefix, kind])
286
+ );
287
+ var newId = (kind) => `${PREFIX[kind]}_${ulid()}`;
288
+ function idKind(id) {
289
+ const [prefix] = id.split("_", 1);
290
+ return PREFIX_TO_KIND.get(prefix) ?? null;
291
+ }
292
+
293
+ // src/domain/time.ts
294
+ var now = () => Date.now();
295
+ var UNIT_MS = {
296
+ s: 1e3,
297
+ m: 60 * 1e3,
298
+ h: 60 * 60 * 1e3,
299
+ d: 24 * 60 * 60 * 1e3,
300
+ y: 365 * 24 * 60 * 60 * 1e3
301
+ };
302
+ function parseDurationMs(value) {
303
+ const match = /^(\d+)(s|m|h|d|y)$/.exec(value);
304
+ if (!match) {
305
+ throw new Error(`Invalid duration "${value}". Expected e.g. 90d, 12h, 30m, 45s, or 5y.`);
306
+ }
307
+ const amount = Number(match[1]);
308
+ const unit = match[2];
309
+ return amount * UNIT_MS[unit];
310
+ }
311
+ var RELATIVE_UNITS = [
312
+ ["year", UNIT_MS.y],
313
+ ["month", 30 * UNIT_MS.d],
314
+ ["week", 7 * UNIT_MS.d],
315
+ ["day", UNIT_MS.d],
316
+ ["hour", UNIT_MS.h],
317
+ ["minute", UNIT_MS.m],
318
+ ["second", UNIT_MS.s]
319
+ ];
320
+ function humanizeRelative(timestamp, reference = now()) {
321
+ const diff = reference - timestamp;
322
+ const abs = Math.abs(diff);
323
+ if (abs < 5 * UNIT_MS.s) return "just now";
324
+ for (const [unit, ms] of RELATIVE_UNITS) {
325
+ if (abs >= ms) {
326
+ const value = Math.floor(abs / ms);
327
+ const label = value === 1 ? unit : `${unit}s`;
328
+ return diff >= 0 ? `${value} ${label} ago` : `in ${value} ${label}`;
329
+ }
330
+ }
331
+ return "just now";
332
+ }
333
+ function relativeMap(record, fields, reference = now()) {
334
+ const out = {};
335
+ for (const field of fields) {
336
+ const value = record[field];
337
+ if (typeof value === "number") out[field] = humanizeRelative(value, reference);
338
+ }
339
+ return out;
340
+ }
341
+
342
+ // src/domain/project.ts
343
+ function git(args, cwd) {
344
+ try {
345
+ return execFileSync("git", args, {
346
+ cwd,
347
+ encoding: "utf8",
348
+ stdio: ["ignore", "pipe", "ignore"]
349
+ }).trim();
350
+ } catch {
351
+ return null;
352
+ }
353
+ }
354
+ function normalizeOriginUrl(raw) {
355
+ let value = raw.trim();
356
+ const scpMatch = /^([^@/:]+@)?([^:]+):(.+)$/.exec(value);
357
+ if (scpMatch && !value.includes("://")) {
358
+ value = `${scpMatch[2]}/${scpMatch[3]}`;
359
+ } else {
360
+ try {
361
+ const url = new URL(value);
362
+ value = `${url.hostname.toLowerCase()}${url.pathname}`;
363
+ } catch {
364
+ value = value.replace(/^[^@/]+@/, "");
365
+ const slash2 = value.indexOf("/");
366
+ if (slash2 > 0) {
367
+ value = `${value.slice(0, slash2).toLowerCase()}${value.slice(slash2)}`;
368
+ }
369
+ }
370
+ }
371
+ value = value.replace(/\/+$/, "").replace(/\.git$/, "");
372
+ const slash = value.indexOf("/");
373
+ if (slash > 0) {
374
+ value = `${value.slice(0, slash).toLowerCase()}${value.slice(slash)}`;
375
+ }
376
+ return value;
377
+ }
378
+ function readProjectConfigForKey(key) {
379
+ const file = projectConfigPathForKey(key);
380
+ if (!fs3.existsSync(file)) {
381
+ return null;
382
+ }
383
+ return JSON.parse(fs3.readFileSync(file, "utf8"));
384
+ }
385
+ function displayNameForKey(key) {
386
+ const trimmed = key.replace(/\/+$/, "");
387
+ return path4.basename(trimmed) || trimmed;
388
+ }
389
+ function getOrCreateProject(db, resolutionKey, fileConfig) {
390
+ const existing = db.prepare("SELECT id, resolution_key, config FROM project WHERE resolution_key = ?").get(resolutionKey);
391
+ if (existing) {
392
+ return {
393
+ id: existing.id,
394
+ resolutionKey: existing.resolution_key,
395
+ configJson: existing.config,
396
+ fileConfig
397
+ };
398
+ }
399
+ const id = newId("project");
400
+ db.prepare("INSERT INTO project(id, resolution_key, display_name, config, created_at) VALUES (?, ?, ?, NULL, ?)").run(
401
+ id,
402
+ resolutionKey,
403
+ displayNameForKey(resolutionKey),
404
+ now()
405
+ );
406
+ return { id, resolutionKey, configJson: null, fileConfig };
407
+ }
408
+ var ProjectResolver = class {
409
+ constructor(db, launchCwd = process.cwd()) {
410
+ this.db = db;
411
+ this.launchCwd = launchCwd;
412
+ }
413
+ db;
414
+ launchCwd;
415
+ launchContext = null;
416
+ resolve(projectOverride) {
417
+ if (projectOverride) {
418
+ return getOrCreateProject(this.db, projectOverride);
419
+ }
420
+ if (!this.launchContext) {
421
+ const resolved = this.resolveLaunchCwd();
422
+ this.launchContext = getOrCreateProject(this.db, resolved.key, resolved.fileConfig);
423
+ }
424
+ return this.launchContext;
425
+ }
426
+ resolveLaunchCwd() {
427
+ const key = this.resolveRepoKey();
428
+ const projectConfig = readProjectConfigForKey(key);
429
+ return { key: projectConfig?.project ?? key, fileConfig: projectConfig?.config };
430
+ }
431
+ resolveRepoKey() {
432
+ const origin = git(["config", "--get", "remote.origin.url"], this.launchCwd);
433
+ if (origin) {
434
+ return normalizeOriginUrl(origin);
435
+ }
436
+ const commonDir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"], this.launchCwd);
437
+ if (commonDir) {
438
+ return realpathIfPossible(path4.dirname(commonDir));
439
+ }
440
+ return realpathIfPossible(path4.resolve(this.launchCwd));
441
+ }
442
+ };
443
+ function realpathIfPossible(value) {
444
+ try {
445
+ return fs3.realpathSync(value);
446
+ } catch {
447
+ return value;
448
+ }
449
+ }
450
+
451
+ // src/server.ts
452
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
453
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
454
+
455
+ // src/tools/schemas.ts
456
+ import { z } from "zod";
457
+ var confidenceLevel = z.enum(["low", "medium", "high", "verified"]).describe(
458
+ "How sure you are: verified = you observed it now this session (ran the command, read the source); high = strong evidence but not re-confirmed now; medium = reasonable inference from solid signals; low = weak guess readers should discount."
459
+ );
460
+ var derivationMethod = z.enum(["direct-observation", "command-output", "user-assertion", "inference", "external-doc"]).describe(
461
+ "How you learned it: direct-observation = you inspected the project/runtime; command-output = a command produced the evidence; user-assertion = the user told you; inference = you reasoned it from surrounding evidence; external-doc = an authoritative external document."
462
+ );
463
+ var projectParam = z.string().optional().describe(
464
+ "Override the auto-resolved project key. Omit in normal use \u2014 only set this to deliberately read or write another project\u2019s memory."
465
+ );
466
+ var memorySearchShape = {
467
+ query: z.string().min(1).describe("Natural-language or keyword query. Hybrid keyword + embedding search."),
468
+ where: z.enum(["knowledge-base", "journal", "both"]).default("both").describe("Scope: knowledge-base (facts), journal (activity log), or both."),
469
+ type: z.string().optional().describe("Restrict KB hits to entities of this type."),
470
+ tags: z.array(z.string()).optional().describe("Restrict KB hits to entities carrying all these tags."),
471
+ include_invalid: z.boolean().default(false).describe("Include retired/invalid records. Leave false unless looking for history or a live search missed."),
472
+ include_deleted_since: z.string().optional().describe('Duration like "30d" widening how far back invalid records remain visible (requires include_invalid).'),
473
+ limit: z.number().int().min(1).max(100).default(20).describe("Maximum records to return (1-100)."),
474
+ project: projectParam
475
+ };
476
+ var memorySearchSchema = z.object(memorySearchShape);
477
+ var memoryGetShape = {
478
+ id: z.string().describe("Id of an entity, statement, relationship, or journal entry to fetch in full."),
479
+ include_invalid_statements: z.boolean().default(false).describe("When fetching an entity, also return its retired statements."),
480
+ project: projectParam
481
+ };
482
+ var memoryGetSchema = z.object(memoryGetShape);
483
+ var kbUpsertEntityShape = {
484
+ id: z.string().optional().describe("Existing entity id to update. Omit to create a new entity."),
485
+ type: z.string().min(1).describe("Category of the subject, e.g. Service, File, Person, Config, Concept. Used to filter searches."),
486
+ title: z.string().min(1).describe("Human-readable name of the subject. Searched as a keyword."),
487
+ summary: z.string().optional().describe("Short description of the subject only. Do NOT put facts here \u2014 those belong in statements."),
488
+ tags: z.array(z.string()).optional().describe("Optional labels for filtering searches."),
489
+ project: projectParam
490
+ };
491
+ var kbUpsertEntitySchema = z.object(kbUpsertEntityShape);
492
+ var kbAddStatementShape = {
493
+ entity_id: z.string(),
494
+ claim: z.string().min(1).describe("One atomic, self-contained fact about the entity. Keep it to a single claim."),
495
+ confidence_level: confidenceLevel,
496
+ confidence_reason: z.string().min(1).describe("One honest sentence on why you chose this confidence and how you learned the fact."),
497
+ derivation_method: derivationMethod,
498
+ citations: z.array(z.string()).optional().describe("Optional source references backing the claim, e.g. file paths, URLs, or commit ids."),
499
+ valid_from: z.number().int().optional().describe("Optional Unix epoch milliseconds: when the fact starts holding. Omit unless time-bounded."),
500
+ valid_to: z.number().int().optional().describe("Optional Unix epoch milliseconds: when the fact stops holding. Omit unless time-bounded."),
501
+ journal_entry_id: z.string().optional().describe("Id from journal.append linking this claim to the work that produced it. If omitted, a stub is created."),
502
+ project: projectParam
503
+ };
504
+ var kbAddStatementSchema = z.object(kbAddStatementShape);
505
+ var kbEditStatementShape = {
506
+ statement_id: z.string().describe("Id of the active statement to correct. It is superseded by the replacement."),
507
+ claim: z.string().optional().describe("New claim text. Omitted fields are inherited from the original statement."),
508
+ confidence_level: confidenceLevel.optional(),
509
+ confidence_reason: z.string().optional(),
510
+ derivation_method: derivationMethod.optional(),
511
+ citations: z.array(z.string()).optional().describe("Replacement source references. Omit to keep the original statement citations."),
512
+ valid_from: z.number().int().optional().describe("Optional Unix epoch milliseconds: when the fact starts holding."),
513
+ valid_to: z.number().int().optional().describe("Optional Unix epoch milliseconds: when the fact stops holding."),
514
+ invalidation_note: z.string().optional().describe("Optional note recorded on the superseded statement explaining the correction."),
515
+ journal_entry_id: z.string().optional().describe("Id from journal.append tying this correction to your work. If omitted, a stub is created."),
516
+ project: projectParam
517
+ };
518
+ var kbEditStatementSchema = z.object(kbEditStatementShape);
519
+ var kbInvalidateShape = {
520
+ id: z.string().describe("Id of the active statement or entity to retire."),
521
+ note: z.string().min(1).describe("Required explanation of why this record is being retired."),
522
+ superseded_by: z.string().optional().describe("Optional id of the same-type record that replaces this one; readers are redirected to it."),
523
+ project: projectParam
524
+ };
525
+ var kbInvalidateSchema = z.object(kbInvalidateShape);
526
+ var journalAppendShape = {
527
+ narrative: z.string().optional().describe("Prose account of what you did and what you concluded. Indexed for search."),
528
+ commands: z.array(z.string()).optional().describe("Commands you ran, as you ran them."),
529
+ proven: z.array(z.string()).optional().describe('Statement ids this work confirmed. Each is linked with role "proven".'),
530
+ disproven: z.array(z.string()).optional().describe('Statement ids this work contradicted. Each is linked with role "disproven".'),
531
+ links: z.array(
532
+ z.object({
533
+ target_type: z.enum(["entity", "statement", "relationship"]),
534
+ target_id: z.string(),
535
+ role: z.enum(["created", "changed", "proven", "disproven"])
536
+ })
537
+ ).optional().describe("Explicit links from this entry to KB records it created or changed."),
538
+ project: projectParam
539
+ };
540
+ var journalAppendSchema = z.object(journalAppendShape);
541
+ var kbDeleteShape = {
542
+ id: z.string().describe("Id of the record to permanently delete. Prefer kb.invalidate unless the content is poisoned."),
543
+ reason: z.string().min(1).describe("Required audit reason for deletion. Do not repeat the secret or sensitive value here."),
544
+ project: projectParam
545
+ };
546
+ var kbDeleteSchema = z.object(kbDeleteShape);
547
+ var memoryRecentShape = {
548
+ where: z.enum(["knowledge-base", "journal", "both"]).default("both").describe("Scope: knowledge-base, journal, or both."),
549
+ kind: z.enum(["entity", "statement", "journal"]).optional().describe("Restrict to one record kind."),
550
+ limit: z.number().int().min(1).max(100).default(20).describe("Page size (1-100)."),
551
+ before: z.number().int().optional().describe("Unix epoch milliseconds cursor: return records created strictly before this, for paging."),
552
+ include_invalid: z.boolean().default(false).describe("Include retired/invalid records."),
553
+ project: projectParam
554
+ };
555
+ var memoryRecentSchema = z.object(memoryRecentShape);
556
+ var memoryStatsShape = {
557
+ project: projectParam
558
+ };
559
+ var memoryStatsSchema = z.object(memoryStatsShape);
560
+ var emptyShape = {};
561
+ var emptySchema = z.object(emptyShape);
562
+
563
+ // src/text/instructions.ts
564
+ var INSTRUCTIONS = `agent-journal is a project-scoped persistent memory server: a confidence-tracked knowledge base of immutable statements plus an append-only journal. The project is resolved automatically from the repo, so you do not need to pass project in normal use.
565
+
566
+ Search before acting: call memory.search at the start of a task and before assuming any project fact. As you learn things, write them as KB statements (attach atomic claims to an entity via kb.upsert_entity then kb.add_statement) and journal what you did or proved with journal.append. Capturing knowledge is part of the task, not optional cleanup.
567
+
568
+ Every statement needs confidence_level, confidence_reason, and derivation_method. verified means you observed it now (ran the command, read the source), not that you feel confident; be honest about how you learned it.
569
+
570
+ Prefer to call journal.append first and pass its id as journal_entry_id when adding or editing statements, so the claim and the work that produced it share one entry. If you omit it, the server records an auto-stub and nudges you to follow up.
571
+
572
+ Statements are immutable: use kb.edit_statement to correct a claim (it supersedes the old one) or kb.invalidate to retire stale knowledge \u2014 never expect in-place mutation. Invalid records stay readable via memory.get but are excluded from search unless you pass include_invalid.
573
+
574
+ Never write credentials, secrets, or PII into statements or journals. If poisoned content slips in, use kb.delete (not kb.invalidate) with a reason that does not repeat the secret. For the full playbook, call memory.guide.`;
575
+
576
+ // src/server.ts
577
+ function jsonResult(value) {
578
+ return {
579
+ content: [{ type: "text", text: JSON.stringify(value) }]
580
+ };
581
+ }
582
+ function errorMessage(err) {
583
+ if (err instanceof Error) return err.message;
584
+ return String(err);
585
+ }
586
+ function wrap(handler) {
587
+ return Promise.resolve().then(handler).then(jsonResult).catch((err) => {
588
+ console.error(err instanceof Error && err.stack ? err.stack : err);
589
+ return {
590
+ isError: true,
591
+ content: [{ type: "text", text: errorMessage(err) }]
592
+ };
593
+ });
594
+ }
595
+ function createMcpServer(api, version = "0.1.0") {
596
+ const server = new McpServer(
597
+ { name: "agent-journal", version },
598
+ {
599
+ instructions: INSTRUCTIONS
600
+ }
601
+ );
602
+ server.registerTool(
603
+ "memory.search",
604
+ {
605
+ description: "Hybrid keyword + embedding search over the project KB and/or journal. Call this first on any task and before assuming a project fact \u2014 it is the primary way to recall memory. Returns compact scored snippets; follow up with memory.get for full records. KB hits are statements grouped under their entity. Invalid records are excluded by default; pass include_invalid only for history or after a live search misses. Read-only: does not bump last_accessed.",
606
+ inputSchema: memorySearchShape
607
+ },
608
+ (args) => wrap(() => api.search(args))
609
+ );
610
+ server.registerTool(
611
+ "memory.get",
612
+ {
613
+ description: "Fetch a full entity, statement, relationship, or journal entry by ID. Direct statement/entity reads bump last_accessed_at. Invalid statements are still returned and clearly flagged, with a redirect when superseded.",
614
+ inputSchema: memoryGetShape
615
+ },
616
+ (args) => wrap(() => api.get(args))
617
+ );
618
+ server.registerTool(
619
+ "memory.recent",
620
+ {
621
+ description: "Chronological latest-to-oldest view for situational awareness and paging. This is not relevance search; use memory.search for retrieval. Invalid records are excluded unless include_invalid is true.",
622
+ inputSchema: memoryRecentShape
623
+ },
624
+ (args) => wrap(() => api.recent(args))
625
+ );
626
+ server.registerTool(
627
+ "memory.stats",
628
+ {
629
+ description: "Return project record counts by status, embedding count, DB file size, model metadata, and freelist_count.",
630
+ inputSchema: memoryStatsShape
631
+ },
632
+ (args) => wrap(() => api.stats(args))
633
+ );
634
+ server.registerTool(
635
+ "memory.guide",
636
+ {
637
+ description: "Return the full agent-journal operating playbook: search discipline, journaling, confidence examples, immutable edits, invalidation vs deletion, and secret/PII rules.",
638
+ inputSchema: emptyShape
639
+ },
640
+ (args) => wrap(() => api.guide(args))
641
+ );
642
+ server.registerTool(
643
+ "memory.agents_md_snippet",
644
+ {
645
+ description: "Return the one-line AGENTS.md/CLAUDE.md pointer explaining that this project has agent-journal and memory.guide.",
646
+ inputSchema: emptyShape
647
+ },
648
+ (args) => wrap(() => api.agentsMdSnippet(args))
649
+ );
650
+ server.registerTool(
651
+ "kb.upsert_entity",
652
+ {
653
+ description: "Create or update a KB entity: the named subject (a service, file, person, config, concept) that statements hang from. An entity carries no facts itself \u2014 keep the summary a short description and put every actual claim in a statement via kb.add_statement. Reuse an existing entity (search first) instead of creating duplicates; pass its id to update. Updates are allowed only while active; invalid entities are read-only. Title changes re-sync statement keyword titles but do not re-embed statements in v0.",
654
+ inputSchema: kbUpsertEntityShape
655
+ },
656
+ (args) => wrap(() => api.upsertEntity(args))
657
+ );
658
+ server.registerTool(
659
+ "kb.add_statement",
660
+ {
661
+ description: "Add one immutable, atomic claim to an active entity \u2014 keep it to a single fact so a future correction can invalidate it without retiring unrelated knowledge. Requires confidence_level, confidence_reason, and derivation_method; verified means you observed it now (ran the command, read the source), not merely that you are confident. Prefer calling journal.append first and passing its id as journal_entry_id; if omitted, the server creates a stub journal entry, links it, and returns a nudge. Optional valid_from/valid_to (Unix epoch milliseconds) bound when the fact holds. Never store secrets, credentials, or PII.",
662
+ inputSchema: kbAddStatementShape
663
+ },
664
+ (args) => wrap(() => api.addStatement(args))
665
+ );
666
+ server.registerTool(
667
+ "kb.edit_statement",
668
+ {
669
+ description: "Correct a statement immutably: creates a replacement statement carrying your changes, invalidates the old active statement, and redirects the old one to the replacement (memory.get follows the redirect). Unspecified fields are inherited from the original. Use this whenever a claim, confidence, or provenance changes \u2014 do not expect in-place edits, and invalid statements cannot be edited (add a fresh statement instead). Pass journal_entry_id to tie the correction to your work, or a stub is created.",
670
+ inputSchema: kbEditStatementShape
671
+ },
672
+ (args) => wrap(() => api.editStatement(args))
673
+ );
674
+ server.registerTool(
675
+ "kb.invalidate",
676
+ {
677
+ description: "Soft-invalidate an active statement or entity with a required note explaining why, plus an optional same-type superseded_by redirect to the record that replaces it. This is the normal, preferred way to retire stale or wrong knowledge (reach for kb.delete only for secrets/PII/garbage). Invalidating an entity cascades to its active statements (count returned as cascaded_statements); relationships referencing it are left untouched. Invalid records stay readable via memory.get and searchable only when include_invalid is passed.",
678
+ inputSchema: kbInvalidateShape
679
+ },
680
+ (args) => wrap(() => api.invalidate(args))
681
+ );
682
+ server.registerTool(
683
+ "kb.delete",
684
+ {
685
+ description: "Hard-delete a record permanently and run a full VACUUM so the content cannot be recovered from disk. Deleting a knowledge-base record writes an audit journal entry; deleting a journal entry does not (the journal is the audit log itself). Use this ONLY for poisoned content \u2014 secrets, credentials, PII, or garbage that must not persist. For ordinary stale or wrong knowledge use kb.invalidate instead, which keeps history. Give a reason that does not repeat the sensitive value.",
686
+ inputSchema: kbDeleteShape
687
+ },
688
+ (args) => wrap(() => api.delete(args))
689
+ );
690
+ server.registerTool(
691
+ "journal.append",
692
+ {
693
+ description: "Append a journal entry recording what you did: a narrative, the commands you ran, and statement ids you proved or disproved. Link the entry to the KB records it created or changed via links. Capture the id this returns and pass it as journal_entry_id when adding or editing statements so the claim and its supporting work stay connected. Append-only in v0 \u2014 entries are never edited, so write a complete account each time.",
694
+ inputSchema: journalAppendShape
695
+ },
696
+ (args) => wrap(() => api.appendJournal(args))
697
+ );
698
+ return server;
699
+ }
700
+ async function connectStdio(server) {
701
+ await server.connect(new StdioServerTransport());
702
+ }
703
+
704
+ // src/tools/api.ts
705
+ import fs4 from "node:fs";
706
+
707
+ // src/config.ts
708
+ import { z as z2 } from "zod";
709
+ var DEFAULTS = {
710
+ rrf_k: 60,
711
+ w_recency: 0.3,
712
+ recency_half_life: "90d",
713
+ w_trust: 0.2,
714
+ trust_confidence: { verified: 1, high: 0.7, medium: 0.4, low: 0.1 },
715
+ trust_derivation: {
716
+ "direct-observation": 1,
717
+ "command-output": 1,
718
+ "external-doc": 0.7,
719
+ "user-assertion": 0.5,
720
+ inference: 0.2
721
+ },
722
+ tombstone_window: "90d",
723
+ k_recall_fts: 100,
724
+ k_recall_vec: 200
725
+ };
726
+ var partialConfigSchema = z2.object({
727
+ rrf_k: z2.number().positive().optional(),
728
+ w_recency: z2.number().nonnegative().optional(),
729
+ recency_half_life: z2.string().optional(),
730
+ w_trust: z2.number().nonnegative().optional(),
731
+ trust_confidence: z2.object({
732
+ verified: z2.number().optional(),
733
+ high: z2.number().optional(),
734
+ medium: z2.number().optional(),
735
+ low: z2.number().optional()
736
+ }).optional(),
737
+ trust_derivation: z2.object({
738
+ "direct-observation": z2.number().optional(),
739
+ "command-output": z2.number().optional(),
740
+ "external-doc": z2.number().optional(),
741
+ "user-assertion": z2.number().optional(),
742
+ inference: z2.number().optional()
743
+ }).optional(),
744
+ tombstone_window: z2.string().optional(),
745
+ k_recall_fts: z2.number().int().positive().optional(),
746
+ k_recall_vec: z2.number().int().positive().optional()
747
+ }).strip();
748
+ var configSchema = z2.object({
749
+ rrf_k: z2.number().positive(),
750
+ w_recency: z2.number().nonnegative(),
751
+ recency_half_life: z2.string(),
752
+ w_trust: z2.number().nonnegative(),
753
+ trust_confidence: z2.object({
754
+ verified: z2.number(),
755
+ high: z2.number(),
756
+ medium: z2.number(),
757
+ low: z2.number()
758
+ }),
759
+ trust_derivation: z2.object({
760
+ "direct-observation": z2.number(),
761
+ "command-output": z2.number(),
762
+ "external-doc": z2.number(),
763
+ "user-assertion": z2.number(),
764
+ inference: z2.number()
765
+ }),
766
+ tombstone_window: z2.string(),
767
+ k_recall_fts: z2.number().int().positive(),
768
+ k_recall_vec: z2.number().int().positive()
769
+ });
770
+ function parsePartialConfig(value) {
771
+ return partialConfigSchema.parse(value ?? {});
772
+ }
773
+ function mergeConfig(base, next) {
774
+ return configSchema.parse({
775
+ ...base,
776
+ ...next,
777
+ trust_confidence: {
778
+ ...base.trust_confidence,
779
+ ...next.trust_confidence
780
+ },
781
+ trust_derivation: {
782
+ ...base.trust_derivation,
783
+ ...next.trust_derivation
784
+ }
785
+ });
786
+ }
787
+ function resolveConfig(projectConfigJson, fileConfig) {
788
+ let merged = configSchema.parse(DEFAULTS);
789
+ if (projectConfigJson) {
790
+ merged = mergeConfig(merged, parsePartialConfig(JSON.parse(projectConfigJson)));
791
+ }
792
+ if (fileConfig) {
793
+ merged = mergeConfig(merged, parsePartialConfig(fileConfig));
794
+ }
795
+ return merged;
796
+ }
797
+
798
+ // src/domain/fts.ts
799
+ function sanitizeFtsQuery(query) {
800
+ const terms = query.match(/[A-Za-z0-9]+/g);
801
+ if (!terms || terms.length === 0) {
802
+ return null;
803
+ }
804
+ return terms.map((term) => `"${term}"`).join(" OR ");
805
+ }
806
+ function upsertStatementFts(db, statementId, claim, entityTitle) {
807
+ deleteStatementFts(db, statementId);
808
+ db.prepare("INSERT INTO fts_statements(statement_id, claim, entity_title) VALUES (?, ?, ?)").run(
809
+ statementId,
810
+ claim,
811
+ entityTitle
812
+ );
813
+ }
814
+ function deleteStatementFts(db, statementId) {
815
+ db.prepare("DELETE FROM fts_statements WHERE statement_id = ?").run(statementId);
816
+ }
817
+ function upsertJournalFts(db, journalId, narrative, commands) {
818
+ deleteJournalFts(db, journalId);
819
+ db.prepare("INSERT INTO fts_journal(journal_id, narrative, commands) VALUES (?, ?, ?)").run(
820
+ journalId,
821
+ narrative ?? "",
822
+ commands ? JSON.stringify(commands) : ""
823
+ );
824
+ }
825
+ function deleteJournalFts(db, journalId) {
826
+ db.prepare("DELETE FROM fts_journal WHERE journal_id = ?").run(journalId);
827
+ }
828
+ function ftsSearch(db, table, query, limit) {
829
+ const sanitized = sanitizeFtsQuery(query);
830
+ if (!sanitized) {
831
+ return [];
832
+ }
833
+ if (table === "fts_statements") {
834
+ return db.prepare(
835
+ "SELECT statement_id AS id, bm25(fts_statements) AS bm25 FROM fts_statements WHERE fts_statements MATCH ? ORDER BY bm25 LIMIT ?"
836
+ ).all(sanitized, limit);
837
+ }
838
+ return db.prepare(
839
+ "SELECT journal_id AS id, bm25(fts_journal) AS bm25 FROM fts_journal WHERE fts_journal MATCH ? ORDER BY bm25 LIMIT ?"
840
+ ).all(sanitized, limit);
841
+ }
842
+
843
+ // src/domain/json.ts
844
+ function jsonArray(value) {
845
+ if (value === null || value === void 0) {
846
+ return null;
847
+ }
848
+ const parsed = JSON.parse(value);
849
+ return Array.isArray(parsed) ? parsed.map(String) : null;
850
+ }
851
+ function jsonStringifyArray(value) {
852
+ return value === void 0 ? null : JSON.stringify(value);
853
+ }
854
+ function hasAllTags(stored, requested) {
855
+ if (!requested || requested.length === 0) {
856
+ return true;
857
+ }
858
+ const tags = new Set(jsonArray(stored) ?? []);
859
+ return requested.every((tag) => tags.has(tag));
860
+ }
861
+
862
+ // src/domain/records.ts
863
+ var STATEMENT_TS_FIELDS = ["created_at", "last_accessed_at", "valid_from", "valid_to", "invalidated_at"];
864
+ var ENTITY_TS_FIELDS = ["created_at", "last_updated_at", "last_accessed_at", "invalidated_at"];
865
+ var JOURNAL_TS_FIELDS = ["created_at", "invalidated_at"];
866
+ function statementOut(row) {
867
+ const base = {
868
+ id: row.id,
869
+ entity_id: row.entity_id,
870
+ claim: row.claim,
871
+ confidence_level: row.confidence_level,
872
+ confidence_reason: row.confidence_reason,
873
+ derivation_method: row.derivation_method,
874
+ citations: jsonArray(row.citations),
875
+ created_at: row.created_at,
876
+ last_accessed_at: row.last_accessed_at,
877
+ valid_from: row.valid_from,
878
+ valid_to: row.valid_to,
879
+ status: row.status,
880
+ invalidation_note: row.invalidation_note,
881
+ invalidated_at: row.invalidated_at,
882
+ superseded_by: row.superseded_by
883
+ };
884
+ return { ...base, _relative: relativeMap(base, STATEMENT_TS_FIELDS) };
885
+ }
886
+ function entityOut(row) {
887
+ const base = {
888
+ id: row.id,
889
+ type: row.type,
890
+ title: row.title,
891
+ summary: row.summary,
892
+ tags: jsonArray(row.tags),
893
+ created_at: row.created_at,
894
+ last_updated_at: row.last_updated_at,
895
+ last_accessed_at: row.last_accessed_at,
896
+ status: row.status,
897
+ invalidation_note: row.invalidation_note,
898
+ invalidated_at: row.invalidated_at,
899
+ superseded_by: row.superseded_by
900
+ };
901
+ return { ...base, _relative: relativeMap(base, ENTITY_TS_FIELDS) };
902
+ }
903
+ function journalOut(db, row) {
904
+ const links = db.prepare(
905
+ "SELECT target_type, target_id, role FROM journal_link WHERE journal_id = ? ORDER BY target_type, target_id, role"
906
+ ).all(row.id);
907
+ const base = {
908
+ id: row.id,
909
+ created_at: row.created_at,
910
+ commands: jsonArray(row.commands),
911
+ proven: jsonArray(row.proven),
912
+ disproven: jsonArray(row.disproven),
913
+ narrative: row.narrative,
914
+ is_stub: row.is_stub === 1,
915
+ status: row.status,
916
+ superseded_by: row.superseded_by,
917
+ invalidated_at: row.invalidated_at,
918
+ links
919
+ };
920
+ return { ...base, _relative: relativeMap(base, JOURNAL_TS_FIELDS) };
921
+ }
922
+ function snippet(text, length = 200) {
923
+ const value = text ?? "";
924
+ return value.length > length ? `${value.slice(0, length)}...` : value;
925
+ }
926
+
927
+ // src/domain/vec.ts
928
+ function vectorJson(vec) {
929
+ return JSON.stringify(Array.from(vec));
930
+ }
931
+ function upsertVector(db, ownerType, ownerId, vec, modelId, dim, contentHash) {
932
+ const existing = db.prepare("SELECT vec_rowid FROM embedding WHERE owner_type = ? AND owner_id = ?").get(ownerType, ownerId);
933
+ if (existing) {
934
+ db.prepare("DELETE FROM vec_index WHERE rowid = ?").run(existing.vec_rowid);
935
+ }
936
+ const insert = db.prepare("INSERT INTO vec_index(embedding) VALUES (?)").run(vectorJson(vec));
937
+ const vecRowid = Number(insert.lastInsertRowid);
938
+ db.prepare(
939
+ `INSERT INTO embedding(owner_type, owner_id, vec_rowid, model_id, dim, content_hash)
940
+ VALUES (?, ?, ?, ?, ?, ?)
941
+ ON CONFLICT(owner_type, owner_id) DO UPDATE SET
942
+ vec_rowid = excluded.vec_rowid,
943
+ model_id = excluded.model_id,
944
+ dim = excluded.dim,
945
+ content_hash = excluded.content_hash`
946
+ ).run(ownerType, ownerId, vecRowid, modelId, dim, contentHash);
947
+ }
948
+ function deleteVector(db, ownerType, ownerId) {
949
+ const existing = db.prepare("SELECT vec_rowid FROM embedding WHERE owner_type = ? AND owner_id = ?").get(ownerType, ownerId);
950
+ if (existing) {
951
+ db.prepare("DELETE FROM vec_index WHERE rowid = ?").run(existing.vec_rowid);
952
+ db.prepare("DELETE FROM embedding WHERE owner_type = ? AND owner_id = ?").run(ownerType, ownerId);
953
+ }
954
+ }
955
+ function knn(db, queryVec, limit) {
956
+ const rows = db.prepare(
957
+ `SELECT e.owner_type, e.owner_id, v.distance
958
+ FROM vec_index v
959
+ JOIN embedding e ON e.vec_rowid = v.rowid
960
+ WHERE v.embedding MATCH ? AND v.k = ?
961
+ ORDER BY v.distance`
962
+ ).all(vectorJson(queryVec), limit);
963
+ return rows;
964
+ }
965
+
966
+ // src/domain/vacuum.ts
967
+ function maybeVacuum(db, random = Math.random) {
968
+ if (random() < 0.1) {
969
+ db.pragma("incremental_vacuum");
970
+ }
971
+ }
972
+
973
+ // src/text/snippet.ts
974
+ var AGENTS_MD_SNIPPET = "This project has an `agent-journal` MCP server providing a persistent knowledge base + journal. Its usage is self-described on connect; call `memory.guide` for the full playbook.";
975
+
976
+ // src/text/guide.ts
977
+ var GUIDE = `agent-journal playbook
978
+
979
+ 1. Search before acting.
980
+ Use memory.search before relying on a project fact. Search returns compact snippets and scores; call memory.get for full records. Search excludes invalid records by default so active knowledge stays prominent. Use include_invalid only when you are looking for history or when live search did not find what you need.
981
+
982
+ 2. Write atomic statements.
983
+ Facts live in statements, not entity summaries. Create or update the entity that the fact is about, then add one discrete claim with kb.add_statement. Keep statements small enough that a future correction can invalidate one fact without retiring unrelated knowledge.
984
+
985
+ 3. Keep the confidence contract honest.
986
+ Every statement requires confidence_level, confidence_reason, and derivation_method.
987
+ verified: you observed it now by running a command, reading authoritative output, or checking the source.
988
+ high: strong evidence, but not directly re-confirmed in this session.
989
+ medium: a reasonable inference from solid signals.
990
+ low: a guess or weak inference that readers should discount.
991
+ direct-observation: you inspected the current project or runtime yourself.
992
+ command-output: a command produced the evidence.
993
+ external-doc: an authoritative external document said it.
994
+ user-assertion: the user told you.
995
+ inference: you inferred it from surrounding evidence.
996
+
997
+ 4. Journal what changed.
998
+ Use journal.append to record what you did, the commands you ran, and which statements were proven or disproven, and link the entry to the KB records it touched. Preferred workflow: call journal.append first, then pass its id as journal_entry_id to kb.add_statement or kb.edit_statement so the claim and the work that produced it share one entry. If you skip that, the server auto-creates a stub journal entry, links it to the new statement, and returns a nudge containing the stub id; follow up with journal.append to capture the real detail. Journal entries are append-only, so the stub remains as a lightweight marker and is never rewritten.
999
+
1000
+ 5. Edit immutably.
1001
+ Statements are never edited in place. Use kb.edit_statement to create the replacement statement and invalidate the old one with a redirect, or use kb.invalidate when a statement should simply be retired. Invalid statements stay readable through memory.get and are searchable only when include_invalid is requested.
1002
+
1003
+ 6. Invalidate vs delete.
1004
+ Use kb.invalidate for stale, wrong, or superseded knowledge. Use kb.delete only for poisoned content such as leaked secrets, credentials, PII, or garbage that must not persist on disk. Deleting a knowledge-base record writes an audit journal entry; deleting a journal entry does not. Either way deletion runs a full VACUUM.
1005
+
1006
+ 7. Secrets and PII.
1007
+ Do not store credentials, tokens, private keys, personal data, or copied sensitive output in statements or journals. If a command prints sensitive material, summarize only the safe lesson learned. If sensitive content is already stored, immediately use kb.delete with a reason that does not repeat the secret.
1008
+
1009
+ 8. Project scoping.
1010
+ Memory is project-scoped and resolved automatically, in this order: an explicit project argument on the tool call; a per-repo config file; the normalized git remote origin URL; the main worktree path (so every worktree of one repo shares the same memory); then the absolute launch cwd when outside git. Pass project only when you deliberately want to read or write another project's memory. Per-repository config lives under XDG_CONFIG_DIR, XDG_CONFIG_HOME, or ~/.config in the agent-memory directory as project_<sha256(repo-key)>.json and can pin the project or tune ranking config.`;
1011
+
1012
+ // src/util/retry.ts
1013
+ var MAX_ATTEMPTS = 5;
1014
+ function isBusyError(err) {
1015
+ return typeof err === "object" && err !== null && "code" in err && (err.code === "SQLITE_BUSY" || err.code === "SQLITE_BUSY_SNAPSHOT");
1016
+ }
1017
+ function sleepSync(ms) {
1018
+ const sab = new SharedArrayBuffer(4);
1019
+ const view = new Int32Array(sab);
1020
+ Atomics.wait(view, 0, 0, ms);
1021
+ }
1022
+ function withRetry(fn) {
1023
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
1024
+ try {
1025
+ return fn();
1026
+ } catch (err) {
1027
+ if (!isBusyError(err) || attempt === MAX_ATTEMPTS - 1) {
1028
+ throw err;
1029
+ }
1030
+ const backoff = 50 * 2 ** attempt + Math.floor(Math.random() * 26);
1031
+ sleepSync(backoff);
1032
+ }
1033
+ }
1034
+ throw new Error("unreachable retry state");
1035
+ }
1036
+
1037
+ // src/tools/api.ts
1038
+ function statementDocumentText(claim, entityTitle) {
1039
+ return `${claim}
1040
+ ${entityTitle}`;
1041
+ }
1042
+ var RELATIONSHIP_TS_FIELDS = ["created_at", "last_accessed_at", "valid_from", "valid_to", "invalidated_at"];
1043
+ function relationshipOut(row) {
1044
+ return { ...row, _relative: relativeMap(row, RELATIONSHIP_TS_FIELDS) };
1045
+ }
1046
+ function rankMap(ids) {
1047
+ const map = /* @__PURE__ */ new Map();
1048
+ ids.forEach((id, index) => {
1049
+ if (!map.has(id)) {
1050
+ map.set(id, index + 1);
1051
+ }
1052
+ });
1053
+ return map;
1054
+ }
1055
+ function unique(values) {
1056
+ return [...new Set(values)];
1057
+ }
1058
+ function placeholders(values) {
1059
+ return values.map(() => "?").join(",");
1060
+ }
1061
+ function scoreRecency(createdAt, timestamp, config) {
1062
+ const ageDays = Math.max(0, (timestamp - createdAt) / 864e5);
1063
+ const halfLifeDays = parseDurationMs(config.recency_half_life) / 864e5;
1064
+ return 0.5 ** (ageDays / halfLifeDays);
1065
+ }
1066
+ function dbTargetForKind(kind) {
1067
+ switch (kind) {
1068
+ case "entity":
1069
+ return { table: "entity", targetType: "entity" };
1070
+ case "statement":
1071
+ return { table: "statement", targetType: "statement", vectorOwner: "statement" };
1072
+ case "relationship":
1073
+ return { table: "relationship", targetType: "relationship" };
1074
+ case "journal":
1075
+ return { table: "journal_entry", targetType: "journal_entry", vectorOwner: "journal_entry" };
1076
+ default:
1077
+ return null;
1078
+ }
1079
+ }
1080
+ var MemoryApi = class {
1081
+ db;
1082
+ resolver;
1083
+ embeddings;
1084
+ dbFile;
1085
+ random;
1086
+ constructor(options) {
1087
+ this.db = options.db;
1088
+ this.resolver = options.resolver;
1089
+ this.embeddings = options.embeddings;
1090
+ this.dbFile = options.dbFile;
1091
+ this.random = options.random ?? Math.random;
1092
+ }
1093
+ async search(input) {
1094
+ const args = memorySearchSchema.parse(input);
1095
+ const project = this.project(args.project);
1096
+ const config = this.config(project);
1097
+ const timestamp = now();
1098
+ await this.embeddings.ready();
1099
+ const queryVec = await this.embeddings.embedQuery(args.query);
1100
+ const candidates = [];
1101
+ if (args.where === "knowledge-base" || args.where === "both") {
1102
+ candidates.push(
1103
+ ...this.searchStatements(args.query, queryVec, project, config, timestamp, {
1104
+ type: args.type,
1105
+ tags: args.tags,
1106
+ includeInvalid: args.include_invalid,
1107
+ includeDeletedSince: args.include_deleted_since
1108
+ })
1109
+ );
1110
+ }
1111
+ if (args.where === "journal" || args.where === "both") {
1112
+ candidates.push(
1113
+ ...this.searchJournal(args.query, queryVec, project, config, timestamp, {
1114
+ includeInvalid: args.include_invalid,
1115
+ includeDeletedSince: args.include_deleted_since
1116
+ })
1117
+ );
1118
+ }
1119
+ if (candidates.length > 0) {
1120
+ const min = Math.min(...candidates.map((candidate) => candidate.rawRrf));
1121
+ const max = Math.max(...candidates.map((candidate) => candidate.rawRrf));
1122
+ for (const candidate of candidates) {
1123
+ const rrfNorm = max === min ? 1 : (candidate.rawRrf - min) / (max - min);
1124
+ let trust = 0;
1125
+ if (candidate.statement) {
1126
+ trust = config.trust_confidence[candidate.statement.confidence_level] + config.trust_derivation[candidate.statement.derivation_method];
1127
+ }
1128
+ candidate.score = rrfNorm + config.w_recency * scoreRecency(candidate.created_at, timestamp, config) + config.w_trust * trust;
1129
+ }
1130
+ }
1131
+ const top = candidates.sort((a, b) => b.score - a.score || b.created_at - a.created_at || b.id.localeCompare(a.id)).slice(0, args.limit);
1132
+ const entityGroups = /* @__PURE__ */ new Map();
1133
+ const journal = [];
1134
+ for (const hit of top) {
1135
+ if (hit.kind === "statement" && hit.statement) {
1136
+ const row = hit.statement;
1137
+ const group = entityGroups.get(row.entity_id) ?? {
1138
+ entity: { id: row.entity_id, type: row.entity_type, title: row.entity_title },
1139
+ statements: []
1140
+ };
1141
+ group.statements.push({
1142
+ kind: "statement",
1143
+ id: row.id,
1144
+ claim_snippet: snippet(row.claim),
1145
+ score: hit.score,
1146
+ confidence_level: row.confidence_level,
1147
+ derivation_method: row.derivation_method,
1148
+ status: row.status,
1149
+ created_at: row.created_at,
1150
+ _relative: { created_at: humanizeRelative(row.created_at, timestamp) }
1151
+ });
1152
+ entityGroups.set(row.entity_id, group);
1153
+ } else if (hit.kind === "journal" && hit.journal) {
1154
+ journal.push({
1155
+ kind: "journal",
1156
+ id: hit.journal.id,
1157
+ narrative_snippet: snippet(hit.journal.narrative),
1158
+ score: hit.score,
1159
+ status: hit.journal.status,
1160
+ created_at: hit.journal.created_at,
1161
+ _relative: { created_at: humanizeRelative(hit.journal.created_at, timestamp) }
1162
+ });
1163
+ }
1164
+ }
1165
+ return {
1166
+ query: args.query,
1167
+ where: args.where,
1168
+ project: project.id,
1169
+ entities: [...entityGroups.values()],
1170
+ journal,
1171
+ total_returned: top.length
1172
+ };
1173
+ }
1174
+ get(input) {
1175
+ const args = memoryGetSchema.parse(input);
1176
+ const project = this.project(args.project);
1177
+ const kind = idKind(args.id);
1178
+ if (kind === "statement") {
1179
+ const row = this.loadStatement(args.id, project.id);
1180
+ if (!row) throw new Error(`No record found for id ${args.id}`);
1181
+ withRetry(
1182
+ () => this.db.transaction(() => {
1183
+ this.db.prepare("UPDATE statement SET last_accessed_at = ? WHERE id = ?").run(now(), args.id);
1184
+ })()
1185
+ );
1186
+ const fresh = this.loadStatement(args.id, project.id);
1187
+ const entity = this.loadEntity(fresh.entity_id, project.id);
1188
+ let redirect = void 0;
1189
+ if (fresh.status === "invalid" && fresh.superseded_by) {
1190
+ const target = this.loadStatement(fresh.superseded_by, project.id);
1191
+ if (target) redirect = statementOut(target);
1192
+ }
1193
+ return {
1194
+ kind: "statement",
1195
+ statement: statementOut(fresh),
1196
+ entity: entity ? { id: entity.id, type: entity.type, title: entity.title } : null,
1197
+ ...redirect ? { redirect } : {},
1198
+ flagged_invalid: fresh.status === "invalid"
1199
+ };
1200
+ }
1201
+ if (kind === "entity") {
1202
+ const row = this.loadEntity(args.id, project.id);
1203
+ if (!row) throw new Error(`No record found for id ${args.id}`);
1204
+ withRetry(
1205
+ () => this.db.transaction(() => {
1206
+ this.db.prepare("UPDATE entity SET last_accessed_at = ? WHERE id = ?").run(now(), args.id);
1207
+ })()
1208
+ );
1209
+ const fresh = this.loadEntity(args.id, project.id);
1210
+ const statements = this.db.prepare(
1211
+ `SELECT * FROM statement
1212
+ WHERE entity_id = ? AND project_id = ? ${args.include_invalid_statements ? "" : "AND status = 'active'"}
1213
+ ORDER BY created_at DESC, id DESC`
1214
+ ).all(args.id, project.id);
1215
+ return {
1216
+ kind: "entity",
1217
+ entity: entityOut(fresh),
1218
+ statements: statements.map(statementOut)
1219
+ };
1220
+ }
1221
+ if (kind === "journal") {
1222
+ const row = this.loadJournal(args.id, project.id);
1223
+ if (!row) throw new Error(`No record found for id ${args.id}`);
1224
+ return { kind: "journal", entry: journalOut(this.db, row) };
1225
+ }
1226
+ if (kind === "relationship") {
1227
+ const row = this.loadRelationship(args.id, project.id);
1228
+ if (!row) throw new Error(`No record found for id ${args.id}`);
1229
+ return { kind: "relationship", relationship: relationshipOut(row) };
1230
+ }
1231
+ throw new Error(`No record found for id ${args.id}`);
1232
+ }
1233
+ upsertEntity(input) {
1234
+ const args = kbUpsertEntitySchema.parse(input);
1235
+ const project = this.project(args.project);
1236
+ const result = this.runMutation(() => {
1237
+ const timestamp = now();
1238
+ if (!args.id) {
1239
+ const id = newId("entity");
1240
+ this.db.prepare(
1241
+ `INSERT INTO entity(id, project_id, type, title, summary, tags, created_at, last_updated_at, status)
1242
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active')`
1243
+ ).run(
1244
+ id,
1245
+ project.id,
1246
+ args.type,
1247
+ args.title,
1248
+ args.summary ?? null,
1249
+ jsonStringifyArray(args.tags),
1250
+ timestamp,
1251
+ timestamp
1252
+ );
1253
+ return entityOut(this.loadEntity(id, project.id));
1254
+ }
1255
+ const existing = this.loadEntity(args.id, project.id);
1256
+ if (!existing) throw new Error(`No active entity found for id ${args.id}`);
1257
+ if (existing.status !== "active") {
1258
+ throw new Error(`Entity ${args.id} is invalid and read-only`);
1259
+ }
1260
+ this.db.prepare(
1261
+ `UPDATE entity
1262
+ SET type = ?, title = ?, summary = ?, tags = ?, last_updated_at = ?
1263
+ WHERE id = ? AND project_id = ?`
1264
+ ).run(
1265
+ args.type,
1266
+ args.title,
1267
+ args.summary ?? existing.summary,
1268
+ args.tags === void 0 ? existing.tags : JSON.stringify(args.tags),
1269
+ timestamp,
1270
+ args.id,
1271
+ project.id
1272
+ );
1273
+ const statements = this.db.prepare("SELECT id, claim FROM statement WHERE entity_id = ? AND project_id = ?").all(args.id, project.id);
1274
+ for (const statement of statements) {
1275
+ upsertStatementFts(this.db, statement.id, statement.claim, args.title);
1276
+ }
1277
+ return entityOut(this.loadEntity(args.id, project.id));
1278
+ });
1279
+ return result;
1280
+ }
1281
+ async addStatement(input) {
1282
+ const args = kbAddStatementSchema.parse(input);
1283
+ const project = this.project(args.project);
1284
+ const entity = this.loadEntity(args.entity_id, project.id);
1285
+ if (!entity) throw new Error(`No active entity found for id ${args.entity_id}`);
1286
+ if (entity.status !== "active") {
1287
+ throw new Error(`Entity ${args.entity_id} is invalid and read-only`);
1288
+ }
1289
+ const statementId = newId("statement");
1290
+ const statementVec = await this.embeddings.embedDocument(statementDocumentText(args.claim, entity.title));
1291
+ const statementHash = this.embeddings.contentHash(statementDocumentText(args.claim, entity.title));
1292
+ const stubJournalId = args.journal_entry_id ? null : newId("journal");
1293
+ const stubNarrative = stubJournalId ? `auto-stub for statement ${statementId}` : null;
1294
+ const stubVec = stubNarrative ? await this.embeddings.embedDocument(stubNarrative) : null;
1295
+ const stubHash = stubNarrative ? this.embeddings.contentHash(stubNarrative) : null;
1296
+ const result = this.runMutation(() => {
1297
+ const timestamp = now();
1298
+ let journalEntryId = args.journal_entry_id ?? stubJournalId;
1299
+ if (args.journal_entry_id) {
1300
+ this.requireJournal(args.journal_entry_id, project.id);
1301
+ } else {
1302
+ this.insertJournal(stubJournalId, project.id, timestamp, null, null, null, stubNarrative, true);
1303
+ upsertJournalFts(this.db, stubJournalId, stubNarrative, null);
1304
+ upsertVector(
1305
+ this.db,
1306
+ "journal_entry",
1307
+ stubJournalId,
1308
+ stubVec,
1309
+ this.embeddings.modelId(),
1310
+ this.embeddings.dim(),
1311
+ stubHash
1312
+ );
1313
+ }
1314
+ this.insertStatement({
1315
+ id: statementId,
1316
+ projectId: project.id,
1317
+ entityId: entity.id,
1318
+ claim: args.claim,
1319
+ confidenceLevel: args.confidence_level,
1320
+ confidenceReason: args.confidence_reason,
1321
+ derivationMethod: args.derivation_method,
1322
+ citations: args.citations,
1323
+ createdAt: timestamp,
1324
+ validFrom: args.valid_from ?? null,
1325
+ validTo: args.valid_to ?? null
1326
+ });
1327
+ upsertVector(
1328
+ this.db,
1329
+ "statement",
1330
+ statementId,
1331
+ statementVec,
1332
+ this.embeddings.modelId(),
1333
+ this.embeddings.dim(),
1334
+ statementHash
1335
+ );
1336
+ upsertStatementFts(this.db, statementId, args.claim, entity.title);
1337
+ this.insertJournalLink(journalEntryId, "statement", statementId, "created");
1338
+ return {
1339
+ statement: statementOut(this.loadStatement(statementId, project.id)),
1340
+ journal_entry_id: journalEntryId,
1341
+ ...stubJournalId ? {
1342
+ nudge: `Recorded with an auto-created journal stub (${stubJournalId}). Consider journal.append with what you did and what it proves, then link it.`
1343
+ } : {}
1344
+ };
1345
+ });
1346
+ return result;
1347
+ }
1348
+ async editStatement(input) {
1349
+ const args = kbEditStatementSchema.parse(input);
1350
+ const valueFields = [
1351
+ "claim",
1352
+ "confidence_level",
1353
+ "confidence_reason",
1354
+ "derivation_method",
1355
+ "citations",
1356
+ "valid_from",
1357
+ "valid_to"
1358
+ ];
1359
+ if (!valueFields.some((field) => args[field] !== void 0)) {
1360
+ throw new Error("nothing to edit");
1361
+ }
1362
+ const project = this.project(args.project);
1363
+ const target = this.loadStatement(args.statement_id, project.id);
1364
+ if (!target) throw new Error(`No active statement found for id ${args.statement_id}`);
1365
+ if (target.status !== "active") {
1366
+ throw new Error(`Statement ${args.statement_id} is invalid and read-only`);
1367
+ }
1368
+ const entity = this.loadEntity(target.entity_id, project.id);
1369
+ if (!entity) throw new Error(`No entity found for statement ${target.id}`);
1370
+ const replacement = {
1371
+ claim: args.claim ?? target.claim,
1372
+ confidenceLevel: args.confidence_level ?? target.confidence_level,
1373
+ confidenceReason: args.confidence_reason ?? target.confidence_reason,
1374
+ derivationMethod: args.derivation_method ?? target.derivation_method,
1375
+ citations: args.citations === void 0 ? target.citations ? JSON.parse(target.citations) : void 0 : args.citations,
1376
+ validFrom: args.valid_from ?? target.valid_from,
1377
+ validTo: args.valid_to ?? target.valid_to
1378
+ };
1379
+ const newStatementId = newId("statement");
1380
+ const documentText = statementDocumentText(replacement.claim, entity.title);
1381
+ const statementVec = await this.embeddings.embedDocument(documentText);
1382
+ const statementHash = this.embeddings.contentHash(documentText);
1383
+ const stubJournalId = args.journal_entry_id ? null : newId("journal");
1384
+ const stubNarrative = stubJournalId ? `auto-stub for statement ${newStatementId}` : null;
1385
+ const stubVec = stubNarrative ? await this.embeddings.embedDocument(stubNarrative) : null;
1386
+ const stubHash = stubNarrative ? this.embeddings.contentHash(stubNarrative) : null;
1387
+ return this.runMutation(() => {
1388
+ const timestamp = now();
1389
+ const journalEntryId = args.journal_entry_id ?? stubJournalId;
1390
+ if (args.journal_entry_id) {
1391
+ this.requireJournal(args.journal_entry_id, project.id);
1392
+ } else {
1393
+ this.insertJournal(stubJournalId, project.id, timestamp, null, null, null, stubNarrative, true);
1394
+ upsertJournalFts(this.db, stubJournalId, stubNarrative, null);
1395
+ upsertVector(
1396
+ this.db,
1397
+ "journal_entry",
1398
+ stubJournalId,
1399
+ stubVec,
1400
+ this.embeddings.modelId(),
1401
+ this.embeddings.dim(),
1402
+ stubHash
1403
+ );
1404
+ }
1405
+ this.insertStatement({
1406
+ id: newStatementId,
1407
+ projectId: project.id,
1408
+ entityId: target.entity_id,
1409
+ claim: replacement.claim,
1410
+ confidenceLevel: replacement.confidenceLevel,
1411
+ confidenceReason: replacement.confidenceReason,
1412
+ derivationMethod: replacement.derivationMethod,
1413
+ citations: replacement.citations,
1414
+ createdAt: timestamp,
1415
+ validFrom: replacement.validFrom,
1416
+ validTo: replacement.validTo
1417
+ });
1418
+ upsertVector(
1419
+ this.db,
1420
+ "statement",
1421
+ newStatementId,
1422
+ statementVec,
1423
+ this.embeddings.modelId(),
1424
+ this.embeddings.dim(),
1425
+ statementHash
1426
+ );
1427
+ upsertStatementFts(this.db, newStatementId, replacement.claim, entity.title);
1428
+ this.db.prepare(
1429
+ `UPDATE statement
1430
+ SET status = 'invalid', invalidated_at = ?, superseded_by = ?, invalidation_note = ?
1431
+ WHERE id = ? AND project_id = ?`
1432
+ ).run(
1433
+ timestamp,
1434
+ newStatementId,
1435
+ args.invalidation_note ?? `edited -> superseded by ${newStatementId}`,
1436
+ target.id,
1437
+ project.id
1438
+ );
1439
+ this.insertJournalLink(journalEntryId, "statement", newStatementId, "created");
1440
+ this.insertJournalLink(journalEntryId, "statement", newStatementId, "changed");
1441
+ return {
1442
+ statement: statementOut(this.loadStatement(newStatementId, project.id)),
1443
+ superseded: target.id,
1444
+ journal_entry_id: journalEntryId,
1445
+ ...stubJournalId ? {
1446
+ nudge: `Recorded with an auto-created journal stub (${stubJournalId}). Consider journal.append with what you did and what it proves, then link it.`
1447
+ } : {}
1448
+ };
1449
+ });
1450
+ }
1451
+ invalidate(input) {
1452
+ const args = kbInvalidateSchema.parse(input);
1453
+ const project = this.project(args.project);
1454
+ const kind = idKind(args.id);
1455
+ const target = kind ? dbTargetForKind(kind) : null;
1456
+ if (!target || target.targetType === "journal_entry") {
1457
+ throw new Error(`No record found for id ${args.id}`);
1458
+ }
1459
+ return this.runMutation(() => {
1460
+ const row = this.loadRecord(target.table, args.id, project.id);
1461
+ if (!row) throw new Error(`No record found for id ${args.id}`);
1462
+ if (row.status !== "active") {
1463
+ throw new Error(`Record ${args.id} is invalid and read-only`);
1464
+ }
1465
+ if (args.superseded_by) {
1466
+ const supersedingKind = idKind(args.superseded_by);
1467
+ const supersedingTarget = supersedingKind ? dbTargetForKind(supersedingKind) : null;
1468
+ if (!supersedingTarget || supersedingTarget.table !== target.table) {
1469
+ throw new Error("superseded_by must reference the same record type");
1470
+ }
1471
+ if (!this.loadRecord(target.table, args.superseded_by, project.id)) {
1472
+ throw new Error(`No record found for superseded_by ${args.superseded_by}`);
1473
+ }
1474
+ }
1475
+ const invalidatedAt = now();
1476
+ this.db.prepare(
1477
+ `UPDATE ${target.table}
1478
+ SET status = 'invalid', invalidated_at = ?, invalidation_note = ?, superseded_by = ?
1479
+ WHERE id = ? AND project_id = ?`
1480
+ ).run(invalidatedAt, args.note, args.superseded_by ?? null, args.id, project.id);
1481
+ if (target.table === "statement") return statementOut(this.loadStatement(args.id, project.id));
1482
+ if (target.table === "entity") {
1483
+ const cascade = this.db.prepare(
1484
+ `UPDATE statement
1485
+ SET status = 'invalid', invalidated_at = ?, invalidation_note = ?
1486
+ WHERE entity_id = ? AND project_id = ? AND status = 'active'`
1487
+ ).run(invalidatedAt, `Parent entity ${args.id} retired: ${args.note}`, args.id, project.id);
1488
+ return { ...entityOut(this.loadEntity(args.id, project.id)), cascaded_statements: cascade.changes };
1489
+ }
1490
+ return relationshipOut(this.loadRelationship(args.id, project.id));
1491
+ });
1492
+ }
1493
+ async appendJournal(input) {
1494
+ const args = journalAppendSchema.parse(input);
1495
+ if (args.narrative === void 0 && args.commands === void 0 && args.proven === void 0 && args.disproven === void 0 && args.links === void 0) {
1496
+ throw new Error("journal.append requires at least one of narrative, commands, proven, disproven, or links");
1497
+ }
1498
+ const project = this.project(args.project);
1499
+ const journalId = newId("journal");
1500
+ const narrative = args.narrative ?? null;
1501
+ const shouldIndex = narrative !== null && narrative.length > 0;
1502
+ const vec = shouldIndex ? await this.embeddings.embedDocument(narrative) : null;
1503
+ const hash = shouldIndex ? this.embeddings.contentHash(narrative) : null;
1504
+ return this.runMutation(() => {
1505
+ const timestamp = now();
1506
+ for (const statementId of args.proven ?? []) {
1507
+ this.requireStatement(statementId, project.id);
1508
+ }
1509
+ for (const statementId of args.disproven ?? []) {
1510
+ this.requireStatement(statementId, project.id);
1511
+ }
1512
+ for (const link of args.links ?? []) {
1513
+ this.requireTarget(link.target_type, link.target_id, project.id);
1514
+ }
1515
+ this.insertJournal(
1516
+ journalId,
1517
+ project.id,
1518
+ timestamp,
1519
+ args.commands ?? null,
1520
+ args.proven ?? null,
1521
+ args.disproven ?? null,
1522
+ narrative,
1523
+ false
1524
+ );
1525
+ for (const link of args.links ?? []) {
1526
+ this.insertJournalLink(journalId, link.target_type, link.target_id, link.role);
1527
+ }
1528
+ for (const statementId of args.proven ?? []) {
1529
+ this.insertJournalLink(journalId, "statement", statementId, "proven");
1530
+ }
1531
+ for (const statementId of args.disproven ?? []) {
1532
+ this.insertJournalLink(journalId, "statement", statementId, "disproven");
1533
+ }
1534
+ if (shouldIndex) {
1535
+ upsertJournalFts(this.db, journalId, narrative, args.commands ?? null);
1536
+ upsertVector(
1537
+ this.db,
1538
+ "journal_entry",
1539
+ journalId,
1540
+ vec,
1541
+ this.embeddings.modelId(),
1542
+ this.embeddings.dim(),
1543
+ hash
1544
+ );
1545
+ }
1546
+ return journalOut(this.db, this.loadJournal(journalId, project.id));
1547
+ });
1548
+ }
1549
+ delete(input) {
1550
+ const args = kbDeleteSchema.parse(input);
1551
+ const project = this.project(args.project);
1552
+ const kind = idKind(args.id);
1553
+ const target = kind ? dbTargetForKind(kind) : null;
1554
+ if (!target) {
1555
+ throw new Error(`No record found for id ${args.id}`);
1556
+ }
1557
+ const result = withRetry(
1558
+ () => this.db.transaction(() => {
1559
+ const row = this.loadRecord(target.table, args.id, project.id);
1560
+ if (!row) throw new Error(`No record found for id ${args.id}`);
1561
+ const isJournalTarget = target.table === "journal_entry";
1562
+ const timestamp = now();
1563
+ let journalId = null;
1564
+ if (!isJournalTarget) {
1565
+ journalId = newId("journal");
1566
+ this.insertJournal(
1567
+ journalId,
1568
+ project.id,
1569
+ timestamp,
1570
+ null,
1571
+ null,
1572
+ null,
1573
+ `DELETED ${target.targetType} ${args.id}: ${args.reason}`,
1574
+ false
1575
+ );
1576
+ this.insertJournalLink(
1577
+ journalId,
1578
+ target.targetType,
1579
+ args.id,
1580
+ "deleted"
1581
+ );
1582
+ }
1583
+ if (isJournalTarget) {
1584
+ this.db.prepare("UPDATE journal_entry SET superseded_by = NULL WHERE superseded_by = ? AND project_id = ?").run(args.id, project.id);
1585
+ } else {
1586
+ this.db.prepare(
1587
+ `UPDATE ${target.table}
1588
+ SET superseded_by = NULL,
1589
+ invalidation_note = COALESCE(invalidation_note, '') || ?
1590
+ WHERE superseded_by = ? AND project_id = ?`
1591
+ ).run(` [redirect target deleted ${args.id}]`, args.id, project.id);
1592
+ }
1593
+ if (isJournalTarget) {
1594
+ this.db.prepare("DELETE FROM journal_link WHERE (target_type = ? AND target_id = ?) OR journal_id = ?").run(target.targetType, args.id, args.id);
1595
+ } else {
1596
+ this.db.prepare(
1597
+ "DELETE FROM journal_link WHERE target_type = ? AND target_id = ? AND NOT (journal_id = ? AND role = 'deleted')"
1598
+ ).run(target.targetType, args.id, journalId);
1599
+ }
1600
+ if (target.vectorOwner) {
1601
+ deleteVector(this.db, target.vectorOwner, args.id);
1602
+ }
1603
+ if (target.table === "statement") {
1604
+ deleteStatementFts(this.db, args.id);
1605
+ } else if (target.table === "journal_entry") {
1606
+ deleteJournalFts(this.db, args.id);
1607
+ }
1608
+ this.db.prepare(`DELETE FROM ${target.table} WHERE id = ? AND project_id = ?`).run(args.id, project.id);
1609
+ return { deleted: args.id, target_type: target.targetType, journal_entry_id: journalId };
1610
+ })()
1611
+ );
1612
+ this.db.pragma("wal_checkpoint(TRUNCATE)");
1613
+ this.db.exec("VACUUM");
1614
+ return result;
1615
+ }
1616
+ recent(input) {
1617
+ const args = memoryRecentSchema.parse(input);
1618
+ const project = this.project(args.project);
1619
+ const includeKb = args.where === "knowledge-base" || args.where === "both";
1620
+ const includeJournal = args.where === "journal" || args.where === "both";
1621
+ const rows = [];
1622
+ if (includeKb && (!args.kind || args.kind === "entity")) {
1623
+ rows.push(
1624
+ ...this.db.prepare(
1625
+ `SELECT 'entity' AS kind, id, created_at, title AS title_or_snippet
1626
+ FROM entity
1627
+ WHERE project_id = ? ${args.include_invalid ? "" : "AND status = 'active'"} ${args.before ? "AND created_at < ?" : ""}`
1628
+ ).all(...args.before ? [project.id, args.before] : [project.id])
1629
+ );
1630
+ }
1631
+ if (includeKb && (!args.kind || args.kind === "statement")) {
1632
+ rows.push(
1633
+ ...this.db.prepare(
1634
+ `SELECT 'statement' AS kind, id, created_at, claim AS title_or_snippet
1635
+ FROM statement
1636
+ WHERE project_id = ? ${args.include_invalid ? "" : "AND status = 'active'"} ${args.before ? "AND created_at < ?" : ""}`
1637
+ ).all(...args.before ? [project.id, args.before] : [project.id])
1638
+ );
1639
+ }
1640
+ if (includeJournal && (!args.kind || args.kind === "journal")) {
1641
+ rows.push(
1642
+ ...this.db.prepare(
1643
+ `SELECT 'journal' AS kind, id, created_at, COALESCE(narrative, commands, '') AS title_or_snippet
1644
+ FROM journal_entry
1645
+ WHERE project_id = ? ${args.include_invalid ? "" : "AND status = 'active'"} ${args.before ? "AND created_at < ?" : ""}`
1646
+ ).all(...args.before ? [project.id, args.before] : [project.id])
1647
+ );
1648
+ }
1649
+ const reference = now();
1650
+ const sorted = rows.map((row) => ({
1651
+ ...row,
1652
+ title_or_snippet: snippet(row.title_or_snippet),
1653
+ _relative: { created_at: humanizeRelative(row.created_at, reference) }
1654
+ })).sort((a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id));
1655
+ const page = sorted.slice(0, args.limit);
1656
+ const last = page.at(-1);
1657
+ const totalRemaining = last ? sorted.filter((row) => row.created_at < last.created_at).length : 0;
1658
+ const oldest = sorted.length ? Math.min(...sorted.map((row) => row.created_at)) : null;
1659
+ const nextBefore = totalRemaining > 0 && last ? last.created_at : null;
1660
+ return {
1661
+ items: page,
1662
+ next_before: nextBefore,
1663
+ total_remaining: totalRemaining,
1664
+ oldest_record_date: oldest,
1665
+ _relative: relativeMap(
1666
+ { next_before: nextBefore, oldest_record_date: oldest },
1667
+ ["next_before", "oldest_record_date"],
1668
+ reference
1669
+ )
1670
+ };
1671
+ }
1672
+ stats(input) {
1673
+ const args = memoryStatsSchema.parse(input);
1674
+ const project = this.project(args.project);
1675
+ const countByStatus = (table) => this.db.prepare(`SELECT status, COUNT(*) AS count FROM ${table} WHERE project_id = ? GROUP BY status`).all(project.id);
1676
+ return {
1677
+ project: project.id,
1678
+ entities: countByStatus("entity"),
1679
+ statements: countByStatus("statement"),
1680
+ journal: countByStatus("journal_entry"),
1681
+ embeddings: this.db.prepare(
1682
+ `SELECT COUNT(*) AS count
1683
+ FROM embedding e
1684
+ WHERE (
1685
+ e.owner_type = 'statement'
1686
+ AND EXISTS (
1687
+ SELECT 1 FROM statement s
1688
+ WHERE s.id = e.owner_id AND s.project_id = ?
1689
+ )
1690
+ ) OR (
1691
+ e.owner_type = 'journal_entry'
1692
+ AND EXISTS (
1693
+ SELECT 1 FROM journal_entry j
1694
+ WHERE j.id = e.owner_id AND j.project_id = ?
1695
+ )
1696
+ )`
1697
+ ).get(project.id, project.id).count,
1698
+ db_file_size: fs4.existsSync(this.dbFile) ? fs4.statSync(this.dbFile).size : 0,
1699
+ model_id: this.embeddings.modelId(),
1700
+ dim: this.embeddings.dim(),
1701
+ freelist_count: this.db.pragma("freelist_count", { simple: true })
1702
+ };
1703
+ }
1704
+ guide(input) {
1705
+ emptySchema.parse(input);
1706
+ return { guide: GUIDE };
1707
+ }
1708
+ agentsMdSnippet(input) {
1709
+ emptySchema.parse(input);
1710
+ return { snippet: AGENTS_MD_SNIPPET };
1711
+ }
1712
+ searchStatements(query, queryVec, project, config, timestamp, filters) {
1713
+ const ftsIds = ftsSearch(this.db, "fts_statements", query, config.k_recall_fts).map((row) => row.id);
1714
+ const vecIds = knn(this.db, queryVec, config.k_recall_vec).filter((row) => row.owner_type === "statement").map((row) => row.owner_id);
1715
+ const ids = unique([...ftsIds, ...vecIds]);
1716
+ if (ids.length === 0) return [];
1717
+ const rows = this.db.prepare(
1718
+ `SELECT s.*, e.type AS entity_type, e.title AS entity_title, e.tags AS entity_tags
1719
+ FROM statement s
1720
+ JOIN entity e ON e.id = s.entity_id
1721
+ WHERE s.id IN (${placeholders(ids)})`
1722
+ ).all(...ids);
1723
+ const ftsRanks = rankMap(ftsIds);
1724
+ const vecRanks = rankMap(vecIds);
1725
+ return rows.filter((row) => row.project_id === project.id).filter((row) => !filters.type || row.entity_type === filters.type).filter((row) => hasAllTags(row.entity_tags, filters.tags)).filter(
1726
+ (row) => this.visibleByStatus(
1727
+ "statement",
1728
+ row.id,
1729
+ row.status,
1730
+ row.invalidated_at,
1731
+ filters.includeInvalid,
1732
+ filters.includeDeletedSince,
1733
+ config,
1734
+ timestamp
1735
+ )
1736
+ ).map((row) => {
1737
+ const rawRrf = (ftsRanks.has(row.id) ? 1 / (config.rrf_k + ftsRanks.get(row.id)) : 0) + (vecRanks.has(row.id) ? 1 / (config.rrf_k + vecRanks.get(row.id)) : 0);
1738
+ return {
1739
+ kind: "statement",
1740
+ id: row.id,
1741
+ created_at: row.created_at,
1742
+ status: row.status,
1743
+ rawRrf,
1744
+ score: 0,
1745
+ statement: row
1746
+ };
1747
+ });
1748
+ }
1749
+ searchJournal(query, queryVec, project, config, timestamp, filters) {
1750
+ const ftsIds = ftsSearch(this.db, "fts_journal", query, config.k_recall_fts).map((row) => row.id);
1751
+ const vecIds = knn(this.db, queryVec, config.k_recall_vec).filter((row) => row.owner_type === "journal_entry").map((row) => row.owner_id);
1752
+ const ids = unique([...ftsIds, ...vecIds]);
1753
+ if (ids.length === 0) return [];
1754
+ const rows = this.db.prepare(`SELECT * FROM journal_entry WHERE id IN (${placeholders(ids)})`).all(...ids);
1755
+ const ftsRanks = rankMap(ftsIds);
1756
+ const vecRanks = rankMap(vecIds);
1757
+ return rows.filter((row) => row.project_id === project.id).filter(
1758
+ (row) => this.visibleByStatus(
1759
+ "journal_entry",
1760
+ row.id,
1761
+ row.status,
1762
+ row.invalidated_at,
1763
+ filters.includeInvalid,
1764
+ filters.includeDeletedSince,
1765
+ config,
1766
+ timestamp
1767
+ )
1768
+ ).map((row) => {
1769
+ const rawRrf = (ftsRanks.has(row.id) ? 1 / (config.rrf_k + ftsRanks.get(row.id)) : 0) + (vecRanks.has(row.id) ? 1 / (config.rrf_k + vecRanks.get(row.id)) : 0);
1770
+ return {
1771
+ kind: "journal",
1772
+ id: row.id,
1773
+ created_at: row.created_at,
1774
+ status: row.status,
1775
+ rawRrf,
1776
+ score: 0,
1777
+ journal: row
1778
+ };
1779
+ });
1780
+ }
1781
+ visibleByStatus(table, id, status, invalidatedAt, includeInvalid, includeDeletedSince, config, timestamp) {
1782
+ if (status === "active") return true;
1783
+ if (!includeInvalid) return false;
1784
+ const window = parseDurationMs(includeDeletedSince ?? config.tombstone_window);
1785
+ if (invalidatedAt !== null && invalidatedAt >= timestamp - window) {
1786
+ return true;
1787
+ }
1788
+ return this.hasLiveRedirectTo(table, id);
1789
+ }
1790
+ hasLiveRedirectTo(table, id) {
1791
+ const row = this.db.prepare(`SELECT 1 AS found FROM ${table} WHERE status = 'active' AND superseded_by = ? LIMIT 1`).get(id);
1792
+ return Boolean(row);
1793
+ }
1794
+ project(projectOverride) {
1795
+ return this.resolver.resolve(projectOverride);
1796
+ }
1797
+ config(project) {
1798
+ return resolveConfig(project.configJson, project.fileConfig);
1799
+ }
1800
+ runMutation(fn) {
1801
+ const result = withRetry(() => this.db.transaction(fn)());
1802
+ maybeVacuum(this.db, this.random);
1803
+ return result;
1804
+ }
1805
+ loadEntity(id, projectId) {
1806
+ return this.db.prepare("SELECT * FROM entity WHERE id = ? AND project_id = ?").get(id, projectId);
1807
+ }
1808
+ loadStatement(id, projectId) {
1809
+ return this.db.prepare("SELECT * FROM statement WHERE id = ? AND project_id = ?").get(id, projectId);
1810
+ }
1811
+ loadJournal(id, projectId) {
1812
+ return this.db.prepare("SELECT * FROM journal_entry WHERE id = ? AND project_id = ?").get(id, projectId);
1813
+ }
1814
+ loadRelationship(id, projectId) {
1815
+ return this.db.prepare("SELECT * FROM relationship WHERE id = ? AND project_id = ?").get(id, projectId);
1816
+ }
1817
+ loadRecord(table, id, projectId) {
1818
+ return this.db.prepare(`SELECT * FROM ${table} WHERE id = ? AND project_id = ?`).get(id, projectId);
1819
+ }
1820
+ requireStatement(id, projectId) {
1821
+ if (!this.loadStatement(id, projectId)) {
1822
+ throw new Error(`No statement found for id ${id}`);
1823
+ }
1824
+ }
1825
+ requireJournal(id, projectId) {
1826
+ if (!this.loadJournal(id, projectId)) {
1827
+ throw new Error(`No journal entry found for id ${id}`);
1828
+ }
1829
+ }
1830
+ requireTarget(targetType, id, projectId) {
1831
+ const table = targetType === "entity" ? "entity" : targetType === "statement" ? "statement" : "relationship";
1832
+ if (!this.loadRecord(table, id, projectId)) {
1833
+ throw new Error(`No ${targetType} found for id ${id}`);
1834
+ }
1835
+ }
1836
+ insertStatement(input) {
1837
+ this.db.prepare(
1838
+ `INSERT INTO statement(
1839
+ id, project_id, entity_id, edge_id, claim, confidence_level, confidence_reason,
1840
+ derivation_method, citations, created_at, valid_from, valid_to, status
1841
+ ) VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`
1842
+ ).run(
1843
+ input.id,
1844
+ input.projectId,
1845
+ input.entityId,
1846
+ input.claim,
1847
+ input.confidenceLevel,
1848
+ input.confidenceReason,
1849
+ input.derivationMethod,
1850
+ input.citations === void 0 ? null : JSON.stringify(input.citations),
1851
+ input.createdAt,
1852
+ input.validFrom,
1853
+ input.validTo
1854
+ );
1855
+ }
1856
+ insertJournal(id, projectId, createdAt, commands, proven, disproven, narrative, isStub) {
1857
+ this.db.prepare(
1858
+ `INSERT INTO journal_entry(id, project_id, created_at, commands, proven, disproven, narrative, is_stub, status)
1859
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active')`
1860
+ ).run(
1861
+ id,
1862
+ projectId,
1863
+ createdAt,
1864
+ commands ? JSON.stringify(commands) : null,
1865
+ proven ? JSON.stringify(proven) : null,
1866
+ disproven ? JSON.stringify(disproven) : null,
1867
+ narrative,
1868
+ isStub ? 1 : 0
1869
+ );
1870
+ }
1871
+ insertJournalLink(journalId, targetType, targetId, role) {
1872
+ this.db.prepare("INSERT OR IGNORE INTO journal_link(journal_id, target_type, target_id, role) VALUES (?, ?, ?, ?)").run(journalId, targetType, targetId, role);
1873
+ }
1874
+ };
1875
+
1876
+ // src/index.ts
1877
+ function readVersion() {
1878
+ try {
1879
+ const pkgUrl = new URL("../package.json", import.meta.url);
1880
+ const pkg = JSON.parse(readFileSync(fileURLToPath(pkgUrl), "utf8"));
1881
+ return pkg.version ?? "0.0.0";
1882
+ } catch {
1883
+ return "0.0.0";
1884
+ }
1885
+ }
1886
+ function helpText(version) {
1887
+ return `agent-journal v${version}
1888
+
1889
+ A local, project-scoped MCP server that gives coding agents a persistent
1890
+ knowledge base and journal. It is meant to be launched by an agent with the
1891
+ --mcp flag, not run directly in a terminal.
1892
+
1893
+ Add it to Claude Code:
1894
+
1895
+ claude mcp add agent-journal -- npx -y agent-journal --mcp
1896
+
1897
+ Add it to Codex:
1898
+
1899
+ codex mcp add agent-journal -- npx -y agent-journal --mcp
1900
+
1901
+ Or add this server entry to your client's MCP config file (commonly .mcp.json):
1902
+
1903
+ {
1904
+ "mcpServers": {
1905
+ "agent-journal": {
1906
+ "command": "npx",
1907
+ "args": ["-y", "agent-journal", "--mcp"]
1908
+ }
1909
+ }
1910
+ }
1911
+
1912
+ Options:
1913
+ --mcp Run as an MCP server
1914
+ -h, --help Show this help text
1915
+ -v, --version Print the version
1916
+
1917
+ Environment:
1918
+ AGENT_JOURNAL_DB Override the database path (defaults to memory.db under the
1919
+ app config directory)
1920
+
1921
+ Docs: https://github.com/steelbrain/agent-journal#readme`;
1922
+ }
1923
+ async function serve(version) {
1924
+ const dbFile = defaultDbPath();
1925
+ const db = openDb(dbFile);
1926
+ const resolver = new ProjectResolver(db, process.cwd());
1927
+ resolver.resolve();
1928
+ const embeddings = new TransformersEmbeddings();
1929
+ const api = new MemoryApi({ db, resolver, embeddings, dbFile });
1930
+ const server = createMcpServer(api, version);
1931
+ embeddings.warmup();
1932
+ await connectStdio(server);
1933
+ }
1934
+ async function main() {
1935
+ const version = readVersion();
1936
+ const args = process.argv.slice(2);
1937
+ if (args.includes("-v") || args.includes("--version")) {
1938
+ process.stdout.write(`${version}
1939
+ `);
1940
+ return;
1941
+ }
1942
+ if (args.includes("--mcp")) {
1943
+ await serve(version);
1944
+ return;
1945
+ }
1946
+ process.stdout.write(`${helpText(version)}
1947
+ `);
1948
+ }
1949
+ main().catch((err) => {
1950
+ console.error(err instanceof Error && err.stack ? err.stack : err);
1951
+ process.exitCode = 1;
1952
+ });