@remnic/cli 1.0.10 → 1.0.12

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 +158 -1
  2. 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: currentSnapshot.files
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.10",
3
+ "version": "1.0.12",
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/plugin-pi": "^1.0.0",
29
+ "@remnic/core": "^1.1.18",
30
30
  "@remnic/server": "^1.0.5",
31
- "@remnic/core": "^1.1.16"
31
+ "@remnic/plugin-pi": "^1.0.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@remnic/bench": "^1.0.0",
@@ -73,12 +73,12 @@
73
73
  "devDependencies": {
74
74
  "tsup": "^8.5.1",
75
75
  "typescript": "^5.9.3",
76
- "@remnic/bench": "1.0.1",
77
76
  "@remnic/export-weclone": "1.0.1",
78
- "@remnic/import-chatgpt": "0.1.0",
77
+ "@remnic/bench": "1.0.1",
79
78
  "@remnic/import-weclone": "1.0.1",
80
- "@remnic/import-claude": "0.1.0",
79
+ "@remnic/import-chatgpt": "0.1.0",
81
80
  "@remnic/import-gemini": "0.1.0",
81
+ "@remnic/import-claude": "0.1.0",
82
82
  "@remnic/import-lossless-claw": "0.1.1",
83
83
  "@remnic/import-mem0": "0.1.0",
84
84
  "@remnic/import-supermemory": "0.1.2"