@remnic/cli 9.43.0 → 9.45.0
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 +97 -11
- package/package.json +29 -29
package/dist/index.js
CHANGED
|
@@ -259,6 +259,10 @@ import {
|
|
|
259
259
|
readConvergeCursor,
|
|
260
260
|
writeConvergeCursor
|
|
261
261
|
} from "@remnic/core/reconcile/cursor.js";
|
|
262
|
+
import {
|
|
263
|
+
buildReconcileManifest,
|
|
264
|
+
collapseActiveFactDuplicates
|
|
265
|
+
} from "@remnic/core/reconcile/manifest.js";
|
|
262
266
|
|
|
263
267
|
// src/offline-storage-io.ts
|
|
264
268
|
import { mkdtemp, readdir, lstat, rm } from "fs/promises";
|
|
@@ -680,12 +684,18 @@ async function fetchPeerFileContent(peerUrl, namespace, filePath, token, fetchIm
|
|
|
680
684
|
}
|
|
681
685
|
return null;
|
|
682
686
|
}
|
|
687
|
+
function withoutTrailingSlashes(value) {
|
|
688
|
+
let end = value.length;
|
|
689
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
690
|
+
return value.slice(0, end);
|
|
691
|
+
}
|
|
683
692
|
async function postPeerFileContent(peerUrl, namespace, filePath, content, metadata, token, fetchImpl = globalThis.fetch) {
|
|
684
|
-
const base = peerUrl
|
|
693
|
+
const base = withoutTrailingSlashes(peerUrl);
|
|
685
694
|
const routes = [
|
|
686
695
|
`/remnic/v1/offline-sync/apply-file-content?namespace=${encodeURIComponent(namespace)}`,
|
|
687
696
|
`/engram/v1/offline-sync/apply-file-content?namespace=${encodeURIComponent(namespace)}`
|
|
688
697
|
];
|
|
698
|
+
let previousAttemptFailed = false;
|
|
689
699
|
for (const route of routes) {
|
|
690
700
|
try {
|
|
691
701
|
let offset = 0;
|
|
@@ -717,7 +727,9 @@ async function postPeerFileContent(peerUrl, namespace, filePath, content, metada
|
|
|
717
727
|
return false;
|
|
718
728
|
}
|
|
719
729
|
if (result.done) {
|
|
720
|
-
|
|
730
|
+
if (result.skipped) return previousAttemptFailed ? "applied" : "skipped";
|
|
731
|
+
if (result.applied && offset + chunk.length === content.length) return "applied";
|
|
732
|
+
return false;
|
|
721
733
|
}
|
|
722
734
|
if (result.applied || result.skipped || chunk.length === 0) {
|
|
723
735
|
return false;
|
|
@@ -726,6 +738,30 @@ async function postPeerFileContent(peerUrl, namespace, filePath, content, metada
|
|
|
726
738
|
} while (offset < content.length);
|
|
727
739
|
return false;
|
|
728
740
|
} catch {
|
|
741
|
+
previousAttemptFailed = true;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
async function postPeerConvergenceComplete(peerUrl, namespaces, token, fetchImpl = globalThis.fetch) {
|
|
747
|
+
const base = withoutTrailingSlashes(peerUrl);
|
|
748
|
+
const query = namespaces.map((namespace) => `namespace=${encodeURIComponent(namespace)}`).join("&");
|
|
749
|
+
const routes = [
|
|
750
|
+
"/remnic/v1/offline-sync/convergence-complete",
|
|
751
|
+
"/engram/v1/offline-sync/convergence-complete"
|
|
752
|
+
];
|
|
753
|
+
for (const route of routes) {
|
|
754
|
+
const response = await fetchImpl(`${base}${route}?${query}`, {
|
|
755
|
+
method: "POST",
|
|
756
|
+
headers: {
|
|
757
|
+
"x-remnic-source-id": encodeURIComponent("remnic-converge"),
|
|
758
|
+
...token ? { authorization: `Bearer ${token}` } : {}
|
|
759
|
+
}
|
|
760
|
+
}).catch(() => null);
|
|
761
|
+
if (!response?.ok) continue;
|
|
762
|
+
const result = await response.json().catch(() => null);
|
|
763
|
+
if (result && typeof result === "object" && "namespaces" in result && Array.isArray(result.namespaces) && result.namespaces.length === namespaces.length && result.namespaces.every((namespace, index) => namespace === namespaces[index]) && "refreshed" in result && result.refreshed === true) {
|
|
764
|
+
return true;
|
|
729
765
|
}
|
|
730
766
|
}
|
|
731
767
|
return false;
|
|
@@ -737,6 +773,8 @@ async function computeConvergePlan(options = {}) {
|
|
|
737
773
|
const localTombstones = /* @__PURE__ */ new Map();
|
|
738
774
|
const peerMap = /* @__PURE__ */ new Map();
|
|
739
775
|
const peerTombstones = /* @__PURE__ */ new Map();
|
|
776
|
+
const localManifests = /* @__PURE__ */ new Map();
|
|
777
|
+
const peerManifests = /* @__PURE__ */ new Map();
|
|
740
778
|
if (options.baseFilesByNamespace) {
|
|
741
779
|
for (const [ns, files] of options.baseFilesByNamespace) {
|
|
742
780
|
namespacesToPlan.add(ns);
|
|
@@ -794,6 +832,26 @@ async function computeConvergePlan(options = {}) {
|
|
|
794
832
|
bytes: record.bytes
|
|
795
833
|
}));
|
|
796
834
|
localMap.set(ns, files);
|
|
835
|
+
try {
|
|
836
|
+
const io = await createOfflineStorageIo(rootInfo.rootDir);
|
|
837
|
+
localManifests.set(
|
|
838
|
+
ns,
|
|
839
|
+
await buildReconcileManifest({
|
|
840
|
+
files,
|
|
841
|
+
readFile: async (file) => {
|
|
842
|
+
const readFile2 = io.readFile;
|
|
843
|
+
if (!readFile2) throw new Error("offline storage cannot read reconciliation manifest files");
|
|
844
|
+
return await readFile2({
|
|
845
|
+
root: rootInfo.rootDir,
|
|
846
|
+
path: file.path,
|
|
847
|
+
filePath: path2.join(rootInfo.rootDir, file.path)
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
})
|
|
851
|
+
);
|
|
852
|
+
} catch {
|
|
853
|
+
localManifests.delete(ns);
|
|
854
|
+
}
|
|
797
855
|
const tombstones = await readLocalTombstones(rootInfo.rootDir);
|
|
798
856
|
localTombstones.set(ns, tombstones);
|
|
799
857
|
} catch {
|
|
@@ -801,7 +859,8 @@ async function computeConvergePlan(options = {}) {
|
|
|
801
859
|
}
|
|
802
860
|
}
|
|
803
861
|
}
|
|
804
|
-
|
|
862
|
+
const peerUrl = options.peerUrl;
|
|
863
|
+
if (!options.peerFilesByNamespace && peerUrl) {
|
|
805
864
|
let resolvedToken;
|
|
806
865
|
if (options.peerToken) {
|
|
807
866
|
try {
|
|
@@ -814,9 +873,23 @@ async function computeConvergePlan(options = {}) {
|
|
|
814
873
|
}
|
|
815
874
|
const fetchFn = options.fetchImpl ?? globalThis.fetch;
|
|
816
875
|
for (const ns of namespacesToPlan) {
|
|
817
|
-
const peerData = await fetchPeerSnapshot(
|
|
876
|
+
const peerData = await fetchPeerSnapshot(peerUrl, ns, resolvedToken, fetchFn);
|
|
818
877
|
peerMap.set(ns, peerData.files);
|
|
819
878
|
peerTombstones.set(ns, peerData.tombstones);
|
|
879
|
+
peerManifests.set(
|
|
880
|
+
ns,
|
|
881
|
+
await buildReconcileManifest({
|
|
882
|
+
files: peerData.files,
|
|
883
|
+
cachedFiles: localManifests.get(ns)?.files,
|
|
884
|
+
readFile: async (file) => {
|
|
885
|
+
const remote = await fetchPeerFileContent(peerUrl, ns, file.path, resolvedToken, fetchFn);
|
|
886
|
+
if (!remote || remote.sha256 !== file.sha256) {
|
|
887
|
+
throw new Error(`failed to read peer reconciliation manifest file: ${file.path}`);
|
|
888
|
+
}
|
|
889
|
+
return remote.content;
|
|
890
|
+
}
|
|
891
|
+
})
|
|
892
|
+
);
|
|
820
893
|
}
|
|
821
894
|
}
|
|
822
895
|
const memoryDir = options.cursorDir ?? config?.memoryDir;
|
|
@@ -840,7 +913,8 @@ async function computeConvergePlan(options = {}) {
|
|
|
840
913
|
peerTombstonedFileSha256: peerTombstones.get(ns) ?? []
|
|
841
914
|
});
|
|
842
915
|
}
|
|
843
|
-
|
|
916
|
+
const plan = planReconciliation(inputs, { conflictPolicy: options.conflictPolicy });
|
|
917
|
+
return collapseActiveFactDuplicates(plan, localManifests, peerManifests);
|
|
844
918
|
}
|
|
845
919
|
async function executeConvergeApply(options = {}) {
|
|
846
920
|
const conflictPolicy = options.conflictPolicy ?? "manual";
|
|
@@ -894,6 +968,7 @@ async function executeConvergeApply(options = {}) {
|
|
|
894
968
|
suppressed: 0,
|
|
895
969
|
failed: 0
|
|
896
970
|
};
|
|
971
|
+
const peerMutatedNamespaces = /* @__PURE__ */ new Set();
|
|
897
972
|
let resolvedToken;
|
|
898
973
|
if (options.peerToken) {
|
|
899
974
|
try {
|
|
@@ -1057,7 +1132,7 @@ async function executeConvergeApply(options = {}) {
|
|
|
1057
1132
|
else actualTransfers.pushed += 1;
|
|
1058
1133
|
} else if (options.peerUrl && entry.localSha256) {
|
|
1059
1134
|
const expectedPeerSha256 = entry.action === "conflict" ? entry.peerSha256 : entry.baseSha256;
|
|
1060
|
-
const
|
|
1135
|
+
const applied = await postPeerFileContent(
|
|
1061
1136
|
options.peerUrl,
|
|
1062
1137
|
entry.namespace,
|
|
1063
1138
|
entry.path,
|
|
@@ -1070,7 +1145,8 @@ async function executeConvergeApply(options = {}) {
|
|
|
1070
1145
|
resolvedToken,
|
|
1071
1146
|
fetchFn
|
|
1072
1147
|
);
|
|
1073
|
-
if (
|
|
1148
|
+
if (applied) {
|
|
1149
|
+
if (applied === "applied") peerMutatedNamespaces.add(entry.namespace);
|
|
1074
1150
|
if (entry.action === "conflict") actualTransfers.conflictsResolved += 1;
|
|
1075
1151
|
else actualTransfers.pushed += 1;
|
|
1076
1152
|
} else {
|
|
@@ -1086,6 +1162,17 @@ async function executeConvergeApply(options = {}) {
|
|
|
1086
1162
|
actualTransfers.suppressed += 1;
|
|
1087
1163
|
}
|
|
1088
1164
|
}
|
|
1165
|
+
if (options.peerUrl && peerMutatedNamespaces.size > 0) {
|
|
1166
|
+
const namespaces = [...peerMutatedNamespaces].sort();
|
|
1167
|
+
if (!await postPeerConvergenceComplete(
|
|
1168
|
+
options.peerUrl,
|
|
1169
|
+
namespaces,
|
|
1170
|
+
resolvedToken,
|
|
1171
|
+
fetchFn
|
|
1172
|
+
)) {
|
|
1173
|
+
actualTransfers.failed += 1;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1089
1176
|
let cursorUpdated = false;
|
|
1090
1177
|
if (actualTransfers.failed === 0) {
|
|
1091
1178
|
await updateCursorsForPlan(plan, options);
|
|
@@ -1111,10 +1198,9 @@ async function updateCursorsForPlan(plan, options) {
|
|
|
1111
1198
|
const namespaces = new Set(plan.byNamespace.map((n) => n.namespace));
|
|
1112
1199
|
for (const ns of namespaces) {
|
|
1113
1200
|
const cursorPath = defaultConvergeCursorPath(memoryDir, peerUrl, ns);
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
sha256: e.localSha256 ?? e.peerSha256 ?? "unknown"
|
|
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"
|
|
1118
1204
|
}));
|
|
1119
1205
|
const cursorState = {
|
|
1120
1206
|
version: 1,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/cli",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.45.0",
|
|
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/plugin-pi": "^9.45.0",
|
|
30
|
+
"@remnic/core": "^9.45.0",
|
|
31
|
+
"@remnic/server": "^9.45.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@remnic/bench": "^9.
|
|
35
|
-
"@remnic/export-weclone": "^9.
|
|
36
|
-
"@remnic/import-weclone": "^9.
|
|
37
|
-
"@remnic/import-chatgpt": "^9.
|
|
38
|
-
"@remnic/import-claude": "^9.
|
|
39
|
-
"@remnic/import-gemini": "^9.
|
|
40
|
-
"@remnic/import-lossless-claw": "^9.
|
|
41
|
-
"@remnic/import-mem0": "^9.
|
|
42
|
-
"@remnic/import-supermemory": "^9.
|
|
43
|
-
"@remnic/connector-limitless": "^9.
|
|
44
|
-
"@remnic/connector-bee": "^9.
|
|
45
|
-
"@remnic/connector-omi": "^9.
|
|
46
|
-
"@remnic/capture-audio": "^9.
|
|
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"
|
|
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.
|
|
93
|
-
"@remnic/export-weclone": "9.
|
|
94
|
-
"@remnic/import-weclone": "9.
|
|
95
|
-
"@remnic/import-chatgpt": "9.
|
|
96
|
-
"@remnic/import-claude": "9.
|
|
97
|
-
"@remnic/import-
|
|
98
|
-
"@remnic/import-
|
|
99
|
-
"@remnic/import-
|
|
100
|
-
"@remnic/
|
|
101
|
-
"@remnic/
|
|
102
|
-
"@remnic/connector-
|
|
103
|
-
"@remnic/connector-
|
|
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"
|
|
104
104
|
},
|
|
105
105
|
"license": "MIT",
|
|
106
106
|
"repository": {
|