@remnic/cli 9.45.0 → 9.45.1

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.
Files changed (2) hide show
  1. package/dist/index.js +145 -21
  2. package/package.json +29 -29
package/dist/index.js CHANGED
@@ -243,9 +243,12 @@ import * as fs3 from "fs";
243
243
  import { createHash as createHash2 } from "crypto";
244
244
  import * as path2 from "path";
245
245
  import {
246
+ CONVERGE_CONFLICT_POLICIES,
247
+ DEFAULT_CONVERGE_CONFLICT_POLICY,
246
248
  parseConfig as parseConfig2,
247
249
  buildOfflineSyncSnapshotFromBase,
248
250
  OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
251
+ OFFLINE_SYNC_CHANGESET_FORMAT,
249
252
  OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES as OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES2,
250
253
  applyOfflineSyncFileContentChunk
251
254
  } from "@remnic/core";
@@ -766,6 +769,45 @@ async function postPeerConvergenceComplete(peerUrl, namespaces, token, fetchImpl
766
769
  }
767
770
  return false;
768
771
  }
772
+ async function postPeerFileDeletion(peerUrl, namespace, filePath, baseSha256, token, fetchImpl = globalThis.fetch) {
773
+ const base = withoutTrailingSlashes(peerUrl);
774
+ const routes = ["/remnic/v1/offline-sync/apply", "/engram/v1/offline-sync/apply"];
775
+ const headers = {
776
+ "content-type": "application/json",
777
+ ...token ? { authorization: `Bearer ${token}` } : {}
778
+ };
779
+ let previousAttemptFailed = false;
780
+ for (const route of routes) {
781
+ try {
782
+ const response = await fetchImpl(`${base}${route}`, {
783
+ method: "POST",
784
+ headers,
785
+ body: JSON.stringify({
786
+ namespace,
787
+ changeset: {
788
+ format: OFFLINE_SYNC_CHANGESET_FORMAT,
789
+ schemaVersion: 1,
790
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
791
+ sourceId: "remnic-converge",
792
+ includeTranscripts: false,
793
+ changes: [{ type: "delete", path: filePath, baseSha256 }]
794
+ }
795
+ })
796
+ });
797
+ if (!response.ok) throw new Error(`offline apply request failed: ${response.status}`);
798
+ const result = await response.json().catch(() => null);
799
+ if (!result || typeof result !== "object" || !("appliedDeletes" in result) || typeof result.appliedDeletes !== "number" || !("skipped" in result) || typeof result.skipped !== "number" || !("conflicts" in result) || !Array.isArray(result.conflicts) || result.conflicts.length > 0) {
800
+ return false;
801
+ }
802
+ if (result.appliedDeletes === 1) return "applied";
803
+ if (result.skipped === 1) return previousAttemptFailed ? "applied" : "skipped";
804
+ return false;
805
+ } catch {
806
+ previousAttemptFailed = true;
807
+ }
808
+ }
809
+ return false;
810
+ }
769
811
  async function computeConvergePlan(options = {}) {
770
812
  const baseMap = /* @__PURE__ */ new Map();
771
813
  const namespacesToPlan = /* @__PURE__ */ new Set();
@@ -773,6 +815,8 @@ async function computeConvergePlan(options = {}) {
773
815
  const localTombstones = /* @__PURE__ */ new Map();
774
816
  const peerMap = /* @__PURE__ */ new Map();
775
817
  const peerTombstones = /* @__PURE__ */ new Map();
818
+ const localDeletionMtimeMs = /* @__PURE__ */ new Map();
819
+ const peerDeletionMtimeMs = /* @__PURE__ */ new Map();
776
820
  const localManifests = /* @__PURE__ */ new Map();
777
821
  const peerManifests = /* @__PURE__ */ new Map();
778
822
  if (options.baseFilesByNamespace) {
@@ -792,6 +836,12 @@ async function computeConvergePlan(options = {}) {
792
836
  localTombstones.set(ns, new Set(tombstones));
793
837
  }
794
838
  }
839
+ if (options.localDeletionMtimeMsByNamespace) {
840
+ for (const [ns, mtimes] of options.localDeletionMtimeMsByNamespace) {
841
+ namespacesToPlan.add(ns);
842
+ localDeletionMtimeMs.set(ns, mtimes);
843
+ }
844
+ }
795
845
  if (options.peerFilesByNamespace) {
796
846
  for (const [ns, files] of options.peerFilesByNamespace) {
797
847
  namespacesToPlan.add(ns);
@@ -803,6 +853,12 @@ async function computeConvergePlan(options = {}) {
803
853
  peerTombstones.set(ns, new Set(tombstones));
804
854
  }
805
855
  }
856
+ if (options.peerDeletionMtimeMsByNamespace) {
857
+ for (const [ns, mtimes] of options.peerDeletionMtimeMsByNamespace) {
858
+ namespacesToPlan.add(ns);
859
+ peerDeletionMtimeMs.set(ns, mtimes);
860
+ }
861
+ }
806
862
  let config = options.config;
807
863
  if (!config) {
808
864
  try {
@@ -904,20 +960,28 @@ async function computeConvergePlan(options = {}) {
904
960
  }
905
961
  const inputs = [];
906
962
  for (const ns of [...namespacesToPlan].sort()) {
963
+ const baseFiles = baseMap.get(ns);
964
+ const localFiles = localMap.get(ns) ?? [];
965
+ const peerFiles = peerMap.get(ns) ?? [];
966
+ const localDeletions = new Map(localDeletionMtimeMs.get(ns) ?? []);
967
+ const peerDeletions = new Map(peerDeletionMtimeMs.get(ns) ?? []);
907
968
  inputs.push({
908
969
  namespace: ns,
909
- local: localMap.get(ns) ?? [],
910
- peer: peerMap.get(ns) ?? [],
911
- base: baseMap.get(ns),
970
+ local: localFiles,
971
+ peer: peerFiles,
972
+ base: baseFiles,
973
+ localDeletionMtimeMs: localDeletions,
974
+ peerDeletionMtimeMs: peerDeletions,
912
975
  tombstonedFileSha256: localTombstones.get(ns) ?? [],
913
976
  peerTombstonedFileSha256: peerTombstones.get(ns) ?? []
914
977
  });
915
978
  }
916
- const plan = planReconciliation(inputs, { conflictPolicy: options.conflictPolicy });
979
+ const conflictPolicy = options.conflictPolicy ?? config?.converge.conflictPolicy ?? DEFAULT_CONVERGE_CONFLICT_POLICY;
980
+ const plan = planReconciliation(inputs, { conflictPolicy });
917
981
  return collapseActiveFactDuplicates(plan, localManifests, peerManifests);
918
982
  }
919
983
  async function executeConvergeApply(options = {}) {
920
- const conflictPolicy = options.conflictPolicy ?? "manual";
984
+ const conflictPolicy = options.conflictPolicy ?? options.config?.converge.conflictPolicy ?? DEFAULT_CONVERGE_CONFLICT_POLICY;
921
985
  const plan = await computeConvergePlan({ ...options, conflictPolicy });
922
986
  if (plan.converged && !options.dryRun) {
923
987
  await updateCursorsForPlan(plan, options);
@@ -930,7 +994,7 @@ async function executeConvergeApply(options = {}) {
930
994
  };
931
995
  }
932
996
  const unresolvedCount = plan.byNamespace.reduce((acc, report) => acc + report.unresolved, 0);
933
- if (unresolvedCount > 0 && conflictPolicy === "manual") {
997
+ if (unresolvedCount > 0) {
934
998
  return {
935
999
  converged: false,
936
1000
  status: "stopped_unresolved_conflicts",
@@ -1008,12 +1072,9 @@ async function executeConvergeApply(options = {}) {
1008
1072
  transferType = "suppress";
1009
1073
  } else if (entry.action === "conflict") {
1010
1074
  if (entry.resolution === "peer-wins") {
1011
- transferType = "pull";
1075
+ transferType = entry.peerSha256 ? "pull" : "delete-local";
1012
1076
  } else if (entry.resolution === "local-wins") {
1013
- transferType = "push";
1014
- } else if (entry.resolution === "supersede-link") {
1015
- if (entry.newerSide === "peer") transferType = "pull";
1016
- else if (entry.newerSide === "local") transferType = "push";
1077
+ transferType = entry.localSha256 ? "push" : "delete-peer";
1017
1078
  }
1018
1079
  }
1019
1080
  if (transferType === "pull") {
@@ -1158,6 +1219,56 @@ async function executeConvergeApply(options = {}) {
1158
1219
  } else {
1159
1220
  actualTransfers.failed += 1;
1160
1221
  }
1222
+ } else if (transferType === "delete-local") {
1223
+ let deleted = false;
1224
+ const bufferedFiles = options.localFileBuffers?.get(entry.namespace);
1225
+ if (options.localFileBuffers) {
1226
+ const current = bufferedFiles?.get(entry.path);
1227
+ if (current && entry.localSha256 && createHash2("sha256").update(current).digest("hex") === entry.localSha256) {
1228
+ bufferedFiles.delete(entry.path);
1229
+ deleted = true;
1230
+ }
1231
+ } else {
1232
+ const rootDir = rootMap.get(entry.namespace);
1233
+ if (rootDir && entry.localSha256) {
1234
+ try {
1235
+ const io = await createOfflineStorageIo(rootDir);
1236
+ const filePath = path2.join(rootDir, entry.path);
1237
+ const current = await io.readFileDigest({ root: rootDir, path: entry.path, filePath });
1238
+ if (current.sha256 === entry.localSha256) {
1239
+ await io.deleteFile({ root: rootDir, path: entry.path, filePath });
1240
+ deleted = true;
1241
+ }
1242
+ } catch {
1243
+ deleted = false;
1244
+ }
1245
+ }
1246
+ }
1247
+ if (deleted) actualTransfers.conflictsResolved += 1;
1248
+ else actualTransfers.failed += 1;
1249
+ } else if (transferType === "delete-peer") {
1250
+ let deleted = false;
1251
+ const bufferedFiles = options.peerFileBuffers?.get(entry.namespace);
1252
+ if (options.peerFileBuffers) {
1253
+ const current = bufferedFiles?.get(entry.path);
1254
+ if (current && entry.peerSha256 && createHash2("sha256").update(current).digest("hex") === entry.peerSha256) {
1255
+ bufferedFiles.delete(entry.path);
1256
+ deleted = true;
1257
+ }
1258
+ } else if (options.peerUrl && entry.peerSha256) {
1259
+ const deletionResult = await postPeerFileDeletion(
1260
+ options.peerUrl,
1261
+ entry.namespace,
1262
+ entry.path,
1263
+ entry.peerSha256,
1264
+ resolvedToken,
1265
+ fetchFn
1266
+ );
1267
+ deleted = Boolean(deletionResult);
1268
+ if (deletionResult === "applied") peerMutatedNamespaces.add(entry.namespace);
1269
+ }
1270
+ if (deleted) actualTransfers.conflictsResolved += 1;
1271
+ else actualTransfers.failed += 1;
1161
1272
  } else if (transferType === "suppress") {
1162
1273
  actualTransfers.suppressed += 1;
1163
1274
  }
@@ -1198,7 +1309,7 @@ async function updateCursorsForPlan(plan, options) {
1198
1309
  const namespaces = new Set(plan.byNamespace.map((n) => n.namespace));
1199
1310
  for (const ns of namespaces) {
1200
1311
  const cursorPath = defaultConvergeCursorPath(memoryDir, peerUrl, ns);
1201
- const baseFiles = plan.entries.filter((entry) => entry.namespace === ns && entry.reason !== "semantic_duplicate").map((entry) => ({
1312
+ const baseFiles = plan.entries.filter((entry) => entry.namespace === ns && entry.reason !== "semantic_duplicate" && !(entry.reason === "local_modified_peer_deleted" && entry.resolution === "peer-wins") && !(entry.reason === "local_deleted_peer_modified" && entry.resolution === "local-wins")).map((entry) => ({
1202
1313
  path: entry.path,
1203
1314
  sha256: entry.localSha256 ?? entry.peerSha256 ?? "unknown"
1204
1315
  }));
@@ -1250,7 +1361,7 @@ function formatConvergeApplyReport(result) {
1250
1361
  lines.push(formatConvergeReport(result.plan));
1251
1362
  return lines.join("\n");
1252
1363
  }
1253
- async function cmdConverge(action, rest, json) {
1364
+ async function cmdConverge(action, rest, json, config = parseConfig2({})) {
1254
1365
  if (action === "help" || action === "--help" || action === "-h" || rest.includes("--help") || rest.includes("-h")) {
1255
1366
  console.log(`Usage: remnic converge <plan|apply> [options]
1256
1367
 
@@ -1262,7 +1373,8 @@ Options:
1262
1373
  --peer <url> Peer server URL (or --remote-url / --remote)
1263
1374
  --token <token> Bearer token or SecretRef for peer authentication
1264
1375
  --conflict-policy <policy>
1265
- Conflict resolution policy (manual|newest-wins|keep-both)
1376
+ Policy override (newest-wins|manual)
1377
+ Default: converge.conflictPolicy (newest-wins)
1266
1378
  --dry-run Simulate transfers without mutating disk or remote peer
1267
1379
  --json Output detailed JSON plan report
1268
1380
  `);
@@ -1288,16 +1400,19 @@ Options:
1288
1400
  i += 1;
1289
1401
  } else if (arg === "--dry-run") {
1290
1402
  dryRun = true;
1291
- } else if (arg === "--conflict-policy" && rest[i + 1]) {
1292
- const pol = rest[i + 1];
1293
- if (pol === "manual" || pol === "newest-wins" || pol === "keep-both") {
1294
- conflictPolicy = pol;
1403
+ } else if (arg === "--conflict-policy") {
1404
+ const policy = rest[i + 1];
1405
+ if (typeof policy !== "string" || !CONVERGE_CONFLICT_POLICIES.includes(policy)) {
1406
+ throw new Error(
1407
+ `converge: --conflict-policy must be one of ${CONVERGE_CONFLICT_POLICIES.join(", ")}`
1408
+ );
1295
1409
  }
1410
+ conflictPolicy = policy;
1296
1411
  i += 1;
1297
1412
  }
1298
1413
  }
1299
1414
  if (action === "plan") {
1300
- const plan = await computeConvergePlan({ peerUrl, peerToken, conflictPolicy });
1415
+ const plan = await computeConvergePlan({ config, peerUrl, peerToken, conflictPolicy });
1301
1416
  if (json) {
1302
1417
  console.log(JSON.stringify(plan, null, 2));
1303
1418
  } else {
@@ -1309,7 +1424,8 @@ Options:
1309
1424
  peerUrl,
1310
1425
  peerToken,
1311
1426
  dryRun,
1312
- conflictPolicy
1427
+ conflictPolicy,
1428
+ config
1313
1429
  });
1314
1430
  if (json) {
1315
1431
  console.log(JSON.stringify(result, null, 2));
@@ -14426,7 +14542,15 @@ Options:
14426
14542
  case "converge": {
14427
14543
  const action = rest[0] ?? "plan";
14428
14544
  const json = rest.includes("--json");
14429
- await cmdConverge(action, rest.slice(1), json);
14545
+ const args = rest.slice(1);
14546
+ if (action === "help" || action === "--help" || action === "-h" || args.includes("--help") || args.includes("-h")) {
14547
+ await cmdConverge(action, args, json);
14548
+ break;
14549
+ }
14550
+ const configPath = resolveConfigPath();
14551
+ const raw = fs12.existsSync(configPath) ? JSON.parse(fs12.readFileSync(configPath, "utf8")) : {};
14552
+ const config = parseConfig5(resolveRemnicConfigRecord4(raw));
14553
+ await cmdConverge(action, args, json, config);
14430
14554
  break;
14431
14555
  }
14432
14556
  case "oauth": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/cli",
3
- "version": "9.45.0",
3
+ "version": "9.45.1",
4
4
  "description": "CLI for Remnic memory — init, query, doctor, daemon management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,24 +26,24 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "yaml": "^2.4.2",
29
- "@remnic/plugin-pi": "^9.45.0",
30
- "@remnic/core": "^9.45.0",
31
- "@remnic/server": "^9.45.0"
29
+ "@remnic/server": "^9.45.1",
30
+ "@remnic/plugin-pi": "^9.45.1",
31
+ "@remnic/core": "^9.45.1"
32
32
  },
33
33
  "peerDependencies": {
34
- "@remnic/bench": "^9.45.0",
35
- "@remnic/export-weclone": "^9.45.0",
36
- "@remnic/import-weclone": "^9.45.0",
37
- "@remnic/import-chatgpt": "^9.45.0",
38
- "@remnic/import-claude": "^9.45.0",
39
- "@remnic/import-gemini": "^9.45.0",
40
- "@remnic/import-lossless-claw": "^9.45.0",
41
- "@remnic/import-mem0": "^9.45.0",
42
- "@remnic/import-supermemory": "^9.45.0",
43
- "@remnic/connector-limitless": "^9.45.0",
44
- "@remnic/connector-bee": "^9.45.0",
45
- "@remnic/connector-omi": "^9.45.0",
46
- "@remnic/capture-audio": "^9.45.0"
34
+ "@remnic/bench": "^9.45.1",
35
+ "@remnic/export-weclone": "^9.45.1",
36
+ "@remnic/import-weclone": "^9.45.1",
37
+ "@remnic/import-chatgpt": "^9.45.1",
38
+ "@remnic/import-claude": "^9.45.1",
39
+ "@remnic/import-gemini": "^9.45.1",
40
+ "@remnic/import-lossless-claw": "^9.45.1",
41
+ "@remnic/import-mem0": "^9.45.1",
42
+ "@remnic/import-supermemory": "^9.45.1",
43
+ "@remnic/connector-limitless": "^9.45.1",
44
+ "@remnic/connector-bee": "^9.45.1",
45
+ "@remnic/connector-omi": "^9.45.1",
46
+ "@remnic/capture-audio": "^9.45.1"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "@remnic/bench": {
@@ -89,18 +89,18 @@
89
89
  "devDependencies": {
90
90
  "tsup": "^8.5.1",
91
91
  "typescript": "^5.9.3",
92
- "@remnic/bench": "9.45.0",
93
- "@remnic/export-weclone": "9.45.0",
94
- "@remnic/import-weclone": "9.45.0",
95
- "@remnic/import-chatgpt": "9.45.0",
96
- "@remnic/import-claude": "9.45.0",
97
- "@remnic/import-gemini": "9.45.0",
98
- "@remnic/import-lossless-claw": "9.45.0",
99
- "@remnic/import-mem0": "9.45.0",
100
- "@remnic/connector-limitless": "9.45.0",
101
- "@remnic/import-supermemory": "9.45.0",
102
- "@remnic/connector-omi": "9.45.0",
103
- "@remnic/connector-bee": "9.45.0"
92
+ "@remnic/bench": "9.45.1",
93
+ "@remnic/export-weclone": "9.45.1",
94
+ "@remnic/import-weclone": "9.45.1",
95
+ "@remnic/import-chatgpt": "9.45.1",
96
+ "@remnic/import-claude": "9.45.1",
97
+ "@remnic/import-mem0": "9.45.1",
98
+ "@remnic/import-gemini": "9.45.1",
99
+ "@remnic/import-lossless-claw": "9.45.1",
100
+ "@remnic/import-supermemory": "9.45.1",
101
+ "@remnic/connector-bee": "9.45.1",
102
+ "@remnic/connector-limitless": "9.45.1",
103
+ "@remnic/connector-omi": "9.45.1"
104
104
  },
105
105
  "license": "MIT",
106
106
  "repository": {