codemem 0.1.0 → 0.20.0-alpha.1

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 (41) hide show
  1. package/.opencode/lib/compat.js +171 -0
  2. package/.opencode/plugin/codemem.js +1851 -0
  3. package/dist/commands/claude-hook-ingest.d.ts +15 -0
  4. package/dist/commands/claude-hook-ingest.d.ts.map +1 -0
  5. package/dist/commands/db.d.ts +3 -0
  6. package/dist/commands/db.d.ts.map +1 -0
  7. package/dist/commands/enqueue-raw-event.d.ts +3 -0
  8. package/dist/commands/enqueue-raw-event.d.ts.map +1 -0
  9. package/dist/commands/export-memories.d.ts +3 -0
  10. package/dist/commands/export-memories.d.ts.map +1 -0
  11. package/dist/commands/import-memories.d.ts +3 -0
  12. package/dist/commands/import-memories.d.ts.map +1 -0
  13. package/dist/commands/mcp.d.ts +3 -0
  14. package/dist/commands/mcp.d.ts.map +1 -0
  15. package/dist/commands/memory.d.ts +10 -0
  16. package/dist/commands/memory.d.ts.map +1 -0
  17. package/dist/commands/pack.d.ts +3 -0
  18. package/dist/commands/pack.d.ts.map +1 -0
  19. package/dist/commands/recent.d.ts +3 -0
  20. package/dist/commands/recent.d.ts.map +1 -0
  21. package/dist/commands/search.d.ts +3 -0
  22. package/dist/commands/search.d.ts.map +1 -0
  23. package/dist/commands/serve.d.ts +3 -0
  24. package/dist/commands/serve.d.ts.map +1 -0
  25. package/dist/commands/setup.d.ts +15 -0
  26. package/dist/commands/setup.d.ts.map +1 -0
  27. package/dist/commands/stats.d.ts +3 -0
  28. package/dist/commands/stats.d.ts.map +1 -0
  29. package/dist/commands/sync.d.ts +6 -0
  30. package/dist/commands/sync.d.ts.map +1 -0
  31. package/dist/commands/version.d.ts +3 -0
  32. package/dist/commands/version.d.ts.map +1 -0
  33. package/dist/help-style.d.ts +9 -0
  34. package/dist/help-style.d.ts.map +1 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +1076 -0
  38. package/dist/index.js.map +1 -0
  39. package/package.json +62 -24
  40. package/README.md +0 -5
  41. package/index.js +0 -6
