codemem 0.21.2 → 0.22.0-alpha.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAEN,KAAK,uBAAuB,EAG5B,MAAM,uBAAuB,CAAC;AAQ/B,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKhE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASjD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ9D;AAED,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,WAAW,EAAE,MAAM,GAAG,IAAI,GACxB,MAAM,GAAG,IAAI,CAGf;AAqLD,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,uBAAuB,EACnC,QAAQ,GAAE,MAAM,EAAqB,GACnC,MAAM,EAAE,CAgBV;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS9D;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAUpF;AA6RD,eAAO,MAAM,YAAY,SAuBtB,CAAC"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAEN,KAAK,uBAAuB,EAG5B,MAAM,uBAAuB,CAAC;AAQ/B,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKhE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASjD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ9D;AAED,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,WAAW,EAAE,MAAM,GAAG,IAAI,GACxB,MAAM,GAAG,IAAI,CAGf;AAqLD,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,uBAAuB,EACnC,QAAQ,GAAE,MAAM,EAAqB,GACnC,MAAM,EAAE,CAgBV;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS9D;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAUpF;AA2SD,eAAO,MAAM,YAAY,SAuBtB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAAA;;GAEG;AA8BH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwHpC,eAAO,MAAM,WAAW,SAE+B,CAAC"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAAA;;GAEG;AAqCH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0JpC,eAAO,MAAM,WAAW,SAE+B,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_COORDINATOR_DB_PATH, MemoryStore, ObserverClient, RawEventSweeper, SyncRetentionRunner, VERSION, backfillTagsText, backfillVectors, buildRawEventEnvelopeFromHook, connect, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorListJoinRequestsAction, coordinatorReviewJoinRequestAction, createCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fingerprintPublicKey, getRawEventStatus, importMemories, initDatabase, isEmbeddingDisabled, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCoordinatorSyncConfig, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
2
+ import { DEFAULT_COORDINATOR_DB_PATH, MemoryStore, ObserverClient, RawEventSweeper, SyncRetentionRunner, VERSION, backfillTagsText, backfillVectors, buildRawEventEnvelopeFromHook, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fingerprintPublicKey, getRawEventStatus, importMemories, initDatabase, isEmbeddingDisabled, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCoordinatorSyncConfig, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -1308,8 +1308,17 @@ async function startForegroundViewer(invocation) {
1308
1308
  else p.log.error(err.message);
1309
1309
  process.exit(1);
1310
1310
  });
