@remnic/cli 1.0.11 → 1.0.13
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 +158 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -88,10 +88,12 @@ import {
|
|
|
88
88
|
formatProcedureStatsText,
|
|
89
89
|
parseXrayCliOptions,
|
|
90
90
|
renderXray,
|
|
91
|
+
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
91
92
|
applyOfflineSyncSnapshot,
|
|
92
93
|
buildOfflineSyncChangeset,
|
|
93
94
|
buildOfflineSyncSnapshot,
|
|
94
95
|
defaultOfflineSyncStatePath,
|
|
96
|
+
normalizeOfflineSyncSnapshot,
|
|
95
97
|
offlineSyncStateFromSnapshot,
|
|
96
98
|
readOfflineSyncState,
|
|
97
99
|
summarizeOfflineSyncChangeset,
|
|
@@ -6667,6 +6669,63 @@ async function fetchOfflineFiles(args) {
|
|
|
6667
6669
|
}
|
|
6668
6670
|
);
|
|
6669
6671
|
}
|
|
6672
|
+
var OFFLINE_SYNC_DIRECT_HYDRATE_MIN_BYTES = 16 * 1024 * 1024;
|
|
6673
|
+
function parseOfflineHeaderNumber(headers, name) {
|
|
6674
|
+
const raw = headers.get(name);
|
|
6675
|
+
if (raw === null) throw new Error(`offline file content response omitted ${name}`);
|
|
6676
|
+
const parsed = Number(raw);
|
|
6677
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
6678
|
+
throw new Error(`offline file content response had invalid ${name}: ${raw}`);
|
|
6679
|
+
}
|
|
6680
|
+
return parsed;
|
|
6681
|
+
}
|
|
6682
|
+
async function fetchOfflineFileContentChunk(args) {
|
|
6683
|
+
const response = await fetch(
|
|
6684
|
+
offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/file-content"),
|
|
6685
|
+
{
|
|
6686
|
+
method: "POST",
|
|
6687
|
+
headers: {
|
|
6688
|
+
authorization: `Bearer ${args.token}`,
|
|
6689
|
+
"content-type": "application/json"
|
|
6690
|
+
},
|
|
6691
|
+
body: JSON.stringify({
|
|
6692
|
+
namespace: args.namespace,
|
|
6693
|
+
includeTranscripts: args.includeTranscripts,
|
|
6694
|
+
path: args.path,
|
|
6695
|
+
offset: args.offset,
|
|
6696
|
+
length: args.length
|
|
6697
|
+
})
|
|
6698
|
+
}
|
|
6699
|
+
);
|
|
6700
|
+
if (!response.ok) {
|
|
6701
|
+
let detail = "";
|
|
6702
|
+
try {
|
|
6703
|
+
detail = await response.text();
|
|
6704
|
+
} catch {
|
|
6705
|
+
detail = "";
|
|
6706
|
+
}
|
|
6707
|
+
throw new Error(
|
|
6708
|
+
`offline sync file-content request failed: ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 500)}` : ""}`
|
|
6709
|
+
);
|
|
6710
|
+
}
|
|
6711
|
+
const encodedPath = response.headers.get("x-remnic-file-path");
|
|
6712
|
+
const relPath = encodedPath ? decodeURIComponent(encodedPath) : args.path;
|
|
6713
|
+
const content = Buffer.from(await response.arrayBuffer());
|
|
6714
|
+
const chunkBytes = parseOfflineHeaderNumber(response.headers, "x-remnic-chunk-bytes");
|
|
6715
|
+
const sha256 = response.headers.get("x-remnic-file-sha256") ?? void 0;
|
|
6716
|
+
if (content.length !== chunkBytes) {
|
|
6717
|
+
throw new Error(`offline file content response length mismatch for ${relPath}`);
|
|
6718
|
+
}
|
|
6719
|
+
return {
|
|
6720
|
+
path: relPath,
|
|
6721
|
+
...sha256 ? { sha256 } : {},
|
|
6722
|
+
bytes: parseOfflineHeaderNumber(response.headers, "x-remnic-file-bytes"),
|
|
6723
|
+
mtimeMs: parseOfflineHeaderNumber(response.headers, "x-remnic-file-mtime-ms"),
|
|
6724
|
+
offset: parseOfflineHeaderNumber(response.headers, "x-remnic-chunk-offset"),
|
|
6725
|
+
chunkBytes,
|
|
6726
|
+
content
|
|
6727
|
+
};
|
|
6728
|
+
}
|
|
6670
6729
|
function resolvedOfflineSnapshotNamespace(snapshot, requestedNamespace) {
|
|
6671
6730
|
const resolved = typeof snapshot.namespace === "string" && snapshot.namespace.trim().length > 0 ? snapshot.namespace.trim() : void 0;
|
|
6672
6731
|
return resolved ?? requestedNamespace;
|
|
@@ -6713,6 +6772,86 @@ function offlineSnapshotContentPathsForApply(options) {
|
|
|
6713
6772
|
}
|
|
6714
6773
|
return paths.sort();
|
|
6715
6774
|
}
|
|
6775
|
+
function shouldDirectHydrateOfflineFile(options) {
|
|
6776
|
+
if (options.incoming.bytes < OFFLINE_SYNC_DIRECT_HYDRATE_MIN_BYTES) return false;
|
|
6777
|
+
if (options.current?.sha256 === options.incoming.sha256) return false;
|
|
6778
|
+
if (options.current && options.base && options.current.sha256 === options.base.sha256) {
|
|
6779
|
+
return true;
|
|
6780
|
+
}
|
|
6781
|
+
return !options.current && !options.base;
|
|
6782
|
+
}
|
|
6783
|
+
function resolveOfflineDirectHydrationPath(memoryDir, relPath) {
|
|
6784
|
+
const base = path11.resolve(memoryDir);
|
|
6785
|
+
const target = path11.resolve(base, relPath);
|
|
6786
|
+
const relative = path11.relative(base, target);
|
|
6787
|
+
if (relative === "" || relative === ".." || relative.startsWith(`..${path11.sep}`) || path11.isAbsolute(relative)) {
|
|
6788
|
+
throw new Error(`offline sync direct hydration path escapes memory dir: ${relPath}`);
|
|
6789
|
+
}
|
|
6790
|
+
return target;
|
|
6791
|
+
}
|
|
6792
|
+
async function fetchOfflineFileContent(args) {
|
|
6793
|
+
const chunks = [];
|
|
6794
|
+
const hash = createHash("sha256");
|
|
6795
|
+
let offset = 0;
|
|
6796
|
+
while (offset < args.expected.bytes) {
|
|
6797
|
+
const chunk = await fetchOfflineFileContentChunk({
|
|
6798
|
+
remoteUrl: args.remoteUrl,
|
|
6799
|
+
token: args.token,
|
|
6800
|
+
namespace: args.namespace,
|
|
6801
|
+
includeTranscripts: args.includeTranscripts,
|
|
6802
|
+
path: args.expected.path,
|
|
6803
|
+
offset,
|
|
6804
|
+
length: Math.min(
|
|
6805
|
+
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
6806
|
+
args.expected.bytes - offset
|
|
6807
|
+
)
|
|
6808
|
+
});
|
|
6809
|
+
if (chunk.path !== args.expected.path || chunk.sha256 !== void 0 && chunk.sha256 !== args.expected.sha256 || chunk.bytes !== args.expected.bytes || chunk.mtimeMs !== args.expected.mtimeMs || chunk.offset !== offset || chunk.chunkBytes !== chunk.content.length) {
|
|
6810
|
+
throw new Error(`remote file changed while fetching offline content: ${args.expected.path}`);
|
|
6811
|
+
}
|
|
6812
|
+
if (chunk.chunkBytes === 0) {
|
|
6813
|
+
throw new Error(`remote offline content chunk was empty before EOF: ${args.expected.path}`);
|
|
6814
|
+
}
|
|
6815
|
+
chunks.push(chunk.content);
|
|
6816
|
+
hash.update(chunk.content);
|
|
6817
|
+
offset += chunk.chunkBytes;
|
|
6818
|
+
}
|
|
6819
|
+
const content = Buffer.concat(chunks, offset);
|
|
6820
|
+
const digest = hash.digest("hex");
|
|
6821
|
+
if (digest !== args.expected.sha256 || content.length !== args.expected.bytes) {
|
|
6822
|
+
throw new Error(`remote offline content checksum mismatch for ${args.expected.path}`);
|
|
6823
|
+
}
|
|
6824
|
+
return content;
|
|
6825
|
+
}
|
|
6826
|
+
async function directHydrateLargeOfflineFiles(args) {
|
|
6827
|
+
if (!args.writeFile) return /* @__PURE__ */ new Set();
|
|
6828
|
+
const snapshot = normalizeOfflineSyncSnapshot(args.snapshot);
|
|
6829
|
+
const base = offlineFileStateMap(args.baseFiles);
|
|
6830
|
+
const current = offlineFileStateMap(args.currentFiles);
|
|
6831
|
+
const hydrated = /* @__PURE__ */ new Set();
|
|
6832
|
+
const candidates = snapshot.files.filter((incoming) => shouldDirectHydrateOfflineFile({
|
|
6833
|
+
incoming,
|
|
6834
|
+
base: base.get(incoming.path),
|
|
6835
|
+
current: current.get(incoming.path)
|
|
6836
|
+
})).sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path));
|
|
6837
|
+
for (const incoming of candidates) {
|
|
6838
|
+
const content = await fetchOfflineFileContent({
|
|
6839
|
+
remoteUrl: args.remoteUrl,
|
|
6840
|
+
token: args.token,
|
|
6841
|
+
namespace: args.namespace,
|
|
6842
|
+
includeTranscripts: args.includeTranscripts,
|
|
6843
|
+
expected: incoming
|
|
6844
|
+
});
|
|
6845
|
+
await args.writeFile({
|
|
6846
|
+
root: args.memoryDir,
|
|
6847
|
+
path: incoming.path,
|
|
6848
|
+
filePath: resolveOfflineDirectHydrationPath(args.memoryDir, incoming.path),
|
|
6849
|
+
content
|
|
6850
|
+
});
|
|
6851
|
+
hydrated.add(incoming.path);
|
|
6852
|
+
}
|
|
6853
|
+
return hydrated;
|
|
6854
|
+
}
|
|
6716
6855
|
function chunkOfflineFilePaths(paths) {
|
|
6717
6856
|
const chunks = [];
|
|
6718
6857
|
let current = [];
|
|
@@ -6910,6 +7049,24 @@ async function runOfflineSyncOnce(options) {
|
|
|
6910
7049
|
includeTranscripts: options.includeTranscripts,
|
|
6911
7050
|
readFile: storageIo.readFile
|
|
6912
7051
|
});
|
|
7052
|
+
const directHydratedPaths = await directHydrateLargeOfflineFiles({
|
|
7053
|
+
remoteUrl: options.remoteUrl,
|
|
7054
|
+
token: options.token,
|
|
7055
|
+
namespace: syncNamespace,
|
|
7056
|
+
includeTranscripts: options.includeTranscripts,
|
|
7057
|
+
snapshot: remoteSnapshotMetadata,
|
|
7058
|
+
baseFiles,
|
|
7059
|
+
currentFiles: currentSnapshot.files,
|
|
7060
|
+
memoryDir: options.memoryDir,
|
|
7061
|
+
writeFile: storageIo.writeFile
|
|
7062
|
+
});
|
|
7063
|
+
const applyCurrentSnapshot = directHydratedPaths.size > 0 ? await buildOfflineSyncSnapshot({
|
|
7064
|
+
root: options.memoryDir,
|
|
7065
|
+
sourceId: localOfflineSourceId(options.memoryDir),
|
|
7066
|
+
includeContent: false,
|
|
7067
|
+
includeTranscripts: options.includeTranscripts,
|
|
7068
|
+
readFile: storageIo.readFile
|
|
7069
|
+
}) : currentSnapshot;
|
|
6913
7070
|
const remoteSnapshot = await hydrateOfflineSnapshotContent({
|
|
6914
7071
|
remoteUrl: options.remoteUrl,
|
|
6915
7072
|
token: options.token,
|
|
@@ -6917,7 +7074,7 @@ async function runOfflineSyncOnce(options) {
|
|
|
6917
7074
|
includeTranscripts: options.includeTranscripts,
|
|
6918
7075
|
snapshot: remoteSnapshotMetadata,
|
|
6919
7076
|
baseFiles,
|
|
6920
|
-
currentFiles:
|
|
7077
|
+
currentFiles: applyCurrentSnapshot.files
|
|
6921
7078
|
});
|
|
6922
7079
|
const resolvedNamespace = resolvedOfflineSnapshotNamespace(remoteSnapshot, syncNamespace);
|
|
6923
7080
|
let pull;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "CLI for Remnic memory — init, query, doctor, daemon management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"yaml": "^2.4.2",
|
|
29
|
-
"@remnic/
|
|
30
|
-
"@remnic/
|
|
31
|
-
"@remnic/
|
|
29
|
+
"@remnic/plugin-pi": "^1.0.0",
|
|
30
|
+
"@remnic/core": "^1.1.19",
|
|
31
|
+
"@remnic/server": "^1.0.5"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@remnic/bench": "^1.0.0",
|
|
@@ -79,9 +79,9 @@
|
|
|
79
79
|
"@remnic/import-chatgpt": "0.1.0",
|
|
80
80
|
"@remnic/import-claude": "0.1.0",
|
|
81
81
|
"@remnic/import-lossless-claw": "0.1.1",
|
|
82
|
+
"@remnic/import-supermemory": "0.1.2",
|
|
82
83
|
"@remnic/import-mem0": "0.1.0",
|
|
83
|
-
"@remnic/import-gemini": "0.1.0"
|
|
84
|
-
"@remnic/import-supermemory": "0.1.2"
|
|
84
|
+
"@remnic/import-gemini": "0.1.0"
|
|
85
85
|
},
|
|
86
86
|
"license": "MIT",
|
|
87
87
|
"repository": {
|