codemem 0.22.3 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1804 -531
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/dist/commands/claude-hook-ingest.d.ts +0 -51
- package/dist/commands/claude-hook-ingest.d.ts.map +0 -1
- package/dist/commands/claude-hook-ingest.test.d.ts +0 -2
- package/dist/commands/claude-hook-ingest.test.d.ts.map +0 -1
- package/dist/commands/db.d.ts +0 -3
- package/dist/commands/db.d.ts.map +0 -1
- package/dist/commands/db.test.d.ts +0 -2
- package/dist/commands/db.test.d.ts.map +0 -1
- package/dist/commands/embed.d.ts +0 -4
- package/dist/commands/embed.d.ts.map +0 -1
- package/dist/commands/embed.test.d.ts +0 -2
- package/dist/commands/embed.test.d.ts.map +0 -1
- package/dist/commands/enqueue-raw-event.d.ts +0 -3
- package/dist/commands/enqueue-raw-event.d.ts.map +0 -1
- package/dist/commands/export-memories.d.ts +0 -3
- package/dist/commands/export-memories.d.ts.map +0 -1
- package/dist/commands/import-memories.d.ts +0 -3
- package/dist/commands/import-memories.d.ts.map +0 -1
- package/dist/commands/mcp.d.ts +0 -3
- package/dist/commands/mcp.d.ts.map +0 -1
- package/dist/commands/memory.d.ts +0 -13
- package/dist/commands/memory.d.ts.map +0 -1
- package/dist/commands/memory.test.d.ts +0 -2
- package/dist/commands/memory.test.d.ts.map +0 -1
- package/dist/commands/pack.d.ts +0 -3
- package/dist/commands/pack.d.ts.map +0 -1
- package/dist/commands/recent.d.ts +0 -3
- package/dist/commands/recent.d.ts.map +0 -1
- package/dist/commands/search.d.ts +0 -3
- package/dist/commands/search.d.ts.map +0 -1
- package/dist/commands/serve-invocation.d.ts +0 -37
- package/dist/commands/serve-invocation.d.ts.map +0 -1
- package/dist/commands/serve.d.ts +0 -13
- package/dist/commands/serve.d.ts.map +0 -1
- package/dist/commands/serve.test.d.ts +0 -2
- package/dist/commands/serve.test.d.ts.map +0 -1
- package/dist/commands/setup-config.d.ts +0 -4
- package/dist/commands/setup-config.d.ts.map +0 -1
- package/dist/commands/setup-config.test.d.ts +0 -2
- package/dist/commands/setup-config.test.d.ts.map +0 -1
- package/dist/commands/setup.d.ts +0 -15
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/stats.d.ts +0 -3
- package/dist/commands/stats.d.ts.map +0 -1
- package/dist/commands/sync-helpers.d.ts +0 -31
- package/dist/commands/sync-helpers.d.ts.map +0 -1
- package/dist/commands/sync.d.ts +0 -6
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.test.d.ts +0 -2
- package/dist/commands/sync.test.d.ts.map +0 -1
- package/dist/commands/version.d.ts +0 -3
- package/dist/commands/version.d.ts.map +0 -1
- package/dist/help-style.d.ts +0 -9
- package/dist/help-style.d.ts.map +0 -1
- package/dist/index.d.ts +0 -13
- package/dist/index.d.ts.map +0 -1
- package/dist/index.smoke.test.d.ts +0 -2
- package/dist/index.smoke.test.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { DEFAULT_COORDINATOR_DB_PATH, MemoryStore, ObserverClient, RawEventSweeper, SyncRetentionRunner, VERSION, backfillTagsText, backfillVectors, buildRawEventEnvelopeFromHook, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fingerprintPublicKey, getRawEventStatus, importMemories, initDatabase, isEmbeddingDisabled, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCoordinatorSyncConfig, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
|
|
3
|
-
import { Command } from "commander";
|
|
2
|
+
import { DEFAULT_COORDINATOR_DB_PATH, MemoryStore, ObserverClient, RawEventSweeper, SyncRetentionRunner, VERSION, applyBootstrapSnapshot, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fetchAllSnapshotPages, fingerprintPublicKey, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getWorkspaceCodememConfigPath, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
|
|
3
|
+
import { Command, Option } from "commander";
|
|
4
4
|
import omelette from "omelette";
|
|
5
5
|
import { 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 { serve } from "@hono/node-server";
|
|
8
9
|
import { homedir, networkInterfaces } from "node:os";
|
|
9
10
|
import { dirname, join } from "node:path";
|
|
10
11
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
12
|
import net from "node:net";
|
|
12
|
-
import { serve } from "@hono/node-server";
|
|
13
13
|
import { desc, eq } from "drizzle-orm";
|
|
14
14
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
15
15
|
//#region src/help-style.ts
|
|
@@ -30,6 +30,52 @@ var helpStyle = {
|
|
|
30
30
|
styleArgumentText: (str) => styleText("yellow", str)
|
|
31
31
|
};
|
|
32
32
|
//#endregion
|
|
33
|
+
//#region src/shared-options.ts
|
|
34
|
+
/** Add -d/--db-path and hidden --db alias to a command. */
|
|
35
|
+
function addDbOption(cmd) {
|
|
36
|
+
cmd.addOption(new Option("-d, --db-path <path>", "database path (overrides $CODEMEM_DB)"));
|
|
37
|
+
cmd.addOption(new Option("--db <path>", "database path").hideHelp());
|
|
38
|
+
return cmd;
|
|
39
|
+
}
|
|
40
|
+
/** Resolve the db path from parsed opts that may have --db or --db-path. */
|
|
41
|
+
function resolveDbOpt(opts) {
|
|
42
|
+
return opts.dbPath ?? opts.db;
|
|
43
|
+
}
|
|
44
|
+
/** Add -c/--config to a command. */
|
|
45
|
+
function addConfigOption(cmd) {
|
|
46
|
+
cmd.addOption(new Option("-c, --config <path>", "config file path (overrides $CODEMEM_CONFIG)"));
|
|
47
|
+
return cmd;
|
|
48
|
+
}
|
|
49
|
+
/** Add -j/--json to a command. */
|
|
50
|
+
function addJsonOption(cmd) {
|
|
51
|
+
cmd.addOption(new Option("-j, --json", "output as JSON"));
|
|
52
|
+
return cmd;
|
|
53
|
+
}
|
|
54
|
+
/** Add --host and --port for the viewer/serve service. */
|
|
55
|
+
function addViewerHostOptions(cmd, defaults = {}) {
|
|
56
|
+
cmd.option("--host <host>", "viewer host", defaults.host ?? "127.0.0.1");
|
|
57
|
+
cmd.option("--port <port>", "viewer port", defaults.port ?? "38888");
|
|
58
|
+
return cmd;
|
|
59
|
+
}
|
|
60
|
+
/** Add hidden --user/--system Typer-era compatibility flags. */
|
|
61
|
+
function addLegacyServiceFlags(cmd) {
|
|
62
|
+
cmd.addOption(new Option("--user", "accepted for compatibility").default(true).hideHelp());
|
|
63
|
+
cmd.addOption(new Option("--system", "accepted for compatibility").hideHelp());
|
|
64
|
+
return cmd;
|
|
65
|
+
}
|
|
66
|
+
/** Emit a deprecation warning to stderr. */
|
|
67
|
+
function emitDeprecationWarning(oldForm, newForm) {
|
|
68
|
+
console.error(`Warning: '${oldForm}' is deprecated, use '${newForm}' instead.`);
|
|
69
|
+
}
|
|
70
|
+
/** Print a structured JSON error to stdout and set the exit code. */
|
|
71
|
+
function emitJsonError(errorCode, message, exitCode = 1) {
|
|
72
|
+
console.log(JSON.stringify({
|
|
73
|
+
error: errorCode,
|
|
74
|
+
message
|
|
75
|
+
}));
|
|
76
|
+
process.exitCode = exitCode;
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
33
79
|
//#region src/commands/claude-hook-ingest.ts
|
|
34
80
|
/**
|
|
35
81
|
* codemem claude-hook-ingest — read a single Claude Code hook payload
|
|
@@ -43,6 +89,13 @@ var helpStyle = {
|
|
|
43
89
|
* echo '{"hook_event_name":"Stop","session_id":"...","last_assistant_message":"..."}' \
|
|
44
90
|
* | codemem claude-hook-ingest
|
|
45
91
|
*/
|
|
92
|
+
function emitStructuredError$1(errorCode, message) {
|
|
93
|
+
console.log(JSON.stringify({
|
|
94
|
+
error: errorCode,
|
|
95
|
+
message
|
|
96
|
+
}));
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
}
|
|
46
99
|
/** Try to POST the hook payload to the running viewer server. */
|
|
47
100
|
async function tryHttpIngest(payload, host, port) {
|
|
48
101
|
const url = `http://${host}:${port}/api/claude-hooks`;
|
|
@@ -130,55 +183,823 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
|
|
|
130
183
|
const httpIngest = deps.httpIngest ?? tryHttpIngest;
|
|
131
184
|
const directIngest = deps.directIngest ?? directEnqueue;
|
|
132
185
|
const resolveDb = deps.resolveDb ?? resolveDbPath;
|
|
133
|
-
const
|
|
186
|
+
const port = typeof opts.port === "number" ? opts.port : Number.parseInt(opts.port, 10);
|
|
187
|
+
const httpResult = await httpIngest(payload, opts.host, port);
|
|
134
188
|
if (httpResult.ok) return {
|
|
135
189
|
inserted: httpResult.inserted,
|
|
136
190
|
skipped: httpResult.skipped,
|
|
137
191
|
via: "http"
|
|
138
192
|
};
|
|
139
193
|
return {
|
|
140
|
-
...directIngest(payload, resolveDb(opts
|
|
194
|
+
...directIngest(payload, resolveDb(resolveDbOpt(opts))),
|
|
141
195
|
via: "direct"
|
|
142
196
|
};
|
|
143
197
|
}
|
|
144
|
-
var
|
|
198
|
+
var claudeHookCmd = new Command("claude-hook-ingest").configureHelp(helpStyle).description("Ingest Claude hook payload: HTTP first, direct DB fallback");
|
|
199
|
+
addDbOption(claudeHookCmd);
|
|
200
|
+
addViewerHostOptions(claudeHookCmd);
|
|
201
|
+
var claudeHookIngestCommand = claudeHookCmd.action(async (opts) => {
|
|
145
202
|
let raw;
|
|
146
203
|
try {
|
|
147
204
|
raw = readFileSync(0, "utf8").trim();
|
|
148
205
|
} catch {
|
|
149
|
-
|
|
206
|
+
emitStructuredError$1("read_error", "failed to read stdin");
|
|
150
207
|
return;
|
|
151
208
|
}
|
|
152
209
|
if (!raw) {
|
|
153
|
-
|
|
210
|
+
emitStructuredError$1("read_error", "empty stdin");
|
|
154
211
|
return;
|
|
155
212
|
}
|
|
156
213
|
let payload;
|
|
157
214
|
try {
|
|
158
215
|
const parsed = JSON.parse(raw);
|
|
159
216
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
217
|
+
emitStructuredError$1("parse_error", "payload must be a JSON object");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
payload = parsed;
|
|
221
|
+
} catch {
|
|
222
|
+
emitStructuredError$1("parse_error", "invalid JSON");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const result = await ingestClaudeHookPayload(payload, opts);
|
|
227
|
+
console.log(JSON.stringify(result));
|
|
228
|
+
} catch (err) {
|
|
229
|
+
emitStructuredError$1("ingest_error", err instanceof Error ? err.message : String(err));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/commands/claude-hook-inject.ts
|
|
234
|
+
var DEFAULT_VIEWER_HOST = "127.0.0.1";
|
|
235
|
+
var DEFAULT_VIEWER_PORT = 38888;
|
|
236
|
+
var DEFAULT_MAX_CHARS = 16e3;
|
|
237
|
+
var DEFAULT_HTTP_MAX_TIME_S = 2;
|
|
238
|
+
function emitJson(value) {
|
|
239
|
+
console.log(JSON.stringify(value));
|
|
240
|
+
}
|
|
241
|
+
function envNotDisabled(value) {
|
|
242
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
243
|
+
return normalized !== "0" && normalized !== "false" && normalized !== "off";
|
|
244
|
+
}
|
|
245
|
+
function parsePositiveInt(value, fallback) {
|
|
246
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
247
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
248
|
+
return parsed;
|
|
249
|
+
}
|
|
250
|
+
function continueResult(additionalContext) {
|
|
251
|
+
if (!additionalContext) return { continue: true };
|
|
252
|
+
return {
|
|
253
|
+
continue: true,
|
|
254
|
+
hookSpecificOutput: { additionalContext }
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function truncateAdditionalContext(text, maxChars) {
|
|
258
|
+
const normalized = text.trim();
|
|
259
|
+
if (!normalized) return "";
|
|
260
|
+
if (!Number.isFinite(maxChars) || maxChars <= 0 || normalized.length <= maxChars) return normalized;
|
|
261
|
+
return normalized.slice(0, maxChars);
|
|
262
|
+
}
|
|
263
|
+
function extractInjectContext(payload) {
|
|
264
|
+
return String(payload.prompt ?? "").trim() || null;
|
|
265
|
+
}
|
|
266
|
+
function resolveInjectProject(payload) {
|
|
267
|
+
return resolveHookProject(typeof payload.cwd === "string" ? payload.cwd : null, payload.project);
|
|
268
|
+
}
|
|
269
|
+
async function buildLocalPack(context, project, dbPath) {
|
|
270
|
+
const store = new MemoryStore(dbPath);
|
|
271
|
+
try {
|
|
272
|
+
const limit = parsePositiveInt(process.env.CODEMEM_INJECT_LIMIT, 8);
|
|
273
|
+
const budget = parsePositiveInt(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
|
|
274
|
+
const filters = {};
|
|
275
|
+
if (project) filters.project = project;
|
|
276
|
+
const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
|
|
277
|
+
return String(pack.pack_text ?? "").trim();
|
|
278
|
+
} finally {
|
|
279
|
+
store.close();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S * 1e3) {
|
|
283
|
+
const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
|
|
284
|
+
const port = parsePositiveInt(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
|
|
285
|
+
const url = new URL(`http://${host}:${port}/api/pack`);
|
|
286
|
+
url.searchParams.set("context", context);
|
|
287
|
+
url.searchParams.set("limit", String(parsePositiveInt(process.env.CODEMEM_INJECT_LIMIT, 8)));
|
|
288
|
+
url.searchParams.set("token_budget", String(parsePositiveInt(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800)));
|
|
289
|
+
if (project) url.searchParams.set("project", project);
|
|
290
|
+
const controller = new AbortController();
|
|
291
|
+
const timeout = setTimeout(() => controller.abort(), maxTimeMs);
|
|
292
|
+
try {
|
|
293
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
294
|
+
if (!res.ok) return "";
|
|
295
|
+
const body = await res.json();
|
|
296
|
+
return String(body.pack_text ?? "").trim();
|
|
297
|
+
} catch {
|
|
298
|
+
return "";
|
|
299
|
+
} finally {
|
|
300
|
+
clearTimeout(timeout);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function buildClaudeHookInjection(payload, opts, deps = {}) {
|
|
304
|
+
if (!envNotDisabled(process.env.CODEMEM_INJECT_CONTEXT || "1")) return continueResult();
|
|
305
|
+
const context = extractInjectContext(payload);
|
|
306
|
+
if (!context) return continueResult();
|
|
307
|
+
const buildPack = deps.buildLocalPack ?? buildLocalPack;
|
|
308
|
+
const httpPack = deps.httpPack ?? tryHttpPack;
|
|
309
|
+
const resolveDb = deps.resolveDb ?? resolveDbPath;
|
|
310
|
+
const project = resolveInjectProject(payload);
|
|
311
|
+
const maxChars = parsePositiveInt(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS);
|
|
312
|
+
const httpMaxTimeMs = parsePositiveInt(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S) * 1e3;
|
|
313
|
+
let additionalContext = "";
|
|
314
|
+
try {
|
|
315
|
+
additionalContext = await buildPack(context, project, resolveDb(resolveDbOpt(opts)));
|
|
316
|
+
} catch {
|
|
317
|
+
additionalContext = "";
|
|
318
|
+
}
|
|
319
|
+
if (!additionalContext && envNotDisabled(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) additionalContext = await httpPack(context, project, httpMaxTimeMs);
|
|
320
|
+
return continueResult(truncateAdditionalContext(additionalContext, maxChars));
|
|
321
|
+
}
|
|
322
|
+
var claudeHookInjectCmd = new Command("claude-hook-inject").configureHelp(helpStyle).description("Return Claude hook additionalContext from local pack generation");
|
|
323
|
+
addDbOption(claudeHookInjectCmd);
|
|
324
|
+
var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
|
|
325
|
+
let raw = "";
|
|
326
|
+
for await (const chunk of process.stdin) raw += String(chunk);
|
|
327
|
+
const trimmed = raw.trim();
|
|
328
|
+
if (!trimmed) {
|
|
329
|
+
emitJson(continueResult());
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
let payload;
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(trimmed);
|
|
335
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
336
|
+
emitJson({
|
|
337
|
+
error: "parse_error",
|
|
338
|
+
message: "payload must be a JSON object"
|
|
339
|
+
});
|
|
160
340
|
process.exitCode = 1;
|
|
161
341
|
return;
|
|
162
342
|
}
|
|
163
343
|
payload = parsed;
|
|
164
344
|
} catch {
|
|
345
|
+
emitJson({
|
|
346
|
+
error: "parse_error",
|
|
347
|
+
message: "invalid JSON"
|
|
348
|
+
});
|
|
165
349
|
process.exitCode = 1;
|
|
166
350
|
return;
|
|
167
351
|
}
|
|
168
|
-
|
|
169
|
-
|
|
352
|
+
emitJson(await buildClaudeHookInjection(payload, opts));
|
|
353
|
+
});
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/commands/config.ts
|
|
356
|
+
function parseIntegerOption(value, flagName) {
|
|
357
|
+
if (value == null) return void 0;
|
|
358
|
+
const trimmed = value.trim();
|
|
359
|
+
if (!/^\d+$/.test(trimmed)) throw new Error(`Invalid ${flagName}: ${value}`);
|
|
360
|
+
return Number.parseInt(trimmed, 10);
|
|
361
|
+
}
|
|
362
|
+
function buildWorkspaceConfigPatch(opts) {
|
|
363
|
+
const patch = {};
|
|
364
|
+
if (opts.syncEnabled === true) patch.sync_enabled = true;
|
|
365
|
+
if (opts.syncEnabled === false) patch.sync_enabled = false;
|
|
366
|
+
if (opts.syncHost?.trim()) patch.sync_host = opts.syncHost.trim();
|
|
367
|
+
const syncPort = parseIntegerOption(opts.syncPort, "--sync-port");
|
|
368
|
+
if (syncPort != null) patch.sync_port = syncPort;
|
|
369
|
+
const syncInterval = parseIntegerOption(opts.syncIntervalS, "--sync-interval-s");
|
|
370
|
+
if (syncInterval != null) patch.sync_interval_s = syncInterval;
|
|
371
|
+
if (opts.coordinatorUrl?.trim()) patch.sync_coordinator_url = opts.coordinatorUrl.trim();
|
|
372
|
+
if (opts.coordinatorGroup?.trim()) patch.sync_coordinator_group = opts.coordinatorGroup.trim();
|
|
373
|
+
return patch;
|
|
374
|
+
}
|
|
375
|
+
function mergeWorkspaceConfig(existingConfig, patch) {
|
|
376
|
+
return {
|
|
377
|
+
...existingConfig,
|
|
378
|
+
...patch
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function runWorkspaceConfigCommand(opts) {
|
|
382
|
+
if (opts.enableSync && opts.disableSync) throw new Error("Use only one of --enable-sync or --disable-sync");
|
|
383
|
+
const workspaceId = opts.workspaceId;
|
|
384
|
+
if (!workspaceId) throw new Error("workspace-id is required");
|
|
385
|
+
const configPath = getWorkspaceCodememConfigPath(workspaceId);
|
|
386
|
+
const existingConfig = existsSync(configPath) ? readCodememConfigFileAtPath(configPath) : readCodememConfigFile();
|
|
387
|
+
const patch = buildWorkspaceConfigPatch({
|
|
388
|
+
workspaceId,
|
|
389
|
+
syncEnabled: opts.enableSync ? true : opts.disableSync ? false : void 0,
|
|
390
|
+
syncHost: opts.syncHost,
|
|
391
|
+
syncPort: opts.syncPort,
|
|
392
|
+
syncIntervalS: opts.syncIntervalS,
|
|
393
|
+
coordinatorUrl: opts.coordinatorUrl,
|
|
394
|
+
coordinatorGroup: opts.coordinatorGroup,
|
|
395
|
+
json: opts.json
|
|
396
|
+
});
|
|
397
|
+
if (Object.keys(patch).length === 0) throw new Error("Provide at least one config field to update");
|
|
398
|
+
const nextConfig = mergeWorkspaceConfig(existingConfig, patch);
|
|
399
|
+
return {
|
|
400
|
+
workspace_id: workspaceId,
|
|
401
|
+
config_path: writeCodememConfigFile(nextConfig, configPath),
|
|
402
|
+
updated_keys: Object.keys(patch).sort(),
|
|
403
|
+
config: nextConfig
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
var configCommand = new Command("config").configureHelp(helpStyle).description("Manage codemem configuration");
|
|
407
|
+
var workspaceCmd = new Command("workspace").configureHelp(helpStyle).description("Create or update workspace-scoped codemem config").argument("[workspace-id]", "workspace identifier").option("--enable-sync", "set sync_enabled=true").option("--disable-sync", "set sync_enabled=false").option("--sync-host <host>", "set sync_host").option("--sync-port <port>", "set sync_port").option("--sync-interval-s <seconds>", "set sync_interval_s").option("--coordinator-url <url>", "set sync_coordinator_url").option("--coordinator-group <group>", "set sync_coordinator_group");
|
|
408
|
+
workspaceCmd.addOption(new Option("--workspace-id <id>", "workspace identifier (use positional instead)").hideHelp());
|
|
409
|
+
addJsonOption(workspaceCmd);
|
|
410
|
+
workspaceCmd.action((workspaceIdArg, opts) => {
|
|
170
411
|
try {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
412
|
+
const workspaceId = workspaceIdArg || opts.workspaceId;
|
|
413
|
+
if (!workspaceId) {
|
|
414
|
+
console.error("Error: missing required argument 'workspace-id'");
|
|
415
|
+
process.exitCode = 2;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const result = runWorkspaceConfigCommand({
|
|
419
|
+
...opts,
|
|
420
|
+
workspaceId
|
|
176
421
|
});
|
|
177
|
-
|
|
178
|
-
|
|
422
|
+
if (opts.json) {
|
|
423
|
+
console.log(JSON.stringify(result, null, 2));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
console.log(`Updated workspace config: ${result.config_path}`);
|
|
427
|
+
console.log(`Updated keys: ${result.updated_keys.join(", ")}`);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
if (opts.json) console.log(JSON.stringify({
|
|
430
|
+
error: "config_error",
|
|
431
|
+
message: err instanceof Error ? err.message : String(err)
|
|
432
|
+
}));
|
|
433
|
+
else console.error(err instanceof Error ? err.message : String(err));
|
|
434
|
+
process.exitCode = 1;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
configCommand.addCommand(workspaceCmd);
|
|
438
|
+
function formatWhereHuman(result) {
|
|
439
|
+
const lines = [];
|
|
440
|
+
const allEntries = [result.resolved, ...result.fallbackChain];
|
|
441
|
+
const sourceOrder = {
|
|
442
|
+
"cli-flag": 0,
|
|
443
|
+
"env-codemem-config": 1,
|
|
444
|
+
"env-runtime-root": 2,
|
|
445
|
+
"env-workspace-id": 3,
|
|
446
|
+
"legacy-global": 4
|
|
447
|
+
};
|
|
448
|
+
allEntries.sort((a, b) => (sourceOrder[a.source] ?? 99) - (sourceOrder[b.source] ?? 99));
|
|
449
|
+
for (const entry of allEntries) {
|
|
450
|
+
const marker = entry === result.resolved ? ">>>" : " ";
|
|
451
|
+
const existsLabel = entry.exists ? "exists" : "missing";
|
|
452
|
+
lines.push(`${marker} [${entry.source}] ${entry.path}`);
|
|
453
|
+
lines.push(` ${entry.reason} (${existsLabel})`);
|
|
454
|
+
}
|
|
455
|
+
return lines.join("\n");
|
|
456
|
+
}
|
|
457
|
+
var whereCmd = new Command("where").configureHelp(helpStyle).description("show resolved config file path");
|
|
458
|
+
addConfigOption(whereCmd);
|
|
459
|
+
addJsonOption(whereCmd);
|
|
460
|
+
whereCmd.action((opts) => {
|
|
461
|
+
try {
|
|
462
|
+
const result = resolveCodememConfigPath(opts.config, "read");
|
|
463
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
464
|
+
else console.log(formatWhereHuman(result));
|
|
465
|
+
} catch (err) {
|
|
466
|
+
if (opts.json) console.log(JSON.stringify({
|
|
467
|
+
error: "config_error",
|
|
468
|
+
message: err instanceof Error ? err.message : String(err)
|
|
469
|
+
}));
|
|
470
|
+
else console.error(err instanceof Error ? err.message : String(err));
|
|
179
471
|
process.exitCode = 1;
|
|
180
472
|
}
|
|
181
473
|
});
|
|
474
|
+
configCommand.addCommand(whereCmd);
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region src/commands/coordinator.ts
|
|
477
|
+
/**
|
|
478
|
+
* Coordinator CLI commands — manage coordinator invites, join requests, and relay server.
|
|
479
|
+
*
|
|
480
|
+
* Extracted from sync.ts to give coordinator admin its own top-level group
|
|
481
|
+
* per cli-design-conventions.md (operator/admin surfaces belong in their own group).
|
|
482
|
+
*
|
|
483
|
+
* buildCoordinatorCommand() is a factory that creates a fresh command tree.
|
|
484
|
+
* This allows both the canonical top-level `coordinator` and the deprecated
|
|
485
|
+
* `sync coordinator` alias to have independent Commander instances (Commander
|
|
486
|
+
* re-parents commands on addCommand, so sharing instances between two parents
|
|
487
|
+
* is not possible).
|
|
488
|
+
*/
|
|
489
|
+
function readCoordinatorPublicKey(opts) {
|
|
490
|
+
const inline = String(opts.publicKey ?? "").trim();
|
|
491
|
+
const filePath = String(opts.publicKeyFile ?? "").trim();
|
|
492
|
+
if (inline && filePath) throw new Error("Use only one of --public-key or --public-key-file");
|
|
493
|
+
if (filePath) {
|
|
494
|
+
const text = readFileSync(filePath, "utf8").trim();
|
|
495
|
+
if (!text) throw new Error(`Public key file is empty: ${filePath}`);
|
|
496
|
+
return text;
|
|
497
|
+
}
|
|
498
|
+
if (!inline) throw new Error("Public key required via --public-key or --public-key-file");
|
|
499
|
+
return inline;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Build a fresh coordinator command tree. Each call returns independent
|
|
503
|
+
* Commander instances so the tree can be mounted under multiple parents.
|
|
504
|
+
*/
|
|
505
|
+
function buildCoordinatorCommand() {
|
|
506
|
+
const cmd = new Command("coordinator").configureHelp(helpStyle).description("Manage coordinator invites, join requests, and relay server");
|
|
507
|
+
const groupCreateCmd = new Command("group-create").configureHelp(helpStyle).description("Create a coordinator group in the local store").argument("<group>", "group id").option("--name <name>", "display name override");
|
|
508
|
+
addDbOption(groupCreateCmd);
|
|
509
|
+
addJsonOption(groupCreateCmd);
|
|
510
|
+
groupCreateCmd.action(async (groupId, opts) => {
|
|
511
|
+
try {
|
|
512
|
+
const group = await coordinatorCreateGroupAction({
|
|
513
|
+
groupId,
|
|
514
|
+
displayName: opts.name?.trim() || null,
|
|
515
|
+
dbPath: resolveDbOpt(opts) ?? null
|
|
516
|
+
});
|
|
517
|
+
if (opts.json) {
|
|
518
|
+
console.log(JSON.stringify(group, null, 2));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
p.intro("codemem coordinator group-create");
|
|
522
|
+
p.log.success(`Group ready: ${groupId.trim()}`);
|
|
523
|
+
p.outro(String(group.display_name ?? group.group_id ?? groupId.trim()));
|
|
524
|
+
} catch (err) {
|
|
525
|
+
if (opts.json) {
|
|
526
|
+
emitJsonError("group_create_failed", err instanceof Error ? err.message : String(err));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
530
|
+
process.exitCode = 1;
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
cmd.addCommand(groupCreateCmd);
|
|
534
|
+
const listGroupsCmd = new Command("list-groups").configureHelp(helpStyle).description("List coordinator groups from the local store");
|
|
535
|
+
addDbOption(listGroupsCmd);
|
|
536
|
+
addJsonOption(listGroupsCmd);
|
|
537
|
+
listGroupsCmd.action(async (opts) => {
|
|
538
|
+
try {
|
|
539
|
+
const groups = await coordinatorListGroupsAction({ dbPath: resolveDbOpt(opts) ?? null });
|
|
540
|
+
if (opts.json) {
|
|
541
|
+
console.log(JSON.stringify(groups, null, 2));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
p.intro("codemem coordinator list-groups");
|
|
545
|
+
if (groups.length === 0) {
|
|
546
|
+
p.outro("No coordinator groups found");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
for (const group of groups) p.log.message(`- ${String(group.group_id ?? "")}${group.display_name ? ` (${String(group.display_name)})` : ""}`);
|
|
550
|
+
p.outro(`${groups.length} group(s)`);
|
|
551
|
+
} catch (err) {
|
|
552
|
+
if (opts.json) {
|
|
553
|
+
emitJsonError("list_groups_failed", err instanceof Error ? err.message : String(err));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
557
|
+
process.exitCode = 1;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
cmd.addCommand(listGroupsCmd);
|
|
561
|
+
const enrollDeviceCmd = new Command("enroll-device").configureHelp(helpStyle).description("Enroll a device in a local coordinator group").argument("<group>", "group id").argument("<device-id>", "device id").option("--fingerprint <fingerprint>", "device fingerprint").option("--public-key <key>", "device public key").option("--public-key-file <path>", "path to device public key").option("--name <name>", "display name");
|
|
562
|
+
addDbOption(enrollDeviceCmd);
|
|
563
|
+
addJsonOption(enrollDeviceCmd);
|
|
564
|
+
enrollDeviceCmd.action(async (groupId, deviceId, opts) => {
|
|
565
|
+
try {
|
|
566
|
+
const publicKey = readCoordinatorPublicKey(opts);
|
|
567
|
+
const fingerprint = String(opts.fingerprint ?? "").trim();
|
|
568
|
+
if (!fingerprint) {
|
|
569
|
+
if (opts.json) {
|
|
570
|
+
emitJsonError("usage_error", "Fingerprint required via --fingerprint", 2);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
p.log.error("Fingerprint required via --fingerprint");
|
|
574
|
+
process.exitCode = 1;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (fingerprintPublicKey(publicKey) !== fingerprint) {
|
|
578
|
+
if (opts.json) {
|
|
579
|
+
emitJsonError("fingerprint_mismatch", "Fingerprint does not match the provided public key");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
p.log.error("Fingerprint does not match the provided public key");
|
|
583
|
+
process.exitCode = 1;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const enrollment = await coordinatorEnrollDeviceAction({
|
|
587
|
+
groupId,
|
|
588
|
+
deviceId,
|
|
589
|
+
fingerprint,
|
|
590
|
+
publicKey,
|
|
591
|
+
displayName: opts.name?.trim() || null,
|
|
592
|
+
dbPath: resolveDbOpt(opts) ?? null
|
|
593
|
+
});
|
|
594
|
+
if (opts.json) {
|
|
595
|
+
console.log(JSON.stringify(enrollment, null, 2));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
p.intro("codemem coordinator enroll-device");
|
|
599
|
+
p.log.success(`Enrolled ${deviceId.trim()} in ${groupId.trim()}`);
|
|
600
|
+
p.outro(String(enrollment.display_name ?? enrollment.device_id ?? deviceId.trim()));
|
|
601
|
+
} catch (err) {
|
|
602
|
+
if (opts.json) {
|
|
603
|
+
emitJsonError("enroll_failed", err instanceof Error ? err.message : String(err));
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
607
|
+
process.exitCode = 1;
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
cmd.addCommand(enrollDeviceCmd);
|
|
611
|
+
const listDevicesCmd = new Command("list-devices").configureHelp(helpStyle).description("List enrolled devices in a local coordinator group").argument("<group>", "group id").option("--include-disabled", "include disabled devices");
|
|
612
|
+
addDbOption(listDevicesCmd);
|
|
613
|
+
addJsonOption(listDevicesCmd);
|
|
614
|
+
listDevicesCmd.action(async (groupId, opts) => {
|
|
615
|
+
try {
|
|
616
|
+
const rows = await coordinatorListDevicesAction({
|
|
617
|
+
groupId,
|
|
618
|
+
includeDisabled: opts.includeDisabled === true,
|
|
619
|
+
dbPath: resolveDbOpt(opts) ?? null
|
|
620
|
+
});
|
|
621
|
+
if (opts.json) {
|
|
622
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
p.intro("codemem coordinator list-devices");
|
|
626
|
+
if (rows.length === 0) {
|
|
627
|
+
p.outro(`No enrolled devices for ${groupId.trim()}`);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
for (const row of rows) {
|
|
631
|
+
const label = String(row.display_name ?? row.device_id ?? "").trim() || String(row.device_id ?? "");
|
|
632
|
+
const enabled = Number(row.enabled ?? 1) === 1 ? "enabled" : "disabled";
|
|
633
|
+
p.log.message(`- ${label} (${String(row.device_id ?? "")}) ${enabled}`);
|
|
634
|
+
}
|
|
635
|
+
p.outro(`${rows.length} device(s)`);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
if (opts.json) {
|
|
638
|
+
emitJsonError("list_devices_failed", err instanceof Error ? err.message : String(err));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
642
|
+
process.exitCode = 1;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
cmd.addCommand(listDevicesCmd);
|
|
646
|
+
const renameDeviceCmd = new Command("rename-device").configureHelp(helpStyle).description("Rename an enrolled device in the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id").requiredOption("--name <name>", "display name");
|
|
647
|
+
addDbOption(renameDeviceCmd);
|
|
648
|
+
addJsonOption(renameDeviceCmd);
|
|
649
|
+
renameDeviceCmd.action(async (groupId, deviceId, opts) => {
|
|
650
|
+
try {
|
|
651
|
+
const result = await coordinatorRenameDeviceAction({
|
|
652
|
+
groupId,
|
|
653
|
+
deviceId,
|
|
654
|
+
displayName: opts.name.trim(),
|
|
655
|
+
dbPath: resolveDbOpt(opts) ?? null
|
|
656
|
+
});
|
|
657
|
+
if (!result) {
|
|
658
|
+
if (opts.json) {
|
|
659
|
+
emitJsonError("device_not_found", `Device not found: ${deviceId.trim()}`);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
p.log.error(`Device not found: ${deviceId.trim()}`);
|
|
663
|
+
process.exitCode = 1;
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (opts.json) {
|
|
667
|
+
console.log(JSON.stringify(result, null, 2));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
p.intro("codemem coordinator rename-device");
|
|
671
|
+
p.log.success(`Renamed ${deviceId.trim()} in ${groupId.trim()}`);
|
|
672
|
+
p.outro(String(result.display_name ?? result.device_id ?? deviceId.trim()));
|
|
673
|
+
} catch (err) {
|
|
674
|
+
if (opts.json) {
|
|
675
|
+
emitJsonError("rename_failed", err instanceof Error ? err.message : String(err));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
679
|
+
process.exitCode = 1;
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
cmd.addCommand(renameDeviceCmd);
|
|
683
|
+
const disableDeviceCmd = new Command("disable-device").configureHelp(helpStyle).description("Disable an enrolled device in the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id");
|
|
684
|
+
addDbOption(disableDeviceCmd);
|
|
685
|
+
addJsonOption(disableDeviceCmd);
|
|
686
|
+
disableDeviceCmd.action(async (groupId, deviceId, opts) => {
|
|
687
|
+
try {
|
|
688
|
+
if (!await coordinatorDisableDeviceAction({
|
|
689
|
+
groupId,
|
|
690
|
+
deviceId,
|
|
691
|
+
dbPath: resolveDbOpt(opts) ?? null
|
|
692
|
+
})) {
|
|
693
|
+
if (opts.json) {
|
|
694
|
+
emitJsonError("device_not_found", `Device not found: ${deviceId.trim()}`);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
p.log.error(`Device not found: ${deviceId.trim()}`);
|
|
698
|
+
process.exitCode = 1;
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (opts.json) {
|
|
702
|
+
console.log(JSON.stringify({
|
|
703
|
+
ok: true,
|
|
704
|
+
group_id: groupId.trim(),
|
|
705
|
+
device_id: deviceId.trim()
|
|
706
|
+
}, null, 2));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
p.intro("codemem coordinator disable-device");
|
|
710
|
+
p.log.success(`Disabled ${deviceId.trim()} in ${groupId.trim()}`);
|
|
711
|
+
p.outro("disabled");
|
|
712
|
+
} catch (err) {
|
|
713
|
+
if (opts.json) {
|
|
714
|
+
emitJsonError("disable_failed", err instanceof Error ? err.message : String(err));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
718
|
+
process.exitCode = 1;
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
cmd.addCommand(disableDeviceCmd);
|
|
722
|
+
const removeDeviceCmd = new Command("remove-device").configureHelp(helpStyle).description("Remove an enrolled device from the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id");
|
|
723
|
+
addDbOption(removeDeviceCmd);
|
|
724
|
+
addJsonOption(removeDeviceCmd);
|
|
725
|
+
removeDeviceCmd.action(async (groupId, deviceId, opts) => {
|
|
726
|
+
try {
|
|
727
|
+
if (!await coordinatorRemoveDeviceAction({
|
|
728
|
+
groupId,
|
|
729
|
+
deviceId,
|
|
730
|
+
dbPath: resolveDbOpt(opts) ?? null
|
|
731
|
+
})) {
|
|
732
|
+
if (opts.json) {
|
|
733
|
+
emitJsonError("device_not_found", `Device not found: ${deviceId.trim()}`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
p.log.error(`Device not found: ${deviceId.trim()}`);
|
|
737
|
+
process.exitCode = 1;
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (opts.json) {
|
|
741
|
+
console.log(JSON.stringify({
|
|
742
|
+
ok: true,
|
|
743
|
+
group_id: groupId.trim(),
|
|
744
|
+
device_id: deviceId.trim()
|
|
745
|
+
}, null, 2));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
p.intro("codemem coordinator remove-device");
|
|
749
|
+
p.log.success(`Removed ${deviceId.trim()} from ${groupId.trim()}`);
|
|
750
|
+
p.outro("removed");
|
|
751
|
+
} catch (err) {
|
|
752
|
+
if (opts.json) {
|
|
753
|
+
emitJsonError("remove_failed", err instanceof Error ? err.message : String(err));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
757
|
+
process.exitCode = 1;
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
cmd.addCommand(removeDeviceCmd);
|
|
761
|
+
const coordServeCmd = new Command("serve").configureHelp(helpStyle).description("Run the coordinator relay HTTP server").option("--coordinator-host <host>", "bind host").option("--coordinator-port <port>", "bind port");
|
|
762
|
+
coordServeCmd.addOption(new Option("-d, --db-path <path>", "coordinator database path"));
|
|
763
|
+
coordServeCmd.addOption(new Option("--db <path>", "coordinator database path").hideHelp());
|
|
764
|
+
coordServeCmd.addOption(new Option("--host <host>", "bind host").hideHelp());
|
|
765
|
+
coordServeCmd.addOption(new Option("--port <port>", "bind port").hideHelp());
|
|
766
|
+
coordServeCmd.action(async (opts) => {
|
|
767
|
+
const host = String(opts.coordinatorHost ?? opts.host ?? "127.0.0.1").trim() || "127.0.0.1";
|
|
768
|
+
const port = Number.parseInt(String(opts.coordinatorPort ?? opts.port ?? "7347"), 10);
|
|
769
|
+
const dbPath = resolveDbOpt(opts) ?? DEFAULT_COORDINATOR_DB_PATH;
|
|
770
|
+
const app = createBetterSqliteCoordinatorApp({ dbPath });
|
|
771
|
+
p.intro("codemem coordinator serve");
|
|
772
|
+
p.log.success(`Coordinator listening at http://${host}:${port}`);
|
|
773
|
+
p.log.info(`DB: ${dbPath}`);
|
|
774
|
+
serve({
|
|
775
|
+
fetch: app.fetch,
|
|
776
|
+
hostname: host,
|
|
777
|
+
port
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
cmd.addCommand(coordServeCmd);
|
|
781
|
+
const listBootstrapGrantsCmd = new Command("list-bootstrap-grants").configureHelp(helpStyle).description("List bootstrap grants for a coordinator group").argument("<group>", "group id").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override");
|
|
782
|
+
addDbOption(listBootstrapGrantsCmd);
|
|
783
|
+
addJsonOption(listBootstrapGrantsCmd);
|
|
784
|
+
listBootstrapGrantsCmd.action(async (groupId, opts) => {
|
|
785
|
+
try {
|
|
786
|
+
const rows = await coordinatorListBootstrapGrantsAction({
|
|
787
|
+
groupId,
|
|
788
|
+
dbPath: resolveDbOpt(opts) ?? null,
|
|
789
|
+
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
790
|
+
adminSecret: opts.adminSecret?.trim() || null
|
|
791
|
+
});
|
|
792
|
+
if (opts.json) {
|
|
793
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
p.intro("codemem coordinator list-bootstrap-grants");
|
|
797
|
+
if (rows.length === 0) {
|
|
798
|
+
p.outro(`No bootstrap grants for ${groupId.trim()}`);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
for (const row of rows) p.log.message(`- ${row.grant_id} seed=${row.seed_device_id} worker=${row.worker_device_id} expires=${row.expires_at} revoked=${row.revoked_at ?? "no"}`);
|
|
802
|
+
p.outro(`${rows.length} bootstrap grant(s)`);
|
|
803
|
+
} catch (err) {
|
|
804
|
+
if (opts.json) {
|
|
805
|
+
emitJsonError("list_bootstrap_grants_failed", err instanceof Error ? err.message : String(err));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
809
|
+
process.exitCode = 1;
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
cmd.addCommand(listBootstrapGrantsCmd);
|
|
813
|
+
const revokeBootstrapGrantCmd = new Command("revoke-bootstrap-grant").configureHelp(helpStyle).description("Revoke a bootstrap grant").argument("<grant-id>", "bootstrap grant id").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override");
|
|
814
|
+
addDbOption(revokeBootstrapGrantCmd);
|
|
815
|
+
addJsonOption(revokeBootstrapGrantCmd);
|
|
816
|
+
revokeBootstrapGrantCmd.action(async (grantId, opts) => {
|
|
817
|
+
try {
|
|
818
|
+
if (!await coordinatorRevokeBootstrapGrantAction({
|
|
819
|
+
grantId,
|
|
820
|
+
dbPath: resolveDbOpt(opts) ?? null,
|
|
821
|
+
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
822
|
+
adminSecret: opts.adminSecret?.trim() || null
|
|
823
|
+
})) {
|
|
824
|
+
if (opts.json) {
|
|
825
|
+
emitJsonError("grant_not_found", `Bootstrap grant not found: ${grantId.trim()}`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
p.log.error(`Bootstrap grant not found: ${grantId.trim()}`);
|
|
829
|
+
process.exitCode = 1;
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (opts.json) {
|
|
833
|
+
console.log(JSON.stringify({
|
|
834
|
+
ok: true,
|
|
835
|
+
grant_id: grantId.trim()
|
|
836
|
+
}, null, 2));
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
p.intro("codemem coordinator revoke-bootstrap-grant");
|
|
840
|
+
p.log.success(`Revoked ${grantId.trim()}`);
|
|
841
|
+
p.outro("revoked");
|
|
842
|
+
} catch (err) {
|
|
843
|
+
if (opts.json) {
|
|
844
|
+
emitJsonError("revoke_failed", err instanceof Error ? err.message : String(err));
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
848
|
+
process.exitCode = 1;
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
cmd.addCommand(revokeBootstrapGrantCmd);
|
|
852
|
+
const createInviteCmd = new Command("create-invite").configureHelp(helpStyle).description("Create a coordinator team invite").argument("[group]", "group id").option("--group <group>", "group id").option("--coordinator-url <url>", "coordinator URL override").option("--policy <policy>", "invite policy", "auto_admit").option("--ttl-hours <hours>", "invite TTL hours", "24").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override");
|
|
853
|
+
addDbOption(createInviteCmd);
|
|
854
|
+
addJsonOption(createInviteCmd);
|
|
855
|
+
createInviteCmd.action(async (groupArg, opts) => {
|
|
856
|
+
try {
|
|
857
|
+
const ttlHours = Number.parseInt(String(opts.ttlHours ?? "24"), 10);
|
|
858
|
+
const groupId = String(opts.group ?? "").trim() || String(groupArg ?? "").trim();
|
|
859
|
+
const result = await coordinatorCreateInviteAction({
|
|
860
|
+
groupId,
|
|
861
|
+
coordinatorUrl: opts.coordinatorUrl?.trim() || null,
|
|
862
|
+
policy: String(opts.policy ?? "auto_admit").trim(),
|
|
863
|
+
ttlHours,
|
|
864
|
+
createdBy: null,
|
|
865
|
+
dbPath: resolveDbOpt(opts) ?? null,
|
|
866
|
+
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
867
|
+
adminSecret: opts.adminSecret?.trim() || null
|
|
868
|
+
});
|
|
869
|
+
if (opts.json) {
|
|
870
|
+
console.log(JSON.stringify(result, null, 2));
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
p.intro("codemem coordinator create-invite");
|
|
874
|
+
p.log.success(`Invite created for ${groupId}`);
|
|
875
|
+
if (typeof result.link === "string") p.log.message(`- link: ${result.link}`);
|
|
876
|
+
if (typeof result.encoded === "string") p.log.message(`- invite: ${result.encoded}`);
|
|
877
|
+
for (const warning of Array.isArray(result.warnings) ? result.warnings : []) p.log.warn(String(warning));
|
|
878
|
+
p.outro("Invite ready");
|
|
879
|
+
} catch (err) {
|
|
880
|
+
if (opts.json) {
|
|
881
|
+
emitJsonError("create_invite_failed", err instanceof Error ? err.message : String(err));
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
885
|
+
process.exitCode = 1;
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
cmd.addCommand(createInviteCmd);
|
|
889
|
+
const importInviteCmd = new Command("import-invite").configureHelp(helpStyle).description("Import a coordinator invite").argument("<invite>", "invite value or link").option("--keys-dir <path>", "keys directory");
|
|
890
|
+
addDbOption(importInviteCmd);
|
|
891
|
+
addConfigOption(importInviteCmd);
|
|
892
|
+
addJsonOption(importInviteCmd);
|
|
893
|
+
importInviteCmd.action(async (invite, opts) => {
|
|
894
|
+
try {
|
|
895
|
+
const result = await coordinatorImportInviteAction({
|
|
896
|
+
inviteValue: invite,
|
|
897
|
+
dbPath: resolveDbOpt(opts) ?? null,
|
|
898
|
+
keysDir: opts.keysDir ?? null,
|
|
899
|
+
configPath: opts.config ?? null
|
|
900
|
+
});
|
|
901
|
+
if (opts.json) {
|
|
902
|
+
console.log(JSON.stringify(result, null, 2));
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
p.intro("codemem coordinator import-invite");
|
|
906
|
+
p.log.success(`Invite imported for ${result.group_id}`);
|
|
907
|
+
p.log.message(`- coordinator: ${result.coordinator_url}`);
|
|
908
|
+
p.log.message(`- status: ${result.status}`);
|
|
909
|
+
p.outro("Coordinator config updated");
|
|
910
|
+
} catch (err) {
|
|
911
|
+
if (opts.json) {
|
|
912
|
+
emitJsonError("import_invite_failed", err instanceof Error ? err.message : String(err));
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
916
|
+
process.exitCode = 1;
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
cmd.addCommand(importInviteCmd);
|
|
920
|
+
const listJoinRequestsCmd = new Command("list-join-requests").configureHelp(helpStyle).description("List pending coordinator join requests").argument("[group]", "group id").option("--group <group>", "group id").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override");
|
|
921
|
+
addDbOption(listJoinRequestsCmd);
|
|
922
|
+
addJsonOption(listJoinRequestsCmd);
|
|
923
|
+
listJoinRequestsCmd.action(async (groupArg, opts) => {
|
|
924
|
+
try {
|
|
925
|
+
const groupId = String(opts.group ?? "").trim() || String(groupArg ?? "").trim();
|
|
926
|
+
const rows = await coordinatorListJoinRequestsAction({
|
|
927
|
+
groupId,
|
|
928
|
+
dbPath: resolveDbOpt(opts) ?? null,
|
|
929
|
+
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
930
|
+
adminSecret: opts.adminSecret?.trim() || null
|
|
931
|
+
});
|
|
932
|
+
if (opts.json) {
|
|
933
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
p.intro("codemem coordinator list-join-requests");
|
|
937
|
+
if (rows.length === 0) {
|
|
938
|
+
p.outro(`No pending join requests for ${groupId}`);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
for (const row of rows) {
|
|
942
|
+
const displayName = row.display_name || row.device_id;
|
|
943
|
+
p.log.message(`- ${displayName} (${row.device_id}) request_id=${row.request_id}`);
|
|
944
|
+
}
|
|
945
|
+
p.outro(`${rows.length} pending join request(s)`);
|
|
946
|
+
} catch (err) {
|
|
947
|
+
if (opts.json) {
|
|
948
|
+
emitJsonError("list_join_requests_failed", err instanceof Error ? err.message : String(err));
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
952
|
+
process.exitCode = 1;
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
cmd.addCommand(listJoinRequestsCmd);
|
|
956
|
+
function addReviewJoinRequestCommand(name, approve) {
|
|
957
|
+
const reviewCmd = new Command(name).configureHelp(helpStyle).description(`${approve ? "Approve" : "Deny"} a coordinator join request`).argument("<request-id>", "join request id").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override");
|
|
958
|
+
addDbOption(reviewCmd);
|
|
959
|
+
addJsonOption(reviewCmd);
|
|
960
|
+
reviewCmd.action(async (requestId, opts) => {
|
|
961
|
+
try {
|
|
962
|
+
const request = await coordinatorReviewJoinRequestAction({
|
|
963
|
+
requestId: requestId.trim(),
|
|
964
|
+
approve,
|
|
965
|
+
reviewedBy: null,
|
|
966
|
+
dbPath: resolveDbOpt(opts) ?? null,
|
|
967
|
+
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
968
|
+
adminSecret: opts.adminSecret?.trim() || null
|
|
969
|
+
});
|
|
970
|
+
if (!request) {
|
|
971
|
+
if (opts.json) {
|
|
972
|
+
emitJsonError("join_request_not_found", `Join request not found: ${requestId.trim()}`);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
p.log.error(`Join request not found: ${requestId.trim()}`);
|
|
976
|
+
process.exitCode = 1;
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (opts.json) {
|
|
980
|
+
console.log(JSON.stringify(request, null, 2));
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
p.intro(`codemem coordinator ${name}`);
|
|
984
|
+
p.log.success(`${approve ? "Approved" : "Denied"} join request ${requestId.trim()}`);
|
|
985
|
+
p.outro(String(request.status ?? "updated"));
|
|
986
|
+
} catch (err) {
|
|
987
|
+
if (opts.json) {
|
|
988
|
+
emitJsonError("review_failed", err instanceof Error ? err.message : String(err));
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
992
|
+
process.exitCode = 1;
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
cmd.addCommand(reviewCmd);
|
|
996
|
+
}
|
|
997
|
+
addReviewJoinRequestCommand("approve-join-request", true);
|
|
998
|
+
addReviewJoinRequestCommand("deny-join-request", false);
|
|
999
|
+
return cmd;
|
|
1000
|
+
}
|
|
1001
|
+
/** Canonical top-level coordinator command for registration in index.ts. */
|
|
1002
|
+
var coordinatorCommand = buildCoordinatorCommand();
|
|
182
1003
|
//#endregion
|
|
183
1004
|
//#region src/commands/db.ts
|
|
184
1005
|
function formatBytes(bytes) {
|
|
@@ -210,18 +1031,28 @@ function estimateReplicationOpsBytes(db) {
|
|
|
210
1031
|
}
|
|
211
1032
|
}
|
|
212
1033
|
var dbCommand = new Command("db").configureHelp(helpStyle).description("Database maintenance");
|
|
213
|
-
|
|
214
|
-
|
|
1034
|
+
var initCmd = new Command("init").configureHelp(helpStyle).description("Create or verify the SQLite database and schema");
|
|
1035
|
+
addDbOption(initCmd);
|
|
1036
|
+
initCmd.action((opts) => {
|
|
1037
|
+
const result = initDatabase(resolveDbOpt(opts));
|
|
215
1038
|
p.intro("codemem db init");
|
|
216
1039
|
p.log.success(`Database ready: ${result.path}`);
|
|
217
1040
|
p.outro(`Size: ${result.sizeBytes.toLocaleString()} bytes`);
|
|
218
|
-
})
|
|
219
|
-
|
|
1041
|
+
});
|
|
1042
|
+
dbCommand.addCommand(initCmd);
|
|
1043
|
+
var vacuumCmd = new Command("vacuum").configureHelp(helpStyle).description("Run VACUUM on the SQLite database");
|
|
1044
|
+
addDbOption(vacuumCmd);
|
|
1045
|
+
vacuumCmd.action((opts) => {
|
|
1046
|
+
const result = vacuumDatabase(resolveDbOpt(opts));
|
|
220
1047
|
p.intro("codemem db vacuum");
|
|
221
1048
|
p.log.success(`Vacuumed: ${result.path}`);
|
|
222
1049
|
p.outro(`Size: ${result.sizeBytes.toLocaleString()} bytes`);
|
|
223
|
-
})
|
|
224
|
-
|
|
1050
|
+
});
|
|
1051
|
+
dbCommand.addCommand(vacuumCmd);
|
|
1052
|
+
var pruneReplCmd = new Command("prune-replication-ops").configureHelp(helpStyle).description("Prune replication op history with approximate oldest-first retention, dry-run, and progress reporting").option("--dry-run", "show current size/targets without deleting").option("--max-age-days <days>", "retention age threshold in days", "30").option("--max-size-mb <mb>", "target replication log budget in MB", "512").option("--batch-ops <n>", "max ops deleted per batch", "5000").option("--batch-runtime-ms <ms>", "max runtime per batch in ms", "2000").option("--vacuum", "run VACUUM explicitly after prune completes");
|
|
1053
|
+
addDbOption(pruneReplCmd);
|
|
1054
|
+
pruneReplCmd.action((opts) => {
|
|
1055
|
+
const dbPath = resolveDbPath(resolveDbOpt(opts));
|
|
225
1056
|
const db = connect(dbPath);
|
|
226
1057
|
let dbOpen = true;
|
|
227
1058
|
try {
|
|
@@ -267,8 +1098,13 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
|
|
|
267
1098
|
} finally {
|
|
268
1099
|
if (dbOpen) db.close();
|
|
269
1100
|
}
|
|
270
|
-
})
|
|
271
|
-
|
|
1101
|
+
});
|
|
1102
|
+
dbCommand.addCommand(pruneReplCmd);
|
|
1103
|
+
var rawEventsStatusCmd = new Command("raw-events-status").configureHelp(helpStyle).description("Show pending raw-event backlog by source stream").option("-n, --limit <n>", "max rows to show", "25");
|
|
1104
|
+
addDbOption(rawEventsStatusCmd);
|
|
1105
|
+
addJsonOption(rawEventsStatusCmd);
|
|
1106
|
+
rawEventsStatusCmd.action((opts) => {
|
|
1107
|
+
const result = getRawEventStatus(resolveDbOpt(opts), Number.parseInt(opts.limit, 10) || 25);
|
|
272
1108
|
if (opts.json) {
|
|
273
1109
|
console.log(JSON.stringify(result, null, 2));
|
|
274
1110
|
return;
|
|
@@ -281,12 +1117,21 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
|
|
|
281
1117
|
}
|
|
282
1118
|
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 ?? ""}`);
|
|
283
1119
|
p.outro("done");
|
|
284
|
-
})
|
|
285
|
-
|
|
1120
|
+
});
|
|
1121
|
+
dbCommand.addCommand(rawEventsStatusCmd);
|
|
1122
|
+
var rawEventsRetryCmd = new Command("raw-events-retry").configureHelp(helpStyle).description("Requeue failed raw-event flush batches").option("-n, --limit <n>", "max failed batches to requeue", "25");
|
|
1123
|
+
addDbOption(rawEventsRetryCmd);
|
|
1124
|
+
rawEventsRetryCmd.action((opts) => {
|
|
1125
|
+
const result = retryRawEventFailures(resolveDbOpt(opts), Number.parseInt(opts.limit, 10) || 25);
|
|
286
1126
|
p.intro("codemem db raw-events-retry");
|
|
287
1127
|
p.outro(`Requeued ${result.retried.toLocaleString()} failed batch(es)`);
|
|
288
|
-
})
|
|
289
|
-
|
|
1128
|
+
});
|
|
1129
|
+
dbCommand.addCommand(rawEventsRetryCmd);
|
|
1130
|
+
var rawEventsGateCmd = new Command("raw-events-gate").configureHelp(helpStyle).description("Validate raw-event reliability thresholds (non-zero exit on failure)").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");
|
|
1131
|
+
addDbOption(rawEventsGateCmd);
|
|
1132
|
+
addJsonOption(rawEventsGateCmd);
|
|
1133
|
+
rawEventsGateCmd.action((opts) => {
|
|
1134
|
+
const result = rawEventsGate(resolveDbOpt(opts), {
|
|
290
1135
|
minFlushSuccessRate: Number.parseFloat(opts.minFlushSuccessRate),
|
|
291
1136
|
maxDroppedEventRate: Number.parseFloat(opts.maxDroppedEventRate),
|
|
292
1137
|
minSessionBoundaryAccuracy: Number.parseFloat(opts.minSessionBoundaryAccuracy),
|
|
@@ -310,8 +1155,12 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
|
|
|
310
1155
|
p.outro("reliability gate FAILED");
|
|
311
1156
|
process.exitCode = 1;
|
|
312
1157
|
}
|
|
313
|
-
})
|
|
314
|
-
|
|
1158
|
+
});
|
|
1159
|
+
dbCommand.addCommand(rawEventsGateCmd);
|
|
1160
|
+
var renameProjectCmd = 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("--apply", "apply changes (default is dry-run)");
|
|
1161
|
+
addDbOption(renameProjectCmd);
|
|
1162
|
+
renameProjectCmd.action((oldName, newName, opts) => {
|
|
1163
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
315
1164
|
try {
|
|
316
1165
|
const dryRun = !opts.apply;
|
|
317
1166
|
const suffixPattern = `%/${oldName.replace(/%/g, "\\%").replace(/_/g, "\\_")}`;
|
|
@@ -338,8 +1187,12 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
|
|
|
338
1187
|
} finally {
|
|
339
1188
|
store.close();
|
|
340
1189
|
}
|
|
341
|
-
})
|
|
342
|
-
|
|
1190
|
+
});
|
|
1191
|
+
dbCommand.addCommand(renameProjectCmd);
|
|
1192
|
+
var normalizeProjectsCmd = new Command("normalize-projects").configureHelp(helpStyle).description("Normalize path-like project identifiers to their basename").option("--apply", "apply changes (default is dry-run)");
|
|
1193
|
+
addDbOption(normalizeProjectsCmd);
|
|
1194
|
+
normalizeProjectsCmd.action((opts) => {
|
|
1195
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
343
1196
|
try {
|
|
344
1197
|
const dryRun = !opts.apply;
|
|
345
1198
|
const tables = ["sessions", "raw_event_sessions"];
|
|
@@ -379,8 +1232,13 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
|
|
|
379
1232
|
} finally {
|
|
380
1233
|
store.close();
|
|
381
1234
|
}
|
|
382
|
-
})
|
|
383
|
-
|
|
1235
|
+
});
|
|
1236
|
+
dbCommand.addCommand(normalizeProjectsCmd);
|
|
1237
|
+
var sizeReportCmd = new Command("size-report").configureHelp(helpStyle).description("Show SQLite file size and major storage consumers").option("--limit <n>", "number of largest tables/indexes to show", "12");
|
|
1238
|
+
addDbOption(sizeReportCmd);
|
|
1239
|
+
addJsonOption(sizeReportCmd);
|
|
1240
|
+
sizeReportCmd.action((opts) => {
|
|
1241
|
+
const dbPath = resolveDbPath(resolveDbOpt(opts));
|
|
384
1242
|
const db = connect(dbPath);
|
|
385
1243
|
try {
|
|
386
1244
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 12);
|
|
@@ -389,10 +1247,10 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
|
|
|
389
1247
|
const freePages = db.prepare("SELECT freelist_count FROM pragma_freelist_count").get();
|
|
390
1248
|
const pageSize = db.prepare("PRAGMA page_size").get();
|
|
391
1249
|
const tables = db.prepare(`SELECT name, SUM(pgsize) as size_bytes
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
1250
|
+
FROM dbstat
|
|
1251
|
+
GROUP BY name
|
|
1252
|
+
ORDER BY size_bytes DESC
|
|
1253
|
+
LIMIT ?`).all(limit);
|
|
396
1254
|
if (opts.json) {
|
|
397
1255
|
console.log(JSON.stringify({
|
|
398
1256
|
file_size_bytes: fileSizeBytes,
|
|
@@ -419,8 +1277,13 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
|
|
|
419
1277
|
} finally {
|
|
420
1278
|
db.close();
|
|
421
1279
|
}
|
|
422
|
-
})
|
|
423
|
-
|
|
1280
|
+
});
|
|
1281
|
+
dbCommand.addCommand(sizeReportCmd);
|
|
1282
|
+
var backfillTagsCmd = new Command("backfill-tags").configureHelp(helpStyle).description("Populate tags_text for memories where tags are empty").option("--limit <n>", "max memories to check").option("--since <iso>", "only memories created at/after this ISO timestamp").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "backfill across all projects").option("--inactive", "include inactive memories").option("--dry-run", "preview updates without writing");
|
|
1283
|
+
addDbOption(backfillTagsCmd);
|
|
1284
|
+
addJsonOption(backfillTagsCmd);
|
|
1285
|
+
backfillTagsCmd.action((opts) => {
|
|
1286
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
424
1287
|
try {
|
|
425
1288
|
const limit = parseOptionalPositiveInt$1(opts.limit);
|
|
426
1289
|
const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
|
|
@@ -445,8 +1308,13 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
|
|
|
445
1308
|
} finally {
|
|
446
1309
|
store.close();
|
|
447
1310
|
}
|
|
448
|
-
})
|
|
449
|
-
|
|
1311
|
+
});
|
|
1312
|
+
dbCommand.addCommand(backfillTagsCmd);
|
|
1313
|
+
var pruneObsCmd = new Command("prune-observations").configureHelp(helpStyle).description("Deactivate low-signal observations (does not delete rows)").option("--limit <n>", "max observations to check").option("--dry-run", "preview deactivations without writing");
|
|
1314
|
+
addDbOption(pruneObsCmd);
|
|
1315
|
+
addJsonOption(pruneObsCmd);
|
|
1316
|
+
pruneObsCmd.action((opts) => {
|
|
1317
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
450
1318
|
try {
|
|
451
1319
|
const limit = parseOptionalPositiveInt$1(opts.limit);
|
|
452
1320
|
const result = deactivateLowSignalObservations(store.db, limit ?? null, opts.dryRun === true);
|
|
@@ -463,8 +1331,13 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
|
|
|
463
1331
|
} finally {
|
|
464
1332
|
store.close();
|
|
465
1333
|
}
|
|
466
|
-
})
|
|
467
|
-
|
|
1334
|
+
});
|
|
1335
|
+
dbCommand.addCommand(pruneObsCmd);
|
|
1336
|
+
var pruneMemCmd = new Command("prune-memories").configureHelp(helpStyle).description("Deactivate low-signal memories across selected kinds").option("--limit <n>", "max memories to check").option("--kinds <csv>", "comma-separated memory kinds (default set when omitted)").option("--dry-run", "preview deactivations without writing");
|
|
1337
|
+
addDbOption(pruneMemCmd);
|
|
1338
|
+
addJsonOption(pruneMemCmd);
|
|
1339
|
+
pruneMemCmd.action((opts) => {
|
|
1340
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
468
1341
|
try {
|
|
469
1342
|
const limit = parseOptionalPositiveInt$1(opts.limit);
|
|
470
1343
|
const kinds = parseKindsCsv(opts.kinds);
|
|
@@ -486,7 +1359,8 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
|
|
|
486
1359
|
} finally {
|
|
487
1360
|
store.close();
|
|
488
1361
|
}
|
|
489
|
-
})
|
|
1362
|
+
});
|
|
1363
|
+
dbCommand.addCommand(pruneMemCmd);
|
|
490
1364
|
//#endregion
|
|
491
1365
|
//#region src/commands/embed.ts
|
|
492
1366
|
function parseOptionalPositiveInt(value) {
|
|
@@ -503,8 +1377,11 @@ function resolveEmbedProjectScope(cwd, projectOpt, allProjects) {
|
|
|
503
1377
|
if (envProject) return envProject;
|
|
504
1378
|
return resolveProject(cwd, null);
|
|
505
1379
|
}
|
|
506
|
-
var
|
|
507
|
-
|
|
1380
|
+
var embedCmd = new Command("embed").configureHelp(helpStyle).description("Backfill semantic embeddings").option("--limit <n>", "max memories to check").option("--since <iso>", "only memories created at/after this ISO timestamp").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "embed across all projects").option("--inactive", "include inactive memories").option("--dry-run", "preview work without writing vectors");
|
|
1381
|
+
addDbOption(embedCmd);
|
|
1382
|
+
addJsonOption(embedCmd);
|
|
1383
|
+
var embedCommand = embedCmd.action(async (opts) => {
|
|
1384
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
508
1385
|
try {
|
|
509
1386
|
const limit = parseOptionalPositiveInt(opts.limit);
|
|
510
1387
|
const project = resolveEmbedProjectScope(process.cwd(), opts.project, opts.allProjects);
|
|
@@ -547,7 +1424,7 @@ function resolveSessionStreamId(payload) {
|
|
|
547
1424
|
if (text) values.set(key, text);
|
|
548
1425
|
}
|
|
549
1426
|
if (values.size === 0) return null;
|
|
550
|
-
if (new Set(values.values()).size > 1)
|
|
1427
|
+
if (new Set(values.values()).size > 1) return null;
|
|
551
1428
|
for (const key of SESSION_ID_KEYS) {
|
|
552
1429
|
const value = values.get(key);
|
|
553
1430
|
if (value) return value;
|
|
@@ -563,39 +1440,61 @@ async function readStdinJson() {
|
|
|
563
1440
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("payload must be an object");
|
|
564
1441
|
return parsed;
|
|
565
1442
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const tsWallMs = Number.isFinite(Number(payload.ts_wall_ms)) ? Math.floor(Number(payload.ts_wall_ms)) : null;
|
|
577
|
-
const tsMonoMs = Number.isFinite(Number(payload.ts_mono_ms)) ? Number(payload.ts_mono_ms) : null;
|
|
578
|
-
const eventId = typeof payload.event_id === "string" ? payload.event_id.trim() : "";
|
|
579
|
-
const eventPayload = payload.payload && typeof payload.payload === "object" && !Array.isArray(payload.payload) ? stripPrivateObj(payload.payload) : {};
|
|
580
|
-
const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
|
|
1443
|
+
function emitStructuredError(errorCode, message) {
|
|
1444
|
+
console.log(JSON.stringify({
|
|
1445
|
+
error: errorCode,
|
|
1446
|
+
message
|
|
1447
|
+
}));
|
|
1448
|
+
process.exitCode = 1;
|
|
1449
|
+
}
|
|
1450
|
+
var enqueueCmd = new Command("enqueue-raw-event").configureHelp(helpStyle).description("Enqueue one raw event from stdin into the durable queue");
|
|
1451
|
+
addDbOption(enqueueCmd);
|
|
1452
|
+
var enqueueRawEventCommand = enqueueCmd.action(async (opts) => {
|
|
581
1453
|
try {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
1454
|
+
const payload = await readStdinJson();
|
|
1455
|
+
const sessionId = resolveSessionStreamId(payload);
|
|
1456
|
+
if (!sessionId) {
|
|
1457
|
+
emitStructuredError("validation_error", "session id required");
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (sessionId.startsWith("msg_")) {
|
|
1461
|
+
emitStructuredError("validation_error", "invalid session id");
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
const eventType = typeof payload.event_type === "string" ? payload.event_type.trim() : "";
|
|
1465
|
+
if (!eventType) {
|
|
1466
|
+
emitStructuredError("validation_error", "event_type required");
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
|
|
1470
|
+
const project = typeof payload.project === "string" ? payload.project : null;
|
|
1471
|
+
const startedAt = typeof payload.started_at === "string" ? payload.started_at : null;
|
|
1472
|
+
const tsWallMs = Number.isFinite(Number(payload.ts_wall_ms)) ? Math.floor(Number(payload.ts_wall_ms)) : null;
|
|
1473
|
+
const tsMonoMs = Number.isFinite(Number(payload.ts_mono_ms)) ? Number(payload.ts_mono_ms) : null;
|
|
1474
|
+
const eventId = typeof payload.event_id === "string" ? payload.event_id.trim() : "";
|
|
1475
|
+
const eventPayload = payload.payload && typeof payload.payload === "object" && !Array.isArray(payload.payload) ? stripPrivateObj(payload.payload) : {};
|
|
1476
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
1477
|
+
try {
|
|
1478
|
+
store.updateRawEventSessionMeta({
|
|
1479
|
+
opencodeSessionId: sessionId,
|
|
1480
|
+
source: "opencode",
|
|
1481
|
+
cwd,
|
|
1482
|
+
project,
|
|
1483
|
+
startedAt,
|
|
1484
|
+
lastSeenTsWallMs: tsWallMs
|
|
1485
|
+
});
|
|
1486
|
+
store.recordRawEventsBatch(sessionId, [{
|
|
1487
|
+
event_id: eventId,
|
|
1488
|
+
event_type: eventType,
|
|
1489
|
+
payload: eventPayload,
|
|
1490
|
+
ts_wall_ms: tsWallMs,
|
|
1491
|
+
ts_mono_ms: tsMonoMs
|
|
1492
|
+
}]);
|
|
1493
|
+
} finally {
|
|
1494
|
+
store.close();
|
|
1495
|
+
}
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
emitStructuredError("enqueue_error", err instanceof Error ? err.message : String(err));
|
|
599
1498
|
}
|
|
600
1499
|
});
|
|
601
1500
|
//#endregion
|
|
@@ -603,9 +1502,11 @@ var enqueueRawEventCommand = new Command("enqueue-raw-event").configureHelp(help
|
|
|
603
1502
|
function expandUserPath(value) {
|
|
604
1503
|
return value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
|
|
605
1504
|
}
|
|
606
|
-
var
|
|
1505
|
+
var cmd$2 = 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("--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");
|
|
1506
|
+
addDbOption(cmd$2);
|
|
1507
|
+
cmd$2.action((output, opts) => {
|
|
607
1508
|
const payload = exportMemories({
|
|
608
|
-
dbPath: resolveDbPath(opts
|
|
1509
|
+
dbPath: resolveDbPath(resolveDbOpt(opts)),
|
|
609
1510
|
project: opts.project,
|
|
610
1511
|
allProjects: opts.allProjects,
|
|
611
1512
|
includeInactive: opts.includeInactive,
|
|
@@ -628,31 +1529,49 @@ var exportMemoriesCommand = new Command("export-memories").configureHelp(helpSty
|
|
|
628
1529
|
].join("\n"));
|
|
629
1530
|
p.outro("done");
|
|
630
1531
|
});
|
|
1532
|
+
var exportMemoriesCommand = cmd$2;
|
|
631
1533
|
//#endregion
|
|
632
1534
|
//#region src/commands/import-memories.ts
|
|
633
|
-
var
|
|
1535
|
+
var cmd$1 = new Command("import-memories").configureHelp(helpStyle).description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing");
|
|
1536
|
+
addDbOption(cmd$1);
|
|
1537
|
+
addJsonOption(cmd$1);
|
|
1538
|
+
cmd$1.action((inputFile, opts) => {
|
|
634
1539
|
let payload;
|
|
635
1540
|
try {
|
|
636
1541
|
payload = readImportPayload(inputFile);
|
|
637
1542
|
} catch (error) {
|
|
638
|
-
|
|
639
|
-
|
|
1543
|
+
const message = error instanceof Error ? error.message : "Invalid import file";
|
|
1544
|
+
if (opts.json) emitJsonError("invalid_input", message);
|
|
1545
|
+
else {
|
|
1546
|
+
p.log.error(message);
|
|
1547
|
+
process.exitCode = 1;
|
|
1548
|
+
}
|
|
640
1549
|
return;
|
|
641
1550
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1551
|
+
if (!opts.json) {
|
|
1552
|
+
p.intro("codemem import-memories");
|
|
1553
|
+
p.log.info([
|
|
1554
|
+
`Export version: ${payload.version}`,
|
|
1555
|
+
`Exported at: ${payload.exported_at}`,
|
|
1556
|
+
`Sessions: ${payload.sessions.length.toLocaleString()}`,
|
|
1557
|
+
`Memories: ${payload.memory_items.length.toLocaleString()}`,
|
|
1558
|
+
`Summaries: ${payload.session_summaries.length.toLocaleString()}`,
|
|
1559
|
+
`Prompts: ${payload.user_prompts.length.toLocaleString()}`
|
|
1560
|
+
].join("\n"));
|
|
1561
|
+
}
|
|
651
1562
|
const result = importMemories(payload, {
|
|
652
|
-
dbPath: resolveDbPath(opts
|
|
1563
|
+
dbPath: resolveDbPath(resolveDbOpt(opts)),
|
|
653
1564
|
remapProject: opts.remapProject,
|
|
654
1565
|
dryRun: opts.dryRun
|
|
655
1566
|
});
|
|
1567
|
+
if (opts.json) {
|
|
1568
|
+
console.log(JSON.stringify({
|
|
1569
|
+
sessions: result.sessions,
|
|
1570
|
+
memory_items: result.memory_items,
|
|
1571
|
+
skipped: result.dryRun
|
|
1572
|
+
}));
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
656
1575
|
if (result.dryRun) {
|
|
657
1576
|
p.outro("dry run complete");
|
|
658
1577
|
return;
|
|
@@ -665,25 +1584,53 @@ var importMemoriesCommand = new Command("import-memories").configureHelp(helpSty
|
|
|
665
1584
|
].join("\n"));
|
|
666
1585
|
p.outro("done");
|
|
667
1586
|
});
|
|
1587
|
+
var importMemoriesCommand = cmd$1;
|
|
668
1588
|
//#endregion
|
|
669
1589
|
//#region src/commands/mcp.ts
|
|
670
|
-
var
|
|
671
|
-
|
|
1590
|
+
var mcpCmd = new Command("mcp").configureHelp(helpStyle).description("Start the MCP stdio server");
|
|
1591
|
+
addDbOption(mcpCmd);
|
|
1592
|
+
var mcpCommand = mcpCmd.action(async (opts) => {
|
|
1593
|
+
const dbPath = resolveDbOpt(opts);
|
|
672
1594
|
if (dbPath) process.env.CODEMEM_DB = dbPath;
|
|
673
|
-
|
|
1595
|
+
try {
|
|
1596
|
+
await import("@codemem/mcp");
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
console.error(`Failed to start MCP server: ${err instanceof Error ? err.message : String(err)}`);
|
|
1599
|
+
process.exitCode = 1;
|
|
1600
|
+
}
|
|
674
1601
|
});
|
|
675
1602
|
//#endregion
|
|
1603
|
+
//#region src/commands/pack-shared.ts
|
|
1604
|
+
function collectWorkingSetFile(value, previous) {
|
|
1605
|
+
return [...previous, value];
|
|
1606
|
+
}
|
|
1607
|
+
function buildPackRequestOptions(opts, ctx = {}) {
|
|
1608
|
+
const limit = Number.parseInt(opts.limit ?? "10", 10) || 10;
|
|
1609
|
+
const budgetRaw = opts.tokenBudget ?? opts.budget;
|
|
1610
|
+
const budget = budgetRaw ? Number.parseInt(budgetRaw, 10) : void 0;
|
|
1611
|
+
const filters = {};
|
|
1612
|
+
if (!opts.allProjects) {
|
|
1613
|
+
const defaultProject = ctx.envProject?.trim() || null;
|
|
1614
|
+
const resolveProjectFn = ctx.resolveProjectFn ?? resolveProject;
|
|
1615
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
1616
|
+
const project = defaultProject || resolveProjectFn(cwd, opts.project ?? null);
|
|
1617
|
+
if (project) filters.project = project;
|
|
1618
|
+
}
|
|
1619
|
+
if ((opts.workingSetFile?.length ?? 0) > 0) filters.working_set_paths = opts.workingSetFile;
|
|
1620
|
+
return {
|
|
1621
|
+
limit,
|
|
1622
|
+
budget,
|
|
1623
|
+
filters
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
//#endregion
|
|
676
1627
|
//#region src/commands/memory.ts
|
|
677
1628
|
/**
|
|
678
|
-
* Memory management CLI commands — show, forget, remember.
|
|
1629
|
+
* Memory management CLI commands — show, forget, remember, inject.
|
|
679
1630
|
*
|
|
680
1631
|
* Ports codemem/commands/memory_cmds.py (show_cmd, forget_cmd, remember_cmd).
|
|
681
|
-
*
|
|
682
|
-
* and inject is just pack_text output which the existing pack command covers.
|
|
1632
|
+
* Inject is deprecated in favor of `codemem pack`.
|
|
683
1633
|
*/
|
|
684
|
-
function collectWorkingSetFile$1(value, previous) {
|
|
685
|
-
return [...previous, value];
|
|
686
|
-
}
|
|
687
1634
|
/** Parse a strict positive integer, rejecting prefixes like "12abc". */
|
|
688
1635
|
function parseStrictPositiveId(value) {
|
|
689
1636
|
if (!/^\d+$/.test(value.trim())) return null;
|
|
@@ -693,19 +1640,35 @@ function parseStrictPositiveId(value) {
|
|
|
693
1640
|
function showMemoryAction(idStr, opts) {
|
|
694
1641
|
const memoryId = parseStrictPositiveId(idStr);
|
|
695
1642
|
if (memoryId === null) {
|
|
696
|
-
|
|
697
|
-
|
|
1643
|
+
if (opts.json) emitJsonError("invalid_id", `Invalid memory ID: ${idStr}`);
|
|
1644
|
+
else {
|
|
1645
|
+
p.log.error(`Invalid memory ID: ${idStr}`);
|
|
1646
|
+
process.exitCode = 1;
|
|
1647
|
+
}
|
|
698
1648
|
return;
|
|
699
1649
|
}
|
|
700
|
-
const store = new MemoryStore(resolveDbPath(opts
|
|
1650
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
701
1651
|
try {
|
|
702
1652
|
const item = store.get(memoryId);
|
|
703
1653
|
if (!item) {
|
|
704
|
-
|
|
705
|
-
|
|
1654
|
+
if (opts.json) emitJsonError("not_found", `Memory ${memoryId} not found`);
|
|
1655
|
+
else {
|
|
1656
|
+
p.log.error(`Memory ${memoryId} not found`);
|
|
1657
|
+
process.exitCode = 1;
|
|
1658
|
+
}
|
|
706
1659
|
return;
|
|
707
1660
|
}
|
|
708
|
-
console.log(JSON.stringify(item, null, 2));
|
|
1661
|
+
if (opts.json) console.log(JSON.stringify(item, null, 2));
|
|
1662
|
+
else {
|
|
1663
|
+
console.log(`#${item.id} [${item.kind}] ${item.title}`);
|
|
1664
|
+
if (item.subtitle) console.log(` ${item.subtitle}`);
|
|
1665
|
+
console.log(` created: ${item.created_at} confidence: ${item.confidence}`);
|
|
1666
|
+
if (item.tags_text) console.log(` tags: ${item.tags_text}`);
|
|
1667
|
+
if (item.body_text) {
|
|
1668
|
+
const preview = item.body_text.length > 300 ? `${item.body_text.slice(0, 300)}…` : item.body_text;
|
|
1669
|
+
console.log(`\n${preview}`);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
709
1672
|
} finally {
|
|
710
1673
|
store.close();
|
|
711
1674
|
}
|
|
@@ -713,20 +1676,27 @@ function showMemoryAction(idStr, opts) {
|
|
|
713
1676
|
function forgetMemoryAction(idStr, opts) {
|
|
714
1677
|
const memoryId = parseStrictPositiveId(idStr);
|
|
715
1678
|
if (memoryId === null) {
|
|
716
|
-
|
|
717
|
-
|
|
1679
|
+
if (opts.json) emitJsonError("invalid_id", `Invalid memory ID: ${idStr}`);
|
|
1680
|
+
else {
|
|
1681
|
+
p.log.error(`Invalid memory ID: ${idStr}`);
|
|
1682
|
+
process.exitCode = 1;
|
|
1683
|
+
}
|
|
718
1684
|
return;
|
|
719
1685
|
}
|
|
720
|
-
const store = new MemoryStore(resolveDbPath(opts
|
|
1686
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
721
1687
|
try {
|
|
722
1688
|
store.forget(memoryId);
|
|
723
|
-
|
|
1689
|
+
if (opts.json) console.log(JSON.stringify({
|
|
1690
|
+
id: memoryId,
|
|
1691
|
+
status: "forgotten"
|
|
1692
|
+
}));
|
|
1693
|
+
else p.log.success(`Memory ${memoryId} marked inactive`);
|
|
724
1694
|
} finally {
|
|
725
1695
|
store.close();
|
|
726
1696
|
}
|
|
727
1697
|
}
|
|
728
1698
|
async function rememberMemoryAction(opts) {
|
|
729
|
-
const store = new MemoryStore(resolveDbPath(opts
|
|
1699
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
730
1700
|
let sessionId = null;
|
|
731
1701
|
try {
|
|
732
1702
|
const project = resolveProject(process.cwd(), opts.project ?? null);
|
|
@@ -740,7 +1710,8 @@ async function rememberMemoryAction(opts) {
|
|
|
740
1710
|
const memId = store.remember(sessionId, opts.kind, opts.title, opts.body, .5, opts.tags);
|
|
741
1711
|
await store.flushPendingVectorWrites();
|
|
742
1712
|
store.endSession(sessionId, { manual: true });
|
|
743
|
-
|
|
1713
|
+
if (opts.json) console.log(JSON.stringify({ id: memId }));
|
|
1714
|
+
else p.log.success(`Stored memory ${memId}`);
|
|
744
1715
|
} catch (err) {
|
|
745
1716
|
if (sessionId !== null) try {
|
|
746
1717
|
store.endSession(sessionId, {
|
|
@@ -748,46 +1719,211 @@ async function rememberMemoryAction(opts) {
|
|
|
748
1719
|
error: true
|
|
749
1720
|
});
|
|
750
1721
|
} catch {}
|
|
751
|
-
|
|
1722
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1723
|
+
if (opts.json) emitJsonError("remember_failed", message);
|
|
1724
|
+
else {
|
|
1725
|
+
p.log.error(`Failed to store memory: ${message}`);
|
|
1726
|
+
process.exitCode = 1;
|
|
1727
|
+
}
|
|
752
1728
|
} finally {
|
|
753
1729
|
store.close();
|
|
754
1730
|
}
|
|
755
1731
|
}
|
|
756
1732
|
function createShowMemoryCommand() {
|
|
757
|
-
|
|
1733
|
+
const cmd = new Command("show").configureHelp(helpStyle).description("Show a memory item").argument("<id>", "memory ID");
|
|
1734
|
+
addDbOption(cmd);
|
|
1735
|
+
addJsonOption(cmd);
|
|
1736
|
+
cmd.action(showMemoryAction);
|
|
1737
|
+
return cmd;
|
|
758
1738
|
}
|
|
759
1739
|
function createForgetMemoryCommand() {
|
|
760
|
-
|
|
1740
|
+
const cmd = new Command("forget").configureHelp(helpStyle).description("Deactivate a memory item").argument("<id>", "memory ID");
|
|
1741
|
+
addDbOption(cmd);
|
|
1742
|
+
addJsonOption(cmd);
|
|
1743
|
+
cmd.action(forgetMemoryAction);
|
|
1744
|
+
return cmd;
|
|
761
1745
|
}
|
|
762
1746
|
function createRememberMemoryCommand() {
|
|
763
|
-
|
|
1747
|
+
const cmd = 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)");
|
|
1748
|
+
addDbOption(cmd);
|
|
1749
|
+
addJsonOption(cmd);
|
|
1750
|
+
cmd.action(rememberMemoryAction);
|
|
1751
|
+
return cmd;
|
|
764
1752
|
}
|
|
765
1753
|
function createInjectMemoryCommand() {
|
|
766
|
-
|
|
767
|
-
|
|
1754
|
+
const cmd = new Command("inject").configureHelp(helpStyle).description("Build raw memory context text for manual prompt injection").argument("<context>", "context string to search for").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").allowUnknownOption(true).allowExcessArguments(true);
|
|
1755
|
+
addDbOption(cmd);
|
|
1756
|
+
cmd.action(async (context, opts) => {
|
|
1757
|
+
emitDeprecationWarning("codemem memory inject", "codemem pack");
|
|
1758
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
768
1759
|
try {
|
|
769
|
-
const limit =
|
|
770
|
-
const budgetRaw = opts.tokenBudget ?? opts.budget;
|
|
771
|
-
const budget = budgetRaw ? Number.parseInt(budgetRaw, 10) : void 0;
|
|
772
|
-
const filters = {};
|
|
773
|
-
if (!opts.allProjects) {
|
|
774
|
-
const project = process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), opts.project ?? null);
|
|
775
|
-
if (project) filters.project = project;
|
|
776
|
-
}
|
|
777
|
-
if ((opts.workingSetFile?.length ?? 0) > 0) filters.working_set_paths = opts.workingSetFile;
|
|
1760
|
+
const { limit, budget, filters } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
|
|
778
1761
|
const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
|
|
779
1762
|
console.log(pack.pack_text ?? "");
|
|
780
1763
|
} finally {
|
|
781
1764
|
store.close();
|
|
782
1765
|
}
|
|
783
1766
|
});
|
|
1767
|
+
return cmd;
|
|
1768
|
+
}
|
|
1769
|
+
function createMemoryRoleReportCommand() {
|
|
1770
|
+
const cmd = new Command("role-report").configureHelp(helpStyle).description("Analyze inferred memory roles in a DB snapshot").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--probe <query>", "run a retrieval probe query against the snapshot", (value, prev) => [...prev, value], []).option("--inactive", "include inactive memories");
|
|
1771
|
+
addDbOption(cmd);
|
|
1772
|
+
addJsonOption(cmd);
|
|
1773
|
+
cmd.action((opts) => {
|
|
1774
|
+
const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
|
|
1775
|
+
const result = getMemoryRoleReport(resolveDbOpt(opts), {
|
|
1776
|
+
project,
|
|
1777
|
+
allProjects: opts.allProjects === true,
|
|
1778
|
+
includeInactive: opts.inactive === true,
|
|
1779
|
+
probes: opts.probe
|
|
1780
|
+
});
|
|
1781
|
+
if (opts.json) {
|
|
1782
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
p.intro("codemem memory role-report");
|
|
1786
|
+
p.log.info([
|
|
1787
|
+
`Memories: ${result.totals.memories}`,
|
|
1788
|
+
`Active: ${result.totals.active}`,
|
|
1789
|
+
`Sessions: ${result.totals.sessions}`
|
|
1790
|
+
].join("\n"));
|
|
1791
|
+
p.log.info("Counts by role:");
|
|
1792
|
+
for (const [role, count] of Object.entries(result.counts_by_role)) p.log.message(` ${role.padEnd(10)} ${String(count)}`);
|
|
1793
|
+
p.log.info("Counts by mapping:");
|
|
1794
|
+
p.log.message(` mapped ${result.counts_by_mapping.mapped}`);
|
|
1795
|
+
p.log.message(` unmapped ${result.counts_by_mapping.unmapped}`);
|
|
1796
|
+
p.log.info("Summary lineages:");
|
|
1797
|
+
p.log.message(` session_summary ${result.summary_lineages.session_summary}`);
|
|
1798
|
+
p.log.message(` legacy_metadata_summary ${result.summary_lineages.legacy_metadata_summary}`);
|
|
1799
|
+
p.log.message(` summary_mapped ${result.summary_mapping.mapped}`);
|
|
1800
|
+
p.log.message(` summary_unmapped ${result.summary_mapping.unmapped}`);
|
|
1801
|
+
p.log.info("Project quality:");
|
|
1802
|
+
for (const [bucket, count] of Object.entries(result.project_quality)) p.log.message(` ${bucket.padEnd(12)} ${String(count)}`);
|
|
1803
|
+
if (result.probe_results.length > 0) {
|
|
1804
|
+
p.log.info("Probe results:");
|
|
1805
|
+
for (const probe of result.probe_results) {
|
|
1806
|
+
p.log.message(` query: ${probe.query}`);
|
|
1807
|
+
p.log.message(` mode: ${probe.mode}`);
|
|
1808
|
+
p.log.message(` top roles: durable=${probe.top_role_counts.durable} recap=${probe.top_role_counts.recap} ephemeral=${probe.top_role_counts.ephemeral} general=${probe.top_role_counts.general}`);
|
|
1809
|
+
p.log.message(` top mapping: mapped=${probe.top_mapping_counts.mapped} unmapped=${probe.top_mapping_counts.unmapped}`);
|
|
1810
|
+
p.log.message(` burden: recap_share=${probe.top_burden.recap_share.toFixed(2)} unmapped_share=${probe.top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.top_burden.recap_unmapped_share.toFixed(2)}`);
|
|
1811
|
+
if (probe.simulated_demoted_unmapped_recap) p.log.message(` simulated demote-unmapped-recap burden: recap_share=${probe.simulated_demoted_unmapped_recap.top_burden.recap_share.toFixed(2)} unmapped_share=${probe.simulated_demoted_unmapped_recap.top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.simulated_demoted_unmapped_recap.top_burden.recap_unmapped_share.toFixed(2)}`);
|
|
1812
|
+
if (probe.simulated_demoted_unmapped_recap_and_ephemeral) p.log.message(` simulated demote-unmapped-recap+ephemeral burden: recap_share=${probe.simulated_demoted_unmapped_recap_and_ephemeral.top_burden.recap_share.toFixed(2)} unmapped_share=${probe.simulated_demoted_unmapped_recap_and_ephemeral.top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.simulated_demoted_unmapped_recap_and_ephemeral.top_burden.recap_unmapped_share.toFixed(2)}`);
|
|
1813
|
+
for (const item of probe.items.slice(0, 5)) p.log.message(` [${item.id}] (${item.kind}/${item.role}/${item.mapping}) ${item.title} — ${item.role_reason}`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
p.outro("done");
|
|
1817
|
+
});
|
|
1818
|
+
return cmd;
|
|
1819
|
+
}
|
|
1820
|
+
function createMemoryRoleCompareCommand() {
|
|
1821
|
+
const cmd = new Command("role-compare").configureHelp(helpStyle).description("Compare inferred memory-role and probe metrics across two DB snapshots").argument("<baseline_db>", "baseline sqlite database path").argument("<candidate_db>", "candidate sqlite database path").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--probe <query>", "run a retrieval probe query against both snapshots", (value, prev) => [...prev, value], []).option("--inactive", "include inactive memories");
|
|
1822
|
+
addJsonOption(cmd);
|
|
1823
|
+
cmd.action((baselineDb, candidateDb, opts) => {
|
|
1824
|
+
const result = compareMemoryRoleReports(baselineDb, candidateDb, {
|
|
1825
|
+
project: opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null),
|
|
1826
|
+
allProjects: opts.allProjects === true,
|
|
1827
|
+
includeInactive: opts.inactive === true,
|
|
1828
|
+
probes: opts.probe
|
|
1829
|
+
});
|
|
1830
|
+
if (opts.json) {
|
|
1831
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
p.intro("codemem memory role-compare");
|
|
1835
|
+
p.log.info([
|
|
1836
|
+
`Baseline sessions: ${result.baseline.totals.sessions}`,
|
|
1837
|
+
`Candidate sessions: ${result.candidate.totals.sessions}`,
|
|
1838
|
+
`Delta sessions: ${result.delta.totals.sessions}`,
|
|
1839
|
+
`Mapped delta: ${result.delta.counts_by_mapping.mapped}`,
|
|
1840
|
+
`Unmapped delta: ${result.delta.counts_by_mapping.unmapped}`,
|
|
1841
|
+
`Summary mapped delta: ${result.delta.summary_mapping.mapped}`,
|
|
1842
|
+
`Summary unmapped delta: ${result.delta.summary_mapping.unmapped}`
|
|
1843
|
+
].join("\n"));
|
|
1844
|
+
p.log.info("Role deltas:");
|
|
1845
|
+
for (const [role, count] of Object.entries(result.delta.counts_by_role)) p.log.message(` ${role.padEnd(10)} ${String(count)}`);
|
|
1846
|
+
if (result.probe_comparisons.length > 0) {
|
|
1847
|
+
p.log.info("Probe comparisons:");
|
|
1848
|
+
for (const probe of result.probe_comparisons) {
|
|
1849
|
+
p.log.message(` query: ${probe.query}`);
|
|
1850
|
+
p.log.message(` modes: baseline=${probe.baseline_mode ?? "-"} candidate=${probe.candidate_mode ?? "-"}`);
|
|
1851
|
+
p.log.message(` overlap: shared_top_keys=${probe.shared_item_keys.length} baseline_top=${probe.baseline_item_ids.slice(0, 5).join(",") || "-"} candidate_top=${probe.candidate_item_ids.slice(0, 5).join(",") || "-"}`);
|
|
1852
|
+
if (probe.delta_top_burden) p.log.message(` burden delta: recap_share=${probe.delta_top_burden.recap_share.toFixed(2)} unmapped_share=${probe.delta_top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.delta_top_burden.recap_unmapped_share.toFixed(2)}`);
|
|
1853
|
+
if (probe.delta_top_mapping_counts) p.log.message(` mapping delta: mapped=${probe.delta_top_mapping_counts.mapped} unmapped=${probe.delta_top_mapping_counts.unmapped}`);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
p.outro("done");
|
|
1857
|
+
});
|
|
1858
|
+
return cmd;
|
|
1859
|
+
}
|
|
1860
|
+
function createMemoryRelinkReportCommand() {
|
|
1861
|
+
const cmd = new Command("relink-report").configureHelp(helpStyle).description("Analyze dry-run raw-event session relinking and compaction opportunities").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--limit <n>", "max groups to print", "25");
|
|
1862
|
+
addDbOption(cmd);
|
|
1863
|
+
addJsonOption(cmd);
|
|
1864
|
+
cmd.action((opts) => {
|
|
1865
|
+
const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
|
|
1866
|
+
const limit = Number.parseInt(opts.limit ?? "25", 10) || 25;
|
|
1867
|
+
const result = getRawEventRelinkReport(resolveDbOpt(opts), {
|
|
1868
|
+
project,
|
|
1869
|
+
allProjects: opts.allProjects === true,
|
|
1870
|
+
limit
|
|
1871
|
+
});
|
|
1872
|
+
if (opts.json) {
|
|
1873
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
p.intro("codemem memory relink-report");
|
|
1877
|
+
p.log.info([
|
|
1878
|
+
`Recoverable sessions: ${result.totals.recoverable_sessions}`,
|
|
1879
|
+
`Distinct stable ids: ${result.totals.distinct_stable_ids}`,
|
|
1880
|
+
`Groups with multiple sessions: ${result.totals.groups_with_multiple_sessions}`,
|
|
1881
|
+
`Groups with mapped session: ${result.totals.groups_with_mapped_session}`,
|
|
1882
|
+
`Groups without mapped session: ${result.totals.groups_without_mapped_session}`,
|
|
1883
|
+
`Active memories in groups: ${result.totals.active_memories}`,
|
|
1884
|
+
`Repointable active memories: ${result.totals.repointable_active_memories}`
|
|
1885
|
+
].join("\n"));
|
|
1886
|
+
p.log.info("Top relink groups:");
|
|
1887
|
+
for (const group of result.groups) p.log.message(` ${group.stable_id} -> canonical ${group.canonical_session_id} | local=${group.local_sessions} mapped=${group.mapped_sessions} unmapped=${group.unmapped_sessions} active=${group.active_memories} repointable=${group.repointable_active_memories}`);
|
|
1888
|
+
p.outro("done");
|
|
1889
|
+
});
|
|
1890
|
+
return cmd;
|
|
784
1891
|
}
|
|
785
|
-
function
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1892
|
+
function createMemoryRelinkPlanCommand() {
|
|
1893
|
+
const cmd = new Command("relink-plan").configureHelp(helpStyle).description("Emit dry-run raw-event relink remediation actions").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--limit <n>", "max groups to include", "25");
|
|
1894
|
+
addDbOption(cmd);
|
|
1895
|
+
addJsonOption(cmd);
|
|
1896
|
+
cmd.action((opts) => {
|
|
1897
|
+
const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
|
|
1898
|
+
const limit = Number.parseInt(opts.limit ?? "25", 10) || 25;
|
|
1899
|
+
const result = getRawEventRelinkPlan(resolveDbOpt(opts), {
|
|
1900
|
+
project,
|
|
1901
|
+
allProjects: opts.allProjects === true,
|
|
1902
|
+
limit
|
|
1903
|
+
});
|
|
1904
|
+
if (opts.json) {
|
|
1905
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
p.intro("codemem memory relink-plan");
|
|
1909
|
+
p.log.info([
|
|
1910
|
+
`Groups: ${result.totals.groups}`,
|
|
1911
|
+
`Eligible groups: ${result.totals.eligible_groups}`,
|
|
1912
|
+
`Skipped groups: ${result.totals.skipped_groups}`,
|
|
1913
|
+
`Actions: ${result.totals.actions}`,
|
|
1914
|
+
`Bridge creations: ${result.totals.bridge_creations}`,
|
|
1915
|
+
`Memory repoints: ${result.totals.memory_repoints}`,
|
|
1916
|
+
`Session compactions: ${result.totals.session_compactions}`
|
|
1917
|
+
].join("\n"));
|
|
1918
|
+
p.log.info("Planned actions:");
|
|
1919
|
+
for (const action of result.actions.slice(0, 15)) p.log.message(` ${action.action} ${action.stable_id} -> canonical ${action.canonical_session_id} | sessions=${action.session_ids.join(",") || "-"} memories=${action.memory_count} reason=${action.reason}`);
|
|
1920
|
+
if (result.skipped_groups.length > 0) {
|
|
1921
|
+
p.log.info("Skipped groups:");
|
|
1922
|
+
for (const group of result.skipped_groups.slice(0, 10)) p.log.message(` ${group.stable_id} | blockers=${group.blockers.join(",")}`);
|
|
1923
|
+
}
|
|
1924
|
+
p.outro("done");
|
|
790
1925
|
});
|
|
1926
|
+
return cmd;
|
|
791
1927
|
}
|
|
792
1928
|
var showMemoryCommand = createShowMemoryCommand();
|
|
793
1929
|
var forgetMemoryCommand = createForgetMemoryCommand();
|
|
@@ -797,24 +1933,19 @@ memoryCommand.addCommand(createShowMemoryCommand());
|
|
|
797
1933
|
memoryCommand.addCommand(createForgetMemoryCommand());
|
|
798
1934
|
memoryCommand.addCommand(createRememberMemoryCommand());
|
|
799
1935
|
memoryCommand.addCommand(createInjectMemoryCommand());
|
|
800
|
-
memoryCommand.addCommand(
|
|
1936
|
+
memoryCommand.addCommand(createMemoryRoleReportCommand());
|
|
1937
|
+
memoryCommand.addCommand(createMemoryRoleCompareCommand());
|
|
1938
|
+
memoryCommand.addCommand(createMemoryRelinkReportCommand());
|
|
1939
|
+
memoryCommand.addCommand(createMemoryRelinkPlanCommand());
|
|
801
1940
|
//#endregion
|
|
802
1941
|
//#region src/commands/pack.ts
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
var packCommand =
|
|
807
|
-
const store = new MemoryStore(resolveDbPath(opts
|
|
1942
|
+
var packCmd = new Command("pack").configureHelp(helpStyle).description("Build a context-aware memory pack").argument("<context>", "context string to search for").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");
|
|
1943
|
+
addDbOption(packCmd);
|
|
1944
|
+
addJsonOption(packCmd);
|
|
1945
|
+
var packCommand = packCmd.action(async (context, opts) => {
|
|
1946
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
808
1947
|
try {
|
|
809
|
-
const limit =
|
|
810
|
-
const budgetRaw = opts.tokenBudget ?? opts.budget;
|
|
811
|
-
const budget = budgetRaw ? Number.parseInt(budgetRaw, 10) : void 0;
|
|
812
|
-
const filters = {};
|
|
813
|
-
if (!opts.allProjects) {
|
|
814
|
-
const project = process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), opts.project ?? null);
|
|
815
|
-
if (project) filters.project = project;
|
|
816
|
-
}
|
|
817
|
-
if ((opts.workingSetFile?.length ?? 0) > 0) filters.working_set_paths = opts.workingSetFile;
|
|
1948
|
+
const { limit, budget, filters } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
|
|
818
1949
|
const result = await store.buildMemoryPackAsync(context, limit, budget, filters);
|
|
819
1950
|
if (opts.json) {
|
|
820
1951
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -837,8 +1968,11 @@ var packCommand = new Command("pack").configureHelp(helpStyle).description("Buil
|
|
|
837
1968
|
});
|
|
838
1969
|
//#endregion
|
|
839
1970
|
//#region src/commands/recent.ts
|
|
840
|
-
var
|
|
841
|
-
|
|
1971
|
+
var cmd = new Command("recent").configureHelp(helpStyle).description("Show recent memories").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");
|
|
1972
|
+
addDbOption(cmd);
|
|
1973
|
+
addJsonOption(cmd);
|
|
1974
|
+
cmd.action((opts) => {
|
|
1975
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
842
1976
|
try {
|
|
843
1977
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
|
|
844
1978
|
const filters = {};
|
|
@@ -848,15 +1982,20 @@ var recentCommand = new Command("recent").configureHelp(helpStyle).description("
|
|
|
848
1982
|
if (project) filters.project = project;
|
|
849
1983
|
}
|
|
850
1984
|
const items = store.recent(limit, filters);
|
|
851
|
-
|
|
1985
|
+
if (opts.json) console.log(JSON.stringify(items));
|
|
1986
|
+
else for (const item of items) console.log(`#${item.id} [${item.kind}] ${item.title}`);
|
|
852
1987
|
} finally {
|
|
853
1988
|
store.close();
|
|
854
1989
|
}
|
|
855
1990
|
});
|
|
1991
|
+
var recentCommand = cmd;
|
|
856
1992
|
//#endregion
|
|
857
1993
|
//#region src/commands/search.ts
|
|
858
|
-
var
|
|
859
|
-
|
|
1994
|
+
var searchCmd = new Command("search").configureHelp(helpStyle).description("Search memories by query").argument("<query>", "search query").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");
|
|
1995
|
+
addDbOption(searchCmd);
|
|
1996
|
+
addJsonOption(searchCmd);
|
|
1997
|
+
var searchCommand = searchCmd.action((query, opts) => {
|
|
1998
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
860
1999
|
try {
|
|
861
2000
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
|
|
862
2001
|
const filters = {};
|
|
@@ -900,6 +2039,10 @@ function timeSince(isoDate) {
|
|
|
900
2039
|
}
|
|
901
2040
|
//#endregion
|
|
902
2041
|
//#region src/commands/serve-invocation.ts
|
|
2042
|
+
/**
|
|
2043
|
+
* Parse and validate a port string. Throws a user-friendly message (no stack trace)
|
|
2044
|
+
* that callers in serve.ts catch at the action boundary.
|
|
2045
|
+
*/
|
|
903
2046
|
function parsePort(rawPort) {
|
|
904
2047
|
const port = Number.parseInt(rawPort, 10);
|
|
905
2048
|
if (!Number.isFinite(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${rawPort}`);
|
|
@@ -908,10 +2051,11 @@ function parsePort(rawPort) {
|
|
|
908
2051
|
function resolveLegacyServeInvocation(opts) {
|
|
909
2052
|
if (opts.stop && opts.restart) throw new Error("Use only one of --stop or --restart");
|
|
910
2053
|
if (opts.foreground && opts.background) throw new Error("Use only one of --background or --foreground");
|
|
911
|
-
const dbPath = opts
|
|
2054
|
+
const dbPath = resolveDbOpt(opts) ?? null;
|
|
912
2055
|
return {
|
|
913
2056
|
mode: opts.stop ? "stop" : opts.restart ? "restart" : "start",
|
|
914
2057
|
dbPath,
|
|
2058
|
+
configPath: opts.config ?? null,
|
|
915
2059
|
host: opts.host,
|
|
916
2060
|
port: parsePort(opts.port),
|
|
917
2061
|
background: opts.restart ? true : opts.background ? true : false
|
|
@@ -926,7 +2070,8 @@ function resolveServeInvocation(action, opts) {
|
|
|
926
2070
|
function resolveStartServeInvocation(opts) {
|
|
927
2071
|
return {
|
|
928
2072
|
mode: "start",
|
|
929
|
-
dbPath: opts
|
|
2073
|
+
dbPath: resolveDbOpt(opts) ?? null,
|
|
2074
|
+
configPath: opts.config ?? null,
|
|
930
2075
|
host: opts.host,
|
|
931
2076
|
port: parsePort(opts.port),
|
|
932
2077
|
background: !opts.foreground
|
|
@@ -935,7 +2080,8 @@ function resolveStartServeInvocation(opts) {
|
|
|
935
2080
|
function resolveStopRestartInvocation(mode, opts) {
|
|
936
2081
|
return {
|
|
937
2082
|
mode,
|
|
938
|
-
dbPath: opts
|
|
2083
|
+
dbPath: resolveDbOpt(opts) ?? null,
|
|
2084
|
+
configPath: opts.config ?? null,
|
|
939
2085
|
host: opts.host,
|
|
940
2086
|
port: parsePort(opts.port),
|
|
941
2087
|
background: mode === "restart"
|
|
@@ -1182,14 +2328,19 @@ async function startBackgroundViewer(invocation) {
|
|
|
1182
2328
|
return;
|
|
1183
2329
|
}
|
|
1184
2330
|
const scriptPath = process.argv[1];
|
|
1185
|
-
if (!scriptPath)
|
|
2331
|
+
if (!scriptPath) {
|
|
2332
|
+
p.log.error("Unable to resolve CLI entrypoint for background launch");
|
|
2333
|
+
process.exitCode = 1;
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
1186
2336
|
const child = spawn(process.execPath, buildForegroundRunnerArgs(scriptPath, invocation), {
|
|
1187
2337
|
cwd: process.cwd(),
|
|
1188
2338
|
detached: true,
|
|
1189
2339
|
stdio: "ignore",
|
|
1190
2340
|
env: {
|
|
1191
2341
|
...process.env,
|
|
1192
|
-
...invocation.dbPath ? { CODEMEM_DB: invocation.dbPath } : {}
|
|
2342
|
+
...invocation.dbPath ? { CODEMEM_DB: invocation.dbPath } : {},
|
|
2343
|
+
...invocation.configPath ? { CODEMEM_CONFIG: invocation.configPath } : {}
|
|
1193
2344
|
}
|
|
1194
2345
|
});
|
|
1195
2346
|
child.unref();
|
|
@@ -1205,6 +2356,7 @@ async function startForegroundViewer(invocation) {
|
|
|
1205
2356
|
const { createApp, createSyncApp, closeStore, getStore } = await import("@codemem/server");
|
|
1206
2357
|
const { serve } = await import("@hono/node-server");
|
|
1207
2358
|
if (invocation.dbPath) process.env.CODEMEM_DB = invocation.dbPath;
|
|
2359
|
+
if (invocation.configPath) process.env.CODEMEM_CONFIG = invocation.configPath;
|
|
1208
2360
|
warnIfViewerExposed(invocation.host, invocation.port);
|
|
1209
2361
|
if (await isPortOpen(invocation.host, invocation.port)) {
|
|
1210
2362
|
p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
|
|
@@ -1229,7 +2381,7 @@ async function startForegroundViewer(invocation) {
|
|
|
1229
2381
|
sweeper.start();
|
|
1230
2382
|
const syncAbort = new AbortController();
|
|
1231
2383
|
const retentionAbort = new AbortController();
|
|
1232
|
-
const syncConfig = readCoordinatorSyncConfig(readCodememConfigFile());
|
|
2384
|
+
const syncConfig = readCoordinatorSyncConfig(invocation.configPath ? readCodememConfigFileAtPath(invocation.configPath) : readCodememConfigFile());
|
|
1233
2385
|
const syncEnabled = syncConfig.syncEnabled;
|
|
1234
2386
|
const retentionRunner = new SyncRetentionRunner({
|
|
1235
2387
|
dbPath: resolveDbPath(invocation.dbPath ?? void 0),
|
|
@@ -1408,17 +2560,31 @@ async function runServeInvocation(invocation) {
|
|
|
1408
2560
|
});
|
|
1409
2561
|
}
|
|
1410
2562
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
2563
|
+
var serveCmd = new Command("serve").configureHelp(helpStyle).description("Run or manage the viewer").argument("[action]", "lifecycle action (start|stop|restart)");
|
|
2564
|
+
addDbOption(serveCmd);
|
|
2565
|
+
addConfigOption(serveCmd);
|
|
2566
|
+
addViewerHostOptions(serveCmd);
|
|
2567
|
+
serveCmd.addOption(new Option("--background", "run viewer in background").hideHelp());
|
|
2568
|
+
serveCmd.addOption(new Option("--foreground", "run viewer in foreground").hideHelp());
|
|
2569
|
+
serveCmd.addOption(new Option("--stop", "stop background viewer").hideHelp());
|
|
2570
|
+
serveCmd.addOption(new Option("--restart", "restart background viewer").hideHelp());
|
|
2571
|
+
var serveCommand = serveCmd.action(async (action, opts) => {
|
|
2572
|
+
try {
|
|
2573
|
+
if (opts.stop) emitDeprecationWarning("--stop", "codemem serve stop");
|
|
2574
|
+
if (opts.restart) emitDeprecationWarning("--restart", "codemem serve restart");
|
|
2575
|
+
if (opts.background) emitDeprecationWarning("--background", "codemem serve start");
|
|
2576
|
+
if (opts.foreground) emitDeprecationWarning("--foreground", "codemem serve start --foreground");
|
|
2577
|
+
const normalizedAction = action === void 0 ? void 0 : action === "start" || action === "stop" || action === "restart" ? action : null;
|
|
2578
|
+
if (normalizedAction === null) {
|
|
2579
|
+
p.log.error(`Unknown serve action: ${action}`);
|
|
2580
|
+
process.exitCode = 1;
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
await runServeInvocation(resolveServeInvocation(normalizedAction, opts));
|
|
2584
|
+
} catch (err) {
|
|
2585
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
1418
2586
|
process.exitCode = 1;
|
|
1419
|
-
return;
|
|
1420
2587
|
}
|
|
1421
|
-
await runServeInvocation(resolveServeInvocation(normalizedAction, opts));
|
|
1422
2588
|
});
|
|
1423
2589
|
//#endregion
|
|
1424
2590
|
//#region src/commands/setup-config.ts
|
|
@@ -1673,8 +2839,11 @@ function fmtTokens(n) {
|
|
|
1673
2839
|
if (n >= 1e3) return `~${(n / 1e3).toFixed(0)}K`;
|
|
1674
2840
|
return `${n}`;
|
|
1675
2841
|
}
|
|
1676
|
-
var
|
|
1677
|
-
|
|
2842
|
+
var statsCmd = new Command("stats").configureHelp(helpStyle).description("Show database statistics");
|
|
2843
|
+
addDbOption(statsCmd);
|
|
2844
|
+
addJsonOption(statsCmd);
|
|
2845
|
+
var statsCommand = statsCmd.action((opts) => {
|
|
2846
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
1678
2847
|
try {
|
|
1679
2848
|
const result = store.stats();
|
|
1680
2849
|
if (opts.json) {
|
|
@@ -1728,7 +2897,9 @@ function buildServeLifecycleArgs(action, opts, scriptPath, execArgv = []) {
|
|
|
1728
2897
|
if (action === "start") args.push("--restart");
|
|
1729
2898
|
else if (action === "stop") args.push("--stop");
|
|
1730
2899
|
else args.push("--restart");
|
|
1731
|
-
|
|
2900
|
+
const dbResolved = resolveDbOpt(opts);
|
|
2901
|
+
if (dbResolved) args.push("--db-path", dbResolved);
|
|
2902
|
+
if (opts.config) args.push("--config", opts.config);
|
|
1732
2903
|
if (opts.host) args.push("--host", opts.host);
|
|
1733
2904
|
if (opts.port) args.push("--port", opts.port);
|
|
1734
2905
|
return args;
|
|
@@ -1752,10 +2923,22 @@ function collectAdvertiseAddresses(explicitAddress, configuredHost, port, interf
|
|
|
1752
2923
|
/**
|
|
1753
2924
|
* Sync CLI commands — enable/disable/status/peers/connect.
|
|
1754
2925
|
*/
|
|
2926
|
+
function readCliConfig(configPath) {
|
|
2927
|
+
return configPath ? readCodememConfigFileAtPath(configPath) : readCodememConfigFile();
|
|
2928
|
+
}
|
|
2929
|
+
function writeCliConfig(config, configPath) {
|
|
2930
|
+
return writeCodememConfigFile(config, configPath || void 0);
|
|
2931
|
+
}
|
|
1755
2932
|
function parseAttemptsLimit(value) {
|
|
1756
2933
|
if (!/^\d+$/.test(value.trim())) throw new Error(`Invalid --limit: ${value}`);
|
|
1757
2934
|
return Number.parseInt(value, 10);
|
|
1758
2935
|
}
|
|
2936
|
+
function parsePositiveIntegerOption(value, flagName) {
|
|
2937
|
+
if (value == null) return void 0;
|
|
2938
|
+
const trimmed = value.trim();
|
|
2939
|
+
if (!/^\d+$/.test(trimmed)) throw new Error(`Invalid ${flagName}: ${value}`);
|
|
2940
|
+
return Number.parseInt(trimmed, 10);
|
|
2941
|
+
}
|
|
1759
2942
|
function resolvePeerMatch(db, peerRef) {
|
|
1760
2943
|
const trimmed = peerRef.trim();
|
|
1761
2944
|
if (!trimmed) return null;
|
|
@@ -1771,18 +2954,6 @@ function resolvePeerMatch(db, peerRef) {
|
|
|
1771
2954
|
if (byName.length > 1) return "ambiguous";
|
|
1772
2955
|
return byName[0] ?? null;
|
|
1773
2956
|
}
|
|
1774
|
-
function readCoordinatorPublicKey(opts) {
|
|
1775
|
-
const inline = String(opts.publicKey ?? "").trim();
|
|
1776
|
-
const filePath = String(opts.publicKeyFile ?? "").trim();
|
|
1777
|
-
if (inline && filePath) throw new Error("Use only one of --public-key or --public-key-file");
|
|
1778
|
-
if (filePath) {
|
|
1779
|
-
const text = readFileSync(filePath, "utf8").trim();
|
|
1780
|
-
if (!text) throw new Error(`Public key file is empty: ${filePath}`);
|
|
1781
|
-
return text;
|
|
1782
|
-
}
|
|
1783
|
-
if (!inline) throw new Error("Public key required via --public-key or --public-key-file");
|
|
1784
|
-
return inline;
|
|
1785
|
-
}
|
|
1786
2957
|
async function portOpen(host, port) {
|
|
1787
2958
|
return new Promise((resolve) => {
|
|
1788
2959
|
const socket = net.createConnection({
|
|
@@ -1828,12 +2999,13 @@ function parseStoredAddressEndpoint(value) {
|
|
|
1828
2999
|
async function runServeLifecycle(action, opts) {
|
|
1829
3000
|
if (opts.user === false || opts.system === true) p.log.warn("TS sync lifecycle currently manages the local viewer process, not separate user/system services.");
|
|
1830
3001
|
if (action === "start") {
|
|
1831
|
-
if (
|
|
3002
|
+
if (readCoordinatorSyncConfig(readCliConfig(opts.config)).syncEnabled !== true) {
|
|
1832
3003
|
p.log.error("Sync is disabled. Run `codemem sync enable` first.");
|
|
1833
3004
|
process.exitCode = 1;
|
|
1834
3005
|
return;
|
|
1835
3006
|
}
|
|
1836
3007
|
}
|
|
3008
|
+
const dbResolved = resolveDbOpt(opts);
|
|
1837
3009
|
const args = buildServeLifecycleArgs(action, opts, process.argv[1] ?? "", process.execArgv);
|
|
1838
3010
|
await new Promise((resolve, reject) => {
|
|
1839
3011
|
const child = spawn(process.execPath, args, {
|
|
@@ -1841,7 +3013,8 @@ async function runServeLifecycle(action, opts) {
|
|
|
1841
3013
|
stdio: "inherit",
|
|
1842
3014
|
env: {
|
|
1843
3015
|
...process.env,
|
|
1844
|
-
...
|
|
3016
|
+
...dbResolved ? { CODEMEM_DB: dbResolved } : {},
|
|
3017
|
+
...opts.config ? { CODEMEM_CONFIG: opts.config } : {}
|
|
1845
3018
|
}
|
|
1846
3019
|
});
|
|
1847
3020
|
child.once("error", reject);
|
|
@@ -1852,8 +3025,11 @@ async function runServeLifecycle(action, opts) {
|
|
|
1852
3025
|
});
|
|
1853
3026
|
}
|
|
1854
3027
|
var syncCommand = new Command("sync").configureHelp(helpStyle).description("Sync configuration and peer management");
|
|
1855
|
-
|
|
1856
|
-
|
|
3028
|
+
var attemptsCmd = new Command("attempts").configureHelp(helpStyle).description("Show recent sync attempts").option("--limit <n>", "max attempts", "10");
|
|
3029
|
+
addDbOption(attemptsCmd);
|
|
3030
|
+
addJsonOption(attemptsCmd);
|
|
3031
|
+
attemptsCmd.action((opts) => {
|
|
3032
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
1857
3033
|
try {
|
|
1858
3034
|
const d = drizzle(store.db, { schema });
|
|
1859
3035
|
const limit = parseAttemptsLimit(opts.limit);
|
|
@@ -1873,19 +3049,30 @@ syncCommand.addCommand(new Command("attempts").configureHelp(helpStyle).descript
|
|
|
1873
3049
|
} finally {
|
|
1874
3050
|
store.close();
|
|
1875
3051
|
}
|
|
1876
|
-
})
|
|
1877
|
-
syncCommand.addCommand(
|
|
1878
|
-
|
|
1879
|
-
})
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
})
|
|
1886
|
-
|
|
1887
|
-
|
|
3052
|
+
});
|
|
3053
|
+
syncCommand.addCommand(attemptsCmd);
|
|
3054
|
+
function addDeprecatedLifecycleCommand(action) {
|
|
3055
|
+
const cmd = new Command(action).configureHelp(helpStyle).description(`[deprecated] ${action} sync daemon — use 'codemem serve ${action}'`);
|
|
3056
|
+
addDbOption(cmd);
|
|
3057
|
+
addConfigOption(cmd);
|
|
3058
|
+
addViewerHostOptions(cmd);
|
|
3059
|
+
addLegacyServiceFlags(cmd);
|
|
3060
|
+
cmd.action(async (opts) => {
|
|
3061
|
+
emitDeprecationWarning(`codemem sync ${action}`, `codemem serve ${action}`);
|
|
3062
|
+
await runServeLifecycle(action, opts);
|
|
3063
|
+
});
|
|
3064
|
+
syncCommand.addCommand(cmd, { hidden: true });
|
|
3065
|
+
}
|
|
3066
|
+
addDeprecatedLifecycleCommand("start");
|
|
3067
|
+
addDeprecatedLifecycleCommand("stop");
|
|
3068
|
+
addDeprecatedLifecycleCommand("restart");
|
|
3069
|
+
var onceCmd = new Command("once").configureHelp(helpStyle).description("Run a single sync pass").option("--peer <peer>", "peer device id or name");
|
|
3070
|
+
addDbOption(onceCmd);
|
|
3071
|
+
addJsonOption(onceCmd);
|
|
3072
|
+
onceCmd.action(async (opts) => {
|
|
3073
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
1888
3074
|
try {
|
|
3075
|
+
const keysDir = process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
|
|
1889
3076
|
syncPassPreflight(store.db);
|
|
1890
3077
|
const d = drizzle(store.db, { schema });
|
|
1891
3078
|
const rows = opts.peer ? (() => {
|
|
@@ -1893,6 +3080,10 @@ syncCommand.addCommand(new Command("once").configureHelp(helpStyle).description(
|
|
|
1893
3080
|
if (deviceMatches.length > 0) return deviceMatches;
|
|
1894
3081
|
const nameMatches = d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).where(eq(schema.syncPeers.name, opts.peer)).all();
|
|
1895
3082
|
if (nameMatches.length > 1) {
|
|
3083
|
+
if (opts.json) {
|
|
3084
|
+
emitJsonError("ambiguous_peer", `Peer name is ambiguous: ${opts.peer}`);
|
|
3085
|
+
return [];
|
|
3086
|
+
}
|
|
1896
3087
|
p.log.error(`Peer name is ambiguous: ${opts.peer}`);
|
|
1897
3088
|
process.exitCode = 1;
|
|
1898
3089
|
return [];
|
|
@@ -1900,31 +3091,59 @@ syncCommand.addCommand(new Command("once").configureHelp(helpStyle).description(
|
|
|
1900
3091
|
return nameMatches;
|
|
1901
3092
|
})() : d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).all();
|
|
1902
3093
|
if (rows.length === 0) {
|
|
3094
|
+
if (process.exitCode) return;
|
|
3095
|
+
if (opts.json) {
|
|
3096
|
+
emitJsonError("no_peers", "No peers available for sync");
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
1903
3099
|
p.log.warn("No peers available for sync");
|
|
1904
3100
|
process.exitCode = 1;
|
|
1905
3101
|
return;
|
|
1906
3102
|
}
|
|
1907
3103
|
let hadFailure = false;
|
|
3104
|
+
const results = [];
|
|
1908
3105
|
for (const row of rows) {
|
|
1909
|
-
const result = await runSyncPass(store.db, row.peer_device_id);
|
|
3106
|
+
const result = await runSyncPass(store.db, row.peer_device_id, { keysDir });
|
|
1910
3107
|
if (!result.ok) hadFailure = true;
|
|
1911
|
-
|
|
3108
|
+
results.push({
|
|
3109
|
+
peer_device_id: row.peer_device_id,
|
|
3110
|
+
ok: result.ok,
|
|
3111
|
+
...result.error ? { error: result.error } : {}
|
|
3112
|
+
});
|
|
3113
|
+
if (!opts.json) console.log(formatSyncOnceResult(row.peer_device_id, result));
|
|
1912
3114
|
}
|
|
3115
|
+
if (opts.json) console.log(JSON.stringify({
|
|
3116
|
+
ok: !hadFailure,
|
|
3117
|
+
results
|
|
3118
|
+
}, null, 2));
|
|
1913
3119
|
if (hadFailure) process.exitCode = 1;
|
|
1914
3120
|
} finally {
|
|
1915
3121
|
store.close();
|
|
1916
3122
|
}
|
|
1917
|
-
})
|
|
1918
|
-
syncCommand.addCommand(
|
|
1919
|
-
|
|
3123
|
+
});
|
|
3124
|
+
syncCommand.addCommand(onceCmd);
|
|
3125
|
+
var pairCmd = 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");
|
|
3126
|
+
addDbOption(pairCmd);
|
|
3127
|
+
addConfigOption(pairCmd);
|
|
3128
|
+
addJsonOption(pairCmd);
|
|
3129
|
+
pairCmd.action(async (opts) => {
|
|
3130
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
1920
3131
|
try {
|
|
1921
3132
|
const acceptModeRequested = opts.accept != null || opts.acceptFile != null;
|
|
1922
3133
|
if (opts.payloadOnly && acceptModeRequested) {
|
|
3134
|
+
if (opts.json) {
|
|
3135
|
+
emitJsonError("usage_error", "--payload-only cannot be combined with --accept or --accept-file", 2);
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
1923
3138
|
p.log.error("--payload-only cannot be combined with --accept or --accept-file");
|
|
1924
3139
|
process.exitCode = 1;
|
|
1925
3140
|
return;
|
|
1926
3141
|
}
|
|
1927
3142
|
if (opts.accept && opts.acceptFile) {
|
|
3143
|
+
if (opts.json) {
|
|
3144
|
+
emitJsonError("usage_error", "Use only one of --accept or --accept-file", 2);
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
1928
3147
|
p.log.error("Use only one of --accept or --accept-file");
|
|
1929
3148
|
process.exitCode = 1;
|
|
1930
3149
|
return;
|
|
@@ -1941,27 +3160,48 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
1941
3160
|
process.stdin.on("error", reject);
|
|
1942
3161
|
}) : readFileSync(opts.acceptFile, "utf8");
|
|
1943
3162
|
} catch (error) {
|
|
1944
|
-
|
|
3163
|
+
const msg = error instanceof Error ? `Failed to read pairing payload from ${opts.acceptFile}: ${error.message}` : `Failed to read pairing payload from ${opts.acceptFile}`;
|
|
3164
|
+
if (opts.json) {
|
|
3165
|
+
emitJsonError("read_error", msg);
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
p.log.error(msg);
|
|
1945
3169
|
process.exitCode = 1;
|
|
1946
3170
|
return;
|
|
1947
3171
|
}
|
|
1948
3172
|
if (acceptModeRequested && !(acceptText ?? "").trim()) {
|
|
3173
|
+
if (opts.json) {
|
|
3174
|
+
emitJsonError("usage_error", "Empty pairing payload; provide JSON via --accept or --accept-file", 2);
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
1949
3177
|
p.log.error("Empty pairing payload; provide JSON via --accept or --accept-file");
|
|
1950
3178
|
process.exitCode = 1;
|
|
1951
3179
|
return;
|
|
1952
3180
|
}
|
|
1953
3181
|
if (!acceptText && (opts.include || opts.exclude || opts.all || opts.default)) {
|
|
3182
|
+
if (opts.json) {
|
|
3183
|
+
emitJsonError("usage_error", "Project filters are outbound-only and must be set on the device running --accept", 2);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
1954
3186
|
p.log.error("Project filters are outbound-only and must be set on the device running --accept");
|
|
1955
3187
|
process.exitCode = 1;
|
|
1956
3188
|
return;
|
|
1957
3189
|
}
|
|
1958
3190
|
if (acceptText?.trim()) {
|
|
1959
3191
|
if (opts.all && opts.default) {
|
|
3192
|
+
if (opts.json) {
|
|
3193
|
+
emitJsonError("usage_error", "Use only one of --all or --default", 2);
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
1960
3196
|
p.log.error("Use only one of --all or --default");
|
|
1961
3197
|
process.exitCode = 1;
|
|
1962
3198
|
return;
|
|
1963
3199
|
}
|
|
1964
3200
|
if ((opts.all || opts.default) && (opts.include || opts.exclude)) {
|
|
3201
|
+
if (opts.json) {
|
|
3202
|
+
emitJsonError("usage_error", "--include/--exclude cannot be combined with --all/--default", 2);
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
1965
3205
|
p.log.error("--include/--exclude cannot be combined with --all/--default");
|
|
1966
3206
|
process.exitCode = 1;
|
|
1967
3207
|
return;
|
|
@@ -1970,7 +3210,12 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
1970
3210
|
try {
|
|
1971
3211
|
payload = JSON.parse(acceptText);
|
|
1972
3212
|
} catch (error) {
|
|
1973
|
-
|
|
3213
|
+
const msg = error instanceof Error ? `Invalid pairing payload: ${error.message}` : "Invalid pairing payload";
|
|
3214
|
+
if (opts.json) {
|
|
3215
|
+
emitJsonError("invalid_payload", msg);
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
p.log.error(msg);
|
|
1974
3219
|
process.exitCode = 1;
|
|
1975
3220
|
return;
|
|
1976
3221
|
}
|
|
@@ -1979,11 +3224,20 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
1979
3224
|
const publicKey = String(payload.public_key || "").trim();
|
|
1980
3225
|
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()) : [];
|
|
1981
3226
|
if (!deviceId || !fingerprint || !publicKey || resolvedAddresses.length === 0) {
|
|
1982
|
-
|
|
3227
|
+
const msg = "Pairing payload missing device_id, fingerprint, public_key, or addresses";
|
|
3228
|
+
if (opts.json) {
|
|
3229
|
+
emitJsonError("invalid_payload", msg);
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
p.log.error(msg);
|
|
1983
3233
|
process.exitCode = 1;
|
|
1984
3234
|
return;
|
|
1985
3235
|
}
|
|
1986
3236
|
if (fingerprintPublicKey(publicKey) !== fingerprint) {
|
|
3237
|
+
if (opts.json) {
|
|
3238
|
+
emitJsonError("fingerprint_mismatch", "Pairing payload fingerprint mismatch");
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
1987
3241
|
p.log.error("Pairing payload fingerprint mismatch");
|
|
1988
3242
|
process.exitCode = 1;
|
|
1989
3243
|
return;
|
|
@@ -2001,6 +3255,13 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
2001
3255
|
include: opts.all ? [] : parseProjectList(opts.include),
|
|
2002
3256
|
exclude: opts.all ? [] : parseProjectList(opts.exclude)
|
|
2003
3257
|
});
|
|
3258
|
+
if (opts.json) {
|
|
3259
|
+
console.log(JSON.stringify({
|
|
3260
|
+
ok: true,
|
|
3261
|
+
peer_device_id: deviceId
|
|
3262
|
+
}));
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
2004
3265
|
p.log.success(`Paired with ${deviceId}`);
|
|
2005
3266
|
return;
|
|
2006
3267
|
}
|
|
@@ -2008,24 +3269,28 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
2008
3269
|
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir });
|
|
2009
3270
|
const publicKey = loadPublicKey(keysDir);
|
|
2010
3271
|
if (!publicKey) {
|
|
3272
|
+
if (opts.json) {
|
|
3273
|
+
emitJsonError("missing_key", "Public key missing");
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
2011
3276
|
p.log.error("Public key missing");
|
|
2012
3277
|
process.exitCode = 1;
|
|
2013
3278
|
return;
|
|
2014
3279
|
}
|
|
2015
|
-
const config =
|
|
3280
|
+
const config = readCliConfig(opts.config);
|
|
2016
3281
|
const explicitAddress = opts.address?.trim();
|
|
2017
3282
|
const configuredHost = typeof config.sync_host === "string" ? config.sync_host : null;
|
|
2018
3283
|
const configuredPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
|
|
2019
3284
|
const addresses = collectAdvertiseAddresses(explicitAddress ?? null, configuredHost, configuredPort, networkInterfaces());
|
|
2020
|
-
const
|
|
3285
|
+
const payloadObj = {
|
|
2021
3286
|
device_id: deviceId,
|
|
2022
3287
|
fingerprint,
|
|
2023
3288
|
public_key: publicKey,
|
|
2024
3289
|
address: addresses[0] ?? "",
|
|
2025
3290
|
addresses
|
|
2026
3291
|
};
|
|
2027
|
-
const payloadText = JSON.stringify(
|
|
2028
|
-
if (opts.payloadOnly) {
|
|
3292
|
+
const payloadText = JSON.stringify(payloadObj);
|
|
3293
|
+
if (opts.payloadOnly || opts.json) {
|
|
2029
3294
|
process.stdout.write(`${payloadText}\n`);
|
|
2030
3295
|
return;
|
|
2031
3296
|
}
|
|
@@ -2042,10 +3307,15 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
2042
3307
|
} finally {
|
|
2043
3308
|
store.close();
|
|
2044
3309
|
}
|
|
2045
|
-
})
|
|
2046
|
-
syncCommand.addCommand(
|
|
2047
|
-
|
|
2048
|
-
|
|
3310
|
+
});
|
|
3311
|
+
syncCommand.addCommand(pairCmd);
|
|
3312
|
+
var doctorCmd = new Command("doctor").configureHelp(helpStyle).description("Diagnose common sync setup and connectivity issues");
|
|
3313
|
+
addDbOption(doctorCmd);
|
|
3314
|
+
addConfigOption(doctorCmd);
|
|
3315
|
+
addJsonOption(doctorCmd);
|
|
3316
|
+
doctorCmd.action(async (opts) => {
|
|
3317
|
+
const config = readCliConfig(opts.config);
|
|
3318
|
+
const dbPath = resolveDbPath(resolveDbOpt(opts));
|
|
2049
3319
|
const store = new MemoryStore(dbPath);
|
|
2050
3320
|
try {
|
|
2051
3321
|
const d = drizzle(store.db, { schema });
|
|
@@ -2061,47 +3331,70 @@ syncCommand.addCommand(new Command("doctor").configureHelp(helpStyle).descriptio
|
|
|
2061
3331
|
const syncHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
|
|
2062
3332
|
const syncPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
|
|
2063
3333
|
const viewerBinding = readViewerBinding(dbPath);
|
|
3334
|
+
const reachable = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
|
|
3335
|
+
if (!reachable) issues.push("daemon not running");
|
|
3336
|
+
if (!device) issues.push("identity missing");
|
|
3337
|
+
if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) issues.push("daemon error");
|
|
3338
|
+
if (peers.length === 0) issues.push("no peers");
|
|
3339
|
+
if (!config.sync_enabled) issues.push("sync is disabled");
|
|
3340
|
+
const peerDetails = [];
|
|
3341
|
+
for (const peer of peers) {
|
|
3342
|
+
const addresses = peer.addresses_json ? JSON.parse(peer.addresses_json) : [];
|
|
3343
|
+
const endpoint = parseStoredAddressEndpoint(addresses[0] ?? "");
|
|
3344
|
+
const reach = endpoint ? await portOpen(endpoint.host, endpoint.port) ? "ok" : "unreachable" : "unknown";
|
|
3345
|
+
const pinned = Boolean(peer.pinned_fingerprint);
|
|
3346
|
+
const hasKey = Boolean(peer.public_key);
|
|
3347
|
+
peerDetails.push({
|
|
3348
|
+
peer_device_id: peer.peer_device_id,
|
|
3349
|
+
addresses: addresses.length,
|
|
3350
|
+
reachable: reach,
|
|
3351
|
+
pinned,
|
|
3352
|
+
has_public_key: hasKey
|
|
3353
|
+
});
|
|
3354
|
+
if (reach !== "ok") issues.push(`peer ${peer.peer_device_id} unreachable`);
|
|
3355
|
+
if (!pinned || !hasKey) issues.push(`peer ${peer.peer_device_id} not pinned`);
|
|
3356
|
+
}
|
|
3357
|
+
if (opts.json) {
|
|
3358
|
+
console.log(JSON.stringify({
|
|
3359
|
+
enabled: config.sync_enabled === true,
|
|
3360
|
+
listen: `${syncHost}:${syncPort}`,
|
|
3361
|
+
mdns: process.env.CODEMEM_SYNC_MDNS ? "env-configured" : "default/off",
|
|
3362
|
+
daemon: reachable ? "running" : "not running",
|
|
3363
|
+
identity: device?.device_id ?? null,
|
|
3364
|
+
daemon_error: daemonState?.last_error ?? null,
|
|
3365
|
+
peers: peerDetails,
|
|
3366
|
+
issues: [...new Set(issues)],
|
|
3367
|
+
ok: issues.length === 0
|
|
3368
|
+
}));
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
2064
3371
|
console.log("Sync doctor");
|
|
2065
3372
|
console.log(`- Enabled: ${config.sync_enabled === true}`);
|
|
2066
3373
|
console.log(`- Listen: ${syncHost}:${syncPort}`);
|
|
2067
3374
|
console.log(`- mDNS: ${process.env.CODEMEM_SYNC_MDNS ? "env-configured" : "default/off"}`);
|
|
2068
|
-
const reachable = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
|
|
2069
3375
|
console.log(`- Daemon: ${reachable ? "running" : "not running"}`);
|
|
2070
|
-
if (!
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) {
|
|
2076
|
-
console.log(`- Daemon error: ${daemonState.last_error} (at ${daemonState.last_error_at ?? "unknown"})`);
|
|
2077
|
-
issues.push("daemon error");
|
|
2078
|
-
}
|
|
2079
|
-
if (peers.length === 0) {
|
|
2080
|
-
console.log("- Peers: none (pair a device first)");
|
|
2081
|
-
issues.push("no peers");
|
|
2082
|
-
} else {
|
|
3376
|
+
if (!device) console.log("- Identity: missing (run `codemem sync enable`)");
|
|
3377
|
+
else console.log(`- Identity: ${device.device_id}`);
|
|
3378
|
+
if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) console.log(`- Daemon error: ${daemonState.last_error} (at ${daemonState.last_error_at ?? "unknown"})`);
|
|
3379
|
+
if (peers.length === 0) console.log("- Peers: none (pair a device first)");
|
|
3380
|
+
else {
|
|
2083
3381
|
console.log(`- Peers: ${peers.length}`);
|
|
2084
|
-
for (const
|
|
2085
|
-
const addresses = peer.addresses_json ? JSON.parse(peer.addresses_json) : [];
|
|
2086
|
-
const endpoint = parseStoredAddressEndpoint(addresses[0] ?? "");
|
|
2087
|
-
const reach = endpoint ? await portOpen(endpoint.host, endpoint.port) ? "ok" : "unreachable" : "unknown";
|
|
2088
|
-
const pinned = Boolean(peer.pinned_fingerprint);
|
|
2089
|
-
const hasKey = Boolean(peer.public_key);
|
|
2090
|
-
console.log(` - ${peer.peer_device_id}: addresses=${addresses.length} reach=${reach} pinned=${pinned} public_key=${hasKey}`);
|
|
2091
|
-
if (reach !== "ok") issues.push(`peer ${peer.peer_device_id} unreachable`);
|
|
2092
|
-
if (!pinned || !hasKey) issues.push(`peer ${peer.peer_device_id} not pinned`);
|
|
2093
|
-
}
|
|
3382
|
+
for (const detail of peerDetails) console.log(` - ${detail.peer_device_id}: addresses=${detail.addresses} reach=${detail.reachable} pinned=${detail.pinned} public_key=${detail.has_public_key}`);
|
|
2094
3383
|
}
|
|
2095
|
-
if (!config.sync_enabled) issues.push("sync is disabled");
|
|
2096
3384
|
if (issues.length > 0) console.log(`WARN: ${[...new Set(issues)].slice(0, 3).join(", ")}`);
|
|
2097
3385
|
else console.log("OK: sync looks healthy");
|
|
2098
3386
|
} finally {
|
|
2099
3387
|
store.close();
|
|
2100
3388
|
}
|
|
2101
|
-
})
|
|
2102
|
-
syncCommand.addCommand(
|
|
2103
|
-
|
|
2104
|
-
|
|
3389
|
+
});
|
|
3390
|
+
syncCommand.addCommand(doctorCmd);
|
|
3391
|
+
var statusCmd = new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary");
|
|
3392
|
+
addDbOption(statusCmd);
|
|
3393
|
+
addConfigOption(statusCmd);
|
|
3394
|
+
addJsonOption(statusCmd);
|
|
3395
|
+
statusCmd.action((opts) => {
|
|
3396
|
+
const config = readCliConfig(opts.config);
|
|
3397
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
2105
3398
|
try {
|
|
2106
3399
|
const d = drizzle(store.db, { schema });
|
|
2107
3400
|
const deviceRow = d.select({
|
|
@@ -2151,17 +3444,41 @@ syncCommand.addCommand(new Command("status").configureHelp(helpStyle).descriptio
|
|
|
2151
3444
|
} finally {
|
|
2152
3445
|
store.close();
|
|
2153
3446
|
}
|
|
2154
|
-
})
|
|
2155
|
-
syncCommand.addCommand(
|
|
2156
|
-
|
|
3447
|
+
});
|
|
3448
|
+
syncCommand.addCommand(statusCmd);
|
|
3449
|
+
var enableCmd = new Command("enable").configureHelp(helpStyle).description("Enable sync and initialize device identity").option("--sync-host <host>", "sync listen host").option("--sync-port <port>", "sync listen port").option("--interval <seconds>", "sync interval in seconds");
|
|
3450
|
+
addDbOption(enableCmd);
|
|
3451
|
+
addConfigOption(enableCmd);
|
|
3452
|
+
addJsonOption(enableCmd);
|
|
3453
|
+
enableCmd.addOption(new Option("--host <host>", "sync listen host").hideHelp());
|
|
3454
|
+
enableCmd.addOption(new Option("--port <port>", "sync listen port").hideHelp());
|
|
3455
|
+
enableCmd.action((opts) => {
|
|
3456
|
+
if (opts.host && !opts.syncHost) emitDeprecationWarning("--host on sync enable", "--sync-host");
|
|
3457
|
+
if (opts.port && !opts.syncPort) emitDeprecationWarning("--port on sync enable", "--sync-port");
|
|
3458
|
+
const effectiveHost = opts.syncHost ?? opts.host;
|
|
3459
|
+
const effectivePort = opts.syncPort ?? opts.port;
|
|
3460
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
2157
3461
|
try {
|
|
2158
3462
|
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db);
|
|
2159
|
-
const config =
|
|
3463
|
+
const config = readCliConfig(opts.config);
|
|
2160
3464
|
config.sync_enabled = true;
|
|
2161
|
-
if (
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
3465
|
+
if (effectiveHost) config.sync_host = effectiveHost;
|
|
3466
|
+
const syncPort = parsePositiveIntegerOption(effectivePort, "--sync-port");
|
|
3467
|
+
const syncInterval = parsePositiveIntegerOption(opts.interval, "--interval");
|
|
3468
|
+
if (syncPort != null) config.sync_port = syncPort;
|
|
3469
|
+
if (syncInterval != null) config.sync_interval_s = syncInterval;
|
|
3470
|
+
writeCliConfig(config, opts.config);
|
|
3471
|
+
if (opts.json) {
|
|
3472
|
+
console.log(JSON.stringify({
|
|
3473
|
+
ok: true,
|
|
3474
|
+
device_id: deviceId,
|
|
3475
|
+
fingerprint,
|
|
3476
|
+
host: config.sync_host ?? "0.0.0.0",
|
|
3477
|
+
port: config.sync_port ?? 7337,
|
|
3478
|
+
interval_s: config.sync_interval_s ?? 120
|
|
3479
|
+
}));
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
2165
3482
|
p.intro("codemem sync enable");
|
|
2166
3483
|
p.log.success([
|
|
2167
3484
|
`Device ID: ${deviceId}`,
|
|
@@ -2174,16 +3491,23 @@ syncCommand.addCommand(new Command("enable").configureHelp(helpStyle).descriptio
|
|
|
2174
3491
|
} finally {
|
|
2175
3492
|
store.close();
|
|
2176
3493
|
}
|
|
2177
|
-
})
|
|
2178
|
-
syncCommand.addCommand(
|
|
2179
|
-
|
|
3494
|
+
});
|
|
3495
|
+
syncCommand.addCommand(enableCmd);
|
|
3496
|
+
var disableCmd = new Command("disable").configureHelp(helpStyle).description("Disable sync without deleting keys or peers");
|
|
3497
|
+
addConfigOption(disableCmd);
|
|
3498
|
+
disableCmd.action((opts) => {
|
|
3499
|
+
const config = readCliConfig(opts.config);
|
|
2180
3500
|
config.sync_enabled = false;
|
|
2181
|
-
|
|
3501
|
+
writeCliConfig(config, opts.config);
|
|
2182
3502
|
p.intro("codemem sync disable");
|
|
2183
3503
|
p.outro("Sync disabled — restart `codemem serve` to take effect");
|
|
2184
|
-
})
|
|
2185
|
-
|
|
2186
|
-
|
|
3504
|
+
});
|
|
3505
|
+
syncCommand.addCommand(disableCmd);
|
|
3506
|
+
var peersCommand = new Command("peers").configureHelp(helpStyle).description("List known sync peers");
|
|
3507
|
+
addDbOption(peersCommand);
|
|
3508
|
+
addJsonOption(peersCommand);
|
|
3509
|
+
peersCommand.action((opts) => {
|
|
3510
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
2187
3511
|
try {
|
|
2188
3512
|
const peers = drizzle(store.db, { schema }).select({
|
|
2189
3513
|
peer_device_id: schema.syncPeers.peer_device_id,
|
|
@@ -2211,17 +3535,28 @@ var peersCommand = new Command("peers").configureHelp(helpStyle).description("Li
|
|
|
2211
3535
|
store.close();
|
|
2212
3536
|
}
|
|
2213
3537
|
});
|
|
2214
|
-
|
|
2215
|
-
|
|
3538
|
+
var peersRemoveCmd = new Command("remove").configureHelp(helpStyle).description("Remove a sync peer by device id or exact name").argument("<peer>", "peer device id or exact name");
|
|
3539
|
+
addDbOption(peersRemoveCmd);
|
|
3540
|
+
addJsonOption(peersRemoveCmd);
|
|
3541
|
+
peersRemoveCmd.action((peerRef, opts) => {
|
|
3542
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
2216
3543
|
try {
|
|
2217
3544
|
const d = drizzle(store.db, { schema });
|
|
2218
3545
|
const match = resolvePeerMatch(d, peerRef);
|
|
2219
3546
|
if (match === "ambiguous") {
|
|
3547
|
+
if (opts.json) {
|
|
3548
|
+
emitJsonError("ambiguous_peer", `Peer name is ambiguous: ${peerRef.trim()}`);
|
|
3549
|
+
return;
|
|
3550
|
+
}
|
|
2220
3551
|
p.log.error(`Peer name is ambiguous: ${peerRef.trim()}`);
|
|
2221
3552
|
process.exitCode = 1;
|
|
2222
3553
|
return;
|
|
2223
3554
|
}
|
|
2224
3555
|
if (!match) {
|
|
3556
|
+
if (opts.json) {
|
|
3557
|
+
emitJsonError("peer_not_found", `Peer not found: ${peerRef.trim()}`);
|
|
3558
|
+
return;
|
|
3559
|
+
}
|
|
2225
3560
|
p.log.error(`Peer not found: ${peerRef.trim()}`);
|
|
2226
3561
|
process.exitCode = 1;
|
|
2227
3562
|
return;
|
|
@@ -2243,267 +3578,180 @@ peersCommand.addCommand(new Command("remove").configureHelp(helpStyle).descripti
|
|
|
2243
3578
|
} finally {
|
|
2244
3579
|
store.close();
|
|
2245
3580
|
}
|
|
2246
|
-
})
|
|
3581
|
+
});
|
|
3582
|
+
peersCommand.addCommand(peersRemoveCmd);
|
|
2247
3583
|
syncCommand.addCommand(peersCommand);
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
groupId,
|
|
2262
|
-
displayName: opts.name?.trim() || null,
|
|
2263
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2264
|
-
});
|
|
2265
|
-
if (opts.json) {
|
|
2266
|
-
console.log(JSON.stringify(group, null, 2));
|
|
2267
|
-
return;
|
|
2268
|
-
}
|
|
2269
|
-
p.intro("codemem sync coordinator group-create");
|
|
2270
|
-
p.log.success(`Group ready: ${groupId.trim()}`);
|
|
2271
|
-
p.outro(String(group.display_name ?? group.group_id ?? groupId.trim()));
|
|
2272
|
-
}));
|
|
2273
|
-
coordinatorCommand.addCommand(new Command("list-groups").configureHelp(helpStyle).description("List coordinator groups from the local store").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (opts) => {
|
|
2274
|
-
const groups = await coordinatorListGroupsAction({ dbPath: opts.db ?? opts.dbPath ?? null });
|
|
2275
|
-
if (opts.json) {
|
|
2276
|
-
console.log(JSON.stringify(groups, null, 2));
|
|
2277
|
-
return;
|
|
2278
|
-
}
|
|
2279
|
-
p.intro("codemem sync coordinator list-groups");
|
|
2280
|
-
if (groups.length === 0) {
|
|
2281
|
-
p.outro("No coordinator groups found");
|
|
2282
|
-
return;
|
|
2283
|
-
}
|
|
2284
|
-
for (const group of groups) p.log.message(`- ${String(group.group_id ?? "")}${group.display_name ? ` (${String(group.display_name)})` : ""}`);
|
|
2285
|
-
p.outro(`${groups.length} group(s)`);
|
|
2286
|
-
}));
|
|
2287
|
-
coordinatorCommand.addCommand(new Command("enroll-device").configureHelp(helpStyle).description("Enroll a device in a local coordinator group").argument("<group>", "group id").argument("<device-id>", "device id").option("--fingerprint <fingerprint>", "device fingerprint").option("--public-key <key>", "device public key").option("--public-key-file <path>", "path to device public key").option("--name <name>", "display name").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, deviceId, opts) => {
|
|
2288
|
-
const publicKey = readCoordinatorPublicKey(opts);
|
|
2289
|
-
const fingerprint = String(opts.fingerprint ?? "").trim();
|
|
2290
|
-
if (!fingerprint) {
|
|
2291
|
-
p.log.error("Fingerprint required via --fingerprint");
|
|
2292
|
-
process.exitCode = 1;
|
|
2293
|
-
return;
|
|
2294
|
-
}
|
|
2295
|
-
if (fingerprintPublicKey(publicKey) !== fingerprint) {
|
|
2296
|
-
p.log.error("Fingerprint does not match the provided public key");
|
|
2297
|
-
process.exitCode = 1;
|
|
2298
|
-
return;
|
|
2299
|
-
}
|
|
2300
|
-
const enrollment = await coordinatorEnrollDeviceAction({
|
|
2301
|
-
groupId,
|
|
2302
|
-
deviceId,
|
|
2303
|
-
fingerprint,
|
|
2304
|
-
publicKey,
|
|
2305
|
-
displayName: opts.name?.trim() || null,
|
|
2306
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2307
|
-
});
|
|
2308
|
-
if (opts.json) {
|
|
2309
|
-
console.log(JSON.stringify(enrollment, null, 2));
|
|
2310
|
-
return;
|
|
2311
|
-
}
|
|
2312
|
-
p.intro("codemem sync coordinator enroll-device");
|
|
2313
|
-
p.log.success(`Enrolled ${deviceId.trim()} in ${groupId.trim()}`);
|
|
2314
|
-
p.outro(String(enrollment.display_name ?? enrollment.device_id ?? deviceId.trim()));
|
|
2315
|
-
}));
|
|
2316
|
-
coordinatorCommand.addCommand(new Command("list-devices").configureHelp(helpStyle).description("List enrolled devices in a local coordinator group").argument("<group>", "group id").option("--include-disabled", "include disabled devices").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, opts) => {
|
|
2317
|
-
const rows = await coordinatorListDevicesAction({
|
|
2318
|
-
groupId,
|
|
2319
|
-
includeDisabled: opts.includeDisabled === true,
|
|
2320
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2321
|
-
});
|
|
2322
|
-
if (opts.json) {
|
|
2323
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
2324
|
-
return;
|
|
2325
|
-
}
|
|
2326
|
-
p.intro("codemem sync coordinator list-devices");
|
|
2327
|
-
if (rows.length === 0) {
|
|
2328
|
-
p.outro(`No enrolled devices for ${groupId.trim()}`);
|
|
2329
|
-
return;
|
|
2330
|
-
}
|
|
2331
|
-
for (const row of rows) {
|
|
2332
|
-
const label = String(row.display_name ?? row.device_id ?? "").trim() || String(row.device_id ?? "");
|
|
2333
|
-
const enabled = Number(row.enabled ?? 1) === 1 ? "enabled" : "disabled";
|
|
2334
|
-
p.log.message(`- ${label} (${String(row.device_id ?? "")}) ${enabled}`);
|
|
2335
|
-
}
|
|
2336
|
-
p.outro(`${rows.length} device(s)`);
|
|
2337
|
-
}));
|
|
2338
|
-
coordinatorCommand.addCommand(new Command("rename-device").configureHelp(helpStyle).description("Rename an enrolled device in the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id").requiredOption("--name <name>", "display name").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, deviceId, opts) => {
|
|
2339
|
-
const result = await coordinatorRenameDeviceAction({
|
|
2340
|
-
groupId,
|
|
2341
|
-
deviceId,
|
|
2342
|
-
displayName: opts.name.trim(),
|
|
2343
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2344
|
-
});
|
|
2345
|
-
if (!result) {
|
|
2346
|
-
p.log.error(`Device not found: ${deviceId.trim()}`);
|
|
2347
|
-
process.exitCode = 1;
|
|
2348
|
-
return;
|
|
2349
|
-
}
|
|
2350
|
-
if (opts.json) {
|
|
2351
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2352
|
-
return;
|
|
2353
|
-
}
|
|
2354
|
-
p.intro("codemem sync coordinator rename-device");
|
|
2355
|
-
p.log.success(`Renamed ${deviceId.trim()} in ${groupId.trim()}`);
|
|
2356
|
-
p.outro(String(result.display_name ?? result.device_id ?? deviceId.trim()));
|
|
2357
|
-
}));
|
|
2358
|
-
coordinatorCommand.addCommand(new Command("disable-device").configureHelp(helpStyle).description("Disable an enrolled device in the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, deviceId, opts) => {
|
|
2359
|
-
if (!await coordinatorDisableDeviceAction({
|
|
2360
|
-
groupId,
|
|
2361
|
-
deviceId,
|
|
2362
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2363
|
-
})) {
|
|
2364
|
-
p.log.error(`Device not found: ${deviceId.trim()}`);
|
|
2365
|
-
process.exitCode = 1;
|
|
2366
|
-
return;
|
|
2367
|
-
}
|
|
2368
|
-
if (opts.json) {
|
|
2369
|
-
console.log(JSON.stringify({
|
|
2370
|
-
ok: true,
|
|
2371
|
-
group_id: groupId.trim(),
|
|
2372
|
-
device_id: deviceId.trim()
|
|
2373
|
-
}, null, 2));
|
|
2374
|
-
return;
|
|
2375
|
-
}
|
|
2376
|
-
p.intro("codemem sync coordinator disable-device");
|
|
2377
|
-
p.log.success(`Disabled ${deviceId.trim()} in ${groupId.trim()}`);
|
|
2378
|
-
p.outro("disabled");
|
|
2379
|
-
}));
|
|
2380
|
-
coordinatorCommand.addCommand(new Command("remove-device").configureHelp(helpStyle).description("Remove an enrolled device from the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, deviceId, opts) => {
|
|
2381
|
-
if (!await coordinatorRemoveDeviceAction({
|
|
2382
|
-
groupId,
|
|
2383
|
-
deviceId,
|
|
2384
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2385
|
-
})) {
|
|
2386
|
-
p.log.error(`Device not found: ${deviceId.trim()}`);
|
|
2387
|
-
process.exitCode = 1;
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2390
|
-
if (opts.json) {
|
|
2391
|
-
console.log(JSON.stringify({
|
|
2392
|
-
ok: true,
|
|
2393
|
-
group_id: groupId.trim(),
|
|
2394
|
-
device_id: deviceId.trim()
|
|
2395
|
-
}, null, 2));
|
|
2396
|
-
return;
|
|
2397
|
-
}
|
|
2398
|
-
p.intro("codemem sync coordinator remove-device");
|
|
2399
|
-
p.log.success(`Removed ${deviceId.trim()} from ${groupId.trim()}`);
|
|
2400
|
-
p.outro("removed");
|
|
2401
|
-
}));
|
|
2402
|
-
coordinatorCommand.addCommand(new Command("serve").configureHelp(helpStyle).description("Run the coordinator relay HTTP server").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--host <host>", "bind host", "127.0.0.1").option("--port <port>", "bind port", "7347").action(async (opts) => {
|
|
2403
|
-
const host = String(opts.host ?? "127.0.0.1").trim() || "127.0.0.1";
|
|
2404
|
-
const port = Number.parseInt(String(opts.port ?? "7347"), 10);
|
|
2405
|
-
const dbPath = opts.db ?? opts.dbPath ?? DEFAULT_COORDINATOR_DB_PATH;
|
|
2406
|
-
const app = createBetterSqliteCoordinatorApp({ dbPath });
|
|
2407
|
-
p.intro("codemem sync coordinator serve");
|
|
2408
|
-
p.log.success(`Coordinator listening at http://${host}:${port}`);
|
|
2409
|
-
p.log.info(`DB: ${dbPath}`);
|
|
2410
|
-
serve({
|
|
2411
|
-
fetch: app.fetch,
|
|
2412
|
-
hostname: host,
|
|
2413
|
-
port
|
|
2414
|
-
});
|
|
2415
|
-
}));
|
|
2416
|
-
coordinatorCommand.addCommand(new Command("create-invite").configureHelp(helpStyle).description("Create a coordinator team invite").argument("[group]", "group id").option("--group <group>", "group id").option("--coordinator-url <url>", "coordinator URL override").option("--policy <policy>", "invite policy", "auto_admit").option("--ttl-hours <hours>", "invite TTL hours", "24").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override").option("--json", "output as JSON").action(async (groupArg, opts) => {
|
|
2417
|
-
const ttlHours = Number.parseInt(String(opts.ttlHours ?? "24"), 10);
|
|
2418
|
-
const groupId = String(opts.group ?? "").trim() || String(groupArg ?? "").trim();
|
|
2419
|
-
const result = await coordinatorCreateInviteAction({
|
|
2420
|
-
groupId,
|
|
2421
|
-
coordinatorUrl: opts.coordinatorUrl?.trim() || null,
|
|
2422
|
-
policy: String(opts.policy ?? "auto_admit").trim(),
|
|
2423
|
-
ttlHours,
|
|
2424
|
-
createdBy: null,
|
|
2425
|
-
dbPath: opts.db ?? opts.dbPath ?? null,
|
|
2426
|
-
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
2427
|
-
adminSecret: opts.adminSecret?.trim() || null
|
|
2428
|
-
});
|
|
2429
|
-
if (opts.json) {
|
|
2430
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2431
|
-
return;
|
|
2432
|
-
}
|
|
2433
|
-
p.intro("codemem sync coordinator create-invite");
|
|
2434
|
-
p.log.success(`Invite created for ${groupId}`);
|
|
2435
|
-
if (typeof result.link === "string") p.log.message(`- link: ${result.link}`);
|
|
2436
|
-
if (typeof result.encoded === "string") p.log.message(`- invite: ${result.encoded}`);
|
|
2437
|
-
for (const warning of Array.isArray(result.warnings) ? result.warnings : []) p.log.warn(String(warning));
|
|
2438
|
-
p.outro("Invite ready");
|
|
2439
|
-
}));
|
|
2440
|
-
coordinatorCommand.addCommand(new Command("import-invite").configureHelp(helpStyle).description("Import a coordinator invite").argument("<invite>", "invite value or link").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--keys-dir <path>", "keys directory").option("--config <path>", "config path").option("--json", "output as JSON").action(async (invite, opts) => {
|
|
2441
|
-
const result = await coordinatorImportInviteAction({
|
|
2442
|
-
inviteValue: invite,
|
|
2443
|
-
dbPath: opts.db ?? opts.dbPath ?? null,
|
|
2444
|
-
keysDir: opts.keysDir ?? null,
|
|
2445
|
-
configPath: opts.config ?? null
|
|
2446
|
-
});
|
|
2447
|
-
if (opts.json) {
|
|
2448
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2449
|
-
return;
|
|
2450
|
-
}
|
|
2451
|
-
p.intro("codemem sync coordinator import-invite");
|
|
2452
|
-
p.log.success(`Invite imported for ${result.group_id}`);
|
|
2453
|
-
p.log.message(`- coordinator: ${result.coordinator_url}`);
|
|
2454
|
-
p.log.message(`- status: ${result.status}`);
|
|
2455
|
-
p.outro("Coordinator config updated");
|
|
2456
|
-
}));
|
|
2457
|
-
coordinatorCommand.addCommand(new Command("list-join-requests").configureHelp(helpStyle).description("List pending coordinator join requests").argument("[group]", "group id").option("--group <group>", "group id").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override").option("--json", "output as JSON").action(async (groupArg, opts) => {
|
|
2458
|
-
const groupId = String(opts.group ?? "").trim() || String(groupArg ?? "").trim();
|
|
2459
|
-
const rows = await coordinatorListJoinRequestsAction({
|
|
2460
|
-
groupId,
|
|
2461
|
-
dbPath: opts.db ?? opts.dbPath ?? null,
|
|
2462
|
-
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
2463
|
-
adminSecret: opts.adminSecret?.trim() || null
|
|
2464
|
-
});
|
|
2465
|
-
if (opts.json) {
|
|
2466
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
2467
|
-
return;
|
|
2468
|
-
}
|
|
2469
|
-
p.intro("codemem sync coordinator list-join-requests");
|
|
2470
|
-
if (rows.length === 0) {
|
|
2471
|
-
p.outro(`No pending join requests for ${groupId}`);
|
|
3584
|
+
var bootstrapCmd = new Command("bootstrap").configureHelp(helpStyle).description("Fast-bootstrap memories from a peer (full snapshot transfer)").argument("[peer-device-id]", "peer device ID to bootstrap from").option("--bootstrap-grant <grant-id>", "bootstrap grant id for seed-authorized bootstrap").option("--page-size <n>", "items per snapshot page (default: 2000)", "2000").option("--keys-dir <path>", "keys directory").option("--force", "skip dirty-local-state safety check");
|
|
3585
|
+
addDbOption(bootstrapCmd);
|
|
3586
|
+
addJsonOption(bootstrapCmd);
|
|
3587
|
+
bootstrapCmd.addOption(new Option("--peer <device-id>", "peer device ID").hideHelp());
|
|
3588
|
+
bootstrapCmd.action(async (peerArg, opts) => {
|
|
3589
|
+
const peerDeviceId = (peerArg || opts.peer || "").trim();
|
|
3590
|
+
if (!peerDeviceId) {
|
|
3591
|
+
if (opts.json) {
|
|
3592
|
+
emitJsonError("usage_error", "missing required argument: <peer-device-id>", 2);
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
p.log.error("missing required argument: <peer-device-id>");
|
|
3596
|
+
process.exitCode = 2;
|
|
2472
3597
|
return;
|
|
2473
3598
|
}
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
const request = await coordinatorReviewJoinRequestAction({
|
|
2483
|
-
requestId: requestId.trim(),
|
|
2484
|
-
approve,
|
|
2485
|
-
reviewedBy: null,
|
|
2486
|
-
dbPath: opts.db ?? opts.dbPath ?? null,
|
|
2487
|
-
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
2488
|
-
adminSecret: opts.adminSecret?.trim() || null
|
|
2489
|
-
});
|
|
2490
|
-
if (!request) {
|
|
2491
|
-
p.log.error(`Join request not found: ${requestId.trim()}`);
|
|
3599
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
3600
|
+
try {
|
|
3601
|
+
const pageSize = Math.max(1, Number.parseInt(opts.pageSize, 10) || 2e3);
|
|
3602
|
+
const keysDir = opts.keysDir ?? void 0;
|
|
3603
|
+
const peer = drizzle(store.db, { schema }).select().from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).get();
|
|
3604
|
+
if (!peer) {
|
|
3605
|
+
if (opts.json) emitJsonError("peer_not_found", `Peer ${peerDeviceId} not found in sync_peers.`);
|
|
3606
|
+
else p.log.error(`Peer ${peerDeviceId} not found in sync_peers.`);
|
|
2492
3607
|
process.exitCode = 1;
|
|
2493
3608
|
return;
|
|
2494
3609
|
}
|
|
2495
|
-
if (
|
|
2496
|
-
|
|
3610
|
+
if (!peer.pinned_fingerprint) {
|
|
3611
|
+
if (opts.json) emitJsonError("peer_not_pinned", `Peer ${peerDeviceId} has no pinned fingerprint. Accept it first.`);
|
|
3612
|
+
else p.log.error(`Peer ${peerDeviceId} has no pinned fingerprint. Accept it first.`);
|
|
3613
|
+
process.exitCode = 1;
|
|
2497
3614
|
return;
|
|
2498
3615
|
}
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
3616
|
+
if (!opts.force) {
|
|
3617
|
+
const dirty = hasUnsyncedSharedMemoryChanges(store.db);
|
|
3618
|
+
if (dirty.dirty) {
|
|
3619
|
+
if (opts.json) emitJsonError("local_unsynced_changes", `${dirty.count} unsynced shared memory change(s) would be lost. Use --force to override.`);
|
|
3620
|
+
else p.log.error(`${dirty.count} unsynced shared memory change(s) would be lost. Use --force to override.`);
|
|
3621
|
+
process.exitCode = 1;
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir });
|
|
3626
|
+
const addresses = JSON.parse(String(peer.addresses_json ?? "[]"));
|
|
3627
|
+
if (!addresses.length) {
|
|
3628
|
+
if (opts.json) emitJsonError("no_peer_addresses", "Peer has no known addresses. Run a sync first or add addresses.");
|
|
3629
|
+
else p.log.error("Peer has no known addresses. Run a sync first or add addresses.");
|
|
3630
|
+
process.exitCode = 1;
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
let boundary = null;
|
|
3634
|
+
let baseUrl = "";
|
|
3635
|
+
const addressResults = [];
|
|
3636
|
+
for (const address of addresses) {
|
|
3637
|
+
const candidate = buildBaseUrl(address);
|
|
3638
|
+
if (!candidate) continue;
|
|
3639
|
+
const statusUrl = `${candidate}/v1/status`;
|
|
3640
|
+
const headers = buildAuthHeaders({
|
|
3641
|
+
deviceId,
|
|
3642
|
+
method: "GET",
|
|
3643
|
+
url: statusUrl,
|
|
3644
|
+
bodyBytes: Buffer.alloc(0),
|
|
3645
|
+
bootstrapGrantId: opts.bootstrapGrant,
|
|
3646
|
+
keysDir
|
|
3647
|
+
});
|
|
3648
|
+
try {
|
|
3649
|
+
const [code, payload] = await requestJson("GET", statusUrl, { headers });
|
|
3650
|
+
if (code !== 200 || !payload) {
|
|
3651
|
+
addressResults.push({
|
|
3652
|
+
address: candidate,
|
|
3653
|
+
result: `status ${code}`
|
|
3654
|
+
});
|
|
3655
|
+
continue;
|
|
3656
|
+
}
|
|
3657
|
+
if (payload.fingerprint !== peer.pinned_fingerprint) {
|
|
3658
|
+
addressResults.push({
|
|
3659
|
+
address: candidate,
|
|
3660
|
+
result: "fingerprint mismatch"
|
|
3661
|
+
});
|
|
3662
|
+
continue;
|
|
3663
|
+
}
|
|
3664
|
+
const reset = payload.sync_reset;
|
|
3665
|
+
if (reset && typeof reset.generation === "number" && typeof reset.snapshot_id === "string") {
|
|
3666
|
+
boundary = {
|
|
3667
|
+
generation: reset.generation,
|
|
3668
|
+
snapshot_id: reset.snapshot_id,
|
|
3669
|
+
baseline_cursor: typeof reset.baseline_cursor === "string" ? reset.baseline_cursor : null
|
|
3670
|
+
};
|
|
3671
|
+
baseUrl = candidate;
|
|
3672
|
+
addressResults.push({
|
|
3673
|
+
address: candidate,
|
|
3674
|
+
result: "ok"
|
|
3675
|
+
});
|
|
3676
|
+
break;
|
|
3677
|
+
}
|
|
3678
|
+
addressResults.push({
|
|
3679
|
+
address: candidate,
|
|
3680
|
+
result: "missing sync_reset boundary"
|
|
3681
|
+
});
|
|
3682
|
+
} catch (err) {
|
|
3683
|
+
addressResults.push({
|
|
3684
|
+
address: candidate,
|
|
3685
|
+
result: err instanceof Error ? err.message : String(err)
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
if (!boundary || !baseUrl) {
|
|
3690
|
+
const summary = addressResults.map((r) => `${r.address}: ${r.result}`).join("; ");
|
|
3691
|
+
const detail = summary ? `no reachable peer with valid reset boundary. Tried: ${summary}` : "peer unreachable or missing reset boundary";
|
|
3692
|
+
if (opts.json) console.log(JSON.stringify({
|
|
3693
|
+
ok: false,
|
|
3694
|
+
error: detail,
|
|
3695
|
+
addresses: addressResults
|
|
3696
|
+
}));
|
|
3697
|
+
else p.log.error(detail);
|
|
3698
|
+
process.exitCode = 1;
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
if (!opts.json) {
|
|
3702
|
+
p.intro("codemem sync bootstrap");
|
|
3703
|
+
p.log.step(`Bootstrapping from ${peer.name || peerDeviceId}...`);
|
|
3704
|
+
}
|
|
3705
|
+
const resetInfo = {
|
|
3706
|
+
generation: boundary.generation,
|
|
3707
|
+
snapshot_id: boundary.snapshot_id,
|
|
3708
|
+
baseline_cursor: boundary.baseline_cursor,
|
|
3709
|
+
retained_floor_cursor: null,
|
|
3710
|
+
reset_required: true,
|
|
3711
|
+
reason: "initial_bootstrap"
|
|
3712
|
+
};
|
|
3713
|
+
const { items } = await fetchAllSnapshotPages(baseUrl, resetInfo, deviceId, {
|
|
3714
|
+
keysDir,
|
|
3715
|
+
bootstrapGrantId: opts.bootstrapGrant,
|
|
3716
|
+
pageSize
|
|
3717
|
+
});
|
|
3718
|
+
const result = applyBootstrapSnapshot(store.db, peerDeviceId, items, resetInfo);
|
|
3719
|
+
if (opts.json) console.log(JSON.stringify({
|
|
3720
|
+
ok: result.ok,
|
|
3721
|
+
applied: result.applied,
|
|
3722
|
+
deleted: result.deleted,
|
|
3723
|
+
error: result.error ?? null
|
|
3724
|
+
}));
|
|
3725
|
+
else {
|
|
3726
|
+
if (result.ok) p.log.success(`Applied ${result.applied} memories (removed ${result.deleted} stale).`);
|
|
3727
|
+
else p.log.error(result.error || "Bootstrap apply failed.");
|
|
3728
|
+
p.outro(result.ok ? "Bootstrap complete" : "Bootstrap failed");
|
|
3729
|
+
}
|
|
3730
|
+
if (!result.ok) process.exitCode = 1;
|
|
3731
|
+
} finally {
|
|
3732
|
+
store.close();
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
syncCommand.addCommand(bootstrapCmd);
|
|
3736
|
+
var connectCmd = 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");
|
|
3737
|
+
addConfigOption(connectCmd);
|
|
3738
|
+
connectCmd.action((url, opts) => {
|
|
3739
|
+
const config = readCliConfig(opts.config);
|
|
3740
|
+
config.sync_coordinator_url = url.trim();
|
|
3741
|
+
if (opts.group) config.sync_coordinator_group = opts.group.trim();
|
|
3742
|
+
writeCliConfig(config, opts.config);
|
|
3743
|
+
p.intro("codemem sync connect");
|
|
3744
|
+
p.log.success(`Coordinator: ${url.trim()}`);
|
|
3745
|
+
if (opts.group) p.log.info(`Group: ${opts.group.trim()}`);
|
|
3746
|
+
p.outro("Restart `codemem serve` to activate coordinator sync");
|
|
3747
|
+
});
|
|
3748
|
+
syncCommand.addCommand(connectCmd);
|
|
3749
|
+
var syncCoordinatorAlias = buildCoordinatorCommand();
|
|
3750
|
+
syncCoordinatorAlias.hook("preAction", (_thisCmd, actionCmd) => {
|
|
3751
|
+
const subName = actionCmd.name();
|
|
3752
|
+
emitDeprecationWarning(`codemem sync coordinator ${subName}`, `codemem coordinator ${subName}`);
|
|
3753
|
+
});
|
|
3754
|
+
syncCommand.addCommand(syncCoordinatorAlias);
|
|
2507
3755
|
//#endregion
|
|
2508
3756
|
//#region src/commands/version.ts
|
|
2509
3757
|
var versionCommand = new Command("version").configureHelp(helpStyle).description("Print codemem version").action(() => {
|
|
@@ -2524,24 +3772,24 @@ var versionCommand = new Command("version").configureHelp(helpStyle).description
|
|
|
2524
3772
|
var completion = omelette("codemem <command>");
|
|
2525
3773
|
completion.on("command", ({ reply }) => {
|
|
2526
3774
|
reply([
|
|
3775
|
+
"claude-hook-inject",
|
|
2527
3776
|
"claude-hook-ingest",
|
|
3777
|
+
"config",
|
|
3778
|
+
"coordinator",
|
|
2528
3779
|
"db",
|
|
2529
3780
|
"embed",
|
|
3781
|
+
"enqueue-raw-event",
|
|
2530
3782
|
"export-memories",
|
|
2531
|
-
"forget",
|
|
2532
|
-
"memory",
|
|
2533
3783
|
"import-memories",
|
|
2534
|
-
"
|
|
2535
|
-
"
|
|
2536
|
-
"
|
|
2537
|
-
"stats",
|
|
3784
|
+
"mcp",
|
|
3785
|
+
"memory",
|
|
3786
|
+
"pack",
|
|
2538
3787
|
"recent",
|
|
2539
|
-
"remember",
|
|
2540
3788
|
"search",
|
|
2541
|
-
"pack",
|
|
2542
3789
|
"serve",
|
|
2543
|
-
"
|
|
2544
|
-
"
|
|
3790
|
+
"setup",
|
|
3791
|
+
"stats",
|
|
3792
|
+
"sync",
|
|
2545
3793
|
"version",
|
|
2546
3794
|
"help",
|
|
2547
3795
|
"--help",
|
|
@@ -2575,8 +3823,33 @@ if (hasRootFlag("--cleanup-completion")) {
|
|
|
2575
3823
|
completion.cleanupShellInitFile();
|
|
2576
3824
|
process.exit(0);
|
|
2577
3825
|
}
|
|
3826
|
+
{
|
|
3827
|
+
const memExport = new Command("export").description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--project <project>", "filter by project").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp").configureHelp(helpStyle).allowUnknownOption(true).allowExcessArguments(true).action(async () => {
|
|
3828
|
+
const idx = process.argv.indexOf("export");
|
|
3829
|
+
const tail = idx >= 0 ? process.argv.slice(idx + 1) : [];
|
|
3830
|
+
await exportMemoriesCommand.parseAsync([
|
|
3831
|
+
"node",
|
|
3832
|
+
"export-memories",
|
|
3833
|
+
...tail
|
|
3834
|
+
]);
|
|
3835
|
+
});
|
|
3836
|
+
const memImport = new Command("import").description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing").configureHelp(helpStyle).allowUnknownOption(true).allowExcessArguments(true).action(async () => {
|
|
3837
|
+
const idx = process.argv.indexOf("import");
|
|
3838
|
+
const tail = idx >= 0 ? process.argv.slice(idx + 1) : [];
|
|
3839
|
+
await importMemoriesCommand.parseAsync([
|
|
3840
|
+
"node",
|
|
3841
|
+
"import-memories",
|
|
3842
|
+
...tail
|
|
3843
|
+
]);
|
|
3844
|
+
});
|
|
3845
|
+
memoryCommand.addCommand(memExport);
|
|
3846
|
+
memoryCommand.addCommand(memImport);
|
|
3847
|
+
}
|
|
2578
3848
|
program.addCommand(serveCommand);
|
|
3849
|
+
program.addCommand(configCommand);
|
|
3850
|
+
program.addCommand(coordinatorCommand);
|
|
2579
3851
|
program.addCommand(mcpCommand);
|
|
3852
|
+
program.addCommand(claudeHookInjectCommand);
|
|
2580
3853
|
program.addCommand(claudeHookIngestCommand);
|
|
2581
3854
|
program.addCommand(dbCommand);
|
|
2582
3855
|
program.addCommand(exportMemoriesCommand);
|
|
@@ -2586,9 +3859,9 @@ program.addCommand(embedCommand);
|
|
|
2586
3859
|
program.addCommand(recentCommand);
|
|
2587
3860
|
program.addCommand(searchCommand);
|
|
2588
3861
|
program.addCommand(packCommand);
|
|
2589
|
-
program.addCommand(showMemoryCommand);
|
|
2590
|
-
program.addCommand(forgetMemoryCommand);
|
|
2591
|
-
program.addCommand(rememberMemoryCommand);
|
|
3862
|
+
program.addCommand(showMemoryCommand, { hidden: true });
|
|
3863
|
+
program.addCommand(forgetMemoryCommand, { hidden: true });
|
|
3864
|
+
program.addCommand(rememberMemoryCommand, { hidden: true });
|
|
2592
3865
|
program.addCommand(memoryCommand);
|
|
2593
3866
|
program.addCommand(syncCommand);
|
|
2594
3867
|
program.addCommand(setupCommand);
|