package/dist/index.js ADDED
@@ -0,0 +1,1076 @@
1
+ #!/usr/bin/env node
2
+ import { MemoryStore, ObserverClient, RawEventSweeper, VERSION, buildRawEventEnvelopeFromHook, connect, ensureDeviceIdentity, exportMemories, getRawEventStatus, importMemories, initDatabase, loadSqliteVec, rawEventsGate, readCodememConfigFile, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
+ import { Command } from "commander";
4
+ import omelette from "omelette";
5
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { styleText } from "node:util";
7
+ import * as p from "@clack/prompts";
8
+ import { homedir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ import { desc } from "drizzle-orm";
11
+ import { drizzle } from "drizzle-orm/better-sqlite3";
12
+ //#region src/help-style.ts
13
+ /**
14
+ * Shared Commander help style configuration.
15
+ *
16
+ * Applied to every Command instance so subcommand --help
17
+ * output gets the same colors as the root.
18
+ */
19
+ var helpStyle = {
20
+ styleTitle: (str) => styleText("bold", str),
21
+ styleCommandText: (str) => styleText("cyan", str),
22
+ styleCommandDescription: (str) => str,
23
+ styleDescriptionText: (str) => styleText("dim", str),
24
+ styleOptionText: (str) => styleText("green", str),
25
+ styleOptionTerm: (str) => styleText("green", str),
26
+ styleSubcommandText: (str) => styleText("cyan", str),
27
+ styleArgumentText: (str) => styleText("yellow", str)
28
+ };
29
+ //#endregion
30
+ //#region src/commands/claude-hook-ingest.ts
31
+ /**
32
+ * codemem claude-hook-ingest — read a single Claude Code hook payload
33
+ * from stdin and enqueue it for processing.
34
+ *
35
+ * Ports codemem/commands/claude_hook_runtime_cmds.py with an HTTP-first
36
+ * strategy: try POST /api/claude-hooks (viewer must be running), then
37
+ * fall back to direct raw-event enqueue via the local store.
38
+ *
39
+ * Usage (from Claude hooks config):
40
+ * echo '{"hook_event_name":"Stop","session_id":"...","last_assistant_message":"..."}' \
41
+ * | codemem claude-hook-ingest
42
+ */
43
+ /** Try to POST the hook payload to the running viewer server. */
44
+ async function tryHttpIngest(payload, host, port) {
45
+ const url = `http://${host}:${port}/api/claude-hooks`;
46
+ const controller = new AbortController();
47
+ const timeout = setTimeout(() => controller.abort(), 5e3);
48
+ try {
49
+ const res = await fetch(url, {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify(payload),
53
+ signal: controller.signal
54
+ });
55
+ if (!res.ok) return {
56
+ ok: false,
57
+ inserted: 0,
58
+ skipped: 0
59
+ };
60
+ const body = await res.json();
61
+ return {
62
+ ok: true,
63
+ inserted: Number(body.inserted ?? 0),
64
+ skipped: Number(body.skipped ?? 0)
65
+ };
66
+ } catch {
67
+ return {
68
+ ok: false,
69
+ inserted: 0,
70
+ skipped: 0
71
+ };
72
+ } finally {
73
+ clearTimeout(timeout);
74
+ }
75
+ }
76
+ /** Fall back to direct raw-event enqueue via the local SQLite store. */
77
+ function directEnqueue(payload, dbPath) {
78
+ const envelope = buildRawEventEnvelopeFromHook(payload);
79
+ if (!envelope) return {
80
+ inserted: 0,
81
+ skipped: 1
82
+ };
83
+ const db = connect(dbPath);
84
+ try {
85
+ try {
86
+ loadSqliteVec(db);
87
+ } catch {}
88
+ const strippedPayload = stripPrivateObj(envelope.payload);
89
+ if (db.prepare("SELECT 1 FROM raw_events WHERE source = ? AND stream_id = ? AND event_id = ? LIMIT 1").get(envelope.source, envelope.session_stream_id, envelope.event_id)) return {
90
+ inserted: 0,
91
+ skipped: 0
92
+ };
93
+ db.prepare(`INSERT INTO raw_events(
94
+ source, stream_id, opencode_session_id, event_id, event_seq,
95
+ event_type, ts_wall_ms, payload_json, created_at
96
+ ) VALUES (?, ?, ?, ?, (
97
+ SELECT COALESCE(MAX(event_seq), 0) + 1
98
+ FROM raw_events WHERE source = ? AND stream_id = ?
99
+ ), ?, ?, ?, datetime('now'))`).run(envelope.source, envelope.session_stream_id, envelope.opencode_session_id, envelope.event_id, envelope.source, envelope.session_stream_id, "claude.hook", envelope.ts_wall_ms, JSON.stringify(strippedPayload));
100
+ const currentMaxSeq = db.prepare("SELECT COALESCE(MAX(event_seq), 0) AS max_seq FROM raw_events WHERE source = ? AND stream_id = ?").get(envelope.source, envelope.session_stream_id).max_seq;
101
+ db.prepare(`INSERT INTO raw_event_sessions(
102
+ source, stream_id, opencode_session_id, cwd, project, started_at,
103
+ last_seen_ts_wall_ms, last_received_event_seq, last_flushed_event_seq, updated_at
104
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, -1, datetime('now'))
105
+ ON CONFLICT(source, stream_id) DO UPDATE SET
106
+ cwd = COALESCE(excluded.cwd, cwd),
107
+ project = COALESCE(excluded.project, project),
108
+ started_at = COALESCE(excluded.started_at, started_at),
109
+ last_seen_ts_wall_ms = MAX(COALESCE(excluded.last_seen_ts_wall_ms, 0), COALESCE(last_seen_ts_wall_ms, 0)),
110
+ last_received_event_seq = MAX(excluded.last_received_event_seq, last_received_event_seq),
111
+ updated_at = datetime('now')`).run(envelope.source, envelope.session_stream_id, envelope.opencode_session_id, envelope.cwd, envelope.project, envelope.started_at, envelope.ts_wall_ms, currentMaxSeq);
112
+ return {
113
+ inserted: 1,
114
+ skipped: 0
115
+ };
116
+ } finally {
117
+ db.close();
118
+ }
119
+ }
120
+ var claudeHookIngestCommand = new Command("claude-hook-ingest").configureHelp(helpStyle).description("Ingest a Claude Code hook payload from stdin").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--host <host>", "viewer server host", "127.0.0.1").option("--port <port>", "viewer server port", "38888").action(async (opts) => {
121
+ let raw;
122
+ try {
123
+ raw = readFileSync(0, "utf8").trim();
124
+ } catch {
125
+ process.exitCode = 1;
126
+ return;
127
+ }
128
+ if (!raw) {
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+ let payload;
133
+ try {
134
+ const parsed = JSON.parse(raw);
135
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+ payload = parsed;
140
+ } catch {
141
+ process.exitCode = 1;
142
+ return;
143
+ }
144
+ const port = Number.parseInt(opts.port, 10);
145
+ const host = opts.host;
146
+ const httpResult = await tryHttpIngest(payload, host, port);
147
+ if (httpResult.ok) {
148
+ console.log(JSON.stringify({
149
+ inserted: httpResult.inserted,
150
+ skipped: httpResult.skipped,
151
+ via: "http"
152
+ }));
153
+ return;
154
+ }
155
+ try {
156
+ const dbPath = resolveDbPath(opts.db);
157
+ const directResult = directEnqueue(payload, dbPath);
158
+ console.log(JSON.stringify({
159
+ ...directResult,
160
+ via: "direct"
161
+ }));
162
+ } catch {
163
+ process.exitCode = 1;
164
+ }
165
+ });
166
+ //#endregion
167
+ //#region src/commands/db.ts
168
+ var dbCommand = new Command("db").configureHelp(helpStyle).description("Database maintenance");
169
+ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("Verify the SQLite database is present and schema-ready").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action((opts) => {
170
+ const result = initDatabase(opts.db);
171
+ p.intro("codemem db init");
172
+ p.log.success(`Database ready: ${result.path}`);
173
+ p.outro(`Size: ${result.sizeBytes.toLocaleString()} bytes`);
174
+ })).addCommand(new Command("vacuum").configureHelp(helpStyle).description("Run VACUUM on the SQLite database").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action((opts) => {
175
+ const result = vacuumDatabase(opts.db);
176
+ p.intro("codemem db vacuum");
177
+ p.log.success(`Vacuumed: ${result.path}`);
178
+ p.outro(`Size: ${result.sizeBytes.toLocaleString()} bytes`);
179
+ })).addCommand(new Command("raw-events-status").configureHelp(helpStyle).description("Show pending raw-event backlog by source stream").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max rows to show", "25").option("--json", "output as JSON").action((opts) => {
180
+ const result = getRawEventStatus(opts.db, Number.parseInt(opts.limit, 10) || 25);
181
+ if (opts.json) {
182
+ console.log(JSON.stringify(result, null, 2));
183
+ return;
184
+ }
185
+ p.intro("codemem db raw-events-status");
186
+ p.log.info(`Totals: ${result.totals.pending.toLocaleString()} pending across ${result.totals.sessions.toLocaleString()} session(s)`);
187
+ if (result.items.length === 0) {
188
+ p.outro("No pending raw events");
189
+ return;
190
+ }
191
+ for (const item of result.items) p.log.message(`${item.source}:${item.stream_id} pending=${Math.max(0, item.last_received_event_seq - item.last_flushed_event_seq)} received=${item.last_received_event_seq} flushed=${item.last_flushed_event_seq} project=${item.project ?? ""}`);
192
+ p.outro("done");
193
+ })).addCommand(new Command("raw-events-retry").configureHelp(helpStyle).description("Requeue failed raw-event flush batches").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max failed batches to requeue", "25").action((opts) => {
194
+ const result = retryRawEventFailures(opts.db, Number.parseInt(opts.limit, 10) || 25);
195
+ p.intro("codemem db raw-events-retry");
196
+ p.outro(`Requeued ${result.retried.toLocaleString()} failed batch(es)`);
197
+ })).addCommand(new Command("raw-events-gate").configureHelp(helpStyle).description("Validate raw-event reliability thresholds (non-zero exit on failure)").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--min-flush-success-rate <rate>", "minimum flush success rate", "0.95").option("--max-dropped-event-rate <rate>", "maximum dropped event rate", "0.05").option("--min-session-boundary-accuracy <rate>", "minimum session boundary accuracy", "0.9").option("--window-hours <hours>", "lookback window in hours", "24").option("--json", "output as JSON").action((opts) => {
198
+ const result = rawEventsGate(opts.db, {
199
+ minFlushSuccessRate: Number.parseFloat(opts.minFlushSuccessRate),
200
+ maxDroppedEventRate: Number.parseFloat(opts.maxDroppedEventRate),
201
+ minSessionBoundaryAccuracy: Number.parseFloat(opts.minSessionBoundaryAccuracy),
202
+ windowHours: Number.parseFloat(opts.windowHours)
203
+ });
204
+ if (opts.json) {
205
+ console.log(JSON.stringify(result, null, 2));
206
+ if (!result.passed) process.exitCode = 1;
207
+ return;
208
+ }
209
+ p.intro("codemem db raw-events-gate");
210
+ p.log.info([
211
+ `flush_success_rate: ${result.metrics.rates.flush_success_rate.toFixed(4)}`,
212
+ `dropped_event_rate: ${result.metrics.rates.dropped_event_rate.toFixed(4)}`,
213
+ `session_boundary_accuracy: ${result.metrics.rates.session_boundary_accuracy.toFixed(4)}`,
214
+ `window_hours: ${result.metrics.window_hours ?? "all"}`
215
+ ].join("\n"));
216
+ if (result.passed) p.outro("reliability gate passed");
217
+ else {
218
+ for (const f of result.failures) p.log.error(f);
219
+ p.outro("reliability gate FAILED");
220
+ process.exitCode = 1;
221
+ }
222
+ }));
223
+ //#endregion
224
+ //#region src/commands/enqueue-raw-event.ts
225
+ var SESSION_ID_KEYS = [
226
+ "session_stream_id",
227
+ "session_id",
228
+ "stream_id",
229
+ "opencode_session_id"
230
+ ];
231
+ function resolveSessionStreamId(payload) {
232
+ const values = /* @__PURE__ */ new Map();
233
+ for (const key of SESSION_ID_KEYS) {
234
+ const value = payload[key];
235
+ if (typeof value !== "string") continue;
236
+ const text = value.trim();
237
+ if (text) values.set(key, text);
238
+ }
239
+ if (values.size === 0) return null;
240
+ if (new Set(values.values()).size > 1) throw new Error("conflicting session id fields");
241
+ for (const key of SESSION_ID_KEYS) {
242
+ const value = values.get(key);
243
+ if (value) return value;
244
+ }
245
+ return null;
246
+ }
247
+ async function readStdinJson() {
248
+ const chunks = [];
249
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
250
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
251
+ if (!raw) throw new Error("stdin JSON required");
252
+ const parsed = JSON.parse(raw);
253
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("payload must be an object");
254
+ return parsed;
255
+ }
256
+ var enqueueRawEventCommand = new Command("enqueue-raw-event").configureHelp(helpStyle).description("Enqueue one raw event from stdin into the durable queue").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action(async (opts) => {
257
+ const payload = await readStdinJson();
258
+ const sessionId = resolveSessionStreamId(payload);
259
+ if (!sessionId) throw new Error("session id required");
260
+ if (sessionId.startsWith("msg_")) throw new Error("invalid session id");
261
+ const eventType = typeof payload.event_type === "string" ? payload.event_type.trim() : "";
262
+ if (!eventType) throw new Error("event_type required");
263
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
264
+ const project = typeof payload.project === "string" ? payload.project : null;
265
+ const startedAt = typeof payload.started_at === "string" ? payload.started_at : null;
266
+ const tsWallMs = Number.isFinite(Number(payload.ts_wall_ms)) ? Math.floor(Number(payload.ts_wall_ms)) : null;
267
+ const tsMonoMs = Number.isFinite(Number(payload.ts_mono_ms)) ? Number(payload.ts_mono_ms) : null;
268
+ const eventId = typeof payload.event_id === "string" ? payload.event_id.trim() : "";
269
+ const eventPayload = payload.payload && typeof payload.payload === "object" && !Array.isArray(payload.payload) ? stripPrivateObj(payload.payload) : {};
270
+ const store = new MemoryStore(resolveDbPath(opts.db));
271
+ try {
272
+ store.updateRawEventSessionMeta({
273
+ opencodeSessionId: sessionId,
274
+ source: "opencode",
275
+ cwd,
276
+ project,
277
+ startedAt,
278
+ lastSeenTsWallMs: tsWallMs
279
+ });
280
+ store.recordRawEventsBatch(sessionId, [{
281
+ event_id: eventId,
282
+ event_type: eventType,
283
+ payload: eventPayload,
284
+ ts_wall_ms: tsWallMs,
285
+ ts_mono_ms: tsMonoMs
286
+ }]);
287
+ } finally {
288
+ store.close();
289
+ }
290
+ });
291
+ //#endregion
292
+ //#region src/commands/export-memories.ts
293
+ function expandUserPath(value) {
294
+ return value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
295
+ }
296
+ var exportMemoriesCommand = new Command("export-memories").configureHelp(helpStyle).description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--project <project>", "filter by project (defaults to git repo root)").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp").action((output, opts) => {
297
+ const payload = exportMemories({
298
+ dbPath: resolveDbPath(opts.db),
299
+ project: opts.project,
300
+ allProjects: opts.allProjects,
301
+ includeInactive: opts.includeInactive,
302
+ since: opts.since
303
+ });
304
+ const text = `${JSON.stringify(payload, null, 2)}\n`;
305
+ if (output === "-") {
306
+ process.stdout.write(text);
307
+ return;
308
+ }
309
+ const outputPath = expandUserPath(output);
310
+ writeFileSync(outputPath, text, "utf8");
311
+ p.intro("codemem export-memories");
312
+ p.log.success([
313
+ `Output: ${outputPath}`,
314
+ `Sessions: ${payload.sessions.length.toLocaleString()}`,
315
+ `Memories: ${payload.memory_items.length.toLocaleString()}`,
316
+ `Summaries: ${payload.session_summaries.length.toLocaleString()}`,
317
+ `Prompts: ${payload.user_prompts.length.toLocaleString()}`
318
+ ].join("\n"));
319
+ p.outro("done");
320
+ });
321
+ //#endregion
322
+ //#region src/commands/import-memories.ts
323
+ var importMemoriesCommand = new Command("import-memories").configureHelp(helpStyle).description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing").action((inputFile, opts) => {
324
+ let payload;
325
+ try {
326
+ payload = readImportPayload(inputFile);
327
+ } catch (error) {
328
+ p.log.error(error instanceof Error ? error.message : "Invalid import file");
329
+ process.exitCode = 1;
330
+ return;
331
+ }
332
+ p.intro("codemem import-memories");
333
+ p.log.info([
334
+ `Export version: ${payload.version}`,
335
+ `Exported at: ${payload.exported_at}`,
336
+ `Sessions: ${payload.sessions.length.toLocaleString()}`,
337
+ `Memories: ${payload.memory_items.length.toLocaleString()}`,
338
+ `Summaries: ${payload.session_summaries.length.toLocaleString()}`,
339
+ `Prompts: ${payload.user_prompts.length.toLocaleString()}`
340
+ ].join("\n"));
341
+ const result = importMemories(payload, {
342
+ dbPath: resolveDbPath(opts.db),
343
+ remapProject: opts.remapProject,
344
+ dryRun: opts.dryRun
345
+ });
346
+ if (result.dryRun) {
347
+ p.outro("dry run complete");
348
+ return;
349
+ }
350
+ p.log.success([
351
+ `Imported sessions: ${result.sessions.toLocaleString()}`,
352
+ `Imported prompts: ${result.user_prompts.toLocaleString()}`,
353
+ `Imported memories: ${result.memory_items.toLocaleString()}`,
354
+ `Imported summaries: ${result.session_summaries.toLocaleString()}`
355
+ ].join("\n"));
356
+ p.outro("done");
357
+ });
358
+ //#endregion
359
+ //#region src/commands/mcp.ts
360
+ var mcpCommand = new Command("mcp").configureHelp(helpStyle).description("Start the MCP stdio server").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action(async (opts) => {
361
+ if (opts.db) process.env.CODEMEM_DB = opts.db;
362
+ await import("@codemem/mcp-server");
363
+ });
364
+ //#endregion
365
+ //#region src/commands/memory.ts
366
+ /**
367
+ * Memory management CLI commands — show, forget, remember.
368
+ *
369
+ * Ports codemem/commands/memory_cmds.py (show_cmd, forget_cmd, remember_cmd).
370
+ * Compact and inject are deferred — compact requires the summarizer pipeline,
371
+ * and inject is just pack_text output which the existing pack command covers.
372
+ */
373
+ /** Parse a strict positive integer, rejecting prefixes like "12abc". */
374
+ function parseStrictPositiveId(value) {
375
+ if (!/^\d+$/.test(value.trim())) return null;
376
+ const n = Number(value.trim());
377
+ return Number.isFinite(n) && n >= 1 && Number.isInteger(n) ? n : null;
378
+ }
379
+ var memoryCommand = new Command("memory").configureHelp(helpStyle).description("Memory item management");
380
+ memoryCommand.addCommand(new Command("show").configureHelp(helpStyle).description("Print a memory item as JSON").argument("<id>", "memory ID").option("--db <path>", "database path").action((idStr, opts) => {
381
+ const memoryId = parseStrictPositiveId(idStr);
382
+ if (memoryId === null) {
383
+ p.log.error(`Invalid memory ID: ${idStr}`);
384
+ process.exitCode = 1;
385
+ return;
386
+ }
387
+ const store = new MemoryStore(resolveDbPath(opts.db));
388
+ try {
389
+ const item = store.get(memoryId);
390
+ if (!item) {
391
+ p.log.error(`Memory ${memoryId} not found`);
392
+ process.exitCode = 1;
393
+ return;
394
+ }
395
+ console.log(JSON.stringify(item, null, 2));
396
+ } finally {
397
+ store.close();
398
+ }
399
+ }));
400
+ memoryCommand.addCommand(new Command("forget").configureHelp(helpStyle).description("Deactivate a memory item").argument("<id>", "memory ID").option("--db <path>", "database path").action((idStr, opts) => {
401
+ const memoryId = parseStrictPositiveId(idStr);
402
+ if (memoryId === null) {
403
+ p.log.error(`Invalid memory ID: ${idStr}`);
404
+ process.exitCode = 1;
405
+ return;
406
+ }
407
+ const store = new MemoryStore(resolveDbPath(opts.db));
408
+ try {
409
+ store.forget(memoryId);
410
+ p.log.success(`Memory ${memoryId} marked inactive`);
411
+ } finally {
412
+ store.close();
413
+ }
414
+ }));
415
+ memoryCommand.addCommand(new Command("remember").configureHelp(helpStyle).description("Manually add a memory item").requiredOption("-k, --kind <kind>", "memory kind (discovery, decision, feature, bugfix, etc.)").requiredOption("-t, --title <title>", "memory title").requiredOption("-b, --body <body>", "memory body text").option("--tags <tags...>", "tags (space-separated)").option("--project <project>", "project name (defaults to git repo root)").option("--db <path>", "database path").action((opts) => {
416
+ const store = new MemoryStore(resolveDbPath(opts.db));
417
+ let sessionId = null;
418
+ try {
419
+ const project = resolveProject(process.cwd(), opts.project ?? null);
420
+ sessionId = store.startSession({
421
+ cwd: process.cwd(),
422
+ project,
423
+ user: process.env.USER ?? "unknown",
424
+ toolVersion: "manual",
425
+ metadata: { manual: true }
426
+ });
427
+ const memId = store.remember(sessionId, opts.kind, opts.title, opts.body, .5, opts.tags);
428
+ store.endSession(sessionId, { manual: true });
429
+ p.log.success(`Stored memory ${memId}`);
430
+ } catch (err) {
431
+ if (sessionId !== null) try {
432
+ store.endSession(sessionId, {
433
+ manual: true,
434
+ error: true
435
+ });
436
+ } catch {}
437
+ throw err;
438
+ } finally {
439
+ store.close();
440
+ }
441
+ }));
442
+ //#endregion
443
+ //#region src/commands/pack.ts
444
+ var packCommand = new Command("pack").configureHelp(helpStyle).description("Build a context-aware memory pack").argument("<context>", "context string to search for").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max items", "10").option("--budget <tokens>", "token budget").option("--json", "output as JSON").action((context, opts) => {
445
+ const store = new MemoryStore(resolveDbPath(opts.db));
446
+ try {
447
+ const limit = Number.parseInt(opts.limit, 10) || 10;
448
+ const budget = opts.budget ? Number.parseInt(opts.budget, 10) : void 0;
449
+ const result = store.buildMemoryPack(context, limit, budget);
450
+ if (opts.json) {
451
+ console.log(JSON.stringify(result, null, 2));
452
+ return;
453
+ }
454
+ p.intro(`Memory pack for "${context}"`);
455
+ if (result.items.length === 0) {
456
+ p.log.warn("No relevant memories found.");
457
+ p.outro("done");
458
+ return;
459
+ }
460
+ const m = result.metrics;
461
+ p.log.info(`${m.total_items} items, ~${m.pack_tokens} tokens` + (m.fallback_used ? " (fallback)" : "") + ` [fts:${m.sources.fts} sem:${m.sources.semantic} fuzzy:${m.sources.fuzzy}]`);
462
+ for (const item of result.items) p.log.step(`#${item.id} ${item.kind} ${item.title}`);
463
+ p.note(result.pack_text, "pack_text");
464
+ p.outro("done");
465
+ } finally {
466
+ store.close();
467
+ }
468
+ });
469
+ //#endregion
470
+ //#region src/commands/recent.ts
471
+ var recentCommand = new Command("recent").configureHelp(helpStyle).description("Show recent memories").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--limit <n>", "max results", "5").option("--kind <kind>", "filter by memory kind").action((opts) => {
472
+ const store = new MemoryStore(resolveDbPath(opts.db));
473
+ try {
474
+ const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
475
+ const filters = opts.kind ? { kind: opts.kind } : void 0;
476
+ const items = store.recent(limit, filters);
477
+ for (const item of items) console.log(`#${item.id} [${item.kind}] ${item.title}`);
478
+ } finally {
479
+ store.close();
480
+ }
481
+ });
482
+ //#endregion
483
+ //#region src/commands/search.ts
484
+ var searchCommand = new Command("search").configureHelp(helpStyle).description("Search memories by query").argument("<query>", "search query").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max results", "10").option("--kind <kind>", "filter by memory kind").option("--json", "output as JSON").action((query, opts) => {
485
+ const store = new MemoryStore(resolveDbPath(opts.db));
486
+ try {
487
+ const limit = Number.parseInt(opts.limit, 10) || 10;
488
+ const filters = opts.kind ? { kind: opts.kind } : void 0;
489
+ const results = store.search(query, limit, filters);
490
+ if (opts.json) {
491
+ console.log(JSON.stringify(results, null, 2));
492
+ return;
493
+ }
494
+ if (results.length === 0) {
495
+ p.log.warn("No results found.");
496
+ return;
497
+ }
498
+ p.intro(`${results.length} result(s) for "${query}"`);
499
+ for (const item of results) {
500
+ const score = item.score.toFixed(3);
501
+ const age = timeSince(item.created_at);
502
+ const preview = item.body_text.length > 120 ? `${item.body_text.slice(0, 120)}…` : item.body_text;
503
+ p.log.message([
504
+ `#${item.id} ${item.kind} ${age} [${score}]`,
505
+ item.title,
506
+ preview
507
+ ].join("\n"));
508
+ }
509
+ p.outro("done");
510
+ } finally {
511
+ store.close();
512
+ }
513
+ });
514
+ function timeSince(isoDate) {
515
+ const ms = Date.now() - new Date(isoDate).getTime();
516
+ const days = Math.floor(ms / 864e5);
517
+ if (days === 0) return "today";
518
+ if (days === 1) return "1d ago";
519
+ if (days < 30) return `${days}d ago`;
520
+ return `${Math.floor(days / 30)}mo ago`;
521
+ }
522
+ //#endregion
523
+ //#region src/commands/serve.ts
524
+ function pidFilePath(dbPath) {
525
+ return join(dirname(dbPath), "viewer.pid");
526
+ }
527
+ function readViewerPidRecord(dbPath) {
528
+ const pidPath = pidFilePath(dbPath);
529
+ if (!existsSync(pidPath)) return null;
530
+ const raw = readFileSync(pidPath, "utf-8").trim();
531
+ try {
532
+ const parsed = JSON.parse(raw);
533
+ if (typeof parsed.pid === "number" && typeof parsed.host === "string" && typeof parsed.port === "number") return {
534
+ pid: parsed.pid,
535
+ host: parsed.host,
536
+ port: parsed.port
537
+ };
538
+ } catch {
539
+ const pid = Number.parseInt(raw, 10);
540
+ if (Number.isFinite(pid) && pid > 0) return {
541
+ pid,
542
+ host: "127.0.0.1",
543
+ port: 38888
544
+ };
545
+ }
546
+ return null;
547
+ }
548
+ async function respondsLikeCodememViewer(record) {
549
+ try {
550
+ const controller = new AbortController();
551
+ const timer = setTimeout(() => controller.abort(), 1e3);
552
+ const res = await fetch(`http://${record.host}:${record.port}/api/stats`, { signal: controller.signal });
553
+ clearTimeout(timer);
554
+ return res.ok;
555
+ } catch {
556
+ return false;
557
+ }
558
+ }
559
+ async function waitForProcessExit(pid, timeoutMs = 5e3) {
560
+ const deadline = Date.now() + timeoutMs;
561
+ while (Date.now() < deadline) try {
562
+ process.kill(pid, 0);
563
+ await new Promise((resolve) => setTimeout(resolve, 100));
564
+ } catch {
565
+ return;
566
+ }
567
+ }
568
+ async function stopExistingViewer(dbPath) {
569
+ const pidPath = pidFilePath(dbPath);
570
+ const record = readViewerPidRecord(dbPath);
571
+ if (!record) return;
572
+ if (await respondsLikeCodememViewer(record)) try {
573
+ process.kill(record.pid, "SIGTERM");
574
+ await waitForProcessExit(record.pid);
575
+ } catch {}
576
+ try {
577
+ rmSync(pidPath);
578
+ } catch {}
579
+ }
580
+ var serveCommand = new Command("serve").configureHelp(helpStyle).description("Start the viewer server").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--host <host>", "bind host", "127.0.0.1").option("--port <port>", "bind port", "38888").option("--background", "run under a caller-managed background process").option("--stop", "stop an existing viewer process").option("--restart", "restart an existing viewer process").action(async (opts) => {
581
+ const { createApp, closeStore, getStore } = await import("@codemem/viewer-server");
582
+ const { serve } = await import("@hono/node-server");
583
+ const dbPath = resolveDbPath(opts.db);
584
+ if (opts.stop || opts.restart) {
585
+ await stopExistingViewer(dbPath);
586
+ if (opts.stop && !opts.restart) return;
587
+ }
588
+ process.env.CODEMEM_DB = dbPath;
589
+ const port = Number.parseInt(opts.port, 10);
590
+ const observer = new ObserverClient();
591
+ const sweeper = new RawEventSweeper(getStore(), { observer });
592
+ sweeper.start();
593
+ const syncAbort = new AbortController();
594
+ let syncRunning = false;
595
+ const config = readCodememConfigFile();
596
+ if (config.sync_enabled === true || process.env.CODEMEM_SYNC_ENABLED?.toLowerCase() === "true" || process.env.CODEMEM_SYNC_ENABLED === "1") {
597
+ syncRunning = true;
598
+ runSyncDaemon({
599
+ dbPath,
600
+ intervalS: typeof config.sync_interval_s === "number" ? config.sync_interval_s : 120,
601
+ host: opts.host,
602
+ port,
603
+ signal: syncAbort.signal
604
+ }).catch((err) => {
605
+ const msg = err instanceof Error ? err.message : String(err);
606
+ p.log.error(`Sync daemon failed: ${msg}`);
607
+ }).finally(() => {
608
+ syncRunning = false;
609
+ });
610
+ }
611
+ const app = createApp({
612
+ storeFactory: getStore,
613
+ sweeper
614
+ });
615
+ const pidPath = pidFilePath(dbPath);
616
+ const server = serve({
617
+ fetch: app.fetch,
618
+ hostname: opts.host,
619
+ port
620
+ }, (info) => {
621
+ writeFileSync(pidPath, JSON.stringify({
622
+ pid: process.pid,
623
+ host: opts.host,
624
+ port
625
+ }), "utf-8");
626
+ p.intro("codemem viewer");
627
+ p.log.success(`Listening on http://${info.address}:${info.port}`);
628
+ p.log.info(`Database: ${dbPath}`);
629
+ p.log.step("Raw event sweeper started");
630
+ if (syncRunning) p.log.step("Sync daemon started");
631
+ });
632
+ const shutdown = async () => {
633
+ p.outro("shutting down");
634
+ syncAbort.abort();
635
+ await sweeper.stop();
636
+ server.close(() => {
637
+ try {
638
+ rmSync(pidPath);
639
+ } catch {}
640
+ closeStore();
641
+ process.exit(0);
642
+ });
643
+ setTimeout(() => {
644
+ try {
645
+ rmSync(pidPath);
646
+ } catch {}
647
+ closeStore();
648
+ process.exit(1);
649
+ }, 5e3).unref();
650
+ };
651
+ process.on("SIGINT", () => {
652
+ shutdown();
653
+ });
654
+ process.on("SIGTERM", () => {
655
+ shutdown();
656
+ });
657
+ });
658
+ //#endregion
659
+ //#region src/commands/setup.ts
660
+ /**
661
+ * codemem setup — one-command installation for OpenCode plugin + MCP config.
662
+ *
663
+ * Replaces Python's install_plugin_cmd + install_mcp_cmd.
664
+ *
665
+ * What it does:
666
+ * 1. Copies the plugin file to ~/.config/opencode/plugin/codemem.js
667
+ * 2. Adds/updates the MCP entry in ~/.config/opencode/opencode.json
668
+ * 3. Copies the compat lib to ~/.config/opencode/lib/compat.js
669
+ *
670
+ * Designed to be safe to run repeatedly (idempotent unless --force).
671
+ */
672
+ function opencodeConfigDir() {
673
+ return join(homedir(), ".config", "opencode");
674
+ }
675
+ function claudeConfigDir() {
676
+ return join(homedir(), ".claude");
677
+ }
678
+ /**
679
+ * Find the plugin source file — walk up from this module's location
680
+ * to find the .opencode/plugin/codemem.js in the package tree.
681
+ */
682
+ function findPluginSource() {
683
+ let dir = dirname(import.meta.url.replace("file://", ""));
684
+ for (let i = 0; i < 6; i++) {
685
+ const candidate = join(dir, ".opencode", "plugin", "codemem.js");
686
+ if (existsSync(candidate)) return candidate;
687
+ const nmCandidate = join(dir, "node_modules", "codemem", ".opencode", "plugin", "codemem.js");
688
+ if (existsSync(nmCandidate)) return nmCandidate;
689
+ const legacyCandidate = join(dir, "node_modules", "@kunickiaj", "codemem", ".opencode", "plugin", "codemem.js");
690
+ if (existsSync(legacyCandidate)) return legacyCandidate;
691
+ const parent = dirname(dir);
692
+ if (parent === dir) break;
693
+ dir = parent;
694
+ }
695
+ return null;
696
+ }
697
+ function findCompatSource() {
698
+ let dir = dirname(import.meta.url.replace("file://", ""));
699
+ for (let i = 0; i < 6; i++) {
700
+ const candidate = join(dir, ".opencode", "lib", "compat.js");
701
+ if (existsSync(candidate)) return candidate;
702
+ const nmCandidate = join(dir, "node_modules", "codemem", ".opencode", "lib", "compat.js");
703
+ if (existsSync(nmCandidate)) return nmCandidate;
704
+ const legacyCandidate = join(dir, "node_modules", "@kunickiaj", "codemem", ".opencode", "lib", "compat.js");
705
+ if (existsSync(legacyCandidate)) return legacyCandidate;
706
+ const parent = dirname(dir);
707
+ if (parent === dir) break;
708
+ dir = parent;
709
+ }
710
+ return null;
711
+ }
712
+ function loadJsoncConfig(path) {
713
+ if (!existsSync(path)) return {};
714
+ const raw = readFileSync(path, "utf-8");
715
+ try {
716
+ return JSON.parse(raw);
717
+ } catch {
718
+ const cleaned = stripTrailingCommas(stripJsonComments(raw));
719
+ return JSON.parse(cleaned);
720
+ }
721
+ }
722
+ function writeJsonConfig(path, data) {
723
+ mkdirSync(dirname(path), { recursive: true });
724
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
725
+ }
726
+ function installPlugin(force) {
727
+ const source = findPluginSource();
728
+ if (!source) {
729
+ p.log.error("Plugin file not found in package tree");
730
+ return false;
731
+ }
732
+ const destDir = join(opencodeConfigDir(), "plugin");
733
+ const dest = join(destDir, "codemem.js");
734
+ if (existsSync(dest) && !force) p.log.info(`Plugin already installed at ${dest}`);
735
+ else {
736
+ mkdirSync(destDir, { recursive: true });
737
+ copyFileSync(source, dest);
738
+ p.log.success(`Plugin installed: ${dest}`);
739
+ }
740
+ const compatSource = findCompatSource();
741
+ if (compatSource) {
742
+ const compatDir = join(opencodeConfigDir(), "lib");
743
+ mkdirSync(compatDir, { recursive: true });
744
+ copyFileSync(compatSource, join(compatDir, "compat.js"));
745
+ }
746
+ return true;
747
+ }
748
+ function installMcp(force) {
749
+ const configPath = join(opencodeConfigDir(), "opencode.json");
750
+ let config;
751
+ try {
752
+ config = loadJsoncConfig(configPath);
753
+ } catch (err) {
754
+ p.log.error(`Failed to parse ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
755
+ return false;
756
+ }
757
+ let mcpConfig = config.mcp;
758
+ if (mcpConfig == null || typeof mcpConfig !== "object" || Array.isArray(mcpConfig)) mcpConfig = {};
759
+ if ("codemem" in mcpConfig && !force) {
760
+ p.log.info(`MCP entry already exists in ${configPath}`);
761
+ return true;
762
+ }
763
+ mcpConfig.codemem = {
764
+ type: "local",
765
+ command: [
766
+ "npx",
767
+ "codemem",
768
+ "mcp"
769
+ ],
770
+ enabled: true
771
+ };
772
+ config.mcp = mcpConfig;
773
+ try {
774
+ writeJsonConfig(configPath, config);
775
+ p.log.success(`MCP entry installed: ${configPath}`);
776
+ } catch (err) {
777
+ p.log.error(`Failed to write ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
778
+ return false;
779
+ }
780
+ return true;
781
+ }
782
+ function installClaudeMcp(force) {
783
+ const settingsPath = join(claudeConfigDir(), "settings.json");
784
+ let settings;
785
+ try {
786
+ settings = loadJsoncConfig(settingsPath);
787
+ } catch {
788
+ settings = {};
789
+ }
790
+ let mcpServers = settings.mcpServers;
791
+ if (mcpServers == null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) mcpServers = {};
792
+ if ("codemem" in mcpServers && !force) {
793
+ p.log.info(`Claude MCP entry already exists in ${settingsPath}`);
794
+ return true;
795
+ }
796
+ mcpServers.codemem = {
797
+ command: "npx",
798
+ args: ["codemem", "mcp"]
799
+ };
800
+ settings.mcpServers = mcpServers;
801
+ try {
802
+ writeJsonConfig(settingsPath, settings);
803
+ p.log.success(`Claude MCP entry installed: ${settingsPath}`);
804
+ } catch (err) {
805
+ p.log.error(`Failed to write ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
806
+ return false;
807
+ }
808
+ return true;
809
+ }
810
+ var setupCommand = new Command("setup").configureHelp(helpStyle).description("Install codemem plugin + MCP config for OpenCode and Claude Code").option("--force", "overwrite existing installations").option("--opencode-only", "only install for OpenCode").option("--claude-only", "only install for Claude Code").action((opts) => {
811
+ p.intro(`codemem setup v${VERSION}`);
812
+ const force = opts.force ?? false;
813
+ let ok = true;
814
+ if (!opts.claudeOnly) {
815
+ p.log.step("Installing OpenCode plugin...");
816
+ ok = installPlugin(force) && ok;
817
+ p.log.step("Installing OpenCode MCP config...");
818
+ ok = installMcp(force) && ok;
819
+ }
820
+ if (!opts.opencodeOnly) {
821
+ p.log.step("Installing Claude Code MCP config...");
822
+ ok = installClaudeMcp(force) && ok;
823
+ }
824
+ if (ok) p.outro("Setup complete — restart your editor to load the plugin");
825
+ else {
826
+ p.outro("Setup completed with warnings");
827
+ process.exitCode = 1;
828
+ }
829
+ });
830
+ //#endregion
831
+ //#region src/commands/stats.ts
832
+ function fmtPct(n) {
833
+ return `${Math.round(n * 100)}%`;
834
+ }
835
+ function fmtTokens(n) {
836
+ if (n >= 1e9) return `~${(n / 1e9).toFixed(1)}B`;
837
+ if (n >= 1e6) return `~${(n / 1e6).toFixed(1)}M`;
838
+ if (n >= 1e3) return `~${(n / 1e3).toFixed(0)}K`;
839
+ return `${n}`;
840
+ }
841
+ var statsCommand = new Command("stats").configureHelp(helpStyle).description("Show database statistics").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--json", "output as JSON").action((opts) => {
842
+ const store = new MemoryStore(resolveDbPath(opts.db));
843
+ try {
844
+ const result = store.stats();
845
+ if (opts.json) {
846
+ console.log(JSON.stringify(result, null, 2));
847
+ return;
848
+ }
849
+ const db = result.database;
850
+ const sizeMb = (db.size_bytes / 1048576).toFixed(1);
851
+ p.intro("codemem stats");
852
+ p.log.info([`Path: ${db.path}`, `Size: ${sizeMb} MB`].join("\n"));
853
+ p.log.success([
854
+ `Sessions: ${db.sessions.toLocaleString()}`,
855
+ `Memories: ${db.active_memory_items.toLocaleString()} active / ${db.memory_items.toLocaleString()} total`,
856
+ `Tags: ${db.tags_filled.toLocaleString()} filled (${fmtPct(db.tags_coverage)} of active)`,
857
+ `Artifacts: ${db.artifacts.toLocaleString()}`,
858
+ `Vectors: ${db.vector_rows.toLocaleString()} (${fmtPct(db.vector_coverage)} coverage)`,
859
+ `Raw events: ${db.raw_events.toLocaleString()}`
860
+ ].join("\n"));
861
+ if (result.usage.events.length > 0) {
862
+ const lines = result.usage.events.map((e) => {
863
+ const parts = [`${e.event}: ${e.count.toLocaleString()}`];
864
+ if (e.tokens_read > 0) parts.push(`read ${fmtTokens(e.tokens_read)} tokens`);
865
+ if (e.tokens_saved > 0) parts.push(`est. saved ${fmtTokens(e.tokens_saved)} tokens`);
866
+ return ` ${parts.join(", ")}`;
867
+ });
868
+ const t = result.usage.totals;
869
+ lines.push("");
870
+ lines.push(` Total: ${t.events.toLocaleString()} events, read ${fmtTokens(t.tokens_read)} tokens, est. saved ${fmtTokens(t.tokens_saved)} tokens`);
871
+ p.log.step(`Usage\n${lines.join("\n")}`);
872
+ }
873
+ p.outro("done");
874
+ } finally {
875
+ store.close();
876
+ }
877
+ });
878
+ //#endregion
879
+ //#region src/commands/sync.ts
880
+ /**
881
+ * Sync CLI commands — enable/disable/status/peers/connect.
882
+ */
883
+ var syncCommand = new Command("sync").configureHelp(helpStyle).description("Sync configuration and peer management");
884
+ syncCommand.addCommand(new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary").option("--db <path>", "database path").option("--json", "output as JSON").action((opts) => {
885
+ const config = readCodememConfigFile();
886
+ const store = new MemoryStore(resolveDbPath(opts.db));
887
+ try {
888
+ const d = drizzle(store.db, { schema });
889
+ const deviceRow = d.select({
890
+ device_id: schema.syncDevice.device_id,
891
+ fingerprint: schema.syncDevice.fingerprint
892
+ }).from(schema.syncDevice).limit(1).get();
893
+ const peers = d.select({
894
+ peer_device_id: schema.syncPeers.peer_device_id,
895
+ name: schema.syncPeers.name,
896
+ last_sync_at: schema.syncPeers.last_sync_at,
897
+ last_error: schema.syncPeers.last_error
898
+ }).from(schema.syncPeers).all();
899
+ if (opts.json) {
900
+ console.log(JSON.stringify({
901
+ enabled: config.sync_enabled === true,
902
+ host: config.sync_host ?? "0.0.0.0",
903
+ port: config.sync_port ?? 7337,
904
+ interval_s: config.sync_interval_s ?? 120,
905
+ device_id: deviceRow?.device_id ?? null,
906
+ fingerprint: deviceRow?.fingerprint ?? null,
907
+ coordinator_url: config.sync_coordinator_url ?? null,
908
+ peers: peers.map((peer) => ({
909
+ device_id: peer.peer_device_id,
910
+ name: peer.name,
911
+ last_sync: peer.last_sync_at,
912
+ status: peer.last_error ?? "ok"
913
+ }))
914
+ }, null, 2));
915
+ return;
916
+ }
917
+ p.intro("codemem sync status");
918
+ p.log.info([
919
+ `Enabled: ${config.sync_enabled === true ? "yes" : "no"}`,
920
+ `Host: ${config.sync_host ?? "0.0.0.0"}`,
921
+ `Port: ${config.sync_port ?? 7337}`,
922
+ `Interval: ${config.sync_interval_s ?? 120}s`,
923
+ `Coordinator: ${config.sync_coordinator_url ?? "(not configured)"}`
924
+ ].join("\n"));
925
+ if (deviceRow) p.log.info(`Device ID: ${deviceRow.device_id}\nFingerprint: ${deviceRow.fingerprint}`);
926
+ else p.log.warn("Device identity not initialized (run `codemem sync enable`)");
927
+ if (peers.length === 0) p.log.info("Peers: none");
928
+ else for (const peer of peers) {
929
+ const label = peer.name || peer.peer_device_id;
930
+ p.log.message(` ${label}: last_sync=${peer.last_sync_at ?? "never"}, status=${peer.last_error ?? "ok"}`);
931
+ }
932
+ p.outro(`${peers.length} peer(s)`);
933
+ } finally {
934
+ store.close();
935
+ }
936
+ }));
937
+ syncCommand.addCommand(new Command("enable").configureHelp(helpStyle).description("Enable sync and initialize device identity").option("--db <path>", "database path").option("--host <host>", "sync listen host").option("--port <port>", "sync listen port").option("--interval <seconds>", "sync interval in seconds").action((opts) => {
938
+ const store = new MemoryStore(resolveDbPath(opts.db));
939
+ try {
940
+ const [deviceId, fingerprint] = ensureDeviceIdentity(store.db);
941
+ const config = readCodememConfigFile();
942
+ config.sync_enabled = true;
943
+ if (opts.host) config.sync_host = opts.host;
944
+ if (opts.port) config.sync_port = Number.parseInt(opts.port, 10);
945
+ if (opts.interval) config.sync_interval_s = Number.parseInt(opts.interval, 10);
946
+ writeCodememConfigFile(config);
947
+ p.intro("codemem sync enable");
948
+ p.log.success([
949
+ `Device ID: ${deviceId}`,
950
+ `Fingerprint: ${fingerprint}`,
951
+ `Host: ${config.sync_host ?? "0.0.0.0"}`,
952
+ `Port: ${config.sync_port ?? 7337}`,
953
+ `Interval: ${config.sync_interval_s ?? 120}s`
954
+ ].join("\n"));
955
+ p.outro("Sync enabled — restart `codemem serve` to activate");
956
+ } finally {
957
+ store.close();
958
+ }
959
+ }));
960
+ syncCommand.addCommand(new Command("disable").configureHelp(helpStyle).description("Disable sync without deleting keys or peers").action(() => {
961
+ const config = readCodememConfigFile();
962
+ config.sync_enabled = false;
963
+ writeCodememConfigFile(config);
964
+ p.intro("codemem sync disable");
965
+ p.outro("Sync disabled — restart `codemem serve` to take effect");
966
+ }));
967
+ syncCommand.addCommand(new Command("peers").configureHelp(helpStyle).description("List known sync peers").option("--db <path>", "database path").option("--json", "output as JSON").action((opts) => {
968
+ const store = new MemoryStore(resolveDbPath(opts.db));
969
+ try {
970
+ const peers = drizzle(store.db, { schema }).select({
971
+ peer_device_id: schema.syncPeers.peer_device_id,
972
+ name: schema.syncPeers.name,
973
+ addresses: schema.syncPeers.addresses_json,
974
+ last_sync_at: schema.syncPeers.last_sync_at,
975
+ last_error: schema.syncPeers.last_error
976
+ }).from(schema.syncPeers).orderBy(desc(schema.syncPeers.last_sync_at)).all();
977
+ if (opts.json) {
978
+ console.log(JSON.stringify(peers, null, 2));
979
+ return;
980
+ }
981
+ p.intro("codemem sync peers");
982
+ if (peers.length === 0) {
983
+ p.outro("No peers configured");
984
+ return;
985
+ }
986
+ for (const peer of peers) {
987
+ const label = peer.name || peer.peer_device_id;
988
+ const addrs = peer.addresses || "(no addresses)";
989
+ p.log.message(`${label}\n addresses: ${addrs}\n last_sync: ${peer.last_sync_at ?? "never"}\n status: ${peer.last_error ?? "ok"}`);
990
+ }
991
+ p.outro(`${peers.length} peer(s)`);
992
+ } finally {
993
+ store.close();
994
+ }
995
+ }));
996
+ syncCommand.addCommand(new Command("connect").configureHelp(helpStyle).description("Configure coordinator URL for cloud sync").argument("<url>", "coordinator URL (e.g. https://coordinator.example.com)").option("--group <group>", "sync group ID").action((url, opts) => {
997
+ const config = readCodememConfigFile();
998
+ config.sync_coordinator_url = url.trim();
999
+ if (opts.group) config.sync_coordinator_group = opts.group.trim();
1000
+ writeCodememConfigFile(config);
1001
+ p.intro("codemem sync connect");
1002
+ p.log.success(`Coordinator: ${url.trim()}`);
1003
+ if (opts.group) p.log.info(`Group: ${opts.group.trim()}`);
1004
+ p.outro("Restart `codemem serve` to activate coordinator sync");
1005
+ }));
1006
+ //#endregion
1007
+ //#region src/commands/version.ts
1008
+ var versionCommand = new Command("version").configureHelp(helpStyle).description("Print codemem version").action(() => {
1009
+ console.log(VERSION);
1010
+ });
1011
+ //#endregion
1012
+ //#region src/index.ts
1013
+ /**
1014
+ * @codemem/cli — CLI entry point.
1015
+ *
1016
+ * Commands:
1017
+ * codemem stats → database statistics
1018
+ * codemem search → FTS5 memory search
1019
+ * codemem pack → context-aware memory pack
1020
+ * codemem serve → viewer server
1021
+ * codemem mcp → MCP stdio server
1022
+ */
1023
+ var completion = omelette("codemem <command>");
1024
+ completion.on("command", ({ reply }) => {
1025
+ reply([
1026
+ "claude-hook-ingest",
1027
+ "db",
1028
+ "export-memories",
1029
+ "memory",
1030
+ "import-memories",
1031
+ "setup",
1032
+ "sync",
1033
+ "stats",
1034
+ "recent",
1035
+ "search",
1036
+ "pack",
1037
+ "serve",
1038
+ "mcp",
1039
+ "enqueue-raw-event",
1040
+ "version",
1041
+ "help",
1042
+ "--help",
1043
+ "--version"
1044
+ ]);
1045
+ });
1046
+ completion.init();
1047
+ if (process.argv.includes("--setup-completion")) {
1048
+ completion.setupShellInitFile();
1049
+ process.exit(0);
1050
+ }
1051
+ if (process.argv.includes("--cleanup-completion")) {
1052
+ completion.cleanupShellInitFile();
1053
+ process.exit(0);
1054
+ }
1055
+ var program = new Command();
1056
+ program.name("codemem").description("codemem — persistent memory for AI coding agents").version(VERSION).configureHelp(helpStyle);
1057
+ program.addCommand(serveCommand);
1058
+ program.addCommand(mcpCommand);
1059
+ program.addCommand(claudeHookIngestCommand);
1060
+ program.addCommand(dbCommand);
1061
+ program.addCommand(exportMemoriesCommand);
1062
+ program.addCommand(importMemoriesCommand);
1063
+ program.addCommand(statsCommand);
1064
+ program.addCommand(recentCommand);
1065
+ program.addCommand(searchCommand);
1066
+ program.addCommand(packCommand);
1067
+ program.addCommand(memoryCommand);
1068
+ program.addCommand(syncCommand);
1069
+ program.addCommand(setupCommand);
1070
+ program.addCommand(enqueueRawEventCommand);
1071
+ program.addCommand(versionCommand);
1072
+ program.parse();
1073
+ //#endregion
1074
+ export {};
1075
+
1076
+ //# sourceMappingURL=index.js.map