codemem 0.22.3 → 0.22.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAAA;;GAEG;AA2CH,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, 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";
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
3
  import { Command } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -2245,6 +2245,137 @@ peersCommand.addCommand(new Command("remove").configureHelp(helpStyle).descripti
2245
2245
  }
2246
2246
  }));
2247
2247
  syncCommand.addCommand(peersCommand);
2248
+ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).description("Fast-bootstrap memories from a peer (full snapshot transfer)").requiredOption("--peer <device-id>", "peer device ID to bootstrap from").option("--page-size <n>", "items per snapshot page (default: 2000)", "2000").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--keys-dir <path>", "keys directory").option("--force", "skip dirty-local-state safety check").option("--json", "output as JSON").action(async (opts) => {
2249
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
2250
+ try {
2251
+ const peerDeviceId = opts.peer.trim();
2252
+ const pageSize = Math.max(1, Number.parseInt(opts.pageSize, 10) || 2e3);
2253
+ const keysDir = opts.keysDir ?? void 0;
2254
+ const peer = drizzle(store.db, { schema }).select().from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).get();
2255
+ if (!peer) {
2256
+ if (opts.json) console.log(JSON.stringify({
2257
+ ok: false,
2258
+ error: "peer not found"
2259
+ }));
2260
+ else p.log.error(`Peer ${peerDeviceId} not found in sync_peers.`);
2261
+ process.exitCode = 1;
2262
+ return;
2263
+ }
2264
+ if (!peer.pinned_fingerprint) {
2265
+ if (opts.json) console.log(JSON.stringify({
2266
+ ok: false,
2267
+ error: "peer not pinned"
2268
+ }));
2269
+ else p.log.error(`Peer ${peerDeviceId} has no pinned fingerprint. Accept it first.`);
2270
+ process.exitCode = 1;
2271
+ return;
2272
+ }
2273
+ if (!opts.force) {
2274
+ const dirty = hasUnsyncedSharedMemoryChanges(store.db);
2275
+ if (dirty.dirty) {
2276
+ if (opts.json) console.log(JSON.stringify({
2277
+ ok: false,
2278
+ error: "local_unsynced_changes",
2279
+ count: dirty.count
2280
+ }));
2281
+ else p.log.error(`${dirty.count} unsynced shared memory change(s) would be lost. Use --force to override.`);
2282
+ process.exitCode = 1;
2283
+ return;
2284
+ }
2285
+ }
2286
+ const [deviceId] = ensureDeviceIdentity(store.db, { keysDir });
2287
+ const addresses = JSON.parse(String(peer.addresses_json ?? "[]"));
2288
+ if (!addresses.length) {
2289
+ if (opts.json) console.log(JSON.stringify({
2290
+ ok: false,
2291
+ error: "no peer addresses"
2292
+ }));
2293
+ else p.log.error("Peer has no known addresses. Run a sync first or add addresses.");
2294
+ process.exitCode = 1;
2295
+ return;
2296
+ }
2297
+ let boundary = null;
2298
+ let baseUrl = "";
2299
+ let lastAddressError = "";
2300
+ for (const address of addresses) {
2301
+ const candidate = buildBaseUrl(address);
2302
+ if (!candidate) continue;
2303
+ const statusUrl = `${candidate}/v1/status`;
2304
+ const headers = buildAuthHeaders({
2305
+ deviceId,
2306
+ method: "GET",
2307
+ url: statusUrl,
2308
+ bodyBytes: Buffer.alloc(0),
2309
+ keysDir
2310
+ });
2311
+ try {
2312
+ const [code, payload] = await requestJson("GET", statusUrl, { headers });
2313
+ if (code !== 200 || !payload) {
2314
+ lastAddressError = `${candidate}: status ${code}`;
2315
+ continue;
2316
+ }
2317
+ if (payload.fingerprint !== peer.pinned_fingerprint) {
2318
+ lastAddressError = `${candidate}: fingerprint mismatch`;
2319
+ continue;
2320
+ }
2321
+ const reset = payload.sync_reset;
2322
+ if (reset && typeof reset.generation === "number" && typeof reset.snapshot_id === "string") {
2323
+ boundary = {
2324
+ generation: reset.generation,
2325
+ snapshot_id: reset.snapshot_id,
2326
+ baseline_cursor: typeof reset.baseline_cursor === "string" ? reset.baseline_cursor : null
2327
+ };
2328
+ baseUrl = candidate;
2329
+ break;
2330
+ }
2331
+ lastAddressError = `${candidate}: missing sync_reset boundary`;
2332
+ } catch (err) {
2333
+ lastAddressError = `${candidate}: ${err instanceof Error ? err.message : String(err)}`;
2334
+ }
2335
+ }
2336
+ if (!boundary || !baseUrl) {
2337
+ const detail = lastAddressError ? `peer unreachable or missing reset boundary (${lastAddressError})` : "peer unreachable or missing reset boundary";
2338
+ if (opts.json) console.log(JSON.stringify({
2339
+ ok: false,
2340
+ error: detail
2341
+ }));
2342
+ else p.log.error(detail);
2343
+ process.exitCode = 1;
2344
+ return;
2345
+ }
2346
+ if (!opts.json) {
2347
+ p.intro("codemem sync bootstrap");
2348
+ p.log.step(`Bootstrapping from ${peer.name || peerDeviceId}...`);
2349
+ }
2350
+ const resetInfo = {
2351
+ generation: boundary.generation,
2352
+ snapshot_id: boundary.snapshot_id,
2353
+ baseline_cursor: boundary.baseline_cursor,
2354
+ retained_floor_cursor: null,
2355
+ reset_required: true,
2356
+ reason: "initial_bootstrap"
2357
+ };
2358
+ const { items } = await fetchAllSnapshotPages(baseUrl, resetInfo, deviceId, {
2359
+ keysDir,
2360
+ pageSize
2361
+ });
2362
+ const result = applyBootstrapSnapshot(store.db, peerDeviceId, items, resetInfo);
2363
+ if (opts.json) console.log(JSON.stringify({
2364
+ ok: result.ok,
2365
+ applied: result.applied,
2366
+ deleted: result.deleted,
2367
+ error: result.error ?? null
2368
+ }));
2369
+ else {
2370
+ if (result.ok) p.log.success(`Applied ${result.applied} memories (removed ${result.deleted} stale).`);
2371
+ else p.log.error(result.error || "Bootstrap apply failed.");
2372
+ p.outro(result.ok ? "Bootstrap complete" : "Bootstrap failed");
2373
+ }
2374
+ if (!result.ok) process.exitCode = 1;
2375
+ } finally {
2376
+ store.close();
2377
+ }
2378
+ }));
2248
2379
  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) => {
2249
2380
  const config = readCodememConfigFile();
2250
2381
  config.sync_coordinator_url = url.trim();