codemem 0.22.4 → 0.24.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 +2004 -566
- 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, applyBootstrapSnapshot, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fetchAllSnapshotPages, fingerprintPublicKey, getRawEventStatus, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCoordinatorSyncConfig, readImportPayload, requestJson, 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, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, loadObserverConfig, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, replayBatchExtraction, replayBatchExtractionWithTierRouting, 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));
|
|
179
434
|
process.exitCode = 1;
|
|
180
435
|
}
|
|
181
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));
|
|
471
|
+
process.exitCode = 1;
|
|
472
|
+
}
|
|
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,504 @@ 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;
|
|
1752
|
+
}
|
|
1753
|
+
function createInjectMemoryCommand() {
|
|
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)));
|
|
1759
|
+
try {
|
|
1760
|
+
const { limit, budget, filters } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
|
|
1761
|
+
const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
|
|
1762
|
+
console.log(pack.pack_text ?? "");
|
|
1763
|
+
} finally {
|
|
1764
|
+
store.close();
|
|
1765
|
+
}
|
|
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("--scenario <id>", "run a named injection-first eval scenario pack (can be repeated)", (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 invalidScenario = (opts.scenario ?? []).find((id) => getInjectionEvalScenarioPack(id) == null);
|
|
1776
|
+
if (invalidScenario) throw new Error(`Unknown eval scenario pack: ${invalidScenario}`);
|
|
1777
|
+
const probes = [...opts.probe ?? [], ...getInjectionEvalScenarioPrompts(opts.scenario ?? [])];
|
|
1778
|
+
const result = getMemoryRoleReport(resolveDbOpt(opts), {
|
|
1779
|
+
project,
|
|
1780
|
+
allProjects: opts.allProjects === true,
|
|
1781
|
+
includeInactive: opts.inactive === true,
|
|
1782
|
+
probes
|
|
1783
|
+
});
|
|
1784
|
+
if (opts.json) {
|
|
1785
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
p.intro("codemem memory role-report");
|
|
1789
|
+
p.log.info([
|
|
1790
|
+
`Memories: ${result.totals.memories}`,
|
|
1791
|
+
`Active: ${result.totals.active}`,
|
|
1792
|
+
`Sessions: ${result.totals.sessions}`
|
|
1793
|
+
].join("\n"));
|
|
1794
|
+
p.log.info("Counts by role:");
|
|
1795
|
+
for (const [role, count] of Object.entries(result.counts_by_role)) p.log.message(` ${role.padEnd(10)} ${String(count)}`);
|
|
1796
|
+
p.log.info("Counts by mapping:");
|
|
1797
|
+
p.log.message(` mapped ${result.counts_by_mapping.mapped}`);
|
|
1798
|
+
p.log.message(` unmapped ${result.counts_by_mapping.unmapped}`);
|
|
1799
|
+
p.log.info("Summary lineages:");
|
|
1800
|
+
p.log.message(` session_summary ${result.summary_lineages.session_summary}`);
|
|
1801
|
+
p.log.message(` legacy_metadata_summary ${result.summary_lineages.legacy_metadata_summary}`);
|
|
1802
|
+
p.log.message(` summary_mapped ${result.summary_mapping.mapped}`);
|
|
1803
|
+
p.log.message(` summary_unmapped ${result.summary_mapping.unmapped}`);
|
|
1804
|
+
p.log.info("Project quality:");
|
|
1805
|
+
for (const [bucket, count] of Object.entries(result.project_quality)) p.log.message(` ${bucket.padEnd(12)} ${String(count)}`);
|
|
1806
|
+
p.log.info("Session classes:");
|
|
1807
|
+
for (const [bucket, count] of Object.entries(result.session_class_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
|
|
1808
|
+
p.log.info("Summary dispositions:");
|
|
1809
|
+
for (const [bucket, count] of Object.entries(result.summary_disposition_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
|
|
1810
|
+
if (result.probe_results.length > 0) {
|
|
1811
|
+
p.log.info("Probe results:");
|
|
1812
|
+
for (const probe of result.probe_results) {
|
|
1813
|
+
p.log.message(` query: ${probe.query}`);
|
|
1814
|
+
if (probe.scenario_id) p.log.message(` scenario: ${probe.scenario_id} (${probe.scenario_category ?? "unknown"})${probe.scenario_title ? ` — ${probe.scenario_title}` : ""}`);
|
|
1815
|
+
p.log.message(` mode: ${probe.mode}`);
|
|
1816
|
+
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}`);
|
|
1817
|
+
p.log.message(` top mapping: mapped=${probe.top_mapping_counts.mapped} unmapped=${probe.top_mapping_counts.unmapped}`);
|
|
1818
|
+
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)}`);
|
|
1819
|
+
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)}`);
|
|
1820
|
+
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)}`);
|
|
1821
|
+
if (probe.scenario_score) p.log.message(` scenario score: mode_match=${probe.scenario_score.mode_match ? "yes" : "no"} top1_primary=${probe.scenario_score.primary_in_top1 ? "yes" : "no"} top3_primary=${probe.scenario_score.primary_in_top3_count} top1_anti=${probe.scenario_score.anti_signal_in_top1 ? "yes" : "no"} primary=${probe.scenario_score.primary_match_count} anti=${probe.scenario_score.anti_signal_count} recap=${probe.scenario_score.recap_count} unmapped_recap=${probe.scenario_score.unmapped_recap_count} chatter=${probe.scenario_score.administrative_chatter_count} net=${probe.scenario_score.score}`);
|
|
1822
|
+
for (const item of probe.items.slice(0, 5)) p.log.message(` [${item.id}] (${item.kind}/${item.role}/${item.mapping}/${item.session_class}/${item.summary_disposition}) ${item.title} — ${item.role_reason}`);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
p.outro("done");
|
|
1826
|
+
});
|
|
1827
|
+
return cmd;
|
|
1828
|
+
}
|
|
1829
|
+
function createMemoryRoleCompareCommand() {
|
|
1830
|
+
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("--scenario <id>", "run a named injection-first eval scenario pack (can be repeated)", (value, prev) => [...prev, value], []).option("--inactive", "include inactive memories");
|
|
1831
|
+
addJsonOption(cmd);
|
|
1832
|
+
cmd.action((baselineDb, candidateDb, opts) => {
|
|
1833
|
+
const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
|
|
1834
|
+
const invalidScenario = (opts.scenario ?? []).find((id) => getInjectionEvalScenarioPack(id) == null);
|
|
1835
|
+
if (invalidScenario) throw new Error(`Unknown eval scenario pack: ${invalidScenario}`);
|
|
1836
|
+
const probes = [...opts.probe ?? [], ...getInjectionEvalScenarioPrompts(opts.scenario ?? [])];
|
|
1837
|
+
const result = compareMemoryRoleReports(baselineDb, candidateDb, {
|
|
1838
|
+
project,
|
|
1839
|
+
allProjects: opts.allProjects === true,
|
|
1840
|
+
includeInactive: opts.inactive === true,
|
|
1841
|
+
probes
|
|
1842
|
+
});
|
|
1843
|
+
if (opts.json) {
|
|
1844
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
p.intro("codemem memory role-compare");
|
|
1848
|
+
p.log.info([
|
|
1849
|
+
`Baseline sessions: ${result.baseline.totals.sessions}`,
|
|
1850
|
+
`Candidate sessions: ${result.candidate.totals.sessions}`,
|
|
1851
|
+
`Delta sessions: ${result.delta.totals.sessions}`,
|
|
1852
|
+
`Mapped delta: ${result.delta.counts_by_mapping.mapped}`,
|
|
1853
|
+
`Unmapped delta: ${result.delta.counts_by_mapping.unmapped}`,
|
|
1854
|
+
`Summary mapped delta: ${result.delta.summary_mapping.mapped}`,
|
|
1855
|
+
`Summary unmapped delta: ${result.delta.summary_mapping.unmapped}`
|
|
1856
|
+
].join("\n"));
|
|
1857
|
+
p.log.info("Role deltas:");
|
|
1858
|
+
for (const [role, count] of Object.entries(result.delta.counts_by_role)) p.log.message(` ${role.padEnd(10)} ${String(count)}`);
|
|
1859
|
+
p.log.info("Session class deltas:");
|
|
1860
|
+
for (const [bucket, count] of Object.entries(result.delta.session_class_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
|
|
1861
|
+
p.log.info("Summary disposition deltas:");
|
|
1862
|
+
for (const [bucket, count] of Object.entries(result.delta.summary_disposition_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
|
|
1863
|
+
if (result.probe_comparisons.length > 0) {
|
|
1864
|
+
p.log.info("Probe comparisons:");
|
|
1865
|
+
for (const probe of result.probe_comparisons) {
|
|
1866
|
+
p.log.message(` query: ${probe.query}`);
|
|
1867
|
+
p.log.message(` modes: baseline=${probe.baseline_mode ?? "-"} candidate=${probe.candidate_mode ?? "-"}`);
|
|
1868
|
+
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(",") || "-"}`);
|
|
1869
|
+
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)}`);
|
|
1870
|
+
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}`);
|
|
1871
|
+
if (probe.baseline_scenario_score || probe.candidate_scenario_score) p.log.message(` scenario scores: baseline=${probe.baseline_scenario_score?.score ?? "-"} candidate=${probe.candidate_scenario_score?.score ?? "-"}`);
|
|
1872
|
+
if (probe.delta_scenario_score) p.log.message(` scenario delta: mode_match=${probe.delta_scenario_score.mode_match ?? "-"} top1_primary=${probe.delta_scenario_score.primary_in_top1 ?? "-"} top3_primary=${probe.delta_scenario_score.primary_in_top3_count ?? "-"} top1_anti=${probe.delta_scenario_score.anti_signal_in_top1 ?? "-"} primary=${probe.delta_scenario_score.primary_match_count ?? "-"} anti=${probe.delta_scenario_score.anti_signal_count ?? "-"} recap=${probe.delta_scenario_score.recap_count ?? "-"} unmapped_recap=${probe.delta_scenario_score.unmapped_recap_count ?? "-"} chatter=${probe.delta_scenario_score.administrative_chatter_count ?? "-"} net=${probe.delta_scenario_score.score ?? "-"}`);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
p.outro("done");
|
|
1876
|
+
});
|
|
1877
|
+
return cmd;
|
|
1878
|
+
}
|
|
1879
|
+
function createMemoryExtractionReportCommand() {
|
|
1880
|
+
const cmd = new Command("extraction-report").configureHelp(helpStyle).description("Score extracted memories for a session against a built-in extraction eval rubric").option("--session-id <id>", "session ID to evaluate").option("--batch-id <id>", "raw-event flush batch ID to evaluate").requiredOption("--scenario <id>", "built-in extraction eval scenario ID").option("--inactive", "include inactive memories");
|
|
1881
|
+
addDbOption(cmd);
|
|
1882
|
+
addJsonOption(cmd);
|
|
1883
|
+
cmd.action((opts) => {
|
|
1884
|
+
const sessionIdInput = opts.sessionId?.trim() ?? "";
|
|
1885
|
+
const batchIdInput = opts.batchId?.trim() ?? "";
|
|
1886
|
+
const hasSessionId = sessionIdInput.length > 0;
|
|
1887
|
+
const hasBatchId = batchIdInput.length > 0;
|
|
1888
|
+
if (hasSessionId === hasBatchId) throw new Error("Provide exactly one of --session-id or --batch-id");
|
|
1889
|
+
const sessionId = hasSessionId ? parseStrictPositiveId(sessionIdInput) : null;
|
|
1890
|
+
if (hasSessionId && sessionId === null) throw new Error(`Invalid session ID: ${sessionIdInput || opts.sessionId}`);
|
|
1891
|
+
const batchId = hasBatchId ? parseStrictPositiveId(batchIdInput) : null;
|
|
1892
|
+
if (hasBatchId && batchId === null) throw new Error(`Invalid batch ID: ${batchIdInput || opts.batchId}`);
|
|
1893
|
+
const scenarioId = opts.scenario?.trim() ?? "";
|
|
1894
|
+
const scenario = getSessionExtractionEvalScenario(scenarioId);
|
|
1895
|
+
if (!scenario) throw new Error(`Unknown extraction eval scenario: ${scenarioId || opts.scenario}`);
|
|
1896
|
+
const result = batchId != null ? getSessionExtractionEval(resolveDbOpt(opts), {
|
|
1897
|
+
batchId,
|
|
1898
|
+
scenarioId: scenario.id,
|
|
1899
|
+
includeInactive: opts.inactive === true
|
|
1900
|
+
}) : getSessionExtractionEval(resolveDbOpt(opts), {
|
|
1901
|
+
sessionId,
|
|
1902
|
+
scenarioId: scenario.id,
|
|
1903
|
+
includeInactive: opts.inactive === true
|
|
1904
|
+
});
|
|
1905
|
+
if (opts.json) {
|
|
1906
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
p.intro("codemem memory extraction-report");
|
|
1910
|
+
p.log.info([
|
|
1911
|
+
`Scenario: ${result.scenario.id} — ${result.scenario.title}`,
|
|
1912
|
+
`Target: ${result.target.type}${result.target.batchId != null ? ` #${result.target.batchId}` : ""}`,
|
|
1913
|
+
`Session: ${result.session.id} (${result.session.project ?? "no-project"})`,
|
|
1914
|
+
`Session class: ${result.session.sessionClass}`,
|
|
1915
|
+
`Summary disposition: ${result.session.summaryDisposition}`
|
|
1916
|
+
].join("\n"));
|
|
1917
|
+
p.log.info([
|
|
1918
|
+
`Pass: ${result.pass ? "yes" : "no"}`,
|
|
1919
|
+
`Summary count: ${result.counts.summaries}`,
|
|
1920
|
+
`Observation count: ${result.counts.observations}`,
|
|
1921
|
+
`Summary thread coverage: ${result.coverage.summaryThreadCoverage}`,
|
|
1922
|
+
`Observation thread coverage: ${result.coverage.observationThreadCoverage}`,
|
|
1923
|
+
`Total thread coverage: ${result.coverage.totalThreadCoverage}`,
|
|
1924
|
+
`Duplicate observation threads: ${result.coverage.duplicateObservationThreads}`
|
|
1925
|
+
].join("\n"));
|
|
1926
|
+
if (result.failureReasons.length > 0) {
|
|
1927
|
+
p.log.warn("Failure reasons:");
|
|
1928
|
+
for (const reason of result.failureReasons) p.log.message(` - ${reason}`);
|
|
1929
|
+
}
|
|
1930
|
+
p.log.info("Thread coverage:");
|
|
1931
|
+
for (const thread of result.threads) p.log.message(` ${thread.id.padEnd(22)} summary=${thread.summaryMatch ? "yes" : "no"} observations=${thread.observationMatch ? "yes" : "no"}`);
|
|
1932
|
+
p.outro("done");
|
|
1933
|
+
});
|
|
1934
|
+
return cmd;
|
|
1935
|
+
}
|
|
1936
|
+
function createMemoryExtractionReplayCommand() {
|
|
1937
|
+
const cmd = new Command("extraction-replay").configureHelp(helpStyle).description("Re-run the observer on a historical flush batch without persisting, then score the fresh output").requiredOption("--batch-id <id>", "raw-event flush batch ID to replay").option("--transcript-budget <chars>", "override replay transcript budget in characters (replay only)").option("--observer-tier-routing", "use replay-only benchmark-backed observer tier routing").option("--observer-temperature <value>", "override observer temperature for replay only").option("--openai-responses", "use OpenAI Responses API for replay only").option("--reasoning-effort <level>", "set OpenAI reasoning.effort for replay only (responses path)").option("--reasoning-summary <mode>", "set OpenAI reasoning.summary for replay only (responses path)").option("--max-output-tokens <n>", "override OpenAI max_output_tokens for replay only (responses path)").requiredOption("--scenario <id>", "built-in extraction eval scenario ID");
|
|
1938
|
+
addDbOption(cmd);
|
|
1939
|
+
addJsonOption(cmd);
|
|
1940
|
+
cmd.action(async (opts) => {
|
|
1941
|
+
const batchIdInput = opts.batchId?.trim() ?? "";
|
|
1942
|
+
const batchId = parseStrictPositiveId(batchIdInput);
|
|
1943
|
+
if (batchId === null) throw new Error(`Invalid batch ID: ${batchIdInput || opts.batchId}`);
|
|
1944
|
+
const scenarioId = opts.scenario?.trim() ?? "";
|
|
1945
|
+
const scenario = getSessionExtractionEvalScenario(scenarioId);
|
|
1946
|
+
if (!scenario) throw new Error(`Unknown extraction eval scenario: ${scenarioId || opts.scenario}`);
|
|
1947
|
+
const transcriptBudgetInput = opts.transcriptBudget?.trim() ?? "";
|
|
1948
|
+
const transcriptBudget = transcriptBudgetInput.length > 0 ? parseStrictPositiveId(transcriptBudgetInput) : null;
|
|
1949
|
+
if (transcriptBudgetInput.length > 0 && transcriptBudget === null) throw new Error(`Invalid transcript budget: ${transcriptBudgetInput || opts.transcriptBudget}`);
|
|
1950
|
+
const observerTemperatureInput = opts.observerTemperature?.trim() ?? "";
|
|
1951
|
+
let observerTemperature;
|
|
1952
|
+
if (observerTemperatureInput.length > 0) {
|
|
1953
|
+
const parsed = Number(observerTemperatureInput);
|
|
1954
|
+
if (!Number.isFinite(parsed)) throw new Error(`Invalid observer temperature: ${observerTemperatureInput || opts.observerTemperature}`);
|
|
1955
|
+
observerTemperature = parsed;
|
|
1956
|
+
}
|
|
1957
|
+
const maxOutputTokensInput = opts.maxOutputTokens?.trim() ?? "";
|
|
1958
|
+
const maxOutputTokens = maxOutputTokensInput.length > 0 ? parseStrictPositiveId(maxOutputTokensInput) : null;
|
|
1959
|
+
if (maxOutputTokensInput.length > 0 && maxOutputTokens === null) throw new Error(`Invalid max output tokens: ${maxOutputTokensInput || opts.maxOutputTokens}`);
|
|
1960
|
+
const observerConfig = loadObserverConfig();
|
|
1961
|
+
const observerConfigWithOverrides = {
|
|
1962
|
+
...observerConfig,
|
|
1963
|
+
observerTemperature: observerTemperature ?? observerConfig.observerTemperature,
|
|
1964
|
+
observerOpenAIUseResponses: opts.openaiResponses === true,
|
|
1965
|
+
observerReasoningEffort: opts.reasoningEffort?.trim() || null,
|
|
1966
|
+
observerReasoningSummary: opts.reasoningSummary?.trim() || null,
|
|
1967
|
+
observerMaxOutputTokens: maxOutputTokens ?? observerConfig.observerMaxTokens
|
|
1968
|
+
};
|
|
1969
|
+
const observer = new ObserverClient(observerConfigWithOverrides);
|
|
1970
|
+
const result = opts.observerTierRouting === true ? await replayBatchExtractionWithTierRouting(resolveDbOpt(opts), observerConfigWithOverrides, {
|
|
1971
|
+
batchId,
|
|
1972
|
+
scenarioId: scenario.id,
|
|
1973
|
+
transcriptBudget: transcriptBudget ?? void 0
|
|
1974
|
+
}) : await replayBatchExtraction(resolveDbOpt(opts), observer, {
|
|
1975
|
+
batchId,
|
|
1976
|
+
scenarioId: scenario.id,
|
|
1977
|
+
transcriptBudget: transcriptBudget ?? void 0
|
|
1978
|
+
});
|
|
1979
|
+
if (opts.json) {
|
|
1980
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
p.intro("codemem memory extraction-replay");
|
|
1984
|
+
p.log.info([
|
|
1985
|
+
`Scenario: ${result.scenario.id} — ${result.scenario.title}`,
|
|
1986
|
+
`Batch: ${result.target.batchId}`,
|
|
1987
|
+
`Session: ${result.target.sessionId}`,
|
|
1988
|
+
`Observer: ${result.observer.provider}/${result.observer.model}`,
|
|
1989
|
+
`Tier: ${result.observer.tier ?? "manual"}`,
|
|
1990
|
+
`OpenAI Responses: ${result.observer.openaiUseResponses ? "yes" : "no"}`,
|
|
1991
|
+
`Reasoning effort: ${result.observer.reasoningEffort ?? "none"}`,
|
|
1992
|
+
`Classification: ${result.classification.status}`,
|
|
1993
|
+
`Pass: ${result.evaluation.pass ? "yes" : "no"}`
|
|
1994
|
+
].join("\n"));
|
|
1995
|
+
if (result.classification.reason) p.log.message(`Classification reason: ${result.classification.reason}`);
|
|
1996
|
+
if (result.evaluation.failureReasons.length > 0) {
|
|
1997
|
+
p.log.warn("Failure reasons:");
|
|
1998
|
+
for (const reason of result.evaluation.failureReasons) p.log.message(` - ${reason}`);
|
|
1999
|
+
}
|
|
2000
|
+
p.log.info([
|
|
2001
|
+
`Fresh summaries: ${result.evaluation.counts.summaries}`,
|
|
2002
|
+
`Fresh observations: ${result.evaluation.counts.observations}`,
|
|
2003
|
+
`Summary thread coverage: ${result.evaluation.coverage.summaryThreadCoverage}`,
|
|
2004
|
+
`Observation thread coverage: ${result.evaluation.coverage.observationThreadCoverage}`,
|
|
2005
|
+
`Total thread coverage: ${result.evaluation.coverage.totalThreadCoverage}`
|
|
2006
|
+
].join("\n"));
|
|
2007
|
+
p.outro("done");
|
|
2008
|
+
});
|
|
2009
|
+
return cmd;
|
|
2010
|
+
}
|
|
2011
|
+
function createMemoryExtractionBenchmarkCommand() {
|
|
2012
|
+
const cmd = new Command("extraction-benchmark").configureHelp(helpStyle).description("Run the formal extraction replay benchmark set and print a cost/quality scoreboard").requiredOption("--benchmark <id>", "benchmark profile id").option("--observer-provider <provider>", "override observer provider for this benchmark run").option("--observer-model <model>", "override observer model for this benchmark run").option("--observer-tier-routing", "use replay-only benchmark-backed observer tier routing").option("--openai-responses", "use OpenAI Responses API for this benchmark run").option("--reasoning-effort <level>", "set OpenAI reasoning.effort for this benchmark run (responses path)").option("--reasoning-summary <mode>", "set OpenAI reasoning.summary for this benchmark run (responses path)").option("--max-output-tokens <n>", "override OpenAI max_output_tokens for this benchmark run (responses path)").option("--observer-temperature <value>", "override observer temperature for this benchmark run").option("--transcript-budget <chars>", "override replay transcript budget in characters for this benchmark run");
|
|
2013
|
+
addDbOption(cmd);
|
|
2014
|
+
addJsonOption(cmd);
|
|
2015
|
+
cmd.action(async (opts) => {
|
|
2016
|
+
const benchmarkId = opts.benchmark?.trim() ?? "";
|
|
2017
|
+
const benchmark = getExtractionBenchmarkProfile(benchmarkId);
|
|
2018
|
+
if (!benchmark) throw new Error(`Unknown extraction benchmark: ${benchmarkId || opts.benchmark}`);
|
|
2019
|
+
const transcriptBudgetInput = opts.transcriptBudget?.trim() ?? "";
|
|
2020
|
+
const transcriptBudget = transcriptBudgetInput.length > 0 ? parseStrictPositiveId(transcriptBudgetInput) : null;
|
|
2021
|
+
if (transcriptBudgetInput.length > 0 && transcriptBudget === null) throw new Error(`Invalid transcript budget: ${transcriptBudgetInput || opts.transcriptBudget}`);
|
|
2022
|
+
const observerTemperatureInput = opts.observerTemperature?.trim() ?? "";
|
|
2023
|
+
let observerTemperature;
|
|
2024
|
+
if (observerTemperatureInput.length > 0) {
|
|
2025
|
+
const parsed = Number(observerTemperatureInput);
|
|
2026
|
+
if (!Number.isFinite(parsed)) throw new Error(`Invalid observer temperature: ${observerTemperatureInput || opts.observerTemperature}`);
|
|
2027
|
+
observerTemperature = parsed;
|
|
2028
|
+
}
|
|
2029
|
+
const maxOutputTokensInput = opts.maxOutputTokens?.trim() ?? "";
|
|
2030
|
+
const maxOutputTokens = maxOutputTokensInput.length > 0 ? parseStrictPositiveId(maxOutputTokensInput) : null;
|
|
2031
|
+
if (maxOutputTokensInput.length > 0 && maxOutputTokens === null) throw new Error(`Invalid max output tokens: ${maxOutputTokensInput || opts.maxOutputTokens}`);
|
|
2032
|
+
const observerConfig = loadObserverConfig();
|
|
2033
|
+
const observerConfigWithOverrides = {
|
|
2034
|
+
...observerConfig,
|
|
2035
|
+
observerProvider: opts.observerProvider?.trim() || observerConfig.observerProvider,
|
|
2036
|
+
observerModel: opts.observerModel?.trim() || observerConfig.observerModel,
|
|
2037
|
+
observerTemperature: observerTemperature ?? observerConfig.observerTemperature,
|
|
2038
|
+
observerOpenAIUseResponses: opts.openaiResponses === true,
|
|
2039
|
+
observerReasoningEffort: opts.reasoningEffort?.trim() || null,
|
|
2040
|
+
observerReasoningSummary: opts.reasoningSummary?.trim() || null,
|
|
2041
|
+
observerMaxOutputTokens: maxOutputTokens ?? observerConfig.observerMaxTokens
|
|
2042
|
+
};
|
|
2043
|
+
const observer = new ObserverClient(observerConfigWithOverrides);
|
|
2044
|
+
const runs = [];
|
|
2045
|
+
for (const batch of benchmark.batches) {
|
|
2046
|
+
const scenarioId = batch.scenarioId ?? benchmark.scenarioId;
|
|
2047
|
+
const result = opts.observerTierRouting === true ? await replayBatchExtractionWithTierRouting(resolveDbOpt(opts), observerConfigWithOverrides, {
|
|
2048
|
+
batchId: batch.batchId,
|
|
2049
|
+
scenarioId,
|
|
2050
|
+
transcriptBudget: transcriptBudget ?? void 0
|
|
2051
|
+
}) : await replayBatchExtraction(resolveDbOpt(opts), observer, {
|
|
2052
|
+
batchId: batch.batchId,
|
|
2053
|
+
scenarioId,
|
|
2054
|
+
transcriptBudget: transcriptBudget ?? void 0
|
|
2055
|
+
});
|
|
2056
|
+
runs.push({
|
|
2057
|
+
batchId: batch.batchId,
|
|
2058
|
+
sessionId: batch.sessionId,
|
|
2059
|
+
label: batch.label,
|
|
2060
|
+
purpose: batch.purpose,
|
|
2061
|
+
complexity: batch.complexity,
|
|
2062
|
+
scenarioId,
|
|
2063
|
+
expectedTier: batch.expectedTier ?? null,
|
|
2064
|
+
analysis: {
|
|
2065
|
+
eventSpan: result.analysis.eventSpan,
|
|
2066
|
+
promptCount: result.analysis.promptCount,
|
|
2067
|
+
toolCount: result.analysis.toolCount,
|
|
2068
|
+
transcriptLength: result.analysis.transcriptLength
|
|
2069
|
+
},
|
|
2070
|
+
status: result.classification.status,
|
|
2071
|
+
reason: result.classification.reason,
|
|
2072
|
+
tier: result.observer.tier ?? "manual",
|
|
2073
|
+
provider: result.observer.provider,
|
|
2074
|
+
model: result.observer.model,
|
|
2075
|
+
openaiUseResponses: result.observer.openaiUseResponses,
|
|
2076
|
+
reasoningEffort: result.observer.reasoningEffort,
|
|
2077
|
+
reasoningSummary: result.observer.reasoningSummary,
|
|
2078
|
+
maxOutputTokens: result.observer.maxOutputTokens,
|
|
2079
|
+
temperature: result.observer.temperature,
|
|
2080
|
+
summaries: result.evaluation.counts.summaries,
|
|
2081
|
+
observations: result.evaluation.counts.observations,
|
|
2082
|
+
repairApplied: result.observer.repairApplied
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
const summary = {
|
|
2086
|
+
total: runs.length,
|
|
2087
|
+
shapeQualityTotal: runs.filter((run) => run.purpose === "shape_quality").length,
|
|
2088
|
+
shapeQualityPasses: runs.filter((run) => run.purpose === "shape_quality" && run.status === "pass").length,
|
|
2089
|
+
shapeQualityFails: runs.filter((run) => run.purpose === "shape_quality" && run.status === "shape_fail").length,
|
|
2090
|
+
expectedTierTotal: runs.filter((run) => run.expectedTier != null).length,
|
|
2091
|
+
expectedTierMatches: runs.filter((run) => run.expectedTier != null && run.expectedTier === run.tier).length,
|
|
2092
|
+
robustnessNoOutput: runs.filter((run) => run.status === "observer_no_output").length
|
|
2093
|
+
};
|
|
2094
|
+
const uniqueObserverKeys = Array.from(new Set(runs.map((run) => `${run.provider}::${run.model}::${run.openaiUseResponses ? "responses" : "chat"}`)));
|
|
2095
|
+
const observerSummary = opts.observerTierRouting === true ? {
|
|
2096
|
+
provider: uniqueObserverKeys.length === 1 ? runs[0]?.provider ?? observer.provider : "mixed",
|
|
2097
|
+
model: uniqueObserverKeys.length === 1 ? runs[0]?.model ?? observer.model : "mixed",
|
|
2098
|
+
tierRouting: true,
|
|
2099
|
+
openaiUseResponses: uniqueObserverKeys.length === 1 ? runs[0]?.openaiUseResponses ?? observer.openaiUseResponses : null,
|
|
2100
|
+
reasoningEffort: uniqueObserverKeys.length === 1 ? runs[0]?.reasoningEffort ?? observer.reasoningEffort : "mixed",
|
|
2101
|
+
reasoningSummary: uniqueObserverKeys.length === 1 ? runs[0]?.reasoningSummary ?? observer.reasoningSummary : "mixed",
|
|
2102
|
+
maxOutputTokens: uniqueObserverKeys.length === 1 ? runs[0]?.maxOutputTokens ?? observer.maxOutputTokens : null,
|
|
2103
|
+
temperature: uniqueObserverKeys.length === 1 ? runs[0]?.temperature ?? observer.temperature : null,
|
|
2104
|
+
transcriptBudget: transcriptBudget ?? null,
|
|
2105
|
+
selectedObservers: uniqueObserverKeys
|
|
2106
|
+
} : {
|
|
2107
|
+
provider: observer.provider,
|
|
2108
|
+
model: observer.model,
|
|
2109
|
+
tierRouting: false,
|
|
2110
|
+
openaiUseResponses: observer.openaiUseResponses,
|
|
2111
|
+
reasoningEffort: observer.reasoningEffort,
|
|
2112
|
+
reasoningSummary: observer.reasoningSummary,
|
|
2113
|
+
maxOutputTokens: observer.maxOutputTokens,
|
|
2114
|
+
temperature: observer.temperature,
|
|
2115
|
+
transcriptBudget: transcriptBudget ?? null,
|
|
2116
|
+
selectedObservers: uniqueObserverKeys
|
|
2117
|
+
};
|
|
2118
|
+
const output = {
|
|
2119
|
+
benchmark: {
|
|
2120
|
+
id: benchmark.id,
|
|
2121
|
+
title: benchmark.title,
|
|
2122
|
+
scenarioId: benchmark.scenarioId
|
|
2123
|
+
},
|
|
2124
|
+
observer: observerSummary,
|
|
2125
|
+
summary,
|
|
2126
|
+
runs
|
|
2127
|
+
};
|
|
2128
|
+
if (opts.json) {
|
|
2129
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
p.intro("codemem memory extraction-benchmark");
|
|
2133
|
+
p.log.info([
|
|
2134
|
+
`Benchmark: ${benchmark.id} — ${benchmark.title}`,
|
|
2135
|
+
`Observer: ${observerSummary.provider}/${observerSummary.model}`,
|
|
2136
|
+
`Tier routing: ${opts.observerTierRouting === true ? "yes" : "no"}`,
|
|
2137
|
+
`OpenAI Responses: ${observerSummary.openaiUseResponses === null ? "mixed" : observerSummary.openaiUseResponses ? "yes" : "no"}`,
|
|
2138
|
+
`Reasoning effort: ${observerSummary.reasoningEffort ?? "none"}`,
|
|
2139
|
+
`Reasoning summary: ${observerSummary.reasoningSummary ?? "none"}`,
|
|
2140
|
+
`Max output tokens: ${observerSummary.maxOutputTokens ?? "mixed"}`,
|
|
2141
|
+
`Temperature: ${observerSummary.temperature ?? "mixed"}`,
|
|
2142
|
+
`Transcript budget override: ${transcriptBudget ?? "default"}`,
|
|
2143
|
+
`Shape-quality passes: ${summary.shapeQualityPasses}/${summary.shapeQualityTotal}`,
|
|
2144
|
+
`Shape-quality fails: ${summary.shapeQualityFails}`,
|
|
2145
|
+
`Expected-tier matches: ${summary.expectedTierMatches}/${summary.expectedTierTotal}`,
|
|
2146
|
+
`Observer no-output cases: ${summary.robustnessNoOutput}`
|
|
2147
|
+
].join("\n"));
|
|
2148
|
+
for (const run of runs) p.log.message(` [${run.batchId}] ${run.status.padEnd(18)} ${run.complexity.padEnd(10)} tier=${run.tier.padEnd(6)} expected=${(run.expectedTier ?? "n/a").padEnd(6)} span=${String(run.analysis.eventSpan).padEnd(3)} prompts=${run.analysis.promptCount} tools=${String(run.analysis.toolCount).padEnd(2)} transcript=${run.analysis.transcriptLength} ${run.provider}/${run.model}${run.openaiUseResponses ? " [responses]" : ""} summaries=${run.summaries} observations=${run.observations} repair=${run.repairApplied ? "yes" : "no"} — ${run.label}`);
|
|
2149
|
+
p.outro("done");
|
|
2150
|
+
});
|
|
2151
|
+
return cmd;
|
|
764
2152
|
}
|
|
765
|
-
function
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
} finally {
|
|
781
|
-
store.close();
|
|
2153
|
+
function createMemoryRelinkReportCommand() {
|
|
2154
|
+
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");
|
|
2155
|
+
addDbOption(cmd);
|
|
2156
|
+
addJsonOption(cmd);
|
|
2157
|
+
cmd.action((opts) => {
|
|
2158
|
+
const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
|
|
2159
|
+
const limit = Number.parseInt(opts.limit ?? "25", 10) || 25;
|
|
2160
|
+
const result = getRawEventRelinkReport(resolveDbOpt(opts), {
|
|
2161
|
+
project,
|
|
2162
|
+
allProjects: opts.allProjects === true,
|
|
2163
|
+
limit
|
|
2164
|
+
});
|
|
2165
|
+
if (opts.json) {
|
|
2166
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2167
|
+
return;
|
|
782
2168
|
}
|
|
2169
|
+
p.intro("codemem memory relink-report");
|
|
2170
|
+
p.log.info([
|
|
2171
|
+
`Recoverable sessions: ${result.totals.recoverable_sessions}`,
|
|
2172
|
+
`Distinct stable ids: ${result.totals.distinct_stable_ids}`,
|
|
2173
|
+
`Groups with multiple sessions: ${result.totals.groups_with_multiple_sessions}`,
|
|
2174
|
+
`Groups with mapped session: ${result.totals.groups_with_mapped_session}`,
|
|
2175
|
+
`Groups without mapped session: ${result.totals.groups_without_mapped_session}`,
|
|
2176
|
+
`Active memories in groups: ${result.totals.active_memories}`,
|
|
2177
|
+
`Repointable active memories: ${result.totals.repointable_active_memories}`
|
|
2178
|
+
].join("\n"));
|
|
2179
|
+
p.log.info("Top relink groups:");
|
|
2180
|
+
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}`);
|
|
2181
|
+
p.outro("done");
|
|
783
2182
|
});
|
|
2183
|
+
return cmd;
|
|
784
2184
|
}
|
|
785
|
-
function
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
2185
|
+
function createMemoryRelinkPlanCommand() {
|
|
2186
|
+
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");
|
|
2187
|
+
addDbOption(cmd);
|
|
2188
|
+
addJsonOption(cmd);
|
|
2189
|
+
cmd.action((opts) => {
|
|
2190
|
+
const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
|
|
2191
|
+
const limit = Number.parseInt(opts.limit ?? "25", 10) || 25;
|
|
2192
|
+
const result = getRawEventRelinkPlan(resolveDbOpt(opts), {
|
|
2193
|
+
project,
|
|
2194
|
+
allProjects: opts.allProjects === true,
|
|
2195
|
+
limit
|
|
2196
|
+
});
|
|
2197
|
+
if (opts.json) {
|
|
2198
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
p.intro("codemem memory relink-plan");
|
|
2202
|
+
p.log.info([
|
|
2203
|
+
`Groups: ${result.totals.groups}`,
|
|
2204
|
+
`Eligible groups: ${result.totals.eligible_groups}`,
|
|
2205
|
+
`Skipped groups: ${result.totals.skipped_groups}`,
|
|
2206
|
+
`Actions: ${result.totals.actions}`,
|
|
2207
|
+
`Bridge creations: ${result.totals.bridge_creations}`,
|
|
2208
|
+
`Memory repoints: ${result.totals.memory_repoints}`,
|
|
2209
|
+
`Session compactions: ${result.totals.session_compactions}`
|
|
2210
|
+
].join("\n"));
|
|
2211
|
+
p.log.info("Planned actions:");
|
|
2212
|
+
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}`);
|
|
2213
|
+
if (result.skipped_groups.length > 0) {
|
|
2214
|
+
p.log.info("Skipped groups:");
|
|
2215
|
+
for (const group of result.skipped_groups.slice(0, 10)) p.log.message(` ${group.stable_id} | blockers=${group.blockers.join(",")}`);
|
|
2216
|
+
}
|
|
2217
|
+
p.outro("done");
|
|
790
2218
|
});
|
|
2219
|
+
return cmd;
|
|
791
2220
|
}
|
|
792
2221
|
var showMemoryCommand = createShowMemoryCommand();
|
|
793
2222
|
var forgetMemoryCommand = createForgetMemoryCommand();
|
|
@@ -797,24 +2226,22 @@ memoryCommand.addCommand(createShowMemoryCommand());
|
|
|
797
2226
|
memoryCommand.addCommand(createForgetMemoryCommand());
|
|
798
2227
|
memoryCommand.addCommand(createRememberMemoryCommand());
|
|
799
2228
|
memoryCommand.addCommand(createInjectMemoryCommand());
|
|
800
|
-
memoryCommand.addCommand(
|
|
2229
|
+
memoryCommand.addCommand(createMemoryRoleReportCommand());
|
|
2230
|
+
memoryCommand.addCommand(createMemoryRoleCompareCommand());
|
|
2231
|
+
memoryCommand.addCommand(createMemoryExtractionReportCommand());
|
|
2232
|
+
memoryCommand.addCommand(createMemoryExtractionReplayCommand());
|
|
2233
|
+
memoryCommand.addCommand(createMemoryExtractionBenchmarkCommand());
|
|
2234
|
+
memoryCommand.addCommand(createMemoryRelinkReportCommand());
|
|
2235
|
+
memoryCommand.addCommand(createMemoryRelinkPlanCommand());
|
|
801
2236
|
//#endregion
|
|
802
2237
|
//#region src/commands/pack.ts
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
var packCommand =
|
|
807
|
-
const store = new MemoryStore(resolveDbPath(opts
|
|
2238
|
+
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");
|
|
2239
|
+
addDbOption(packCmd);
|
|
2240
|
+
addJsonOption(packCmd);
|
|
2241
|
+
var packCommand = packCmd.action(async (context, opts) => {
|
|
2242
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
808
2243
|
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;
|
|
2244
|
+
const { limit, budget, filters } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
|
|
818
2245
|
const result = await store.buildMemoryPackAsync(context, limit, budget, filters);
|
|
819
2246
|
if (opts.json) {
|
|
820
2247
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -837,8 +2264,11 @@ var packCommand = new Command("pack").configureHelp(helpStyle).description("Buil
|
|
|
837
2264
|
});
|
|
838
2265
|
//#endregion
|
|
839
2266
|
//#region src/commands/recent.ts
|
|
840
|
-
var
|
|
841
|
-
|
|
2267
|
+
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");
|
|
2268
|
+
addDbOption(cmd);
|
|
2269
|
+
addJsonOption(cmd);
|
|
2270
|
+
cmd.action((opts) => {
|
|
2271
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
842
2272
|
try {
|
|
843
2273
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
|
|
844
2274
|
const filters = {};
|
|
@@ -848,15 +2278,20 @@ var recentCommand = new Command("recent").configureHelp(helpStyle).description("
|
|
|
848
2278
|
if (project) filters.project = project;
|
|
849
2279
|
}
|
|
850
2280
|
const items = store.recent(limit, filters);
|
|
851
|
-
|
|
2281
|
+
if (opts.json) console.log(JSON.stringify(items));
|
|
2282
|
+
else for (const item of items) console.log(`#${item.id} [${item.kind}] ${item.title}`);
|
|
852
2283
|
} finally {
|
|
853
2284
|
store.close();
|
|
854
2285
|
}
|
|
855
2286
|
});
|
|
2287
|
+
var recentCommand = cmd;
|
|
856
2288
|
//#endregion
|
|
857
2289
|
//#region src/commands/search.ts
|
|
858
|
-
var
|
|
859
|
-
|
|
2290
|
+
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");
|
|
2291
|
+
addDbOption(searchCmd);
|
|
2292
|
+
addJsonOption(searchCmd);
|
|
2293
|
+
var searchCommand = searchCmd.action((query, opts) => {
|
|
2294
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
860
2295
|
try {
|
|
861
2296
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
|
|
862
2297
|
const filters = {};
|
|
@@ -900,6 +2335,10 @@ function timeSince(isoDate) {
|
|
|
900
2335
|
}
|
|
901
2336
|
//#endregion
|
|
902
2337
|
//#region src/commands/serve-invocation.ts
|
|
2338
|
+
/**
|
|
2339
|
+
* Parse and validate a port string. Throws a user-friendly message (no stack trace)
|
|
2340
|
+
* that callers in serve.ts catch at the action boundary.
|
|
2341
|
+
*/
|
|
903
2342
|
function parsePort(rawPort) {
|
|
904
2343
|
const port = Number.parseInt(rawPort, 10);
|
|
905
2344
|
if (!Number.isFinite(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${rawPort}`);
|
|
@@ -908,10 +2347,11 @@ function parsePort(rawPort) {
|
|
|
908
2347
|
function resolveLegacyServeInvocation(opts) {
|
|
909
2348
|
if (opts.stop && opts.restart) throw new Error("Use only one of --stop or --restart");
|
|
910
2349
|
if (opts.foreground && opts.background) throw new Error("Use only one of --background or --foreground");
|
|
911
|
-
const dbPath = opts
|
|
2350
|
+
const dbPath = resolveDbOpt(opts) ?? null;
|
|
912
2351
|
return {
|
|
913
2352
|
mode: opts.stop ? "stop" : opts.restart ? "restart" : "start",
|
|
914
2353
|
dbPath,
|
|
2354
|
+
configPath: opts.config ?? null,
|
|
915
2355
|
host: opts.host,
|
|
916
2356
|
port: parsePort(opts.port),
|
|
917
2357
|
background: opts.restart ? true : opts.background ? true : false
|
|
@@ -926,7 +2366,8 @@ function resolveServeInvocation(action, opts) {
|
|
|
926
2366
|
function resolveStartServeInvocation(opts) {
|
|
927
2367
|
return {
|
|
928
2368
|
mode: "start",
|
|
929
|
-
dbPath: opts
|
|
2369
|
+
dbPath: resolveDbOpt(opts) ?? null,
|
|
2370
|
+
configPath: opts.config ?? null,
|
|
930
2371
|
host: opts.host,
|
|
931
2372
|
port: parsePort(opts.port),
|
|
932
2373
|
background: !opts.foreground
|
|
@@ -935,7 +2376,8 @@ function resolveStartServeInvocation(opts) {
|
|
|
935
2376
|
function resolveStopRestartInvocation(mode, opts) {
|
|
936
2377
|
return {
|
|
937
2378
|
mode,
|
|
938
|
-
dbPath: opts
|
|
2379
|
+
dbPath: resolveDbOpt(opts) ?? null,
|
|
2380
|
+
configPath: opts.config ?? null,
|
|
939
2381
|
host: opts.host,
|
|
940
2382
|
port: parsePort(opts.port),
|
|
941
2383
|
background: mode === "restart"
|
|
@@ -1182,14 +2624,19 @@ async function startBackgroundViewer(invocation) {
|
|
|
1182
2624
|
return;
|
|
1183
2625
|
}
|
|
1184
2626
|
const scriptPath = process.argv[1];
|
|
1185
|
-
if (!scriptPath)
|
|
2627
|
+
if (!scriptPath) {
|
|
2628
|
+
p.log.error("Unable to resolve CLI entrypoint for background launch");
|
|
2629
|
+
process.exitCode = 1;
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
1186
2632
|
const child = spawn(process.execPath, buildForegroundRunnerArgs(scriptPath, invocation), {
|
|
1187
2633
|
cwd: process.cwd(),
|
|
1188
2634
|
detached: true,
|
|
1189
2635
|
stdio: "ignore",
|
|
1190
2636
|
env: {
|
|
1191
2637
|
...process.env,
|
|
1192
|
-
...invocation.dbPath ? { CODEMEM_DB: invocation.dbPath } : {}
|
|
2638
|
+
...invocation.dbPath ? { CODEMEM_DB: invocation.dbPath } : {},
|
|
2639
|
+
...invocation.configPath ? { CODEMEM_CONFIG: invocation.configPath } : {}
|
|
1193
2640
|
}
|
|
1194
2641
|
});
|
|
1195
2642
|
child.unref();
|
|
@@ -1205,6 +2652,7 @@ async function startForegroundViewer(invocation) {
|
|
|
1205
2652
|
const { createApp, createSyncApp, closeStore, getStore } = await import("@codemem/server");
|
|
1206
2653
|
const { serve } = await import("@hono/node-server");
|
|
1207
2654
|
if (invocation.dbPath) process.env.CODEMEM_DB = invocation.dbPath;
|
|
2655
|
+
if (invocation.configPath) process.env.CODEMEM_CONFIG = invocation.configPath;
|
|
1208
2656
|
warnIfViewerExposed(invocation.host, invocation.port);
|
|
1209
2657
|
if (await isPortOpen(invocation.host, invocation.port)) {
|
|
1210
2658
|
p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
|
|
@@ -1229,7 +2677,7 @@ async function startForegroundViewer(invocation) {
|
|
|
1229
2677
|
sweeper.start();
|
|
1230
2678
|
const syncAbort = new AbortController();
|
|
1231
2679
|
const retentionAbort = new AbortController();
|
|
1232
|
-
const syncConfig = readCoordinatorSyncConfig(readCodememConfigFile());
|
|
2680
|
+
const syncConfig = readCoordinatorSyncConfig(invocation.configPath ? readCodememConfigFileAtPath(invocation.configPath) : readCodememConfigFile());
|
|
1233
2681
|
const syncEnabled = syncConfig.syncEnabled;
|
|
1234
2682
|
const retentionRunner = new SyncRetentionRunner({
|
|
1235
2683
|
dbPath: resolveDbPath(invocation.dbPath ?? void 0),
|
|
@@ -1408,17 +2856,31 @@ async function runServeInvocation(invocation) {
|
|
|
1408
2856
|
});
|
|
1409
2857
|
}
|
|
1410
2858
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
2859
|
+
var serveCmd = new Command("serve").configureHelp(helpStyle).description("Run or manage the viewer").argument("[action]", "lifecycle action (start|stop|restart)");
|
|
2860
|
+
addDbOption(serveCmd);
|
|
2861
|
+
addConfigOption(serveCmd);
|
|
2862
|
+
addViewerHostOptions(serveCmd);
|
|
2863
|
+
serveCmd.addOption(new Option("--background", "run viewer in background").hideHelp());
|
|
2864
|
+
serveCmd.addOption(new Option("--foreground", "run viewer in foreground").hideHelp());
|
|
2865
|
+
serveCmd.addOption(new Option("--stop", "stop background viewer").hideHelp());
|
|
2866
|
+
serveCmd.addOption(new Option("--restart", "restart background viewer").hideHelp());
|
|
2867
|
+
var serveCommand = serveCmd.action(async (action, opts) => {
|
|
2868
|
+
try {
|
|
2869
|
+
if (opts.stop) emitDeprecationWarning("--stop", "codemem serve stop");
|
|
2870
|
+
if (opts.restart) emitDeprecationWarning("--restart", "codemem serve restart");
|
|
2871
|
+
if (opts.background) emitDeprecationWarning("--background", "codemem serve start");
|
|
2872
|
+
if (opts.foreground) emitDeprecationWarning("--foreground", "codemem serve start --foreground");
|
|
2873
|
+
const normalizedAction = action === void 0 ? void 0 : action === "start" || action === "stop" || action === "restart" ? action : null;
|
|
2874
|
+
if (normalizedAction === null) {
|
|
2875
|
+
p.log.error(`Unknown serve action: ${action}`);
|
|
2876
|
+
process.exitCode = 1;
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
await runServeInvocation(resolveServeInvocation(normalizedAction, opts));
|
|
2880
|
+
} catch (err) {
|
|
2881
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
1418
2882
|
process.exitCode = 1;
|
|
1419
|
-
return;
|
|
1420
2883
|
}
|
|
1421
|
-
await runServeInvocation(resolveServeInvocation(normalizedAction, opts));
|
|
1422
2884
|
});
|
|
1423
2885
|
//#endregion
|
|
1424
2886
|
//#region src/commands/setup-config.ts
|
|
@@ -1673,8 +3135,11 @@ function fmtTokens(n) {
|
|
|
1673
3135
|
if (n >= 1e3) return `~${(n / 1e3).toFixed(0)}K`;
|
|
1674
3136
|
return `${n}`;
|
|
1675
3137
|
}
|
|
1676
|
-
var
|
|
1677
|
-
|
|
3138
|
+
var statsCmd = new Command("stats").configureHelp(helpStyle).description("Show database statistics");
|
|
3139
|
+
addDbOption(statsCmd);
|
|
3140
|
+
addJsonOption(statsCmd);
|
|
3141
|
+
var statsCommand = statsCmd.action((opts) => {
|
|
3142
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
1678
3143
|
try {
|
|
1679
3144
|
const result = store.stats();
|
|
1680
3145
|
if (opts.json) {
|
|
@@ -1728,7 +3193,9 @@ function buildServeLifecycleArgs(action, opts, scriptPath, execArgv = []) {
|
|
|
1728
3193
|
if (action === "start") args.push("--restart");
|
|
1729
3194
|
else if (action === "stop") args.push("--stop");
|
|
1730
3195
|
else args.push("--restart");
|
|
1731
|
-
|
|
3196
|
+
const dbResolved = resolveDbOpt(opts);
|
|
3197
|
+
if (dbResolved) args.push("--db-path", dbResolved);
|
|
3198
|
+
if (opts.config) args.push("--config", opts.config);
|
|
1732
3199
|
if (opts.host) args.push("--host", opts.host);
|
|
1733
3200
|
if (opts.port) args.push("--port", opts.port);
|
|
1734
3201
|
return args;
|
|
@@ -1752,10 +3219,22 @@ function collectAdvertiseAddresses(explicitAddress, configuredHost, port, interf
|
|
|
1752
3219
|
/**
|
|
1753
3220
|
* Sync CLI commands — enable/disable/status/peers/connect.
|
|
1754
3221
|
*/
|
|
3222
|
+
function readCliConfig(configPath) {
|
|
3223
|
+
return configPath ? readCodememConfigFileAtPath(configPath) : readCodememConfigFile();
|
|
3224
|
+
}
|
|
3225
|
+
function writeCliConfig(config, configPath) {
|
|
3226
|
+
return writeCodememConfigFile(config, configPath || void 0);
|
|
3227
|
+
}
|
|
1755
3228
|
function parseAttemptsLimit(value) {
|
|
1756
3229
|
if (!/^\d+$/.test(value.trim())) throw new Error(`Invalid --limit: ${value}`);
|
|
1757
3230
|
return Number.parseInt(value, 10);
|
|
1758
3231
|
}
|
|
3232
|
+
function parsePositiveIntegerOption(value, flagName) {
|
|
3233
|
+
if (value == null) return void 0;
|
|
3234
|
+
const trimmed = value.trim();
|
|
3235
|
+
if (!/^\d+$/.test(trimmed)) throw new Error(`Invalid ${flagName}: ${value}`);
|
|
3236
|
+
return Number.parseInt(trimmed, 10);
|
|
3237
|
+
}
|
|
1759
3238
|
function resolvePeerMatch(db, peerRef) {
|
|
1760
3239
|
const trimmed = peerRef.trim();
|
|
1761
3240
|
if (!trimmed) return null;
|
|
@@ -1771,18 +3250,6 @@ function resolvePeerMatch(db, peerRef) {
|
|
|
1771
3250
|
if (byName.length > 1) return "ambiguous";
|
|
1772
3251
|
return byName[0] ?? null;
|
|
1773
3252
|
}
|
|
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
3253
|
async function portOpen(host, port) {
|
|
1787
3254
|
return new Promise((resolve) => {
|
|
1788
3255
|
const socket = net.createConnection({
|
|
@@ -1828,12 +3295,13 @@ function parseStoredAddressEndpoint(value) {
|
|
|
1828
3295
|
async function runServeLifecycle(action, opts) {
|
|
1829
3296
|
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
3297
|
if (action === "start") {
|
|
1831
|
-
if (
|
|
3298
|
+
if (readCoordinatorSyncConfig(readCliConfig(opts.config)).syncEnabled !== true) {
|
|
1832
3299
|
p.log.error("Sync is disabled. Run `codemem sync enable` first.");
|
|
1833
3300
|
process.exitCode = 1;
|
|
1834
3301
|
return;
|
|
1835
3302
|
}
|
|
1836
3303
|
}
|
|
3304
|
+
const dbResolved = resolveDbOpt(opts);
|
|
1837
3305
|
const args = buildServeLifecycleArgs(action, opts, process.argv[1] ?? "", process.execArgv);
|
|
1838
3306
|
await new Promise((resolve, reject) => {
|
|
1839
3307
|
const child = spawn(process.execPath, args, {
|
|
@@ -1841,7 +3309,8 @@ async function runServeLifecycle(action, opts) {
|
|
|
1841
3309
|
stdio: "inherit",
|
|
1842
3310
|
env: {
|
|
1843
3311
|
...process.env,
|
|
1844
|
-
...
|
|
3312
|
+
...dbResolved ? { CODEMEM_DB: dbResolved } : {},
|
|
3313
|
+
...opts.config ? { CODEMEM_CONFIG: opts.config } : {}
|
|
1845
3314
|
}
|
|
1846
3315
|
});
|
|
1847
3316
|
child.once("error", reject);
|
|
@@ -1852,8 +3321,11 @@ async function runServeLifecycle(action, opts) {
|
|
|
1852
3321
|
});
|
|
1853
3322
|
}
|
|
1854
3323
|
var syncCommand = new Command("sync").configureHelp(helpStyle).description("Sync configuration and peer management");
|
|
1855
|
-
|
|
1856
|
-
|
|
3324
|
+
var attemptsCmd = new Command("attempts").configureHelp(helpStyle).description("Show recent sync attempts").option("--limit <n>", "max attempts", "10");
|
|
3325
|
+
addDbOption(attemptsCmd);
|
|
3326
|
+
addJsonOption(attemptsCmd);
|
|
3327
|
+
attemptsCmd.action((opts) => {
|
|
3328
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
1857
3329
|
try {
|
|
1858
3330
|
const d = drizzle(store.db, { schema });
|
|
1859
3331
|
const limit = parseAttemptsLimit(opts.limit);
|
|
@@ -1873,19 +3345,30 @@ syncCommand.addCommand(new Command("attempts").configureHelp(helpStyle).descript
|
|
|
1873
3345
|
} finally {
|
|
1874
3346
|
store.close();
|
|
1875
3347
|
}
|
|
1876
|
-
})
|
|
1877
|
-
syncCommand.addCommand(
|
|
1878
|
-
|
|
1879
|
-
})
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
})
|
|
1886
|
-
|
|
1887
|
-
|
|
3348
|
+
});
|
|
3349
|
+
syncCommand.addCommand(attemptsCmd);
|
|
3350
|
+
function addDeprecatedLifecycleCommand(action) {
|
|
3351
|
+
const cmd = new Command(action).configureHelp(helpStyle).description(`[deprecated] ${action} sync daemon — use 'codemem serve ${action}'`);
|
|
3352
|
+
addDbOption(cmd);
|
|
3353
|
+
addConfigOption(cmd);
|
|
3354
|
+
addViewerHostOptions(cmd);
|
|
3355
|
+
addLegacyServiceFlags(cmd);
|
|
3356
|
+
cmd.action(async (opts) => {
|
|
3357
|
+
emitDeprecationWarning(`codemem sync ${action}`, `codemem serve ${action}`);
|
|
3358
|
+
await runServeLifecycle(action, opts);
|
|
3359
|
+
});
|
|
3360
|
+
syncCommand.addCommand(cmd, { hidden: true });
|
|
3361
|
+
}
|
|
3362
|
+
addDeprecatedLifecycleCommand("start");
|
|
3363
|
+
addDeprecatedLifecycleCommand("stop");
|
|
3364
|
+
addDeprecatedLifecycleCommand("restart");
|
|
3365
|
+
var onceCmd = new Command("once").configureHelp(helpStyle).description("Run a single sync pass").option("--peer <peer>", "peer device id or name");
|
|
3366
|
+
addDbOption(onceCmd);
|
|
3367
|
+
addJsonOption(onceCmd);
|
|
3368
|
+
onceCmd.action(async (opts) => {
|
|
3369
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
1888
3370
|
try {
|
|
3371
|
+
const keysDir = process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
|
|
1889
3372
|
syncPassPreflight(store.db);
|
|
1890
3373
|
const d = drizzle(store.db, { schema });
|
|
1891
3374
|
const rows = opts.peer ? (() => {
|
|
@@ -1893,6 +3376,10 @@ syncCommand.addCommand(new Command("once").configureHelp(helpStyle).description(
|
|
|
1893
3376
|
if (deviceMatches.length > 0) return deviceMatches;
|
|
1894
3377
|
const nameMatches = d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).where(eq(schema.syncPeers.name, opts.peer)).all();
|
|
1895
3378
|
if (nameMatches.length > 1) {
|
|
3379
|
+
if (opts.json) {
|
|
3380
|
+
emitJsonError("ambiguous_peer", `Peer name is ambiguous: ${opts.peer}`);
|
|
3381
|
+
return [];
|
|
3382
|
+
}
|
|
1896
3383
|
p.log.error(`Peer name is ambiguous: ${opts.peer}`);
|
|
1897
3384
|
process.exitCode = 1;
|
|
1898
3385
|
return [];
|
|
@@ -1900,31 +3387,59 @@ syncCommand.addCommand(new Command("once").configureHelp(helpStyle).description(
|
|
|
1900
3387
|
return nameMatches;
|
|
1901
3388
|
})() : d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).all();
|
|
1902
3389
|
if (rows.length === 0) {
|
|
3390
|
+
if (process.exitCode) return;
|
|
3391
|
+
if (opts.json) {
|
|
3392
|
+
emitJsonError("no_peers", "No peers available for sync");
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
1903
3395
|
p.log.warn("No peers available for sync");
|
|
1904
3396
|
process.exitCode = 1;
|
|
1905
3397
|
return;
|
|
1906
3398
|
}
|
|
1907
3399
|
let hadFailure = false;
|
|
3400
|
+
const results = [];
|
|
1908
3401
|
for (const row of rows) {
|
|
1909
|
-
const result = await runSyncPass(store.db, row.peer_device_id);
|
|
3402
|
+
const result = await runSyncPass(store.db, row.peer_device_id, { keysDir });
|
|
1910
3403
|
if (!result.ok) hadFailure = true;
|
|
1911
|
-
|
|
3404
|
+
results.push({
|
|
3405
|
+
peer_device_id: row.peer_device_id,
|
|
3406
|
+
ok: result.ok,
|
|
3407
|
+
...result.error ? { error: result.error } : {}
|
|
3408
|
+
});
|
|
3409
|
+
if (!opts.json) console.log(formatSyncOnceResult(row.peer_device_id, result));
|
|
1912
3410
|
}
|
|
3411
|
+
if (opts.json) console.log(JSON.stringify({
|
|
3412
|
+
ok: !hadFailure,
|
|
3413
|
+
results
|
|
3414
|
+
}, null, 2));
|
|
1913
3415
|
if (hadFailure) process.exitCode = 1;
|
|
1914
3416
|
} finally {
|
|
1915
3417
|
store.close();
|
|
1916
3418
|
}
|
|
1917
|
-
})
|
|
1918
|
-
syncCommand.addCommand(
|
|
1919
|
-
|
|
3419
|
+
});
|
|
3420
|
+
syncCommand.addCommand(onceCmd);
|
|
3421
|
+
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");
|
|
3422
|
+
addDbOption(pairCmd);
|
|
3423
|
+
addConfigOption(pairCmd);
|
|
3424
|
+
addJsonOption(pairCmd);
|
|
3425
|
+
pairCmd.action(async (opts) => {
|
|
3426
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
1920
3427
|
try {
|
|
1921
3428
|
const acceptModeRequested = opts.accept != null || opts.acceptFile != null;
|
|
1922
3429
|
if (opts.payloadOnly && acceptModeRequested) {
|
|
3430
|
+
if (opts.json) {
|
|
3431
|
+
emitJsonError("usage_error", "--payload-only cannot be combined with --accept or --accept-file", 2);
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
1923
3434
|
p.log.error("--payload-only cannot be combined with --accept or --accept-file");
|
|
1924
3435
|
process.exitCode = 1;
|
|
1925
3436
|
return;
|
|
1926
3437
|
}
|
|
1927
3438
|
if (opts.accept && opts.acceptFile) {
|
|
3439
|
+
if (opts.json) {
|
|
3440
|
+
emitJsonError("usage_error", "Use only one of --accept or --accept-file", 2);
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
1928
3443
|
p.log.error("Use only one of --accept or --accept-file");
|
|
1929
3444
|
process.exitCode = 1;
|
|
1930
3445
|
return;
|
|
@@ -1941,27 +3456,48 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
1941
3456
|
process.stdin.on("error", reject);
|
|
1942
3457
|
}) : readFileSync(opts.acceptFile, "utf8");
|
|
1943
3458
|
} catch (error) {
|
|
1944
|
-
|
|
3459
|
+
const msg = error instanceof Error ? `Failed to read pairing payload from ${opts.acceptFile}: ${error.message}` : `Failed to read pairing payload from ${opts.acceptFile}`;
|
|
3460
|
+
if (opts.json) {
|
|
3461
|
+
emitJsonError("read_error", msg);
|
|
3462
|
+
return;
|
|
3463
|
+
}
|
|
3464
|
+
p.log.error(msg);
|
|
1945
3465
|
process.exitCode = 1;
|
|
1946
3466
|
return;
|
|
1947
3467
|
}
|
|
1948
3468
|
if (acceptModeRequested && !(acceptText ?? "").trim()) {
|
|
3469
|
+
if (opts.json) {
|
|
3470
|
+
emitJsonError("usage_error", "Empty pairing payload; provide JSON via --accept or --accept-file", 2);
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
1949
3473
|
p.log.error("Empty pairing payload; provide JSON via --accept or --accept-file");
|
|
1950
3474
|
process.exitCode = 1;
|
|
1951
3475
|
return;
|
|
1952
3476
|
}
|
|
1953
3477
|
if (!acceptText && (opts.include || opts.exclude || opts.all || opts.default)) {
|
|
3478
|
+
if (opts.json) {
|
|
3479
|
+
emitJsonError("usage_error", "Project filters are outbound-only and must be set on the device running --accept", 2);
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
1954
3482
|
p.log.error("Project filters are outbound-only and must be set on the device running --accept");
|
|
1955
3483
|
process.exitCode = 1;
|
|
1956
3484
|
return;
|
|
1957
3485
|
}
|
|
1958
3486
|
if (acceptText?.trim()) {
|
|
1959
3487
|
if (opts.all && opts.default) {
|
|
3488
|
+
if (opts.json) {
|
|
3489
|
+
emitJsonError("usage_error", "Use only one of --all or --default", 2);
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
1960
3492
|
p.log.error("Use only one of --all or --default");
|
|
1961
3493
|
process.exitCode = 1;
|
|
1962
3494
|
return;
|
|
1963
3495
|
}
|
|
1964
3496
|
if ((opts.all || opts.default) && (opts.include || opts.exclude)) {
|
|
3497
|
+
if (opts.json) {
|
|
3498
|
+
emitJsonError("usage_error", "--include/--exclude cannot be combined with --all/--default", 2);
|
|
3499
|
+
return;
|
|
3500
|
+
}
|
|
1965
3501
|
p.log.error("--include/--exclude cannot be combined with --all/--default");
|
|
1966
3502
|
process.exitCode = 1;
|
|
1967
3503
|
return;
|
|
@@ -1970,7 +3506,12 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
1970
3506
|
try {
|
|
1971
3507
|
payload = JSON.parse(acceptText);
|
|
1972
3508
|
} catch (error) {
|
|
1973
|
-
|
|
3509
|
+
const msg = error instanceof Error ? `Invalid pairing payload: ${error.message}` : "Invalid pairing payload";
|
|
3510
|
+
if (opts.json) {
|
|
3511
|
+
emitJsonError("invalid_payload", msg);
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
3514
|
+
p.log.error(msg);
|
|
1974
3515
|
process.exitCode = 1;
|
|
1975
3516
|
return;
|
|
1976
3517
|
}
|
|
@@ -1979,11 +3520,20 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
1979
3520
|
const publicKey = String(payload.public_key || "").trim();
|
|
1980
3521
|
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
3522
|
if (!deviceId || !fingerprint || !publicKey || resolvedAddresses.length === 0) {
|
|
1982
|
-
|
|
3523
|
+
const msg = "Pairing payload missing device_id, fingerprint, public_key, or addresses";
|
|
3524
|
+
if (opts.json) {
|
|
3525
|
+
emitJsonError("invalid_payload", msg);
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
p.log.error(msg);
|
|
1983
3529
|
process.exitCode = 1;
|
|
1984
3530
|
return;
|
|
1985
3531
|
}
|
|
1986
3532
|
if (fingerprintPublicKey(publicKey) !== fingerprint) {
|
|
3533
|
+
if (opts.json) {
|
|
3534
|
+
emitJsonError("fingerprint_mismatch", "Pairing payload fingerprint mismatch");
|
|
3535
|
+
return;
|
|
3536
|
+
}
|
|
1987
3537
|
p.log.error("Pairing payload fingerprint mismatch");
|
|
1988
3538
|
process.exitCode = 1;
|
|
1989
3539
|
return;
|
|
@@ -2001,6 +3551,13 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
2001
3551
|
include: opts.all ? [] : parseProjectList(opts.include),
|
|
2002
3552
|
exclude: opts.all ? [] : parseProjectList(opts.exclude)
|
|
2003
3553
|
});
|
|
3554
|
+
if (opts.json) {
|
|
3555
|
+
console.log(JSON.stringify({
|
|
3556
|
+
ok: true,
|
|
3557
|
+
peer_device_id: deviceId
|
|
3558
|
+
}));
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
2004
3561
|
p.log.success(`Paired with ${deviceId}`);
|
|
2005
3562
|
return;
|
|
2006
3563
|
}
|
|
@@ -2008,24 +3565,28 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
2008
3565
|
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir });
|
|
2009
3566
|
const publicKey = loadPublicKey(keysDir);
|
|
2010
3567
|
if (!publicKey) {
|
|
3568
|
+
if (opts.json) {
|
|
3569
|
+
emitJsonError("missing_key", "Public key missing");
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
2011
3572
|
p.log.error("Public key missing");
|
|
2012
3573
|
process.exitCode = 1;
|
|
2013
3574
|
return;
|
|
2014
3575
|
}
|
|
2015
|
-
const config =
|
|
3576
|
+
const config = readCliConfig(opts.config);
|
|
2016
3577
|
const explicitAddress = opts.address?.trim();
|
|
2017
3578
|
const configuredHost = typeof config.sync_host === "string" ? config.sync_host : null;
|
|
2018
3579
|
const configuredPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
|
|
2019
3580
|
const addresses = collectAdvertiseAddresses(explicitAddress ?? null, configuredHost, configuredPort, networkInterfaces());
|
|
2020
|
-
const
|
|
3581
|
+
const payloadObj = {
|
|
2021
3582
|
device_id: deviceId,
|
|
2022
3583
|
fingerprint,
|
|
2023
3584
|
public_key: publicKey,
|
|
2024
3585
|
address: addresses[0] ?? "",
|
|
2025
3586
|
addresses
|
|
2026
3587
|
};
|
|
2027
|
-
const payloadText = JSON.stringify(
|
|
2028
|
-
if (opts.payloadOnly) {
|
|
3588
|
+
const payloadText = JSON.stringify(payloadObj);
|
|
3589
|
+
if (opts.payloadOnly || opts.json) {
|
|
2029
3590
|
process.stdout.write(`${payloadText}\n`);
|
|
2030
3591
|
return;
|
|
2031
3592
|
}
|
|
@@ -2042,10 +3603,15 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
|
|
|
2042
3603
|
} finally {
|
|
2043
3604
|
store.close();
|
|
2044
3605
|
}
|
|
2045
|
-
})
|
|
2046
|
-
syncCommand.addCommand(
|
|
2047
|
-
|
|
2048
|
-
|
|
3606
|
+
});
|
|
3607
|
+
syncCommand.addCommand(pairCmd);
|
|
3608
|
+
var doctorCmd = new Command("doctor").configureHelp(helpStyle).description("Diagnose common sync setup and connectivity issues");
|
|
3609
|
+
addDbOption(doctorCmd);
|
|
3610
|
+
addConfigOption(doctorCmd);
|
|
3611
|
+
addJsonOption(doctorCmd);
|
|
3612
|
+
doctorCmd.action(async (opts) => {
|
|
3613
|
+
const config = readCliConfig(opts.config);
|
|
3614
|
+
const dbPath = resolveDbPath(resolveDbOpt(opts));
|
|
2049
3615
|
const store = new MemoryStore(dbPath);
|
|
2050
3616
|
try {
|
|
2051
3617
|
const d = drizzle(store.db, { schema });
|
|
@@ -2061,47 +3627,70 @@ syncCommand.addCommand(new Command("doctor").configureHelp(helpStyle).descriptio
|
|
|
2061
3627
|
const syncHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
|
|
2062
3628
|
const syncPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
|
|
2063
3629
|
const viewerBinding = readViewerBinding(dbPath);
|
|
3630
|
+
const reachable = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
|
|
3631
|
+
if (!reachable) issues.push("daemon not running");
|
|
3632
|
+
if (!device) issues.push("identity missing");
|
|
3633
|
+
if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) issues.push("daemon error");
|
|
3634
|
+
if (peers.length === 0) issues.push("no peers");
|
|
3635
|
+
if (!config.sync_enabled) issues.push("sync is disabled");
|
|
3636
|
+
const peerDetails = [];
|
|
3637
|
+
for (const peer of peers) {
|
|
3638
|
+
const addresses = peer.addresses_json ? JSON.parse(peer.addresses_json) : [];
|
|
3639
|
+
const endpoint = parseStoredAddressEndpoint(addresses[0] ?? "");
|
|
3640
|
+
const reach = endpoint ? await portOpen(endpoint.host, endpoint.port) ? "ok" : "unreachable" : "unknown";
|
|
3641
|
+
const pinned = Boolean(peer.pinned_fingerprint);
|
|
3642
|
+
const hasKey = Boolean(peer.public_key);
|
|
3643
|
+
peerDetails.push({
|
|
3644
|
+
peer_device_id: peer.peer_device_id,
|
|
3645
|
+
addresses: addresses.length,
|
|
3646
|
+
reachable: reach,
|
|
3647
|
+
pinned,
|
|
3648
|
+
has_public_key: hasKey
|
|
3649
|
+
});
|
|
3650
|
+
if (reach !== "ok") issues.push(`peer ${peer.peer_device_id} unreachable`);
|
|
3651
|
+
if (!pinned || !hasKey) issues.push(`peer ${peer.peer_device_id} not pinned`);
|
|
3652
|
+
}
|
|
3653
|
+
if (opts.json) {
|
|
3654
|
+
console.log(JSON.stringify({
|
|
3655
|
+
enabled: config.sync_enabled === true,
|
|
3656
|
+
listen: `${syncHost}:${syncPort}`,
|
|
3657
|
+
mdns: process.env.CODEMEM_SYNC_MDNS ? "env-configured" : "default/off",
|
|
3658
|
+
daemon: reachable ? "running" : "not running",
|
|
3659
|
+
identity: device?.device_id ?? null,
|
|
3660
|
+
daemon_error: daemonState?.last_error ?? null,
|
|
3661
|
+
peers: peerDetails,
|
|
3662
|
+
issues: [...new Set(issues)],
|
|
3663
|
+
ok: issues.length === 0
|
|
3664
|
+
}));
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
2064
3667
|
console.log("Sync doctor");
|
|
2065
3668
|
console.log(`- Enabled: ${config.sync_enabled === true}`);
|
|
2066
3669
|
console.log(`- Listen: ${syncHost}:${syncPort}`);
|
|
2067
3670
|
console.log(`- mDNS: ${process.env.CODEMEM_SYNC_MDNS ? "env-configured" : "default/off"}`);
|
|
2068
|
-
const reachable = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
|
|
2069
3671
|
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 {
|
|
3672
|
+
if (!device) console.log("- Identity: missing (run `codemem sync enable`)");
|
|
3673
|
+
else console.log(`- Identity: ${device.device_id}`);
|
|
3674
|
+
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"})`);
|
|
3675
|
+
if (peers.length === 0) console.log("- Peers: none (pair a device first)");
|
|
3676
|
+
else {
|
|
2083
3677
|
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
|
-
}
|
|
3678
|
+
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
3679
|
}
|
|
2095
|
-
if (!config.sync_enabled) issues.push("sync is disabled");
|
|
2096
3680
|
if (issues.length > 0) console.log(`WARN: ${[...new Set(issues)].slice(0, 3).join(", ")}`);
|
|
2097
3681
|
else console.log("OK: sync looks healthy");
|
|
2098
3682
|
} finally {
|
|
2099
3683
|
store.close();
|
|
2100
3684
|
}
|
|
2101
|
-
})
|
|
2102
|
-
syncCommand.addCommand(
|
|
2103
|
-
|
|
2104
|
-
|
|
3685
|
+
});
|
|
3686
|
+
syncCommand.addCommand(doctorCmd);
|
|
3687
|
+
var statusCmd = new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary");
|
|
3688
|
+
addDbOption(statusCmd);
|
|
3689
|
+
addConfigOption(statusCmd);
|
|
3690
|
+
addJsonOption(statusCmd);
|
|
3691
|
+
statusCmd.action((opts) => {
|
|
3692
|
+
const config = readCliConfig(opts.config);
|
|
3693
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
2105
3694
|
try {
|
|
2106
3695
|
const d = drizzle(store.db, { schema });
|
|
2107
3696
|
const deviceRow = d.select({
|
|
@@ -2151,17 +3740,41 @@ syncCommand.addCommand(new Command("status").configureHelp(helpStyle).descriptio
|
|
|
2151
3740
|
} finally {
|
|
2152
3741
|
store.close();
|
|
2153
3742
|
}
|
|
2154
|
-
})
|
|
2155
|
-
syncCommand.addCommand(
|
|
2156
|
-
|
|
3743
|
+
});
|
|
3744
|
+
syncCommand.addCommand(statusCmd);
|
|
3745
|
+
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");
|
|
3746
|
+
addDbOption(enableCmd);
|
|
3747
|
+
addConfigOption(enableCmd);
|
|
3748
|
+
addJsonOption(enableCmd);
|
|
3749
|
+
enableCmd.addOption(new Option("--host <host>", "sync listen host").hideHelp());
|
|
3750
|
+
enableCmd.addOption(new Option("--port <port>", "sync listen port").hideHelp());
|
|
3751
|
+
enableCmd.action((opts) => {
|
|
3752
|
+
if (opts.host && !opts.syncHost) emitDeprecationWarning("--host on sync enable", "--sync-host");
|
|
3753
|
+
if (opts.port && !opts.syncPort) emitDeprecationWarning("--port on sync enable", "--sync-port");
|
|
3754
|
+
const effectiveHost = opts.syncHost ?? opts.host;
|
|
3755
|
+
const effectivePort = opts.syncPort ?? opts.port;
|
|
3756
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
2157
3757
|
try {
|
|
2158
3758
|
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db);
|
|
2159
|
-
const config =
|
|
3759
|
+
const config = readCliConfig(opts.config);
|
|
2160
3760
|
config.sync_enabled = true;
|
|
2161
|
-
if (
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
3761
|
+
if (effectiveHost) config.sync_host = effectiveHost;
|
|
3762
|
+
const syncPort = parsePositiveIntegerOption(effectivePort, "--sync-port");
|
|
3763
|
+
const syncInterval = parsePositiveIntegerOption(opts.interval, "--interval");
|
|
3764
|
+
if (syncPort != null) config.sync_port = syncPort;
|
|
3765
|
+
if (syncInterval != null) config.sync_interval_s = syncInterval;
|
|
3766
|
+
writeCliConfig(config, opts.config);
|
|
3767
|
+
if (opts.json) {
|
|
3768
|
+
console.log(JSON.stringify({
|
|
3769
|
+
ok: true,
|
|
3770
|
+
device_id: deviceId,
|
|
3771
|
+
fingerprint,
|
|
3772
|
+
host: config.sync_host ?? "0.0.0.0",
|
|
3773
|
+
port: config.sync_port ?? 7337,
|
|
3774
|
+
interval_s: config.sync_interval_s ?? 120
|
|
3775
|
+
}));
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
2165
3778
|
p.intro("codemem sync enable");
|
|
2166
3779
|
p.log.success([
|
|
2167
3780
|
`Device ID: ${deviceId}`,
|
|
@@ -2174,16 +3787,23 @@ syncCommand.addCommand(new Command("enable").configureHelp(helpStyle).descriptio
|
|
|
2174
3787
|
} finally {
|
|
2175
3788
|
store.close();
|
|
2176
3789
|
}
|
|
2177
|
-
})
|
|
2178
|
-
syncCommand.addCommand(
|
|
2179
|
-
|
|
3790
|
+
});
|
|
3791
|
+
syncCommand.addCommand(enableCmd);
|
|
3792
|
+
var disableCmd = new Command("disable").configureHelp(helpStyle).description("Disable sync without deleting keys or peers");
|
|
3793
|
+
addConfigOption(disableCmd);
|
|
3794
|
+
disableCmd.action((opts) => {
|
|
3795
|
+
const config = readCliConfig(opts.config);
|
|
2180
3796
|
config.sync_enabled = false;
|
|
2181
|
-
|
|
3797
|
+
writeCliConfig(config, opts.config);
|
|
2182
3798
|
p.intro("codemem sync disable");
|
|
2183
3799
|
p.outro("Sync disabled — restart `codemem serve` to take effect");
|
|
2184
|
-
})
|
|
2185
|
-
|
|
2186
|
-
|
|
3800
|
+
});
|
|
3801
|
+
syncCommand.addCommand(disableCmd);
|
|
3802
|
+
var peersCommand = new Command("peers").configureHelp(helpStyle).description("List known sync peers");
|
|
3803
|
+
addDbOption(peersCommand);
|
|
3804
|
+
addJsonOption(peersCommand);
|
|
3805
|
+
peersCommand.action((opts) => {
|
|
3806
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
2187
3807
|
try {
|
|
2188
3808
|
const peers = drizzle(store.db, { schema }).select({
|
|
2189
3809
|
peer_device_id: schema.syncPeers.peer_device_id,
|
|
@@ -2211,17 +3831,28 @@ var peersCommand = new Command("peers").configureHelp(helpStyle).description("Li
|
|
|
2211
3831
|
store.close();
|
|
2212
3832
|
}
|
|
2213
3833
|
});
|
|
2214
|
-
|
|
2215
|
-
|
|
3834
|
+
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");
|
|
3835
|
+
addDbOption(peersRemoveCmd);
|
|
3836
|
+
addJsonOption(peersRemoveCmd);
|
|
3837
|
+
peersRemoveCmd.action((peerRef, opts) => {
|
|
3838
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
2216
3839
|
try {
|
|
2217
3840
|
const d = drizzle(store.db, { schema });
|
|
2218
3841
|
const match = resolvePeerMatch(d, peerRef);
|
|
2219
3842
|
if (match === "ambiguous") {
|
|
3843
|
+
if (opts.json) {
|
|
3844
|
+
emitJsonError("ambiguous_peer", `Peer name is ambiguous: ${peerRef.trim()}`);
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
2220
3847
|
p.log.error(`Peer name is ambiguous: ${peerRef.trim()}`);
|
|
2221
3848
|
process.exitCode = 1;
|
|
2222
3849
|
return;
|
|
2223
3850
|
}
|
|
2224
3851
|
if (!match) {
|
|
3852
|
+
if (opts.json) {
|
|
3853
|
+
emitJsonError("peer_not_found", `Peer not found: ${peerRef.trim()}`);
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
2225
3856
|
p.log.error(`Peer not found: ${peerRef.trim()}`);
|
|
2226
3857
|
process.exitCode = 1;
|
|
2227
3858
|
return;
|
|
@@ -2243,29 +3874,37 @@ peersCommand.addCommand(new Command("remove").configureHelp(helpStyle).descripti
|
|
|
2243
3874
|
} finally {
|
|
2244
3875
|
store.close();
|
|
2245
3876
|
}
|
|
2246
|
-
})
|
|
3877
|
+
});
|
|
3878
|
+
peersCommand.addCommand(peersRemoveCmd);
|
|
2247
3879
|
syncCommand.addCommand(peersCommand);
|
|
2248
|
-
|
|
2249
|
-
|
|
3880
|
+
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");
|
|
3881
|
+
addDbOption(bootstrapCmd);
|
|
3882
|
+
addJsonOption(bootstrapCmd);
|
|
3883
|
+
bootstrapCmd.addOption(new Option("--peer <device-id>", "peer device ID").hideHelp());
|
|
3884
|
+
bootstrapCmd.action(async (peerArg, opts) => {
|
|
3885
|
+
const peerDeviceId = (peerArg || opts.peer || "").trim();
|
|
3886
|
+
if (!peerDeviceId) {
|
|
3887
|
+
if (opts.json) {
|
|
3888
|
+
emitJsonError("usage_error", "missing required argument: <peer-device-id>", 2);
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
p.log.error("missing required argument: <peer-device-id>");
|
|
3892
|
+
process.exitCode = 2;
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
|
|
2250
3896
|
try {
|
|
2251
|
-
const peerDeviceId = opts.peer.trim();
|
|
2252
3897
|
const pageSize = Math.max(1, Number.parseInt(opts.pageSize, 10) || 2e3);
|
|
2253
3898
|
const keysDir = opts.keysDir ?? void 0;
|
|
2254
3899
|
const peer = drizzle(store.db, { schema }).select().from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).get();
|
|
2255
3900
|
if (!peer) {
|
|
2256
|
-
if (opts.json)
|
|
2257
|
-
ok: false,
|
|
2258
|
-
error: "peer not found"
|
|
2259
|
-
}));
|
|
3901
|
+
if (opts.json) emitJsonError("peer_not_found", `Peer ${peerDeviceId} not found in sync_peers.`);
|
|
2260
3902
|
else p.log.error(`Peer ${peerDeviceId} not found in sync_peers.`);
|
|
2261
3903
|
process.exitCode = 1;
|
|
2262
3904
|
return;
|
|
2263
3905
|
}
|
|
2264
3906
|
if (!peer.pinned_fingerprint) {
|
|
2265
|
-
if (opts.json)
|
|
2266
|
-
ok: false,
|
|
2267
|
-
error: "peer not pinned"
|
|
2268
|
-
}));
|
|
3907
|
+
if (opts.json) emitJsonError("peer_not_pinned", `Peer ${peerDeviceId} has no pinned fingerprint. Accept it first.`);
|
|
2269
3908
|
else p.log.error(`Peer ${peerDeviceId} has no pinned fingerprint. Accept it first.`);
|
|
2270
3909
|
process.exitCode = 1;
|
|
2271
3910
|
return;
|
|
@@ -2273,11 +3912,7 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
|
|
|
2273
3912
|
if (!opts.force) {
|
|
2274
3913
|
const dirty = hasUnsyncedSharedMemoryChanges(store.db);
|
|
2275
3914
|
if (dirty.dirty) {
|
|
2276
|
-
if (opts.json)
|
|
2277
|
-
ok: false,
|
|
2278
|
-
error: "local_unsynced_changes",
|
|
2279
|
-
count: dirty.count
|
|
2280
|
-
}));
|
|
3915
|
+
if (opts.json) emitJsonError("local_unsynced_changes", `${dirty.count} unsynced shared memory change(s) would be lost. Use --force to override.`);
|
|
2281
3916
|
else p.log.error(`${dirty.count} unsynced shared memory change(s) would be lost. Use --force to override.`);
|
|
2282
3917
|
process.exitCode = 1;
|
|
2283
3918
|
return;
|
|
@@ -2286,17 +3921,14 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
|
|
|
2286
3921
|
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir });
|
|
2287
3922
|
const addresses = JSON.parse(String(peer.addresses_json ?? "[]"));
|
|
2288
3923
|
if (!addresses.length) {
|
|
2289
|
-
if (opts.json)
|
|
2290
|
-
ok: false,
|
|
2291
|
-
error: "no peer addresses"
|
|
2292
|
-
}));
|
|
3924
|
+
if (opts.json) emitJsonError("no_peer_addresses", "Peer has no known addresses. Run a sync first or add addresses.");
|
|
2293
3925
|
else p.log.error("Peer has no known addresses. Run a sync first or add addresses.");
|
|
2294
3926
|
process.exitCode = 1;
|
|
2295
3927
|
return;
|
|
2296
3928
|
}
|
|
2297
3929
|
let boundary = null;
|
|
2298
3930
|
let baseUrl = "";
|
|
2299
|
-
|
|
3931
|
+
const addressResults = [];
|
|
2300
3932
|
for (const address of addresses) {
|
|
2301
3933
|
const candidate = buildBaseUrl(address);
|
|
2302
3934
|
if (!candidate) continue;
|
|
@@ -2306,16 +3938,23 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
|
|
|
2306
3938
|
method: "GET",
|
|
2307
3939
|
url: statusUrl,
|
|
2308
3940
|
bodyBytes: Buffer.alloc(0),
|
|
3941
|
+
bootstrapGrantId: opts.bootstrapGrant,
|
|
2309
3942
|
keysDir
|
|
2310
3943
|
});
|
|
2311
3944
|
try {
|
|
2312
3945
|
const [code, payload] = await requestJson("GET", statusUrl, { headers });
|
|
2313
3946
|
if (code !== 200 || !payload) {
|
|
2314
|
-
|
|
3947
|
+
addressResults.push({
|
|
3948
|
+
address: candidate,
|
|
3949
|
+
result: `status ${code}`
|
|
3950
|
+
});
|
|
2315
3951
|
continue;
|
|
2316
3952
|
}
|
|
2317
3953
|
if (payload.fingerprint !== peer.pinned_fingerprint) {
|
|
2318
|
-
|
|
3954
|
+
addressResults.push({
|
|
3955
|
+
address: candidate,
|
|
3956
|
+
result: "fingerprint mismatch"
|
|
3957
|
+
});
|
|
2319
3958
|
continue;
|
|
2320
3959
|
}
|
|
2321
3960
|
const reset = payload.sync_reset;
|
|
@@ -2326,18 +3965,30 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
|
|
|
2326
3965
|
baseline_cursor: typeof reset.baseline_cursor === "string" ? reset.baseline_cursor : null
|
|
2327
3966
|
};
|
|
2328
3967
|
baseUrl = candidate;
|
|
3968
|
+
addressResults.push({
|
|
3969
|
+
address: candidate,
|
|
3970
|
+
result: "ok"
|
|
3971
|
+
});
|
|
2329
3972
|
break;
|
|
2330
3973
|
}
|
|
2331
|
-
|
|
3974
|
+
addressResults.push({
|
|
3975
|
+
address: candidate,
|
|
3976
|
+
result: "missing sync_reset boundary"
|
|
3977
|
+
});
|
|
2332
3978
|
} catch (err) {
|
|
2333
|
-
|
|
3979
|
+
addressResults.push({
|
|
3980
|
+
address: candidate,
|
|
3981
|
+
result: err instanceof Error ? err.message : String(err)
|
|
3982
|
+
});
|
|
2334
3983
|
}
|
|
2335
3984
|
}
|
|
2336
3985
|
if (!boundary || !baseUrl) {
|
|
2337
|
-
const
|
|
3986
|
+
const summary = addressResults.map((r) => `${r.address}: ${r.result}`).join("; ");
|
|
3987
|
+
const detail = summary ? `no reachable peer with valid reset boundary. Tried: ${summary}` : "peer unreachable or missing reset boundary";
|
|
2338
3988
|
if (opts.json) console.log(JSON.stringify({
|
|
2339
3989
|
ok: false,
|
|
2340
|
-
error: detail
|
|
3990
|
+
error: detail,
|
|
3991
|
+
addresses: addressResults
|
|
2341
3992
|
}));
|
|
2342
3993
|
else p.log.error(detail);
|
|
2343
3994
|
process.exitCode = 1;
|
|
@@ -2357,6 +4008,7 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
|
|
|
2357
4008
|
};
|
|
2358
4009
|
const { items } = await fetchAllSnapshotPages(baseUrl, resetInfo, deviceId, {
|
|
2359
4010
|
keysDir,
|
|
4011
|
+
bootstrapGrantId: opts.bootstrapGrant,
|
|
2360
4012
|
pageSize
|
|
2361
4013
|
});
|
|
2362
4014
|
const result = applyBootstrapSnapshot(store.db, peerDeviceId, items, resetInfo);
|
|
@@ -2375,266 +4027,27 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
|
|
|
2375
4027
|
} finally {
|
|
2376
4028
|
store.close();
|
|
2377
4029
|
}
|
|
2378
|
-
})
|
|
2379
|
-
syncCommand.addCommand(
|
|
2380
|
-
|
|
4030
|
+
});
|
|
4031
|
+
syncCommand.addCommand(bootstrapCmd);
|
|
4032
|
+
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");
|
|
4033
|
+
addConfigOption(connectCmd);
|
|
4034
|
+
connectCmd.action((url, opts) => {
|
|
4035
|
+
const config = readCliConfig(opts.config);
|
|
2381
4036
|
config.sync_coordinator_url = url.trim();
|
|
2382
4037
|
if (opts.group) config.sync_coordinator_group = opts.group.trim();
|
|
2383
|
-
|
|
4038
|
+
writeCliConfig(config, opts.config);
|
|
2384
4039
|
p.intro("codemem sync connect");
|
|
2385
4040
|
p.log.success(`Coordinator: ${url.trim()}`);
|
|
2386
4041
|
if (opts.group) p.log.info(`Group: ${opts.group.trim()}`);
|
|
2387
4042
|
p.outro("Restart `codemem serve` to activate coordinator sync");
|
|
2388
|
-
})
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
if (opts.json) {
|
|
2397
|
-
console.log(JSON.stringify(group, null, 2));
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
p.intro("codemem sync coordinator group-create");
|
|
2401
|
-
p.log.success(`Group ready: ${groupId.trim()}`);
|
|
2402
|
-
p.outro(String(group.display_name ?? group.group_id ?? groupId.trim()));
|
|
2403
|
-
}));
|
|
2404
|
-
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) => {
|
|
2405
|
-
const groups = await coordinatorListGroupsAction({ dbPath: opts.db ?? opts.dbPath ?? null });
|
|
2406
|
-
if (opts.json) {
|
|
2407
|
-
console.log(JSON.stringify(groups, null, 2));
|
|
2408
|
-
return;
|
|
2409
|
-
}
|
|
2410
|
-
p.intro("codemem sync coordinator list-groups");
|
|
2411
|
-
if (groups.length === 0) {
|
|
2412
|
-
p.outro("No coordinator groups found");
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
|
-
for (const group of groups) p.log.message(`- ${String(group.group_id ?? "")}${group.display_name ? ` (${String(group.display_name)})` : ""}`);
|
|
2416
|
-
p.outro(`${groups.length} group(s)`);
|
|
2417
|
-
}));
|
|
2418
|
-
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) => {
|
|
2419
|
-
const publicKey = readCoordinatorPublicKey(opts);
|
|
2420
|
-
const fingerprint = String(opts.fingerprint ?? "").trim();
|
|
2421
|
-
if (!fingerprint) {
|
|
2422
|
-
p.log.error("Fingerprint required via --fingerprint");
|
|
2423
|
-
process.exitCode = 1;
|
|
2424
|
-
return;
|
|
2425
|
-
}
|
|
2426
|
-
if (fingerprintPublicKey(publicKey) !== fingerprint) {
|
|
2427
|
-
p.log.error("Fingerprint does not match the provided public key");
|
|
2428
|
-
process.exitCode = 1;
|
|
2429
|
-
return;
|
|
2430
|
-
}
|
|
2431
|
-
const enrollment = await coordinatorEnrollDeviceAction({
|
|
2432
|
-
groupId,
|
|
2433
|
-
deviceId,
|
|
2434
|
-
fingerprint,
|
|
2435
|
-
publicKey,
|
|
2436
|
-
displayName: opts.name?.trim() || null,
|
|
2437
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2438
|
-
});
|
|
2439
|
-
if (opts.json) {
|
|
2440
|
-
console.log(JSON.stringify(enrollment, null, 2));
|
|
2441
|
-
return;
|
|
2442
|
-
}
|
|
2443
|
-
p.intro("codemem sync coordinator enroll-device");
|
|
2444
|
-
p.log.success(`Enrolled ${deviceId.trim()} in ${groupId.trim()}`);
|
|
2445
|
-
p.outro(String(enrollment.display_name ?? enrollment.device_id ?? deviceId.trim()));
|
|
2446
|
-
}));
|
|
2447
|
-
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) => {
|
|
2448
|
-
const rows = await coordinatorListDevicesAction({
|
|
2449
|
-
groupId,
|
|
2450
|
-
includeDisabled: opts.includeDisabled === true,
|
|
2451
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2452
|
-
});
|
|
2453
|
-
if (opts.json) {
|
|
2454
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
2455
|
-
return;
|
|
2456
|
-
}
|
|
2457
|
-
p.intro("codemem sync coordinator list-devices");
|
|
2458
|
-
if (rows.length === 0) {
|
|
2459
|
-
p.outro(`No enrolled devices for ${groupId.trim()}`);
|
|
2460
|
-
return;
|
|
2461
|
-
}
|
|
2462
|
-
for (const row of rows) {
|
|
2463
|
-
const label = String(row.display_name ?? row.device_id ?? "").trim() || String(row.device_id ?? "");
|
|
2464
|
-
const enabled = Number(row.enabled ?? 1) === 1 ? "enabled" : "disabled";
|
|
2465
|
-
p.log.message(`- ${label} (${String(row.device_id ?? "")}) ${enabled}`);
|
|
2466
|
-
}
|
|
2467
|
-
p.outro(`${rows.length} device(s)`);
|
|
2468
|
-
}));
|
|
2469
|
-
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) => {
|
|
2470
|
-
const result = await coordinatorRenameDeviceAction({
|
|
2471
|
-
groupId,
|
|
2472
|
-
deviceId,
|
|
2473
|
-
displayName: opts.name.trim(),
|
|
2474
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2475
|
-
});
|
|
2476
|
-
if (!result) {
|
|
2477
|
-
p.log.error(`Device not found: ${deviceId.trim()}`);
|
|
2478
|
-
process.exitCode = 1;
|
|
2479
|
-
return;
|
|
2480
|
-
}
|
|
2481
|
-
if (opts.json) {
|
|
2482
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2483
|
-
return;
|
|
2484
|
-
}
|
|
2485
|
-
p.intro("codemem sync coordinator rename-device");
|
|
2486
|
-
p.log.success(`Renamed ${deviceId.trim()} in ${groupId.trim()}`);
|
|
2487
|
-
p.outro(String(result.display_name ?? result.device_id ?? deviceId.trim()));
|
|
2488
|
-
}));
|
|
2489
|
-
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) => {
|
|
2490
|
-
if (!await coordinatorDisableDeviceAction({
|
|
2491
|
-
groupId,
|
|
2492
|
-
deviceId,
|
|
2493
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2494
|
-
})) {
|
|
2495
|
-
p.log.error(`Device not found: ${deviceId.trim()}`);
|
|
2496
|
-
process.exitCode = 1;
|
|
2497
|
-
return;
|
|
2498
|
-
}
|
|
2499
|
-
if (opts.json) {
|
|
2500
|
-
console.log(JSON.stringify({
|
|
2501
|
-
ok: true,
|
|
2502
|
-
group_id: groupId.trim(),
|
|
2503
|
-
device_id: deviceId.trim()
|
|
2504
|
-
}, null, 2));
|
|
2505
|
-
return;
|
|
2506
|
-
}
|
|
2507
|
-
p.intro("codemem sync coordinator disable-device");
|
|
2508
|
-
p.log.success(`Disabled ${deviceId.trim()} in ${groupId.trim()}`);
|
|
2509
|
-
p.outro("disabled");
|
|
2510
|
-
}));
|
|
2511
|
-
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) => {
|
|
2512
|
-
if (!await coordinatorRemoveDeviceAction({
|
|
2513
|
-
groupId,
|
|
2514
|
-
deviceId,
|
|
2515
|
-
dbPath: opts.db ?? opts.dbPath ?? null
|
|
2516
|
-
})) {
|
|
2517
|
-
p.log.error(`Device not found: ${deviceId.trim()}`);
|
|
2518
|
-
process.exitCode = 1;
|
|
2519
|
-
return;
|
|
2520
|
-
}
|
|
2521
|
-
if (opts.json) {
|
|
2522
|
-
console.log(JSON.stringify({
|
|
2523
|
-
ok: true,
|
|
2524
|
-
group_id: groupId.trim(),
|
|
2525
|
-
device_id: deviceId.trim()
|
|
2526
|
-
}, null, 2));
|
|
2527
|
-
return;
|
|
2528
|
-
}
|
|
2529
|
-
p.intro("codemem sync coordinator remove-device");
|
|
2530
|
-
p.log.success(`Removed ${deviceId.trim()} from ${groupId.trim()}`);
|
|
2531
|
-
p.outro("removed");
|
|
2532
|
-
}));
|
|
2533
|
-
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) => {
|
|
2534
|
-
const host = String(opts.host ?? "127.0.0.1").trim() || "127.0.0.1";
|
|
2535
|
-
const port = Number.parseInt(String(opts.port ?? "7347"), 10);
|
|
2536
|
-
const dbPath = opts.db ?? opts.dbPath ?? DEFAULT_COORDINATOR_DB_PATH;
|
|
2537
|
-
const app = createBetterSqliteCoordinatorApp({ dbPath });
|
|
2538
|
-
p.intro("codemem sync coordinator serve");
|
|
2539
|
-
p.log.success(`Coordinator listening at http://${host}:${port}`);
|
|
2540
|
-
p.log.info(`DB: ${dbPath}`);
|
|
2541
|
-
serve({
|
|
2542
|
-
fetch: app.fetch,
|
|
2543
|
-
hostname: host,
|
|
2544
|
-
port
|
|
2545
|
-
});
|
|
2546
|
-
}));
|
|
2547
|
-
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) => {
|
|
2548
|
-
const ttlHours = Number.parseInt(String(opts.ttlHours ?? "24"), 10);
|
|
2549
|
-
const groupId = String(opts.group ?? "").trim() || String(groupArg ?? "").trim();
|
|
2550
|
-
const result = await coordinatorCreateInviteAction({
|
|
2551
|
-
groupId,
|
|
2552
|
-
coordinatorUrl: opts.coordinatorUrl?.trim() || null,
|
|
2553
|
-
policy: String(opts.policy ?? "auto_admit").trim(),
|
|
2554
|
-
ttlHours,
|
|
2555
|
-
createdBy: null,
|
|
2556
|
-
dbPath: opts.db ?? opts.dbPath ?? null,
|
|
2557
|
-
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
2558
|
-
adminSecret: opts.adminSecret?.trim() || null
|
|
2559
|
-
});
|
|
2560
|
-
if (opts.json) {
|
|
2561
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2562
|
-
return;
|
|
2563
|
-
}
|
|
2564
|
-
p.intro("codemem sync coordinator create-invite");
|
|
2565
|
-
p.log.success(`Invite created for ${groupId}`);
|
|
2566
|
-
if (typeof result.link === "string") p.log.message(`- link: ${result.link}`);
|
|
2567
|
-
if (typeof result.encoded === "string") p.log.message(`- invite: ${result.encoded}`);
|
|
2568
|
-
for (const warning of Array.isArray(result.warnings) ? result.warnings : []) p.log.warn(String(warning));
|
|
2569
|
-
p.outro("Invite ready");
|
|
2570
|
-
}));
|
|
2571
|
-
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) => {
|
|
2572
|
-
const result = await coordinatorImportInviteAction({
|
|
2573
|
-
inviteValue: invite,
|
|
2574
|
-
dbPath: opts.db ?? opts.dbPath ?? null,
|
|
2575
|
-
keysDir: opts.keysDir ?? null,
|
|
2576
|
-
configPath: opts.config ?? null
|
|
2577
|
-
});
|
|
2578
|
-
if (opts.json) {
|
|
2579
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2580
|
-
return;
|
|
2581
|
-
}
|
|
2582
|
-
p.intro("codemem sync coordinator import-invite");
|
|
2583
|
-
p.log.success(`Invite imported for ${result.group_id}`);
|
|
2584
|
-
p.log.message(`- coordinator: ${result.coordinator_url}`);
|
|
2585
|
-
p.log.message(`- status: ${result.status}`);
|
|
2586
|
-
p.outro("Coordinator config updated");
|
|
2587
|
-
}));
|
|
2588
|
-
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) => {
|
|
2589
|
-
const groupId = String(opts.group ?? "").trim() || String(groupArg ?? "").trim();
|
|
2590
|
-
const rows = await coordinatorListJoinRequestsAction({
|
|
2591
|
-
groupId,
|
|
2592
|
-
dbPath: opts.db ?? opts.dbPath ?? null,
|
|
2593
|
-
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
2594
|
-
adminSecret: opts.adminSecret?.trim() || null
|
|
2595
|
-
});
|
|
2596
|
-
if (opts.json) {
|
|
2597
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
2598
|
-
return;
|
|
2599
|
-
}
|
|
2600
|
-
p.intro("codemem sync coordinator list-join-requests");
|
|
2601
|
-
if (rows.length === 0) {
|
|
2602
|
-
p.outro(`No pending join requests for ${groupId}`);
|
|
2603
|
-
return;
|
|
2604
|
-
}
|
|
2605
|
-
for (const row of rows) {
|
|
2606
|
-
const displayName = row.display_name || row.device_id;
|
|
2607
|
-
p.log.message(`- ${displayName} (${row.device_id}) request_id=${row.request_id}`);
|
|
2608
|
-
}
|
|
2609
|
-
p.outro(`${rows.length} pending join request(s)`);
|
|
2610
|
-
}));
|
|
2611
|
-
function addReviewJoinRequestCommand(name, approve) {
|
|
2612
|
-
coordinatorCommand.addCommand(new Command(name).configureHelp(helpStyle).description(`${approve ? "Approve" : "Deny"} a coordinator join request`).argument("<request-id>", "join request 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 (requestId, opts) => {
|
|
2613
|
-
const request = await coordinatorReviewJoinRequestAction({
|
|
2614
|
-
requestId: requestId.trim(),
|
|
2615
|
-
approve,
|
|
2616
|
-
reviewedBy: null,
|
|
2617
|
-
dbPath: opts.db ?? opts.dbPath ?? null,
|
|
2618
|
-
remoteUrl: opts.remoteUrl?.trim() || null,
|
|
2619
|
-
adminSecret: opts.adminSecret?.trim() || null
|
|
2620
|
-
});
|
|
2621
|
-
if (!request) {
|
|
2622
|
-
p.log.error(`Join request not found: ${requestId.trim()}`);
|
|
2623
|
-
process.exitCode = 1;
|
|
2624
|
-
return;
|
|
2625
|
-
}
|
|
2626
|
-
if (opts.json) {
|
|
2627
|
-
console.log(JSON.stringify(request, null, 2));
|
|
2628
|
-
return;
|
|
2629
|
-
}
|
|
2630
|
-
p.intro(`codemem sync coordinator ${name}`);
|
|
2631
|
-
p.log.success(`${approve ? "Approved" : "Denied"} join request ${requestId.trim()}`);
|
|
2632
|
-
p.outro(String(request.status ?? "updated"));
|
|
2633
|
-
}));
|
|
2634
|
-
}
|
|
2635
|
-
addReviewJoinRequestCommand("approve-join-request", true);
|
|
2636
|
-
addReviewJoinRequestCommand("deny-join-request", false);
|
|
2637
|
-
syncCommand.addCommand(coordinatorCommand);
|
|
4043
|
+
});
|
|
4044
|
+
syncCommand.addCommand(connectCmd);
|
|
4045
|
+
var syncCoordinatorAlias = buildCoordinatorCommand();
|
|
4046
|
+
syncCoordinatorAlias.hook("preAction", (_thisCmd, actionCmd) => {
|
|
4047
|
+
const subName = actionCmd.name();
|
|
4048
|
+
emitDeprecationWarning(`codemem sync coordinator ${subName}`, `codemem coordinator ${subName}`);
|
|
4049
|
+
});
|
|
4050
|
+
syncCommand.addCommand(syncCoordinatorAlias);
|
|
2638
4051
|
//#endregion
|
|
2639
4052
|
//#region src/commands/version.ts
|
|
2640
4053
|
var versionCommand = new Command("version").configureHelp(helpStyle).description("Print codemem version").action(() => {
|
|
@@ -2655,24 +4068,24 @@ var versionCommand = new Command("version").configureHelp(helpStyle).description
|
|
|
2655
4068
|
var completion = omelette("codemem <command>");
|
|
2656
4069
|
completion.on("command", ({ reply }) => {
|
|
2657
4070
|
reply([
|
|
4071
|
+
"claude-hook-inject",
|
|
2658
4072
|
"claude-hook-ingest",
|
|
4073
|
+
"config",
|
|
4074
|
+
"coordinator",
|
|
2659
4075
|
"db",
|
|
2660
4076
|
"embed",
|
|
4077
|
+
"enqueue-raw-event",
|
|
2661
4078
|
"export-memories",
|
|
2662
|
-
"forget",
|
|
2663
|
-
"memory",
|
|
2664
4079
|
"import-memories",
|
|
2665
|
-
"
|
|
2666
|
-
"
|
|
2667
|
-
"
|
|
2668
|
-
"stats",
|
|
4080
|
+
"mcp",
|
|
4081
|
+
"memory",
|
|
4082
|
+
"pack",
|
|
2669
4083
|
"recent",
|
|
2670
|
-
"remember",
|
|
2671
4084
|
"search",
|
|
2672
|
-
"pack",
|
|
2673
4085
|
"serve",
|
|
2674
|
-
"
|
|
2675
|
-
"
|
|
4086
|
+
"setup",
|
|
4087
|
+
"stats",
|
|
4088
|
+
"sync",
|
|
2676
4089
|
"version",
|
|
2677
4090
|
"help",
|
|
2678
4091
|
"--help",
|
|
@@ -2706,8 +4119,33 @@ if (hasRootFlag("--cleanup-completion")) {
|
|
|
2706
4119
|
completion.cleanupShellInitFile();
|
|
2707
4120
|
process.exit(0);
|
|
2708
4121
|
}
|
|
4122
|
+
{
|
|
4123
|
+
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 () => {
|
|
4124
|
+
const idx = process.argv.indexOf("export");
|
|
4125
|
+
const tail = idx >= 0 ? process.argv.slice(idx + 1) : [];
|
|
4126
|
+
await exportMemoriesCommand.parseAsync([
|
|
4127
|
+
"node",
|
|
4128
|
+
"export-memories",
|
|
4129
|
+
...tail
|
|
4130
|
+
]);
|
|
4131
|
+
});
|
|
4132
|
+
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 () => {
|
|
4133
|
+
const idx = process.argv.indexOf("import");
|
|
4134
|
+
const tail = idx >= 0 ? process.argv.slice(idx + 1) : [];
|
|
4135
|
+
await importMemoriesCommand.parseAsync([
|
|
4136
|
+
"node",
|
|
4137
|
+
"import-memories",
|
|
4138
|
+
...tail
|
|
4139
|
+
]);
|
|
4140
|
+
});
|
|
4141
|
+
memoryCommand.addCommand(memExport);
|
|
4142
|
+
memoryCommand.addCommand(memImport);
|
|
4143
|
+
}
|
|
2709
4144
|
program.addCommand(serveCommand);
|
|
4145
|
+
program.addCommand(configCommand);
|
|
4146
|
+
program.addCommand(coordinatorCommand);
|
|
2710
4147
|
program.addCommand(mcpCommand);
|
|
4148
|
+
program.addCommand(claudeHookInjectCommand);
|
|
2711
4149
|
program.addCommand(claudeHookIngestCommand);
|
|
2712
4150
|
program.addCommand(dbCommand);
|
|
2713
4151
|
program.addCommand(exportMemoriesCommand);
|
|
@@ -2717,9 +4155,9 @@ program.addCommand(embedCommand);
|
|
|
2717
4155
|
program.addCommand(recentCommand);
|
|
2718
4156
|
program.addCommand(searchCommand);
|
|
2719
4157
|
program.addCommand(packCommand);
|
|
2720
|
-
program.addCommand(showMemoryCommand);
|
|
2721
|
-
program.addCommand(forgetMemoryCommand);
|
|
2722
|
-
program.addCommand(rememberMemoryCommand);
|
|
4158
|
+
program.addCommand(showMemoryCommand, { hidden: true });
|
|
4159
|
+
program.addCommand(forgetMemoryCommand, { hidden: true });
|
|
4160
|
+
program.addCommand(rememberMemoryCommand, { hidden: true });
|
|
2723
4161
|
program.addCommand(memoryCommand);
|
|
2724
4162
|
program.addCommand(syncCommand);
|
|
2725
4163
|
program.addCommand(setupCommand);
|