@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.
- package/dist/index.js +145 -21
- 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:
|
|
910
|
-
peer:
|
|
911
|
-
base:
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
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"
|
|
1292
|
-
const
|
|
1293
|
-
if (
|
|
1294
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
30
|
-
"@remnic/
|
|
31
|
-
"@remnic/
|
|
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.
|
|
35
|
-
"@remnic/export-weclone": "^9.45.
|
|
36
|
-
"@remnic/import-weclone": "^9.45.
|
|
37
|
-
"@remnic/import-chatgpt": "^9.45.
|
|
38
|
-
"@remnic/import-claude": "^9.45.
|
|
39
|
-
"@remnic/import-gemini": "^9.45.
|
|
40
|
-
"@remnic/import-lossless-claw": "^9.45.
|
|
41
|
-
"@remnic/import-mem0": "^9.45.
|
|
42
|
-
"@remnic/import-supermemory": "^9.45.
|
|
43
|
-
"@remnic/connector-limitless": "^9.45.
|
|
44
|
-
"@remnic/connector-bee": "^9.45.
|
|
45
|
-
"@remnic/connector-omi": "^9.45.
|
|
46
|
-
"@remnic/capture-audio": "^9.45.
|
|
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.
|
|
93
|
-
"@remnic/export-weclone": "9.45.
|
|
94
|
-
"@remnic/import-weclone": "9.45.
|
|
95
|
-
"@remnic/import-chatgpt": "9.45.
|
|
96
|
-
"@remnic/import-claude": "9.45.
|
|
97
|
-
"@remnic/import-
|
|
98
|
-
"@remnic/import-
|
|
99
|
-
"@remnic/import-
|
|
100
|
-
"@remnic/
|
|
101
|
-
"@remnic/
|
|
102
|
-
"@remnic/connector-
|
|
103
|
-
"@remnic/connector-
|
|
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": {
|