1311
+ const walCheckpointTimer = setInterval(() => {
1312
+ try {
1313
+ store.db.pragma("wal_checkpoint(TRUNCATE)");
1314
+ } catch (err) {
1315
+ p.log.warn(`WAL checkpoint failed: ${err instanceof Error ? err.message : String(err)}`);
1316
+ }
1317
+ }, 300 * 1e3);
1318
+ walCheckpointTimer.unref();
1311
1319
  const shutdown = async () => {
1312
1320
  p.outro("shutting down");
1321
+ clearInterval(walCheckpointTimer);
1313
1322
  syncAbort.abort();
1314
1323
  retentionAbort.abort();
1315
1324
  await sweeper.stop();
@@ -1734,6 +1743,33 @@ function parseAttemptsLimit(value) {
1734
1743
  if (!/^\d+$/.test(value.trim())) throw new Error(`Invalid --limit: ${value}`);
1735
1744
  return Number.parseInt(value, 10);
1736
1745
  }
1746
+ function resolvePeerMatch(db, peerRef) {
1747
+ const trimmed = peerRef.trim();
1748
+ if (!trimmed) return null;
1749
+ const byId = db.select({
1750
+ peer_device_id: schema.syncPeers.peer_device_id,
1751
+ name: schema.syncPeers.name
1752
+ }).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, trimmed)).get();
1753
+ if (byId) return byId;
1754
+ const byName = db.select({
1755
+ peer_device_id: schema.syncPeers.peer_device_id,
1756
+ name: schema.syncPeers.name
1757
+ }).from(schema.syncPeers).where(eq(schema.syncPeers.name, trimmed)).all();
1758
+ if (byName.length > 1) return "ambiguous";
1759
+ return byName[0] ?? null;
1760
+ }
1761
+ function readCoordinatorPublicKey(opts) {
1762
+ const inline = String(opts.publicKey ?? "").trim();
1763
+ const filePath = String(opts.publicKeyFile ?? "").trim();
1764
+ if (inline && filePath) throw new Error("Use only one of --public-key or --public-key-file");
1765
+ if (filePath) {
1766
+ const text = readFileSync(filePath, "utf8").trim();
1767
+ if (!text) throw new Error(`Public key file is empty: ${filePath}`);
1768
+ return text;
1769
+ }
1770
+ if (!inline) throw new Error("Public key required via --public-key or --public-key-file");
1771
+ return inline;
1772
+ }
1737
1773
  async function portOpen(host, port) {
1738
1774
  return new Promise((resolve) => {
1739
1775
  const socket = net.createConnection({
@@ -2133,7 +2169,7 @@ syncCommand.addCommand(new Command("disable").configureHelp(helpStyle).descripti
2133
2169
  p.intro("codemem sync disable");
2134
2170
  p.outro("Sync disabled — restart `codemem serve` to take effect");
2135
2171
  }));
2136
- syncCommand.addCommand(new Command("peers").configureHelp(helpStyle).description("List known sync peers").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--json", "output as JSON").action((opts) => {
2172
+ var peersCommand = new Command("peers").configureHelp(helpStyle).description("List known sync peers").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--json", "output as JSON").action((opts) => {
2137
2173
  const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
2138
2174
  try {
2139
2175
  const peers = drizzle(store.db, { schema }).select({
@@ -2161,7 +2197,41 @@ syncCommand.addCommand(new Command("peers").configureHelp(helpStyle).description
2161
2197
  } finally {
2162
2198
  store.close();
2163
2199
  }
2200
+ });
2201
+ peersCommand.addCommand(new Command("remove").configureHelp(helpStyle).description("Remove a sync peer by device id or exact name").argument("<peer>", "peer device id or exact name").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--json", "output as JSON").action((peerRef, opts) => {
2202
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
2203
+ try {
2204
+ const d = drizzle(store.db, { schema });
2205
+ const match = resolvePeerMatch(d, peerRef);
2206
+ if (match === "ambiguous") {
2207
+ p.log.error(`Peer name is ambiguous: ${peerRef.trim()}`);
2208
+ process.exitCode = 1;
2209
+ return;
2210
+ }
2211
+ if (!match) {
2212
+ p.log.error(`Peer not found: ${peerRef.trim()}`);
2213
+ process.exitCode = 1;
2214
+ return;
2215
+ }
2216
+ d.delete(schema.replicationCursors).where(eq(schema.replicationCursors.peer_device_id, match.peer_device_id)).run();
2217
+ d.delete(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, match.peer_device_id)).run();
2218
+ const payload = {
2219
+ ok: true,
2220
+ peer_device_id: match.peer_device_id,
2221
+ name: match.name
2222
+ };
2223
+ if (opts.json) {
2224
+ console.log(JSON.stringify(payload, null, 2));
2225
+ return;
2226
+ }
2227
+ p.intro("codemem sync peers remove");
2228
+ p.log.success(`Removed peer ${match.name || match.peer_device_id}`);
2229
+ p.outro(match.peer_device_id);
2230
+ } finally {
2231
+ store.close();
2232
+ }
2164
2233
  }));
2234
+ syncCommand.addCommand(peersCommand);
2165
2235
  syncCommand.addCommand(new Command("connect").configureHelp(helpStyle).description("Configure coordinator URL for cloud sync").argument("<url>", "coordinator URL (e.g. https://coordinator.example.com)").option("--group <group>", "sync group ID").action((url, opts) => {
2166
2236
  const config = readCodememConfigFile();
2167
2237
  config.sync_coordinator_url = url.trim();
@@ -2173,11 +2243,154 @@ syncCommand.addCommand(new Command("connect").configureHelp(helpStyle).descripti
2173
2243
  p.outro("Restart `codemem serve` to activate coordinator sync");
2174
2244
  }));
2175
2245
  var coordinatorCommand = new Command("coordinator").configureHelp(helpStyle).description("Manage coordinator invites, join requests, and relay server");
2176
- 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", "7340").action(async (opts) => {
2246
+ coordinatorCommand.addCommand(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").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, opts) => {
2247
+ const group = await coordinatorCreateGroupAction({
2248
+ groupId,
2249
+ displayName: opts.name?.trim() || null,
2250
+ dbPath: opts.db ?? opts.dbPath ?? null
2251
+ });
2252
+ if (opts.json) {
2253
+ console.log(JSON.stringify(group, null, 2));
2254
+ return;
2255
+ }
2256
+ p.intro("codemem sync coordinator group-create");
2257
+ p.log.success(`Group ready: ${groupId.trim()}`);
2258
+ p.outro(String(group.display_name ?? group.group_id ?? groupId.trim()));
2259
+ }));
2260
+ 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) => {
2261
+ const groups = await coordinatorListGroupsAction({ dbPath: opts.db ?? opts.dbPath ?? null });
2262
+ if (opts.json) {
2263
+ console.log(JSON.stringify(groups, null, 2));
2264
+ return;
2265
+ }
2266
+ p.intro("codemem sync coordinator list-groups");
2267
+ if (groups.length === 0) {
2268
+ p.outro("No coordinator groups found");
2269
+ return;
2270
+ }
2271
+ for (const group of groups) p.log.message(`- ${String(group.group_id ?? "")}${group.display_name ? ` (${String(group.display_name)})` : ""}`);
2272
+ p.outro(`${groups.length} group(s)`);
2273
+ }));
2274
+ 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) => {
2275
+ const publicKey = readCoordinatorPublicKey(opts);
2276
+ const fingerprint = String(opts.fingerprint ?? "").trim();
2277
+ if (!fingerprint) {
2278
+ p.log.error("Fingerprint required via --fingerprint");
2279
+ process.exitCode = 1;
2280
+ return;
2281
+ }
2282
+ if (fingerprintPublicKey(publicKey) !== fingerprint) {
2283
+ p.log.error("Fingerprint does not match the provided public key");
2284
+ process.exitCode = 1;
2285
+ return;
2286
+ }
2287
+ const enrollment = await coordinatorEnrollDeviceAction({
2288
+ groupId,
2289
+ deviceId,
2290
+ fingerprint,
2291
+ publicKey,
2292
+ displayName: opts.name?.trim() || null,
2293
+ dbPath: opts.db ?? opts.dbPath ?? null
2294
+ });
2295
+ if (opts.json) {
2296
+ console.log(JSON.stringify(enrollment, null, 2));
2297
+ return;
2298
+ }
2299
+ p.intro("codemem sync coordinator enroll-device");
2300
+ p.log.success(`Enrolled ${deviceId.trim()} in ${groupId.trim()}`);
2301
+ p.outro(String(enrollment.display_name ?? enrollment.device_id ?? deviceId.trim()));
2302
+ }));
2303
+ 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) => {
2304
+ const rows = await coordinatorListDevicesAction({
2305
+ groupId,
2306
+ includeDisabled: opts.includeDisabled === true,
2307
+ dbPath: opts.db ?? opts.dbPath ?? null
2308
+ });
2309
+ if (opts.json) {
2310
+ console.log(JSON.stringify(rows, null, 2));
2311
+ return;
2312
+ }
2313
+ p.intro("codemem sync coordinator list-devices");
2314
+ if (rows.length === 0) {
2315
+ p.outro(`No enrolled devices for ${groupId.trim()}`);
2316
+ return;
2317
+ }
2318
+ for (const row of rows) {
2319
+ const label = String(row.display_name ?? row.device_id ?? "").trim() || String(row.device_id ?? "");
2320
+ const enabled = Number(row.enabled ?? 1) === 1 ? "enabled" : "disabled";
2321
+ p.log.message(`- ${label} (${String(row.device_id ?? "")}) ${enabled}`);
2322
+ }
2323
+ p.outro(`${rows.length} device(s)`);
2324
+ }));
2325
+ 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) => {
2326
+ const result = await coordinatorRenameDeviceAction({
2327
+ groupId,
2328
+ deviceId,
2329
+ displayName: opts.name.trim(),
2330
+ dbPath: opts.db ?? opts.dbPath ?? null
2331
+ });
2332
+ if (!result) {
2333
+ p.log.error(`Device not found: ${deviceId.trim()}`);
2334
+ process.exitCode = 1;
2335
+ return;
2336
+ }
2337
+ if (opts.json) {
2338
+ console.log(JSON.stringify(result, null, 2));
2339
+ return;
2340
+ }
2341
+ p.intro("codemem sync coordinator rename-device");
2342
+ p.log.success(`Renamed ${deviceId.trim()} in ${groupId.trim()}`);
2343
+ p.outro(String(result.display_name ?? result.device_id ?? deviceId.trim()));
2344
+ }));
2345
+ 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) => {
2346
+ if (!await coordinatorDisableDeviceAction({
2347
+ groupId,
2348
+ deviceId,
2349
+ dbPath: opts.db ?? opts.dbPath ?? null
2350
+ })) {
2351
+ p.log.error(`Device not found: ${deviceId.trim()}`);
2352
+ process.exitCode = 1;
2353
+ return;
2354
+ }
2355
+ if (opts.json) {
2356
+ console.log(JSON.stringify({
2357
+ ok: true,
2358
+ group_id: groupId.trim(),
2359
+ device_id: deviceId.trim()
2360
+ }, null, 2));
2361
+ return;
2362
+ }
2363
+ p.intro("codemem sync coordinator disable-device");
2364
+ p.log.success(`Disabled ${deviceId.trim()} in ${groupId.trim()}`);
2365
+ p.outro("disabled");
2366
+ }));
2367
+ 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) => {
2368
+ if (!await coordinatorRemoveDeviceAction({
2369
+ groupId,
2370
+ deviceId,
2371
+ dbPath: opts.db ?? opts.dbPath ?? null
2372
+ })) {
2373
+ p.log.error(`Device not found: ${deviceId.trim()}`);
2374
+ process.exitCode = 1;
2375
+ return;
2376
+ }
2377
+ if (opts.json) {
2378
+ console.log(JSON.stringify({
2379
+ ok: true,
2380
+ group_id: groupId.trim(),
2381
+ device_id: deviceId.trim()
2382
+ }, null, 2));
2383
+ return;
2384
+ }
2385
+ p.intro("codemem sync coordinator remove-device");
2386
+ p.log.success(`Removed ${deviceId.trim()} from ${groupId.trim()}`);
2387
+ p.outro("removed");
2388
+ }));
2389
+ 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) => {
2177
2390
  const host = String(opts.host ?? "127.0.0.1").trim() || "127.0.0.1";
2178
- const port = Number.parseInt(String(opts.port ?? "7340"), 10);
2391
+ const port = Number.parseInt(String(opts.port ?? "7347"), 10);
2179
2392
  const dbPath = opts.db ?? opts.dbPath ?? DEFAULT_COORDINATOR_DB_PATH;
2180
- const app = createCoordinatorApp({ dbPath });
2393
+ const app = createBetterSqliteCoordinatorApp({ dbPath });
2181
2394
  p.intro("codemem sync coordinator serve");
2182
2395
  p.log.success(`Coordinator listening at http://${host}:${port}`);
2183
2396
  p.log.info(`DB: ${dbPath}`);
@@ -2208,6 +2421,7 @@ coordinatorCommand.addCommand(new Command("create-invite").configureHelp(helpSty
2208
2421
  p.log.success(`Invite created for ${groupId}`);
2209
2422
  if (typeof result.link === "string") p.log.message(`- link: ${result.link}`);
2210
2423
  if (typeof result.encoded === "string") p.log.message(`- invite: ${result.encoded}`);
2424
+ for (const warning of Array.isArray(result.warnings) ? result.warnings : []) p.log.warn(String(warning));
2211
2425
  p.outro("Invite ready");
2212
2426
  }));
2213
2427
  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) => {