codemem 0.20.0-alpha.2 → 0.20.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/{plugin → plugins}/codemem.js +98 -113
- package/dist/commands/claude-hook-ingest.d.ts.map +1 -1
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/enqueue-raw-event.d.ts.map +1 -1
- package/dist/commands/export-memories.d.ts.map +1 -1
- package/dist/commands/import-memories.d.ts.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/memory.d.ts +3 -0
- package/dist/commands/memory.d.ts.map +1 -1
- package/dist/commands/memory.test.d.ts +2 -0
- package/dist/commands/memory.test.d.ts.map +1 -0
- package/dist/commands/pack.d.ts.map +1 -1
- package/dist/commands/recent.d.ts.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/serve-invocation.d.ts +37 -0
- package/dist/commands/serve-invocation.d.ts.map +1 -0
- package/dist/commands/serve.d.ts +2 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.test.d.ts +2 -0
- package/dist/commands/serve.test.d.ts.map +1 -0
- package/dist/commands/setup-config.d.ts +4 -0
- package/dist/commands/setup-config.d.ts.map +1 -0
- package/dist/commands/setup-config.test.d.ts +2 -0
- package/dist/commands/setup-config.test.d.ts.map +1 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/stats.d.ts.map +1 -1
- package/dist/commands/sync-helpers.d.ts +31 -0
- package/dist/commands/sync-helpers.d.ts.map +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.test.d.ts +2 -0
- package/dist/commands/sync.test.d.ts.map +1 -0
- package/dist/index.js +801 -98
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
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";
|
|
2
|
+
import { MemoryStore, ObserverClient, RawEventSweeper, VERSION, buildRawEventEnvelopeFromHook, connect, ensureDeviceIdentity, exportMemories, fingerprintPublicKey, getRawEventStatus, importMemories, initDatabase, loadPublicKey, loadSqliteVec, rawEventsGate, readCodememConfigFile, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import omelette from "omelette";
|
|
5
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { styleText } from "node:util";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
8
|
-
import { homedir } from "node:os";
|
|
8
|
+
import { homedir, networkInterfaces } from "node:os";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import net from "node:net";
|
|
12
|
+
import { desc, eq } from "drizzle-orm";
|
|
11
13
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
12
14
|
//#region src/help-style.ts
|
|
13
15
|
/**
|
|
@@ -117,7 +119,7 @@ function directEnqueue(payload, dbPath) {
|
|
|
117
119
|
db.close();
|
|
118
120
|
}
|
|
119
121
|
}
|
|
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) => {
|
|
122
|
+
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("--db-path <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
123
|
let raw;
|
|
122
124
|
try {
|
|
123
125
|
raw = readFileSync(0, "utf8").trim();
|
|
@@ -153,7 +155,7 @@ var claudeHookIngestCommand = new Command("claude-hook-ingest").configureHelp(he
|
|
|
153
155
|
return;
|
|
154
156
|
}
|
|
155
157
|
try {
|
|
156
|
-
const dbPath = resolveDbPath(opts.db);
|
|
158
|
+
const dbPath = resolveDbPath(opts.db ?? opts.dbPath);
|
|
157
159
|
const directResult = directEnqueue(payload, dbPath);
|
|
158
160
|
console.log(JSON.stringify({
|
|
159
161
|
...directResult,
|
|
@@ -165,19 +167,24 @@ var claudeHookIngestCommand = new Command("claude-hook-ingest").configureHelp(he
|
|
|
165
167
|
});
|
|
166
168
|
//#endregion
|
|
167
169
|
//#region src/commands/db.ts
|
|
170
|
+
function formatBytes(bytes) {
|
|
171
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
172
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
173
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
174
|
+
}
|
|
168
175
|
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);
|
|
176
|
+
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)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action((opts) => {
|
|
177
|
+
const result = initDatabase(opts.db ?? opts.dbPath);
|
|
171
178
|
p.intro("codemem db init");
|
|
172
179
|
p.log.success(`Database ready: ${result.path}`);
|
|
173
180
|
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);
|
|
181
|
+
})).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)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action((opts) => {
|
|
182
|
+
const result = vacuumDatabase(opts.db ?? opts.dbPath);
|
|
176
183
|
p.intro("codemem db vacuum");
|
|
177
184
|
p.log.success(`Vacuumed: ${result.path}`);
|
|
178
185
|
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);
|
|
186
|
+
})).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("--db-path <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) => {
|
|
187
|
+
const result = getRawEventStatus(opts.db ?? opts.dbPath, Number.parseInt(opts.limit, 10) || 25);
|
|
181
188
|
if (opts.json) {
|
|
182
189
|
console.log(JSON.stringify(result, null, 2));
|
|
183
190
|
return;
|
|
@@ -190,12 +197,12 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("V
|
|
|
190
197
|
}
|
|
191
198
|
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
199
|
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);
|
|
200
|
+
})).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("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max failed batches to requeue", "25").action((opts) => {
|
|
201
|
+
const result = retryRawEventFailures(opts.db ?? opts.dbPath, Number.parseInt(opts.limit, 10) || 25);
|
|
195
202
|
p.intro("codemem db raw-events-retry");
|
|
196
203
|
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, {
|
|
204
|
+
})).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("--db-path <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) => {
|
|
205
|
+
const result = rawEventsGate(opts.db ?? opts.dbPath, {
|
|
199
206
|
minFlushSuccessRate: Number.parseFloat(opts.minFlushSuccessRate),
|
|
200
207
|
maxDroppedEventRate: Number.parseFloat(opts.maxDroppedEventRate),
|
|
201
208
|
minSessionBoundaryAccuracy: Number.parseFloat(opts.minSessionBoundaryAccuracy),
|
|
@@ -219,6 +226,115 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("V
|
|
|
219
226
|
p.outro("reliability gate FAILED");
|
|
220
227
|
process.exitCode = 1;
|
|
221
228
|
}
|
|
229
|
+
})).addCommand(new Command("rename-project").configureHelp(helpStyle).description("Rename a project across sessions and related tables").argument("<old-name>", "current project name").argument("<new-name>", "new project name").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--apply", "apply changes (default is dry-run)").action((oldName, newName, opts) => {
|
|
230
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
231
|
+
try {
|
|
232
|
+
const dryRun = !opts.apply;
|
|
233
|
+
const suffixPattern = `%/${oldName.replace(/%/g, "\\%").replace(/_/g, "\\_")}`;
|
|
234
|
+
const tables = ["sessions", "raw_event_sessions"];
|
|
235
|
+
const counts = {};
|
|
236
|
+
const run = () => {
|
|
237
|
+
for (const table of tables) {
|
|
238
|
+
const rows = store.db.prepare(`SELECT COUNT(*) as cnt FROM ${table} WHERE project = ? OR project LIKE ? ESCAPE '\\'`).get(oldName, suffixPattern);
|
|
239
|
+
counts[table] = rows.cnt;
|
|
240
|
+
if (!dryRun && rows.cnt > 0) {
|
|
241
|
+
store.db.prepare(`UPDATE ${table} SET project = ? WHERE project = ?`).run(newName, oldName);
|
|
242
|
+
store.db.prepare(`UPDATE ${table} SET project = ? WHERE project LIKE ? ESCAPE '\\' AND project != ?`).run(newName, suffixPattern, newName);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
if (dryRun) run();
|
|
247
|
+
else store.db.transaction(run)();
|
|
248
|
+
const action = dryRun ? "Will rename" : "Renamed";
|
|
249
|
+
p.intro("codemem db rename-project");
|
|
250
|
+
p.log.info(`${action} ${oldName} → ${newName}`);
|
|
251
|
+
p.log.info([`Sessions: ${counts.sessions}`, `Raw event sessions: ${counts.raw_event_sessions}`].join("\n"));
|
|
252
|
+
if (dryRun) p.outro("Pass --apply to execute");
|
|
253
|
+
else p.outro("done");
|
|
254
|
+
} finally {
|
|
255
|
+
store.close();
|
|
256
|
+
}
|
|
257
|
+
})).addCommand(new Command("normalize-projects").configureHelp(helpStyle).description("Normalize path-like project identifiers to their basename").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--apply", "apply changes (default is dry-run)").action((opts) => {
|
|
258
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
259
|
+
try {
|
|
260
|
+
const dryRun = !opts.apply;
|
|
261
|
+
const tables = ["sessions", "raw_event_sessions"];
|
|
262
|
+
const rewrites = /* @__PURE__ */ new Map();
|
|
263
|
+
const counts = {};
|
|
264
|
+
const run = () => {
|
|
265
|
+
for (const table of tables) {
|
|
266
|
+
const projects = store.db.prepare(`SELECT DISTINCT project FROM ${table} WHERE project IS NOT NULL AND project LIKE '%/%'`).all();
|
|
267
|
+
let updated = 0;
|
|
268
|
+
for (const row of projects) {
|
|
269
|
+
const basename = row.project.split("/").pop() ?? row.project;
|
|
270
|
+
if (basename !== row.project) {
|
|
271
|
+
rewrites.set(row.project, basename);
|
|
272
|
+
if (!dryRun) {
|
|
273
|
+
const info = store.db.prepare(`UPDATE ${table} SET project = ? WHERE project = ?`).run(basename, row.project);
|
|
274
|
+
updated += info.changes;
|
|
275
|
+
} else {
|
|
276
|
+
const cnt = store.db.prepare(`SELECT COUNT(*) as cnt FROM ${table} WHERE project = ?`).get(row.project);
|
|
277
|
+
updated += cnt.cnt;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
counts[table] = updated;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
if (dryRun) run();
|
|
285
|
+
else store.db.transaction(run)();
|
|
286
|
+
p.intro("codemem db normalize-projects");
|
|
287
|
+
p.log.info(`Dry run: ${dryRun}`);
|
|
288
|
+
p.log.info([`Sessions to update: ${counts.sessions}`, `Raw event sessions to update: ${counts.raw_event_sessions}`].join("\n"));
|
|
289
|
+
if (rewrites.size > 0) {
|
|
290
|
+
p.log.info("Rewritten paths:");
|
|
291
|
+
for (const [from, to] of [...rewrites.entries()].sort()) p.log.message(` ${from} → ${to}`);
|
|
292
|
+
}
|
|
293
|
+
if (dryRun) p.outro("Pass --apply to execute");
|
|
294
|
+
else p.outro("done");
|
|
295
|
+
} finally {
|
|
296
|
+
store.close();
|
|
297
|
+
}
|
|
298
|
+
})).addCommand(new Command("size-report").configureHelp(helpStyle).description("Show SQLite file size and major storage consumers").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--limit <n>", "number of largest tables/indexes to show", "12").option("--json", "output as JSON").action((opts) => {
|
|
299
|
+
const dbPath = resolveDbPath(opts.db ?? opts.dbPath);
|
|
300
|
+
const db = connect(dbPath);
|
|
301
|
+
try {
|
|
302
|
+
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 12);
|
|
303
|
+
const fileSizeBytes = statSync(dbPath).size;
|
|
304
|
+
const pageInfo = db.prepare("SELECT page_count * page_size as total FROM pragma_page_count, pragma_page_size").get();
|
|
305
|
+
const freePages = db.prepare("SELECT freelist_count FROM pragma_freelist_count").get();
|
|
306
|
+
const pageSize = db.prepare("PRAGMA page_size").get();
|
|
307
|
+
const tables = db.prepare(`SELECT name, SUM(pgsize) as size_bytes
|
|
308
|
+
FROM dbstat
|
|
309
|
+
GROUP BY name
|
|
310
|
+
ORDER BY size_bytes DESC
|
|
311
|
+
LIMIT ?`).all(limit);
|
|
312
|
+
if (opts.json) {
|
|
313
|
+
console.log(JSON.stringify({
|
|
314
|
+
file_size_bytes: fileSizeBytes,
|
|
315
|
+
db_size_bytes: pageInfo?.total ?? 0,
|
|
316
|
+
free_bytes: (freePages?.freelist_count ?? 0) * (pageSize?.page_size ?? 4096),
|
|
317
|
+
tables: tables.map((t) => ({
|
|
318
|
+
name: t.name,
|
|
319
|
+
size_bytes: t.size_bytes
|
|
320
|
+
}))
|
|
321
|
+
}, null, 2));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
p.intro("codemem db size-report");
|
|
325
|
+
p.log.info([
|
|
326
|
+
`File size: ${formatBytes(fileSizeBytes)}`,
|
|
327
|
+
`DB size: ${formatBytes(pageInfo?.total ?? 0)}`,
|
|
328
|
+
`Free space: ${formatBytes((freePages?.freelist_count ?? 0) * (pageSize?.page_size ?? 4096))}`
|
|
329
|
+
].join("\n"));
|
|
330
|
+
if (tables.length > 0) {
|
|
331
|
+
p.log.info("Largest objects:");
|
|
332
|
+
for (const t of tables) p.log.message(` ${t.name.padEnd(40)} ${formatBytes(t.size_bytes).padStart(10)}`);
|
|
333
|
+
}
|
|
334
|
+
p.outro("done");
|
|
335
|
+
} finally {
|
|
336
|
+
db.close();
|
|
337
|
+
}
|
|
222
338
|
}));
|
|
223
339
|
//#endregion
|
|
224
340
|
//#region src/commands/enqueue-raw-event.ts
|
|
@@ -253,7 +369,7 @@ async function readStdinJson() {
|
|
|
253
369
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("payload must be an object");
|
|
254
370
|
return parsed;
|
|
255
371
|
}
|
|
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) => {
|
|
372
|
+
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)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action(async (opts) => {
|
|
257
373
|
const payload = await readStdinJson();
|
|
258
374
|
const sessionId = resolveSessionStreamId(payload);
|
|
259
375
|
if (!sessionId) throw new Error("session id required");
|
|
@@ -267,7 +383,7 @@ var enqueueRawEventCommand = new Command("enqueue-raw-event").configureHelp(help
|
|
|
267
383
|
const tsMonoMs = Number.isFinite(Number(payload.ts_mono_ms)) ? Number(payload.ts_mono_ms) : null;
|
|
268
384
|
const eventId = typeof payload.event_id === "string" ? payload.event_id.trim() : "";
|
|
269
385
|
const eventPayload = payload.payload && typeof payload.payload === "object" && !Array.isArray(payload.payload) ? stripPrivateObj(payload.payload) : {};
|
|
270
|
-
const store = new MemoryStore(resolveDbPath(opts.db));
|
|
386
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
271
387
|
try {
|
|
272
388
|
store.updateRawEventSessionMeta({
|
|
273
389
|
opencodeSessionId: sessionId,
|
|
@@ -293,9 +409,9 @@ var enqueueRawEventCommand = new Command("enqueue-raw-event").configureHelp(help
|
|
|
293
409
|
function expandUserPath(value) {
|
|
294
410
|
return value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
|
|
295
411
|
}
|
|
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) => {
|
|
412
|
+
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("--db-path <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
413
|
const payload = exportMemories({
|
|
298
|
-
dbPath: resolveDbPath(opts.db),
|
|
414
|
+
dbPath: resolveDbPath(opts.db ?? opts.dbPath),
|
|
299
415
|
project: opts.project,
|
|
300
416
|
allProjects: opts.allProjects,
|
|
301
417
|
includeInactive: opts.includeInactive,
|
|
@@ -320,7 +436,7 @@ var exportMemoriesCommand = new Command("export-memories").configureHelp(helpSty
|
|
|
320
436
|
});
|
|
321
437
|
//#endregion
|
|
322
438
|
//#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) => {
|
|
439
|
+
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("--db-path <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
440
|
let payload;
|
|
325
441
|
try {
|
|
326
442
|
payload = readImportPayload(inputFile);
|
|
@@ -339,7 +455,7 @@ var importMemoriesCommand = new Command("import-memories").configureHelp(helpSty
|
|
|
339
455
|
`Prompts: ${payload.user_prompts.length.toLocaleString()}`
|
|
340
456
|
].join("\n"));
|
|
341
457
|
const result = importMemories(payload, {
|
|
342
|
-
dbPath: resolveDbPath(opts.db),
|
|
458
|
+
dbPath: resolveDbPath(opts.db ?? opts.dbPath),
|
|
343
459
|
remapProject: opts.remapProject,
|
|
344
460
|
dryRun: opts.dryRun
|
|
345
461
|
});
|
|
@@ -357,8 +473,9 @@ var importMemoriesCommand = new Command("import-memories").configureHelp(helpSty
|
|
|
357
473
|
});
|
|
358
474
|
//#endregion
|
|
359
475
|
//#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
|
-
|
|
476
|
+
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)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action(async (opts) => {
|
|
477
|
+
const dbPath = opts.db ?? opts.dbPath;
|
|
478
|
+
if (dbPath) process.env.CODEMEM_DB = dbPath;
|
|
362
479
|
await import("@codemem/mcp");
|
|
363
480
|
});
|
|
364
481
|
//#endregion
|
|
@@ -376,15 +493,14 @@ function parseStrictPositiveId(value) {
|
|
|
376
493
|
const n = Number(value.trim());
|
|
377
494
|
return Number.isFinite(n) && n >= 1 && Number.isInteger(n) ? n : null;
|
|
378
495
|
}
|
|
379
|
-
|
|
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) => {
|
|
496
|
+
function showMemoryAction(idStr, opts) {
|
|
381
497
|
const memoryId = parseStrictPositiveId(idStr);
|
|
382
498
|
if (memoryId === null) {
|
|
383
499
|
p.log.error(`Invalid memory ID: ${idStr}`);
|
|
384
500
|
process.exitCode = 1;
|
|
385
501
|
return;
|
|
386
502
|
}
|
|
387
|
-
const store = new MemoryStore(resolveDbPath(opts.db));
|
|
503
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
388
504
|
try {
|
|
389
505
|
const item = store.get(memoryId);
|
|
390
506
|
if (!item) {
|
|
@@ -396,24 +512,24 @@ memoryCommand.addCommand(new Command("show").configureHelp(helpStyle).descriptio
|
|
|
396
512
|
} finally {
|
|
397
513
|
store.close();
|
|
398
514
|
}
|
|
399
|
-
}
|
|
400
|
-
|
|
515
|
+
}
|
|
516
|
+
function forgetMemoryAction(idStr, opts) {
|
|
401
517
|
const memoryId = parseStrictPositiveId(idStr);
|
|
402
518
|
if (memoryId === null) {
|
|
403
519
|
p.log.error(`Invalid memory ID: ${idStr}`);
|
|
404
520
|
process.exitCode = 1;
|
|
405
521
|
return;
|
|
406
522
|
}
|
|
407
|
-
const store = new MemoryStore(resolveDbPath(opts.db));
|
|
523
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
408
524
|
try {
|
|
409
525
|
store.forget(memoryId);
|
|
410
526
|
p.log.success(`Memory ${memoryId} marked inactive`);
|
|
411
527
|
} finally {
|
|
412
528
|
store.close();
|
|
413
529
|
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const store = new MemoryStore(resolveDbPath(opts.db));
|
|
530
|
+
}
|
|
531
|
+
function rememberMemoryAction(opts) {
|
|
532
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
417
533
|
let sessionId = null;
|
|
418
534
|
try {
|
|
419
535
|
const project = resolveProject(process.cwd(), opts.project ?? null);
|
|
@@ -438,15 +554,41 @@ memoryCommand.addCommand(new Command("remember").configureHelp(helpStyle).descri
|
|
|
438
554
|
} finally {
|
|
439
555
|
store.close();
|
|
440
556
|
}
|
|
441
|
-
}
|
|
557
|
+
}
|
|
558
|
+
function createShowMemoryCommand() {
|
|
559
|
+
return new Command("show").configureHelp(helpStyle).description("Print a memory item as JSON").argument("<id>", "memory ID").option("--db <path>", "database path").option("--db-path <path>", "database path").action(showMemoryAction);
|
|
560
|
+
}
|
|
561
|
+
function createForgetMemoryCommand() {
|
|
562
|
+
return new Command("forget").configureHelp(helpStyle).description("Deactivate a memory item").argument("<id>", "memory ID").option("--db <path>", "database path").option("--db-path <path>", "database path").action(forgetMemoryAction);
|
|
563
|
+
}
|
|
564
|
+
function createRememberMemoryCommand() {
|
|
565
|
+
return 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").option("--db-path <path>", "database path").action(rememberMemoryAction);
|
|
566
|
+
}
|
|
567
|
+
var showMemoryCommand = createShowMemoryCommand();
|
|
568
|
+
var forgetMemoryCommand = createForgetMemoryCommand();
|
|
569
|
+
var rememberMemoryCommand = createRememberMemoryCommand();
|
|
570
|
+
var memoryCommand = new Command("memory").configureHelp(helpStyle).description("Memory item management");
|
|
571
|
+
memoryCommand.addCommand(createShowMemoryCommand());
|
|
572
|
+
memoryCommand.addCommand(createForgetMemoryCommand());
|
|
573
|
+
memoryCommand.addCommand(createRememberMemoryCommand());
|
|
442
574
|
//#endregion
|
|
443
575
|
//#region src/commands/pack.ts
|
|
444
|
-
|
|
445
|
-
|
|
576
|
+
function collectWorkingSetFile(value, previous) {
|
|
577
|
+
return [...previous, value];
|
|
578
|
+
}
|
|
579
|
+
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("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max items", "10").option("--budget <tokens>", "token budget").option("--token-budget <tokens>", "token budget").option("--working-set-file <path>", "recently modified file path used as ranking hint", collectWorkingSetFile, []).option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").option("--json", "output as JSON").action((context, opts) => {
|
|
580
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
446
581
|
try {
|
|
447
582
|
const limit = Number.parseInt(opts.limit, 10) || 10;
|
|
448
|
-
const
|
|
449
|
-
const
|
|
583
|
+
const budgetRaw = opts.tokenBudget ?? opts.budget;
|
|
584
|
+
const budget = budgetRaw ? Number.parseInt(budgetRaw, 10) : void 0;
|
|
585
|
+
const filters = {};
|
|
586
|
+
if (!opts.allProjects) {
|
|
587
|
+
const project = process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), opts.project ?? null);
|
|
588
|
+
if (project) filters.project = project;
|
|
589
|
+
}
|
|
590
|
+
if ((opts.workingSetFile?.length ?? 0) > 0) p.log.warn("--working-set-file is accepted for compatibility but is not yet used by the TS pack builder.");
|
|
591
|
+
const result = store.buildMemoryPack(context, limit, budget, filters);
|
|
450
592
|
if (opts.json) {
|
|
451
593
|
console.log(JSON.stringify(result, null, 2));
|
|
452
594
|
return;
|
|
@@ -468,11 +610,16 @@ var packCommand = new Command("pack").configureHelp(helpStyle).description("Buil
|
|
|
468
610
|
});
|
|
469
611
|
//#endregion
|
|
470
612
|
//#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));
|
|
613
|
+
var recentCommand = new Command("recent").configureHelp(helpStyle).description("Show recent memories").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--limit <n>", "max results", "5").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").option("--kind <kind>", "filter by memory kind").action((opts) => {
|
|
614
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
473
615
|
try {
|
|
474
616
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
|
|
475
|
-
const filters =
|
|
617
|
+
const filters = {};
|
|
618
|
+
if (opts.kind) filters.kind = opts.kind;
|
|
619
|
+
if (!opts.allProjects) {
|
|
620
|
+
const project = process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), opts.project ?? null);
|
|
621
|
+
if (project) filters.project = project;
|
|
622
|
+
}
|
|
476
623
|
const items = store.recent(limit, filters);
|
|
477
624
|
for (const item of items) console.log(`#${item.id} [${item.kind}] ${item.title}`);
|
|
478
625
|
} finally {
|
|
@@ -481,11 +628,16 @@ var recentCommand = new Command("recent").configureHelp(helpStyle).description("
|
|
|
481
628
|
});
|
|
482
629
|
//#endregion
|
|
483
630
|
//#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", "
|
|
485
|
-
const store = new MemoryStore(resolveDbPath(opts.db));
|
|
631
|
+
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("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max results", "5").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").option("--kind <kind>", "filter by memory kind").option("--json", "output as JSON").action((query, opts) => {
|
|
632
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
486
633
|
try {
|
|
487
|
-
const limit = Number.parseInt(opts.limit, 10) ||
|
|
488
|
-
const filters =
|
|
634
|
+
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
|
|
635
|
+
const filters = {};
|
|
636
|
+
if (opts.kind) filters.kind = opts.kind;
|
|
637
|
+
if (!opts.allProjects) {
|
|
638
|
+
const project = process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), opts.project ?? null);
|
|
639
|
+
if (project) filters.project = project;
|
|
640
|
+
}
|
|
489
641
|
const results = store.search(query, limit, filters);
|
|
490
642
|
if (opts.json) {
|
|
491
643
|
console.log(JSON.stringify(results, null, 2));
|
|
@@ -520,6 +672,49 @@ function timeSince(isoDate) {
|
|
|
520
672
|
return `${Math.floor(days / 30)}mo ago`;
|
|
521
673
|
}
|
|
522
674
|
//#endregion
|
|
675
|
+
//#region src/commands/serve-invocation.ts
|
|
676
|
+
function parsePort(rawPort) {
|
|
677
|
+
const port = Number.parseInt(rawPort, 10);
|
|
678
|
+
if (!Number.isFinite(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${rawPort}`);
|
|
679
|
+
return port;
|
|
680
|
+
}
|
|
681
|
+
function resolveLegacyServeInvocation(opts) {
|
|
682
|
+
if (opts.stop && opts.restart) throw new Error("Use only one of --stop or --restart");
|
|
683
|
+
if (opts.foreground && opts.background) throw new Error("Use only one of --background or --foreground");
|
|
684
|
+
const dbPath = opts.db ?? opts.dbPath ?? null;
|
|
685
|
+
return {
|
|
686
|
+
mode: opts.stop ? "stop" : opts.restart ? "restart" : "start",
|
|
687
|
+
dbPath,
|
|
688
|
+
host: opts.host,
|
|
689
|
+
port: parsePort(opts.port),
|
|
690
|
+
background: opts.restart ? true : opts.background ? true : false
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function resolveServeInvocation(action, opts) {
|
|
694
|
+
if (action === void 0) return resolveLegacyServeInvocation(opts);
|
|
695
|
+
if (opts.stop || opts.restart) throw new Error("Do not combine lifecycle flags with a serve action");
|
|
696
|
+
if (action === "start") return resolveStartServeInvocation(opts);
|
|
697
|
+
return resolveStopRestartInvocation(action, opts);
|
|
698
|
+
}
|
|
699
|
+
function resolveStartServeInvocation(opts) {
|
|
700
|
+
return {
|
|
701
|
+
mode: "start",
|
|
702
|
+
dbPath: opts.db ?? opts.dbPath ?? null,
|
|
703
|
+
host: opts.host,
|
|
704
|
+
port: parsePort(opts.port),
|
|
705
|
+
background: !opts.foreground
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function resolveStopRestartInvocation(mode, opts) {
|
|
709
|
+
return {
|
|
710
|
+
mode,
|
|
711
|
+
dbPath: opts.db ?? opts.dbPath ?? null,
|
|
712
|
+
host: opts.host,
|
|
713
|
+
port: parsePort(opts.port),
|
|
714
|
+
background: mode === "restart"
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
//#endregion
|
|
523
718
|
//#region src/commands/serve.ts
|
|
524
719
|
function pidFilePath(dbPath) {
|
|
525
720
|
return join(dirname(dbPath), "viewer.pid");
|
|
@@ -545,6 +740,14 @@ function readViewerPidRecord(dbPath) {
|
|
|
545
740
|
}
|
|
546
741
|
return null;
|
|
547
742
|
}
|
|
743
|
+
function isProcessRunning(pid) {
|
|
744
|
+
try {
|
|
745
|
+
process.kill(pid, 0);
|
|
746
|
+
return true;
|
|
747
|
+
} catch {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
548
751
|
async function respondsLikeCodememViewer(record) {
|
|
549
752
|
try {
|
|
550
753
|
const controller = new AbortController();
|
|
@@ -556,19 +759,37 @@ async function respondsLikeCodememViewer(record) {
|
|
|
556
759
|
return false;
|
|
557
760
|
}
|
|
558
761
|
}
|
|
762
|
+
async function isPortOpen(host, port) {
|
|
763
|
+
return new Promise((resolve) => {
|
|
764
|
+
const socket = net.createConnection({
|
|
765
|
+
host,
|
|
766
|
+
port
|
|
767
|
+
});
|
|
768
|
+
const done = (open) => {
|
|
769
|
+
socket.removeAllListeners();
|
|
770
|
+
socket.destroy();
|
|
771
|
+
resolve(open);
|
|
772
|
+
};
|
|
773
|
+
socket.setTimeout(300);
|
|
774
|
+
socket.once("connect", () => done(true));
|
|
775
|
+
socket.once("timeout", () => done(false));
|
|
776
|
+
socket.once("error", () => done(false));
|
|
777
|
+
});
|
|
778
|
+
}
|
|
559
779
|
async function waitForProcessExit(pid, timeoutMs = 5e3) {
|
|
560
780
|
const deadline = Date.now() + timeoutMs;
|
|
561
|
-
while (Date.now() < deadline)
|
|
562
|
-
|
|
781
|
+
while (Date.now() < deadline) {
|
|
782
|
+
if (!isProcessRunning(pid)) return;
|
|
563
783
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
564
|
-
} catch {
|
|
565
|
-
return;
|
|
566
784
|
}
|
|
567
785
|
}
|
|
568
786
|
async function stopExistingViewer(dbPath) {
|
|
569
787
|
const pidPath = pidFilePath(dbPath);
|
|
570
788
|
const record = readViewerPidRecord(dbPath);
|
|
571
|
-
if (!record) return
|
|
789
|
+
if (!record) return {
|
|
790
|
+
stopped: false,
|
|
791
|
+
pid: null
|
|
792
|
+
};
|
|
572
793
|
if (await respondsLikeCodememViewer(record)) try {
|
|
573
794
|
process.kill(record.pid, "SIGTERM");
|
|
574
795
|
await waitForProcessExit(record.pid);
|
|
@@ -576,17 +797,60 @@ async function stopExistingViewer(dbPath) {
|
|
|
576
797
|
try {
|
|
577
798
|
rmSync(pidPath);
|
|
578
799
|
} catch {}
|
|
800
|
+
return {
|
|
801
|
+
stopped: true,
|
|
802
|
+
pid: record.pid
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function buildForegroundRunnerArgs(scriptPath, invocation, execArgv = process.execArgv) {
|
|
806
|
+
const args = [
|
|
807
|
+
...execArgv,
|
|
808
|
+
scriptPath,
|
|
809
|
+
"serve",
|
|
810
|
+
"start",
|
|
811
|
+
"--foreground",
|
|
812
|
+
"--host",
|
|
813
|
+
invocation.host,
|
|
814
|
+
"--port",
|
|
815
|
+
String(invocation.port)
|
|
816
|
+
];
|
|
817
|
+
if (invocation.dbPath) args.push("--db-path", invocation.dbPath);
|
|
818
|
+
return args;
|
|
819
|
+
}
|
|
820
|
+
async function startBackgroundViewer(invocation) {
|
|
821
|
+
if (await isPortOpen(invocation.host, invocation.port)) {
|
|
822
|
+
p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const scriptPath = process.argv[1];
|
|
826
|
+
if (!scriptPath) throw new Error("Unable to resolve CLI entrypoint for background launch");
|
|
827
|
+
const child = spawn(process.execPath, buildForegroundRunnerArgs(scriptPath, invocation), {
|
|
828
|
+
cwd: process.cwd(),
|
|
829
|
+
detached: true,
|
|
830
|
+
stdio: "ignore",
|
|
831
|
+
env: {
|
|
832
|
+
...process.env,
|
|
833
|
+
...invocation.dbPath ? { CODEMEM_DB: invocation.dbPath } : {}
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
child.unref();
|
|
837
|
+
if (invocation.dbPath) writeFileSync(pidFilePath(invocation.dbPath), JSON.stringify({
|
|
838
|
+
pid: child.pid,
|
|
839
|
+
host: invocation.host,
|
|
840
|
+
port: invocation.port
|
|
841
|
+
}), "utf-8");
|
|
842
|
+
p.intro("codemem viewer");
|
|
843
|
+
p.outro(`Viewer started in background (pid ${child.pid}) at http://${invocation.host}:${invocation.port}`);
|
|
579
844
|
}
|
|
580
|
-
|
|
845
|
+
async function startForegroundViewer(invocation) {
|
|
581
846
|
const { createApp, closeStore, getStore } = await import("@codemem/server");
|
|
582
847
|
const { serve } = await import("@hono/node-server");
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
848
|
+
if (invocation.dbPath) process.env.CODEMEM_DB = invocation.dbPath;
|
|
849
|
+
if (await isPortOpen(invocation.host, invocation.port)) {
|
|
850
|
+
p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
|
|
851
|
+
process.exitCode = 1;
|
|
852
|
+
return;
|
|
587
853
|
}
|
|
588
|
-
process.env.CODEMEM_DB = dbPath;
|
|
589
|
-
const port = Number.parseInt(opts.port, 10);
|
|
590
854
|
const observer = new ObserverClient();
|
|
591
855
|
const sweeper = new RawEventSweeper(getStore(), { observer });
|
|
592
856
|
sweeper.start();
|
|
@@ -595,11 +859,12 @@ var serveCommand = new Command("serve").configureHelp(helpStyle).description("St
|
|
|
595
859
|
const config = readCodememConfigFile();
|
|
596
860
|
if (config.sync_enabled === true || process.env.CODEMEM_SYNC_ENABLED?.toLowerCase() === "true" || process.env.CODEMEM_SYNC_ENABLED === "1") {
|
|
597
861
|
syncRunning = true;
|
|
862
|
+
const syncIntervalS = typeof config.sync_interval_s === "number" ? config.sync_interval_s : 120;
|
|
598
863
|
runSyncDaemon({
|
|
599
|
-
dbPath,
|
|
600
|
-
intervalS:
|
|
601
|
-
host:
|
|
602
|
-
port,
|
|
864
|
+
dbPath: resolveDbPath(invocation.dbPath ?? void 0),
|
|
865
|
+
intervalS: syncIntervalS,
|
|
866
|
+
host: invocation.host,
|
|
867
|
+
port: invocation.port,
|
|
603
868
|
signal: syncAbort.signal
|
|
604
869
|
}).catch((err) => {
|
|
605
870
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -612,16 +877,17 @@ var serveCommand = new Command("serve").configureHelp(helpStyle).description("St
|
|
|
612
877
|
storeFactory: getStore,
|
|
613
878
|
sweeper
|
|
614
879
|
});
|
|
880
|
+
const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
|
|
615
881
|
const pidPath = pidFilePath(dbPath);
|
|
616
882
|
const server = serve({
|
|
617
883
|
fetch: app.fetch,
|
|
618
|
-
hostname:
|
|
619
|
-
port
|
|
884
|
+
hostname: invocation.host,
|
|
885
|
+
port: invocation.port
|
|
620
886
|
}, (info) => {
|
|
621
887
|
writeFileSync(pidPath, JSON.stringify({
|
|
622
888
|
pid: process.pid,
|
|
623
|
-
host:
|
|
624
|
-
port
|
|
889
|
+
host: invocation.host,
|
|
890
|
+
port: invocation.port
|
|
625
891
|
}), "utf-8");
|
|
626
892
|
p.intro("codemem viewer");
|
|
627
893
|
p.log.success(`Listening on http://${info.address}:${info.port}`);
|
|
@@ -629,6 +895,11 @@ var serveCommand = new Command("serve").configureHelp(helpStyle).description("St
|
|
|
629
895
|
p.log.step("Raw event sweeper started");
|
|
630
896
|
if (syncRunning) p.log.step("Sync daemon started");
|
|
631
897
|
});
|
|
898
|
+
server.on("error", (err) => {
|
|
899
|
+
if (err.code === "EADDRINUSE") p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
|
|
900
|
+
else p.log.error(err.message);
|
|
901
|
+
process.exit(1);
|
|
902
|
+
});
|
|
632
903
|
const shutdown = async () => {
|
|
633
904
|
p.outro("shutting down");
|
|
634
905
|
syncAbort.abort();
|
|
@@ -654,8 +925,74 @@ var serveCommand = new Command("serve").configureHelp(helpStyle).description("St
|
|
|
654
925
|
process.on("SIGTERM", () => {
|
|
655
926
|
shutdown();
|
|
656
927
|
});
|
|
928
|
+
}
|
|
929
|
+
async function runServeInvocation(invocation) {
|
|
930
|
+
const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
|
|
931
|
+
if (invocation.mode === "stop" || invocation.mode === "restart") {
|
|
932
|
+
const result = await stopExistingViewer(dbPath);
|
|
933
|
+
if (result.stopped) {
|
|
934
|
+
p.intro("codemem viewer");
|
|
935
|
+
p.log.success(`Stopped viewer${result.pid ? ` (pid ${result.pid})` : ""}`);
|
|
936
|
+
if (invocation.mode === "stop") {
|
|
937
|
+
p.outro("done");
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
} else if (invocation.mode === "stop") {
|
|
941
|
+
p.intro("codemem viewer");
|
|
942
|
+
p.outro("No background viewer found");
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (invocation.mode === "start" || invocation.mode === "restart") {
|
|
947
|
+
if (invocation.background) {
|
|
948
|
+
await startBackgroundViewer({
|
|
949
|
+
...invocation,
|
|
950
|
+
dbPath
|
|
951
|
+
});
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
await startForegroundViewer({
|
|
955
|
+
...invocation,
|
|
956
|
+
dbPath
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function addSharedServeOptions(command) {
|
|
961
|
+
return command.option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <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");
|
|
962
|
+
}
|
|
963
|
+
var serveCommand = addSharedServeOptions(new Command("serve").configureHelp(helpStyle).description("Run or manage the viewer").argument("[action]", "lifecycle action (start|stop|restart)")).option("--background", "run viewer in background").option("--foreground", "run viewer in foreground").option("--stop", "stop background viewer").option("--restart", "restart background viewer").action(async (action, opts) => {
|
|
964
|
+
const normalizedAction = action === void 0 ? void 0 : action === "start" || action === "stop" || action === "restart" ? action : null;
|
|
965
|
+
if (normalizedAction === null) {
|
|
966
|
+
p.log.error(`Unknown serve action: ${action}`);
|
|
967
|
+
process.exitCode = 1;
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
await runServeInvocation(resolveServeInvocation(normalizedAction, opts));
|
|
657
971
|
});
|
|
658
972
|
//#endregion
|
|
973
|
+
//#region src/commands/setup-config.ts
|
|
974
|
+
function resolveOpencodeConfigPath(configDir) {
|
|
975
|
+
const jsonPath = join(configDir, "opencode.json");
|
|
976
|
+
if (existsSync(jsonPath)) return jsonPath;
|
|
977
|
+
const jsoncPath = join(configDir, "opencode.jsonc");
|
|
978
|
+
if (existsSync(jsoncPath)) return jsoncPath;
|
|
979
|
+
return jsoncPath;
|
|
980
|
+
}
|
|
981
|
+
function loadJsoncConfig(path) {
|
|
982
|
+
if (!existsSync(path)) return {};
|
|
983
|
+
const raw = readFileSync(path, "utf-8");
|
|
984
|
+
try {
|
|
985
|
+
return JSON.parse(raw);
|
|
986
|
+
} catch {
|
|
987
|
+
const cleaned = stripTrailingCommas(stripJsonComments(raw));
|
|
988
|
+
return JSON.parse(cleaned);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function writeJsonConfig(path, data) {
|
|
992
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
993
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
994
|
+
}
|
|
995
|
+
//#endregion
|
|
659
996
|
//#region src/commands/setup.ts
|
|
660
997
|
/**
|
|
661
998
|
* codemem setup — one-command installation for OpenCode plugin + MCP config.
|
|
@@ -677,17 +1014,15 @@ function claudeConfigDir() {
|
|
|
677
1014
|
}
|
|
678
1015
|
/**
|
|
679
1016
|
* Find the plugin source file — walk up from this module's location
|
|
680
|
-
* to find the .opencode/
|
|
1017
|
+
* to find the .opencode/plugins/codemem.js in the package tree.
|
|
681
1018
|
*/
|
|
682
1019
|
function findPluginSource() {
|
|
683
1020
|
let dir = dirname(import.meta.url.replace("file://", ""));
|
|
684
1021
|
for (let i = 0; i < 6; i++) {
|
|
685
|
-
const candidate = join(dir, ".opencode", "
|
|
1022
|
+
const candidate = join(dir, ".opencode", "plugins", "codemem.js");
|
|
686
1023
|
if (existsSync(candidate)) return candidate;
|
|
687
|
-
const nmCandidate = join(dir, "node_modules", "codemem", ".opencode", "
|
|
1024
|
+
const nmCandidate = join(dir, "node_modules", "codemem", ".opencode", "plugins", "codemem.js");
|
|
688
1025
|
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
1026
|
const parent = dirname(dir);
|
|
692
1027
|
if (parent === dir) break;
|
|
693
1028
|
dir = parent;
|
|
@@ -709,27 +1044,13 @@ function findCompatSource() {
|
|
|
709
1044
|
}
|
|
710
1045
|
return null;
|
|
711
1046
|
}
|
|
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
1047
|
function installPlugin(force) {
|
|
727
1048
|
const source = findPluginSource();
|
|
728
1049
|
if (!source) {
|
|
729
1050
|
p.log.error("Plugin file not found in package tree");
|
|
730
1051
|
return false;
|
|
731
1052
|
}
|
|
732
|
-
const destDir = join(opencodeConfigDir(), "
|
|
1053
|
+
const destDir = join(opencodeConfigDir(), "plugins");
|
|
733
1054
|
const dest = join(destDir, "codemem.js");
|
|
734
1055
|
if (existsSync(dest) && !force) p.log.info(`Plugin already installed at ${dest}`);
|
|
735
1056
|
else {
|
|
@@ -746,7 +1067,7 @@ function installPlugin(force) {
|
|
|
746
1067
|
return true;
|
|
747
1068
|
}
|
|
748
1069
|
function installMcp(force) {
|
|
749
|
-
const configPath =
|
|
1070
|
+
const configPath = resolveOpencodeConfigPath(opencodeConfigDir());
|
|
750
1071
|
let config;
|
|
751
1072
|
try {
|
|
752
1073
|
config = loadJsoncConfig(configPath);
|
|
@@ -838,8 +1159,8 @@ function fmtTokens(n) {
|
|
|
838
1159
|
if (n >= 1e3) return `~${(n / 1e3).toFixed(0)}K`;
|
|
839
1160
|
return `${n}`;
|
|
840
1161
|
}
|
|
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));
|
|
1162
|
+
var statsCommand = new Command("stats").configureHelp(helpStyle).description("Show database statistics").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--json", "output as JSON").action((opts) => {
|
|
1163
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
843
1164
|
try {
|
|
844
1165
|
const result = store.stats();
|
|
845
1166
|
if (opts.json) {
|
|
@@ -876,14 +1197,382 @@ var statsCommand = new Command("stats").configureHelp(helpStyle).description("Sh
|
|
|
876
1197
|
}
|
|
877
1198
|
});
|
|
878
1199
|
//#endregion
|
|
1200
|
+
//#region src/commands/sync-helpers.ts
|
|
1201
|
+
function formatSyncAttempt(row) {
|
|
1202
|
+
const status = row.ok ? "ok" : "error";
|
|
1203
|
+
const error = String(row.error || "");
|
|
1204
|
+
const suffix = error ? ` | ${error}` : "";
|
|
1205
|
+
return `${row.peer_device_id}|${status}|in=${row.ops_in}|out=${row.ops_out}|${row.finished_at ?? ""}${suffix}`;
|
|
1206
|
+
}
|
|
1207
|
+
function buildServeLifecycleArgs(action, opts, scriptPath, execArgv = []) {
|
|
1208
|
+
if (!scriptPath) throw new Error("Unable to resolve CLI entrypoint for sync lifecycle command");
|
|
1209
|
+
const args = [
|
|
1210
|
+
...execArgv,
|
|
1211
|
+
scriptPath,
|
|
1212
|
+
"serve"
|
|
1213
|
+
];
|
|
1214
|
+
if (action === "start") args.push("--restart");
|
|
1215
|
+
else if (action === "stop") args.push("--stop");
|
|
1216
|
+
else args.push("--restart");
|
|
1217
|
+
if (opts.db ?? opts.dbPath) args.push("--db-path", opts.db ?? opts.dbPath ?? "");
|
|
1218
|
+
if (opts.host) args.push("--host", opts.host);
|
|
1219
|
+
if (opts.port) args.push("--port", opts.port);
|
|
1220
|
+
return args;
|
|
1221
|
+
}
|
|
1222
|
+
function formatSyncOnceResult(peerDeviceId, result) {
|
|
1223
|
+
if (result.ok) return `- ${peerDeviceId}: ok`;
|
|
1224
|
+
return `- ${peerDeviceId}: error${result.error ? `: ${result.error}` : ""}`;
|
|
1225
|
+
}
|
|
1226
|
+
function parseProjectList(value) {
|
|
1227
|
+
if (!value) return [];
|
|
1228
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1229
|
+
}
|
|
1230
|
+
function collectAdvertiseAddresses(explicitAddress, configuredHost, port, interfaces) {
|
|
1231
|
+
if (explicitAddress && !["auto", "default"].includes(explicitAddress.toLowerCase())) return [explicitAddress];
|
|
1232
|
+
if (configuredHost && configuredHost !== "0.0.0.0") return [`${configuredHost}:${port}`];
|
|
1233
|
+
const addresses = Object.values(interfaces).flatMap((entries) => entries ?? []).filter((entry) => !entry.internal).map((entry) => entry.address).filter((address) => address && address !== "127.0.0.1" && address !== "::1").map((address) => `${address}:${port}`);
|
|
1234
|
+
return [...new Set(addresses)];
|
|
1235
|
+
}
|
|
1236
|
+
//#endregion
|
|
879
1237
|
//#region src/commands/sync.ts
|
|
880
1238
|
/**
|
|
881
1239
|
* Sync CLI commands — enable/disable/status/peers/connect.
|
|
882
1240
|
*/
|
|
1241
|
+
function parseAttemptsLimit(value) {
|
|
1242
|
+
if (!/^\d+$/.test(value.trim())) throw new Error(`Invalid --limit: ${value}`);
|
|
1243
|
+
return Number.parseInt(value, 10);
|
|
1244
|
+
}
|
|
1245
|
+
async function portOpen(host, port) {
|
|
1246
|
+
return new Promise((resolve) => {
|
|
1247
|
+
const socket = net.createConnection({
|
|
1248
|
+
host,
|
|
1249
|
+
port
|
|
1250
|
+
});
|
|
1251
|
+
const done = (ok) => {
|
|
1252
|
+
socket.removeAllListeners();
|
|
1253
|
+
socket.destroy();
|
|
1254
|
+
resolve(ok);
|
|
1255
|
+
};
|
|
1256
|
+
socket.setTimeout(300);
|
|
1257
|
+
socket.once("connect", () => done(true));
|
|
1258
|
+
socket.once("timeout", () => done(false));
|
|
1259
|
+
socket.once("error", () => done(false));
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
function readViewerBinding(dbPath) {
|
|
1263
|
+
try {
|
|
1264
|
+
const raw = readFileSync(join(dirname(dbPath), "viewer.pid"), "utf8");
|
|
1265
|
+
const parsed = JSON.parse(raw);
|
|
1266
|
+
if (typeof parsed.host === "string" && typeof parsed.port === "number") return {
|
|
1267
|
+
host: parsed.host,
|
|
1268
|
+
port: parsed.port
|
|
1269
|
+
};
|
|
1270
|
+
} catch {}
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1273
|
+
function parseStoredAddressEndpoint(value) {
|
|
1274
|
+
try {
|
|
1275
|
+
const normalized = value.includes("://") ? value : `http://${value}`;
|
|
1276
|
+
const url = new URL(normalized);
|
|
1277
|
+
const port = url.port ? Number.parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
|
|
1278
|
+
if (!url.hostname || !Number.isFinite(port)) return null;
|
|
1279
|
+
return {
|
|
1280
|
+
host: url.hostname,
|
|
1281
|
+
port
|
|
1282
|
+
};
|
|
1283
|
+
} catch {
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
async function runServeLifecycle(action, opts) {
|
|
1288
|
+
if (opts.user === false || opts.system === true) p.log.warn("TS sync lifecycle currently manages the local viewer process, not separate user/system services.");
|
|
1289
|
+
if (action === "start") {
|
|
1290
|
+
const config = readCodememConfigFile();
|
|
1291
|
+
if (config.sync_enabled !== true) {
|
|
1292
|
+
p.log.error("Sync is disabled. Run `codemem sync enable` first.");
|
|
1293
|
+
process.exitCode = 1;
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const configuredHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
|
|
1297
|
+
const configuredPort = typeof config.sync_port === "number" ? String(config.sync_port) : "7337";
|
|
1298
|
+
opts.host ??= configuredHost;
|
|
1299
|
+
opts.port ??= configuredPort;
|
|
1300
|
+
} else if (action === "restart") {
|
|
1301
|
+
const config = readCodememConfigFile();
|
|
1302
|
+
const configuredHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
|
|
1303
|
+
const configuredPort = typeof config.sync_port === "number" ? String(config.sync_port) : "7337";
|
|
1304
|
+
opts.host ??= configuredHost;
|
|
1305
|
+
opts.port ??= configuredPort;
|
|
1306
|
+
}
|
|
1307
|
+
const args = buildServeLifecycleArgs(action, opts, process.argv[1] ?? "", process.execArgv);
|
|
1308
|
+
await new Promise((resolve, reject) => {
|
|
1309
|
+
const child = spawn(process.execPath, args, {
|
|
1310
|
+
cwd: process.cwd(),
|
|
1311
|
+
stdio: "inherit",
|
|
1312
|
+
env: {
|
|
1313
|
+
...process.env,
|
|
1314
|
+
...opts.db ?? opts.dbPath ? { CODEMEM_DB: opts.db ?? opts.dbPath } : {}
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
child.once("error", reject);
|
|
1318
|
+
child.once("exit", (code) => {
|
|
1319
|
+
if (code && code !== 0) process.exitCode = code;
|
|
1320
|
+
resolve();
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
883
1324
|
var syncCommand = new Command("sync").configureHelp(helpStyle).description("Sync configuration and peer management");
|
|
884
|
-
syncCommand.addCommand(new Command("
|
|
1325
|
+
syncCommand.addCommand(new Command("attempts").configureHelp(helpStyle).description("Show recent sync attempts").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--limit <n>", "max attempts", "10").option("--json", "output as JSON").action((opts) => {
|
|
1326
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
1327
|
+
try {
|
|
1328
|
+
const d = drizzle(store.db, { schema });
|
|
1329
|
+
const limit = parseAttemptsLimit(opts.limit);
|
|
1330
|
+
const rows = d.select({
|
|
1331
|
+
peer_device_id: schema.syncAttempts.peer_device_id,
|
|
1332
|
+
ok: schema.syncAttempts.ok,
|
|
1333
|
+
ops_in: schema.syncAttempts.ops_in,
|
|
1334
|
+
ops_out: schema.syncAttempts.ops_out,
|
|
1335
|
+
error: schema.syncAttempts.error,
|
|
1336
|
+
finished_at: schema.syncAttempts.finished_at
|
|
1337
|
+
}).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(limit).all();
|
|
1338
|
+
if (opts.json) {
|
|
1339
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
for (const row of rows) console.log(formatSyncAttempt(row));
|
|
1343
|
+
} finally {
|
|
1344
|
+
store.close();
|
|
1345
|
+
}
|
|
1346
|
+
}));
|
|
1347
|
+
syncCommand.addCommand(new Command("start").configureHelp(helpStyle).description("Start sync daemon").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--host <host>", "viewer host").option("--port <port>", "viewer port").option("--user", "accepted for compatibility", true).option("--system", "accepted for compatibility").action(async (opts) => {
|
|
1348
|
+
await runServeLifecycle("start", opts);
|
|
1349
|
+
}));
|
|
1350
|
+
syncCommand.addCommand(new Command("stop").configureHelp(helpStyle).description("Stop sync daemon").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--host <host>", "viewer host").option("--port <port>", "viewer port").option("--user", "accepted for compatibility", true).option("--system", "accepted for compatibility").action(async (opts) => {
|
|
1351
|
+
await runServeLifecycle("stop", opts);
|
|
1352
|
+
}));
|
|
1353
|
+
syncCommand.addCommand(new Command("restart").configureHelp(helpStyle).description("Restart sync daemon").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--host <host>", "viewer host").option("--port <port>", "viewer port").option("--user", "accepted for compatibility", true).option("--system", "accepted for compatibility").action(async (opts) => {
|
|
1354
|
+
await runServeLifecycle("restart", opts);
|
|
1355
|
+
}));
|
|
1356
|
+
syncCommand.addCommand(new Command("once").configureHelp(helpStyle).description("Run a single sync pass").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--peer <peer>", "peer device id or name").action(async (opts) => {
|
|
1357
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
1358
|
+
try {
|
|
1359
|
+
syncPassPreflight(store.db);
|
|
1360
|
+
const d = drizzle(store.db, { schema });
|
|
1361
|
+
const rows = opts.peer ? (() => {
|
|
1362
|
+
const deviceMatches = d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, opts.peer)).all();
|
|
1363
|
+
if (deviceMatches.length > 0) return deviceMatches;
|
|
1364
|
+
const nameMatches = d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).where(eq(schema.syncPeers.name, opts.peer)).all();
|
|
1365
|
+
if (nameMatches.length > 1) {
|
|
1366
|
+
p.log.error(`Peer name is ambiguous: ${opts.peer}`);
|
|
1367
|
+
process.exitCode = 1;
|
|
1368
|
+
return [];
|
|
1369
|
+
}
|
|
1370
|
+
return nameMatches;
|
|
1371
|
+
})() : d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).all();
|
|
1372
|
+
if (rows.length === 0) {
|
|
1373
|
+
p.log.warn("No peers available for sync");
|
|
1374
|
+
process.exitCode = 1;
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
let hadFailure = false;
|
|
1378
|
+
for (const row of rows) {
|
|
1379
|
+
const result = await runSyncPass(store.db, row.peer_device_id);
|
|
1380
|
+
if (!result.ok) hadFailure = true;
|
|
1381
|
+
console.log(formatSyncOnceResult(row.peer_device_id, result));
|
|
1382
|
+
}
|
|
1383
|
+
if (hadFailure) process.exitCode = 1;
|
|
1384
|
+
} finally {
|
|
1385
|
+
store.close();
|
|
1386
|
+
}
|
|
1387
|
+
}));
|
|
1388
|
+
syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description("Print pairing payload or accept a peer payload").option("--accept <json>", "accept pairing payload JSON from another device").option("--accept-file <path>", "accept pairing payload from file path, or '-' for stdin").option("--payload-only", "when generating pairing payload, print JSON only").option("--name <name>", "label for the peer").option("--address <host:port>", "override peer address (host:port)").option("--include <projects>", "outbound-only allowlist for accepted peer").option("--exclude <projects>", "outbound-only blocklist for accepted peer").option("--all", "with --accept, push all projects to that peer").option("--default", "with --accept, use default/global push filters").option("--db-path <path>", "database path").action(async (opts) => {
|
|
1389
|
+
const store = new MemoryStore(resolveDbPath(opts.dbPath));
|
|
1390
|
+
try {
|
|
1391
|
+
const acceptModeRequested = opts.accept != null || opts.acceptFile != null;
|
|
1392
|
+
if (opts.payloadOnly && acceptModeRequested) {
|
|
1393
|
+
p.log.error("--payload-only cannot be combined with --accept or --accept-file");
|
|
1394
|
+
process.exitCode = 1;
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
if (opts.accept && opts.acceptFile) {
|
|
1398
|
+
p.log.error("Use only one of --accept or --accept-file");
|
|
1399
|
+
process.exitCode = 1;
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
let acceptText = opts.accept;
|
|
1403
|
+
if (opts.acceptFile) try {
|
|
1404
|
+
acceptText = opts.acceptFile === "-" ? await new Promise((resolve, reject) => {
|
|
1405
|
+
let text = "";
|
|
1406
|
+
process.stdin.setEncoding("utf8");
|
|
1407
|
+
process.stdin.on("data", (chunk) => {
|
|
1408
|
+
text += chunk;
|
|
1409
|
+
});
|
|
1410
|
+
process.stdin.on("end", () => resolve(text));
|
|
1411
|
+
process.stdin.on("error", reject);
|
|
1412
|
+
}) : readFileSync(opts.acceptFile, "utf8");
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
p.log.error(error instanceof Error ? `Failed to read pairing payload from ${opts.acceptFile}: ${error.message}` : `Failed to read pairing payload from ${opts.acceptFile}`);
|
|
1415
|
+
process.exitCode = 1;
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (acceptModeRequested && !(acceptText ?? "").trim()) {
|
|
1419
|
+
p.log.error("Empty pairing payload; provide JSON via --accept or --accept-file");
|
|
1420
|
+
process.exitCode = 1;
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (!acceptText && (opts.include || opts.exclude || opts.all || opts.default)) {
|
|
1424
|
+
p.log.error("Project filters are outbound-only and must be set on the device running --accept");
|
|
1425
|
+
process.exitCode = 1;
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (acceptText?.trim()) {
|
|
1429
|
+
if (opts.all && opts.default) {
|
|
1430
|
+
p.log.error("Use only one of --all or --default");
|
|
1431
|
+
process.exitCode = 1;
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
if ((opts.all || opts.default) && (opts.include || opts.exclude)) {
|
|
1435
|
+
p.log.error("--include/--exclude cannot be combined with --all/--default");
|
|
1436
|
+
process.exitCode = 1;
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
let payload;
|
|
1440
|
+
try {
|
|
1441
|
+
payload = JSON.parse(acceptText);
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
p.log.error(error instanceof Error ? `Invalid pairing payload: ${error.message}` : "Invalid pairing payload");
|
|
1444
|
+
process.exitCode = 1;
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const deviceId = String(payload.device_id || "").trim();
|
|
1448
|
+
const fingerprint = String(payload.fingerprint || "").trim();
|
|
1449
|
+
const publicKey = String(payload.public_key || "").trim();
|
|
1450
|
+
const resolvedAddresses = opts.address?.trim() ? [opts.address.trim()] : Array.isArray(payload.addresses) ? payload.addresses.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim()) : [];
|
|
1451
|
+
if (!deviceId || !fingerprint || !publicKey || resolvedAddresses.length === 0) {
|
|
1452
|
+
p.log.error("Pairing payload missing device_id, fingerprint, public_key, or addresses");
|
|
1453
|
+
process.exitCode = 1;
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (fingerprintPublicKey(publicKey) !== fingerprint) {
|
|
1457
|
+
p.log.error("Pairing payload fingerprint mismatch");
|
|
1458
|
+
process.exitCode = 1;
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
updatePeerAddresses(store.db, deviceId, resolvedAddresses, {
|
|
1462
|
+
name: opts.name,
|
|
1463
|
+
pinnedFingerprint: fingerprint,
|
|
1464
|
+
publicKey
|
|
1465
|
+
});
|
|
1466
|
+
if (opts.default) setPeerProjectFilter(store.db, deviceId, {
|
|
1467
|
+
include: null,
|
|
1468
|
+
exclude: null
|
|
1469
|
+
});
|
|
1470
|
+
else if (opts.all || opts.include || opts.exclude) setPeerProjectFilter(store.db, deviceId, {
|
|
1471
|
+
include: opts.all ? [] : parseProjectList(opts.include),
|
|
1472
|
+
exclude: opts.all ? [] : parseProjectList(opts.exclude)
|
|
1473
|
+
});
|
|
1474
|
+
p.log.success(`Paired with ${deviceId}`);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
const keysDir = process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
|
|
1478
|
+
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir });
|
|
1479
|
+
const publicKey = loadPublicKey(keysDir);
|
|
1480
|
+
if (!publicKey) {
|
|
1481
|
+
p.log.error("Public key missing");
|
|
1482
|
+
process.exitCode = 1;
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const config = readCodememConfigFile();
|
|
1486
|
+
const explicitAddress = opts.address?.trim();
|
|
1487
|
+
const configuredHost = typeof config.sync_host === "string" ? config.sync_host : null;
|
|
1488
|
+
const configuredPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
|
|
1489
|
+
const addresses = collectAdvertiseAddresses(explicitAddress ?? null, configuredHost, configuredPort, networkInterfaces());
|
|
1490
|
+
const payload = {
|
|
1491
|
+
device_id: deviceId,
|
|
1492
|
+
fingerprint,
|
|
1493
|
+
public_key: publicKey,
|
|
1494
|
+
address: addresses[0] ?? "",
|
|
1495
|
+
addresses
|
|
1496
|
+
};
|
|
1497
|
+
const payloadText = JSON.stringify(payload);
|
|
1498
|
+
if (opts.payloadOnly) {
|
|
1499
|
+
process.stdout.write(`${payloadText}\n`);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const escaped = payloadText.replaceAll("'", "'\\''");
|
|
1503
|
+
console.log("Pairing payload");
|
|
1504
|
+
console.log(payloadText);
|
|
1505
|
+
console.log("On the other device, save this JSON to pairing.json, then run:");
|
|
1506
|
+
console.log(" codemem sync pair --accept-file pairing.json");
|
|
1507
|
+
console.log("If you prefer inline JSON, run:");
|
|
1508
|
+
console.log(` codemem sync pair --accept '${escaped}'`);
|
|
1509
|
+
console.log("For machine-friendly output next time, run:");
|
|
1510
|
+
console.log(" codemem sync pair --payload-only");
|
|
1511
|
+
console.log("On the accepting device, --include/--exclude only control what it sends to peers.");
|
|
1512
|
+
console.log("This device does not yet enforce incoming project filters.");
|
|
1513
|
+
} finally {
|
|
1514
|
+
store.close();
|
|
1515
|
+
}
|
|
1516
|
+
}));
|
|
1517
|
+
syncCommand.addCommand(new Command("doctor").configureHelp(helpStyle).description("Diagnose common sync setup and connectivity issues").option("--db <path>", "database path").option("--db-path <path>", "database path").action(async (opts) => {
|
|
885
1518
|
const config = readCodememConfigFile();
|
|
886
|
-
const
|
|
1519
|
+
const dbPath = resolveDbPath(opts.db ?? opts.dbPath);
|
|
1520
|
+
const store = new MemoryStore(dbPath);
|
|
1521
|
+
try {
|
|
1522
|
+
const d = drizzle(store.db, { schema });
|
|
1523
|
+
const device = d.select({ device_id: schema.syncDevice.device_id }).from(schema.syncDevice).limit(1).get();
|
|
1524
|
+
const daemonState = d.select().from(schema.syncDaemonState).where(eq(schema.syncDaemonState.id, 1)).get();
|
|
1525
|
+
const peers = d.select({
|
|
1526
|
+
peer_device_id: schema.syncPeers.peer_device_id,
|
|
1527
|
+
addresses_json: schema.syncPeers.addresses_json,
|
|
1528
|
+
pinned_fingerprint: schema.syncPeers.pinned_fingerprint,
|
|
1529
|
+
public_key: schema.syncPeers.public_key
|
|
1530
|
+
}).from(schema.syncPeers).all();
|
|
1531
|
+
const issues = [];
|
|
1532
|
+
const syncHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
|
|
1533
|
+
const syncPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
|
|
1534
|
+
const viewerBinding = readViewerBinding(dbPath);
|
|
1535
|
+
console.log("Sync doctor");
|
|
1536
|
+
console.log(`- Enabled: ${config.sync_enabled === true}`);
|
|
1537
|
+
console.log(`- Listen: ${syncHost}:${syncPort}`);
|
|
1538
|
+
console.log(`- mDNS: ${process.env.CODEMEM_SYNC_MDNS ? "env-configured" : "default/off"}`);
|
|
1539
|
+
const reachable = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
|
|
1540
|
+
console.log(`- Daemon: ${reachable ? "running" : "not running"}`);
|
|
1541
|
+
if (!reachable) issues.push("daemon not running");
|
|
1542
|
+
if (!device) {
|
|
1543
|
+
console.log("- Identity: missing (run `codemem sync enable`)");
|
|
1544
|
+
issues.push("identity missing");
|
|
1545
|
+
} else console.log(`- Identity: ${device.device_id}`);
|
|
1546
|
+
if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) {
|
|
1547
|
+
console.log(`- Daemon error: ${daemonState.last_error} (at ${daemonState.last_error_at ?? "unknown"})`);
|
|
1548
|
+
issues.push("daemon error");
|
|
1549
|
+
}
|
|
1550
|
+
if (peers.length === 0) {
|
|
1551
|
+
console.log("- Peers: none (pair a device first)");
|
|
1552
|
+
issues.push("no peers");
|
|
1553
|
+
} else {
|
|
1554
|
+
console.log(`- Peers: ${peers.length}`);
|
|
1555
|
+
for (const peer of peers) {
|
|
1556
|
+
const addresses = peer.addresses_json ? JSON.parse(peer.addresses_json) : [];
|
|
1557
|
+
const endpoint = parseStoredAddressEndpoint(addresses[0] ?? "");
|
|
1558
|
+
const reach = endpoint ? await portOpen(endpoint.host, endpoint.port) ? "ok" : "unreachable" : "unknown";
|
|
1559
|
+
const pinned = Boolean(peer.pinned_fingerprint);
|
|
1560
|
+
const hasKey = Boolean(peer.public_key);
|
|
1561
|
+
console.log(` - ${peer.peer_device_id}: addresses=${addresses.length} reach=${reach} pinned=${pinned} public_key=${hasKey}`);
|
|
1562
|
+
if (reach !== "ok") issues.push(`peer ${peer.peer_device_id} unreachable`);
|
|
1563
|
+
if (!pinned || !hasKey) issues.push(`peer ${peer.peer_device_id} not pinned`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (!config.sync_enabled) issues.push("sync is disabled");
|
|
1567
|
+
if (issues.length > 0) console.log(`WARN: ${[...new Set(issues)].slice(0, 3).join(", ")}`);
|
|
1568
|
+
else console.log("OK: sync looks healthy");
|
|
1569
|
+
} finally {
|
|
1570
|
+
store.close();
|
|
1571
|
+
}
|
|
1572
|
+
}));
|
|
1573
|
+
syncCommand.addCommand(new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--json", "output as JSON").action((opts) => {
|
|
1574
|
+
const config = readCodememConfigFile();
|
|
1575
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
887
1576
|
try {
|
|
888
1577
|
const d = drizzle(store.db, { schema });
|
|
889
1578
|
const deviceRow = d.select({
|
|
@@ -934,8 +1623,8 @@ syncCommand.addCommand(new Command("status").configureHelp(helpStyle).descriptio
|
|
|
934
1623
|
store.close();
|
|
935
1624
|
}
|
|
936
1625
|
}));
|
|
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));
|
|
1626
|
+
syncCommand.addCommand(new Command("enable").configureHelp(helpStyle).description("Enable sync and initialize device identity").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--host <host>", "sync listen host").option("--port <port>", "sync listen port").option("--interval <seconds>", "sync interval in seconds").action((opts) => {
|
|
1627
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
939
1628
|
try {
|
|
940
1629
|
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db);
|
|
941
1630
|
const config = readCodememConfigFile();
|
|
@@ -964,8 +1653,8 @@ syncCommand.addCommand(new Command("disable").configureHelp(helpStyle).descripti
|
|
|
964
1653
|
p.intro("codemem sync disable");
|
|
965
1654
|
p.outro("Sync disabled — restart `codemem serve` to take effect");
|
|
966
1655
|
}));
|
|
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));
|
|
1656
|
+
syncCommand.addCommand(new Command("peers").configureHelp(helpStyle).description("List known sync peers").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--json", "output as JSON").action((opts) => {
|
|
1657
|
+
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
969
1658
|
try {
|
|
970
1659
|
const peers = drizzle(store.db, { schema }).select({
|
|
971
1660
|
peer_device_id: schema.syncPeers.peer_device_id,
|
|
@@ -1026,12 +1715,15 @@ completion.on("command", ({ reply }) => {
|
|
|
1026
1715
|
"claude-hook-ingest",
|
|
1027
1716
|
"db",
|
|
1028
1717
|
"export-memories",
|
|
1718
|
+
"forget",
|
|
1029
1719
|
"memory",
|
|
1030
1720
|
"import-memories",
|
|
1031
1721
|
"setup",
|
|
1722
|
+
"show",
|
|
1032
1723
|
"sync",
|
|
1033
1724
|
"stats",
|
|
1034
1725
|
"recent",
|
|
1726
|
+
"remember",
|
|
1035
1727
|
"search",
|
|
1036
1728
|
"pack",
|
|
1037
1729
|
"serve",
|
|
@@ -1044,16 +1736,24 @@ completion.on("command", ({ reply }) => {
|
|
|
1044
1736
|
]);
|
|
1045
1737
|
});
|
|
1046
1738
|
completion.init();
|
|
1047
|
-
|
|
1739
|
+
function hasRootFlag(flag) {
|
|
1740
|
+
for (const arg of process.argv.slice(2)) {
|
|
1741
|
+
if (arg === "--") return false;
|
|
1742
|
+
if (arg === flag) return true;
|
|
1743
|
+
if (!arg.startsWith("-")) return false;
|
|
1744
|
+
}
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
var program = new Command();
|
|
1748
|
+
program.name("codemem").description("codemem — persistent memory for AI coding agents").version(VERSION).configureHelp(helpStyle);
|
|
1749
|
+
if (hasRootFlag("--setup-completion")) {
|
|
1048
1750
|
completion.setupShellInitFile();
|
|
1049
1751
|
process.exit(0);
|
|
1050
1752
|
}
|
|
1051
|
-
if (
|
|
1753
|
+
if (hasRootFlag("--cleanup-completion")) {
|
|
1052
1754
|
completion.cleanupShellInitFile();
|
|
1053
1755
|
process.exit(0);
|
|
1054
1756
|
}
|
|
1055
|
-
var program = new Command();
|
|
1056
|
-
program.name("codemem").description("codemem — persistent memory for AI coding agents").version(VERSION).configureHelp(helpStyle);
|
|
1057
1757
|
program.addCommand(serveCommand);
|
|
1058
1758
|
program.addCommand(mcpCommand);
|
|
1059
1759
|
program.addCommand(claudeHookIngestCommand);
|
|
@@ -1064,6 +1764,9 @@ program.addCommand(statsCommand);
|
|
|
1064
1764
|
program.addCommand(recentCommand);
|
|
1065
1765
|
program.addCommand(searchCommand);
|
|
1066
1766
|
program.addCommand(packCommand);
|
|
1767
|
+
program.addCommand(showMemoryCommand);
|
|
1768
|
+
program.addCommand(forgetMemoryCommand);
|
|
1769
|
+
program.addCommand(rememberMemoryCommand);
|
|
1067
1770
|
program.addCommand(memoryCommand);
|
|
1068
1771
|
program.addCommand(syncCommand);
|
|
1069
1772
|
program.addCommand(setupCommand);
|