@remnic/cli 9.45.0 → 9.45.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.
Files changed (2) hide show
  1. package/dist/index.js +166 -28
  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";
@@ -256,6 +259,7 @@ import {
256
259
  } from "@remnic/core/reconcile/plan.js";
257
260
  import {
258
261
  defaultConvergeCursorPath,
262
+ deriveConvergeCursorBase,
259
263
  readConvergeCursor,
260
264
  writeConvergeCursor
261
265
  } from "@remnic/core/reconcile/cursor.js";
@@ -766,13 +770,55 @@ async function postPeerConvergenceComplete(peerUrl, namespaces, token, fetchImpl
766
770
  }
767
771
  return false;
768
772
  }
773
+ async function postPeerFileDeletion(peerUrl, namespace, filePath, baseSha256, token, fetchImpl = globalThis.fetch) {
774
+ const base = withoutTrailingSlashes(peerUrl);
775
+ const routes = ["/remnic/v1/offline-sync/apply", "/engram/v1/offline-sync/apply"];
776
+ const headers = {
777
+ "content-type": "application/json",
778
+ ...token ? { authorization: `Bearer ${token}` } : {}
779
+ };
780
+ let previousAttemptFailed = false;
781
+ for (const route of routes) {
782
+ try {
783
+ const response = await fetchImpl(`${base}${route}`, {
784
+ method: "POST",
785
+ headers,
786
+ body: JSON.stringify({
787
+ namespace,
788
+ changeset: {
789
+ format: OFFLINE_SYNC_CHANGESET_FORMAT,
790
+ schemaVersion: 1,
791
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
792
+ sourceId: "remnic-converge",
793
+ includeTranscripts: false,
794
+ changes: [{ type: "delete", path: filePath, baseSha256 }]
795
+ }
796
+ })
797
+ });
798
+ if (!response.ok) throw new Error(`offline apply request failed: ${response.status}`);
799
+ const result = await response.json().catch(() => null);
800
+ 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) {
801
+ return false;
802
+ }
803
+ if (result.appliedDeletes === 1) return "applied";
804
+ if (result.skipped === 1) return previousAttemptFailed ? "applied" : "skipped";
805
+ return false;
806
+ } catch {
807
+ previousAttemptFailed = true;
808
+ }
809
+ }
810
+ return false;
811
+ }
769
812
  async function computeConvergePlan(options = {}) {
770
813
  const baseMap = /* @__PURE__ */ new Map();
814
+ const semanticAgreementMap = /* @__PURE__ */ new Map();
771
815
  const namespacesToPlan = /* @__PURE__ */ new Set();
772
816
  const localMap = /* @__PURE__ */ new Map();
773
817
  const localTombstones = /* @__PURE__ */ new Map();
774
818
  const peerMap = /* @__PURE__ */ new Map();
775
819
  const peerTombstones = /* @__PURE__ */ new Map();
820
+ const localDeletionMtimeMs = /* @__PURE__ */ new Map();
821
+ const peerDeletionMtimeMs = /* @__PURE__ */ new Map();
776
822
  const localManifests = /* @__PURE__ */ new Map();
777
823
  const peerManifests = /* @__PURE__ */ new Map();
778
824
  if (options.baseFilesByNamespace) {
@@ -781,6 +827,12 @@ async function computeConvergePlan(options = {}) {
781
827
  baseMap.set(ns, files);
782
828
  }
783
829
  }
830
+ if (options.semanticAgreementsByNamespace) {
831
+ for (const [ns, agreements] of options.semanticAgreementsByNamespace) {
832
+ namespacesToPlan.add(ns);
833
+ semanticAgreementMap.set(ns, agreements);
834
+ }
835
+ }
784
836
  if (options.localFilesByNamespace) {
785
837
  for (const [ns, files] of options.localFilesByNamespace) {
786
838
  namespacesToPlan.add(ns);
@@ -792,6 +844,12 @@ async function computeConvergePlan(options = {}) {
792
844
  localTombstones.set(ns, new Set(tombstones));
793
845
  }
794
846
  }
847
+ if (options.localDeletionMtimeMsByNamespace) {
848
+ for (const [ns, mtimes] of options.localDeletionMtimeMsByNamespace) {
849
+ namespacesToPlan.add(ns);
850
+ localDeletionMtimeMs.set(ns, mtimes);
851
+ }
852
+ }
795
853
  if (options.peerFilesByNamespace) {
796
854
  for (const [ns, files] of options.peerFilesByNamespace) {
797
855
  namespacesToPlan.add(ns);
@@ -803,6 +861,12 @@ async function computeConvergePlan(options = {}) {
803
861
  peerTombstones.set(ns, new Set(tombstones));
804
862
  }
805
863
  }
864
+ if (options.peerDeletionMtimeMsByNamespace) {
865
+ for (const [ns, mtimes] of options.peerDeletionMtimeMsByNamespace) {
866
+ namespacesToPlan.add(ns);
867
+ peerDeletionMtimeMs.set(ns, mtimes);
868
+ }
869
+ }
806
870
  let config = options.config;
807
871
  if (!config) {
808
872
  try {
@@ -893,31 +957,42 @@ async function computeConvergePlan(options = {}) {
893
957
  }
894
958
  }
895
959
  const memoryDir = options.cursorDir ?? config?.memoryDir;
896
- if (!options.baseFilesByNamespace && memoryDir && options.peerUrl) {
960
+ if (memoryDir && options.peerUrl && (!options.baseFilesByNamespace || !options.semanticAgreementsByNamespace)) {
897
961
  for (const ns of namespacesToPlan) {
898
962
  const cursorPath = defaultConvergeCursorPath(memoryDir, options.peerUrl, ns);
899
963
  const cursor = await readConvergeCursor(cursorPath);
900
- if (cursor?.baseFiles && cursor.baseFiles.length > 0) {
964
+ if (!options.baseFilesByNamespace && cursor?.baseFiles && cursor.baseFiles.length > 0) {
901
965
  baseMap.set(ns, cursor.baseFiles);
902
966
  }
967
+ if (!options.semanticAgreementsByNamespace && cursor?.semanticAgreements && cursor.semanticAgreements.length > 0) {
968
+ semanticAgreementMap.set(ns, cursor.semanticAgreements);
969
+ }
903
970
  }
904
971
  }
905
972
  const inputs = [];
906
973
  for (const ns of [...namespacesToPlan].sort()) {
974
+ const baseFiles = baseMap.get(ns);
975
+ const localFiles = localMap.get(ns) ?? [];
976
+ const peerFiles = peerMap.get(ns) ?? [];
977
+ const localDeletions = new Map(localDeletionMtimeMs.get(ns) ?? []);
978
+ const peerDeletions = new Map(peerDeletionMtimeMs.get(ns) ?? []);
907
979
  inputs.push({
908
980
  namespace: ns,
909
- local: localMap.get(ns) ?? [],
910
- peer: peerMap.get(ns) ?? [],
911
- base: baseMap.get(ns),
981
+ local: localFiles,
982
+ peer: peerFiles,
983
+ base: baseFiles,
984
+ localDeletionMtimeMs: localDeletions,
985
+ peerDeletionMtimeMs: peerDeletions,
912
986
  tombstonedFileSha256: localTombstones.get(ns) ?? [],
913
987
  peerTombstonedFileSha256: peerTombstones.get(ns) ?? []
914
988
  });
915
989
  }
916
- const plan = planReconciliation(inputs, { conflictPolicy: options.conflictPolicy });
917
- return collapseActiveFactDuplicates(plan, localManifests, peerManifests);
990
+ const conflictPolicy = options.conflictPolicy ?? config?.converge.conflictPolicy ?? DEFAULT_CONVERGE_CONFLICT_POLICY;
991
+ const plan = planReconciliation(inputs, { conflictPolicy });
992
+ return collapseActiveFactDuplicates(plan, localManifests, peerManifests, semanticAgreementMap);
918
993
  }
919
994
  async function executeConvergeApply(options = {}) {
920
- const conflictPolicy = options.conflictPolicy ?? "manual";
995
+ const conflictPolicy = options.conflictPolicy ?? options.config?.converge.conflictPolicy ?? DEFAULT_CONVERGE_CONFLICT_POLICY;
921
996
  const plan = await computeConvergePlan({ ...options, conflictPolicy });
922
997
  if (plan.converged && !options.dryRun) {
923
998
  await updateCursorsForPlan(plan, options);
@@ -930,7 +1005,7 @@ async function executeConvergeApply(options = {}) {
930
1005
  };
931
1006
  }
932
1007
  const unresolvedCount = plan.byNamespace.reduce((acc, report) => acc + report.unresolved, 0);
933
- if (unresolvedCount > 0 && conflictPolicy === "manual") {
1008
+ if (unresolvedCount > 0) {
934
1009
  return {
935
1010
  converged: false,
936
1011
  status: "stopped_unresolved_conflicts",
@@ -1008,12 +1083,9 @@ async function executeConvergeApply(options = {}) {
1008
1083
  transferType = "suppress";
1009
1084
  } else if (entry.action === "conflict") {
1010
1085
  if (entry.resolution === "peer-wins") {
1011
- transferType = "pull";
1086
+ transferType = entry.peerSha256 ? "pull" : "delete-local";
1012
1087
  } 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";
1088
+ transferType = entry.localSha256 ? "push" : "delete-peer";
1017
1089
  }
1018
1090
  }
1019
1091
  if (transferType === "pull") {
@@ -1158,6 +1230,56 @@ async function executeConvergeApply(options = {}) {
1158
1230
  } else {
1159
1231
  actualTransfers.failed += 1;
1160
1232
  }
1233
+ } else if (transferType === "delete-local") {
1234
+ let deleted = false;
1235
+ const bufferedFiles = options.localFileBuffers?.get(entry.namespace);
1236
+ if (options.localFileBuffers) {
1237
+ const current = bufferedFiles?.get(entry.path);
1238
+ if (current && entry.localSha256 && createHash2("sha256").update(current).digest("hex") === entry.localSha256) {
1239
+ bufferedFiles.delete(entry.path);
1240
+ deleted = true;
1241
+ }
1242
+ } else {
1243
+ const rootDir = rootMap.get(entry.namespace);
1244
+ if (rootDir && entry.localSha256) {
1245
+ try {
1246
+ const io = await createOfflineStorageIo(rootDir);
1247
+ const filePath = path2.join(rootDir, entry.path);
1248
+ const current = await io.readFileDigest({ root: rootDir, path: entry.path, filePath });
1249
+ if (current.sha256 === entry.localSha256) {
1250
+ await io.deleteFile({ root: rootDir, path: entry.path, filePath });
1251
+ deleted = true;
1252
+ }
1253
+ } catch {
1254
+ deleted = false;
1255
+ }
1256
+ }
1257
+ }
1258
+ if (deleted) actualTransfers.conflictsResolved += 1;
1259
+ else actualTransfers.failed += 1;
1260
+ } else if (transferType === "delete-peer") {
1261
+ let deleted = false;
1262
+ const bufferedFiles = options.peerFileBuffers?.get(entry.namespace);
1263
+ if (options.peerFileBuffers) {
1264
+ const current = bufferedFiles?.get(entry.path);
1265
+ if (current && entry.peerSha256 && createHash2("sha256").update(current).digest("hex") === entry.peerSha256) {
1266
+ bufferedFiles.delete(entry.path);
1267
+ deleted = true;
1268
+ }
1269
+ } else if (options.peerUrl && entry.peerSha256) {
1270
+ const deletionResult = await postPeerFileDeletion(
1271
+ options.peerUrl,
1272
+ entry.namespace,
1273
+ entry.path,
1274
+ entry.peerSha256,
1275
+ resolvedToken,
1276
+ fetchFn
1277
+ );
1278
+ deleted = Boolean(deletionResult);
1279
+ if (deletionResult === "applied") peerMutatedNamespaces.add(entry.namespace);
1280
+ }
1281
+ if (deleted) actualTransfers.conflictsResolved += 1;
1282
+ else actualTransfers.failed += 1;
1161
1283
  } else if (transferType === "suppress") {
1162
1284
  actualTransfers.suppressed += 1;
1163
1285
  }
@@ -1198,16 +1320,19 @@ async function updateCursorsForPlan(plan, options) {
1198
1320
  const namespaces = new Set(plan.byNamespace.map((n) => n.namespace));
1199
1321
  for (const ns of namespaces) {
1200
1322
  const cursorPath = defaultConvergeCursorPath(memoryDir, peerUrl, ns);
1201
- const baseFiles = plan.entries.filter((entry) => entry.namespace === ns && entry.reason !== "semantic_duplicate").map((entry) => ({
1202
- path: entry.path,
1203
- sha256: entry.localSha256 ?? entry.peerSha256 ?? "unknown"
1204
- }));
1323
+ const priorSemanticAgreements = options.semanticAgreementsByNamespace?.get(ns) ?? (await readConvergeCursor(cursorPath))?.semanticAgreements ?? [];
1324
+ const { baseFiles, semanticAgreements } = deriveConvergeCursorBase(
1325
+ plan.entries,
1326
+ ns,
1327
+ priorSemanticAgreements
1328
+ );
1205
1329
  const cursorState = {
1206
1330
  version: 1,
1207
1331
  peerUrl,
1208
1332
  namespace: ns,
1209
1333
  lastConvergedAt: (/* @__PURE__ */ new Date()).toISOString(),
1210
- baseFiles
1334
+ baseFiles,
1335
+ semanticAgreements
1211
1336
  };
1212
1337
  try {
1213
1338
  await writeConvergeCursor(cursorPath, cursorState);
@@ -1250,7 +1375,7 @@ function formatConvergeApplyReport(result) {
1250
1375
  lines.push(formatConvergeReport(result.plan));
1251
1376
  return lines.join("\n");
1252
1377
  }
1253
- async function cmdConverge(action, rest, json) {
1378
+ async function cmdConverge(action, rest, json, config = parseConfig2({})) {
1254
1379
  if (action === "help" || action === "--help" || action === "-h" || rest.includes("--help") || rest.includes("-h")) {
1255
1380
  console.log(`Usage: remnic converge <plan|apply> [options]
1256
1381
 
@@ -1262,7 +1387,8 @@ Options:
1262
1387
  --peer <url> Peer server URL (or --remote-url / --remote)
1263
1388
  --token <token> Bearer token or SecretRef for peer authentication
1264
1389
  --conflict-policy <policy>
1265
- Conflict resolution policy (manual|newest-wins|keep-both)
1390
+ Policy override (newest-wins|manual)
1391
+ Default: converge.conflictPolicy (newest-wins)
1266
1392
  --dry-run Simulate transfers without mutating disk or remote peer
1267
1393
  --json Output detailed JSON plan report
1268
1394
  `);
@@ -1288,16 +1414,19 @@ Options:
1288
1414
  i += 1;
1289
1415
  } else if (arg === "--dry-run") {
1290
1416
  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;
1417
+ } else if (arg === "--conflict-policy") {
1418
+ const policy = rest[i + 1];
1419
+ if (typeof policy !== "string" || !CONVERGE_CONFLICT_POLICIES.includes(policy)) {
1420
+ throw new Error(
1421
+ `converge: --conflict-policy must be one of ${CONVERGE_CONFLICT_POLICIES.join(", ")}`
1422
+ );
1295
1423
  }
1424
+ conflictPolicy = policy;
1296
1425
  i += 1;
1297
1426
  }
1298
1427
  }
1299
1428
  if (action === "plan") {
1300
- const plan = await computeConvergePlan({ peerUrl, peerToken, conflictPolicy });
1429
+ const plan = await computeConvergePlan({ config, peerUrl, peerToken, conflictPolicy });
1301
1430
  if (json) {
1302
1431
  console.log(JSON.stringify(plan, null, 2));
1303
1432
  } else {
@@ -1309,7 +1438,8 @@ Options:
1309
1438
  peerUrl,
1310
1439
  peerToken,
1311
1440
  dryRun,
1312
- conflictPolicy
1441
+ conflictPolicy,
1442
+ config
1313
1443
  });
1314
1444
  if (json) {
1315
1445
  console.log(JSON.stringify(result, null, 2));
@@ -14426,7 +14556,15 @@ Options:
14426
14556
  case "converge": {
14427
14557
  const action = rest[0] ?? "plan";
14428
14558
  const json = rest.includes("--json");
14429
- await cmdConverge(action, rest.slice(1), json);
14559
+ const args = rest.slice(1);
14560
+ if (action === "help" || action === "--help" || action === "-h" || args.includes("--help") || args.includes("-h")) {
14561
+ await cmdConverge(action, args, json);
14562
+ break;
14563
+ }
14564
+ const configPath = resolveConfigPath();
14565
+ const raw = fs12.existsSync(configPath) ? JSON.parse(fs12.readFileSync(configPath, "utf8")) : {};
14566
+ const config = parseConfig5(resolveRemnicConfigRecord4(raw));
14567
+ await cmdConverge(action, args, json, config);
14430
14568
  break;
14431
14569
  }
14432
14570
  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.2",
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.2",
30
+ "@remnic/core": "^9.45.2",
31
+ "@remnic/plugin-pi": "^9.45.2"
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.2",
35
+ "@remnic/export-weclone": "^9.45.2",
36
+ "@remnic/import-weclone": "^9.45.2",
37
+ "@remnic/import-chatgpt": "^9.45.2",
38
+ "@remnic/import-claude": "^9.45.2",
39
+ "@remnic/import-gemini": "^9.45.2",
40
+ "@remnic/import-lossless-claw": "^9.45.2",
41
+ "@remnic/import-mem0": "^9.45.2",
42
+ "@remnic/import-supermemory": "^9.45.2",
43
+ "@remnic/connector-limitless": "^9.45.2",
44
+ "@remnic/connector-bee": "^9.45.2",
45
+ "@remnic/connector-omi": "^9.45.2",
46
+ "@remnic/capture-audio": "^9.45.2"
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.2",
93
+ "@remnic/export-weclone": "9.45.2",
94
+ "@remnic/import-claude": "9.45.2",
95
+ "@remnic/import-gemini": "9.45.2",
96
+ "@remnic/import-weclone": "9.45.2",
97
+ "@remnic/import-lossless-claw": "9.45.2",
98
+ "@remnic/import-supermemory": "9.45.2",
99
+ "@remnic/connector-limitless": "9.45.2",
100
+ "@remnic/import-mem0": "9.45.2",
101
+ "@remnic/import-chatgpt": "9.45.2",
102
+ "@remnic/connector-omi": "9.45.2",
103
+ "@remnic/connector-bee": "9.45.2"
104
104
  },
105
105
  "license": "MIT",
106
106
  "repository": {