@remnic/cli 1.0.9 → 1.0.10

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 +157 -10
  2. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -90,6 +90,7 @@ import {
90
90
  renderXray,
91
91
  applyOfflineSyncSnapshot,
92
92
  buildOfflineSyncChangeset,
93
+ buildOfflineSyncSnapshot,
93
94
  defaultOfflineSyncStatePath,
94
95
  offlineSyncStateFromSnapshot,
95
96
  readOfflineSyncState,
@@ -6647,11 +6648,25 @@ async function fetchOfflineSnapshot(args) {
6647
6648
  offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/snapshot", {
6648
6649
  namespace: args.namespace,
6649
6650
  include_transcripts: args.includeTranscripts ? "true" : "false",
6650
- content: "true"
6651
+ content: args.includeContent === false ? "false" : "true"
6651
6652
  }),
6652
6653
  args.token
6653
6654
  );
6654
6655
  }
6656
+ async function fetchOfflineFiles(args) {
6657
+ return fetchOfflineJson(
6658
+ offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/files"),
6659
+ args.token,
6660
+ {
6661
+ method: "POST",
6662
+ body: JSON.stringify({
6663
+ namespace: args.namespace,
6664
+ includeTranscripts: args.includeTranscripts,
6665
+ paths: args.paths
6666
+ })
6667
+ }
6668
+ );
6669
+ }
6655
6670
  function resolvedOfflineSnapshotNamespace(snapshot, requestedNamespace) {
6656
6671
  const resolved = typeof snapshot.namespace === "string" && snapshot.namespace.trim().length > 0 ? snapshot.namespace.trim() : void 0;
6657
6672
  return resolved ?? requestedNamespace;
@@ -6684,6 +6699,99 @@ async function readFirstOfflineSyncState(paths) {
6684
6699
  }
6685
6700
  return null;
6686
6701
  }
6702
+ function offlineFileStateMap(files) {
6703
+ return new Map(files.map((file) => [file.path, file]));
6704
+ }
6705
+ function offlineSnapshotContentPathsForApply(options) {
6706
+ const base = offlineFileStateMap(options.baseFiles);
6707
+ const current = options.currentFiles ? offlineFileStateMap(options.currentFiles) : null;
6708
+ const paths = [];
6709
+ for (const incoming of options.snapshot.files) {
6710
+ if (current?.get(incoming.path)?.sha256 === incoming.sha256) continue;
6711
+ if (base.get(incoming.path)?.sha256 === incoming.sha256) continue;
6712
+ paths.push(incoming.path);
6713
+ }
6714
+ return paths.sort();
6715
+ }
6716
+ function chunkOfflineFilePaths(paths) {
6717
+ const chunks = [];
6718
+ let current = [];
6719
+ let currentBytes = 256;
6720
+ for (const relPath of paths) {
6721
+ const cost = Buffer.byteLength(JSON.stringify(relPath), "utf-8") + 1;
6722
+ if (current.length > 0 && (current.length >= 1e3 || currentBytes + cost > 96e3)) {
6723
+ chunks.push(current);
6724
+ current = [];
6725
+ currentBytes = 256;
6726
+ }
6727
+ current.push(relPath);
6728
+ currentBytes += cost;
6729
+ }
6730
+ if (current.length > 0) chunks.push(current);
6731
+ return chunks;
6732
+ }
6733
+ function isOfflineFilesUnsupportedError(error) {
6734
+ const message = error instanceof Error ? error.message : String(error);
6735
+ return /offline sync request failed: 404\b/.test(message);
6736
+ }
6737
+ function isMissingOfflineContentError(error) {
6738
+ const message = error instanceof Error ? error.message : String(error);
6739
+ return /^missing decoded content for /.test(message);
6740
+ }
6741
+ async function hydrateOfflineSnapshotContent(args) {
6742
+ const neededPaths = offlineSnapshotContentPathsForApply({
6743
+ snapshot: args.snapshot,
6744
+ baseFiles: args.baseFiles,
6745
+ currentFiles: args.currentFiles
6746
+ });
6747
+ if (neededPaths.length === 0) return args.snapshot;
6748
+ const expectedByPath = new Map(args.snapshot.files.map((file) => [file.path, file]));
6749
+ const contentByPath = /* @__PURE__ */ new Map();
6750
+ try {
6751
+ for (const batch of chunkOfflineFilePaths(neededPaths)) {
6752
+ const partial = await fetchOfflineFiles({
6753
+ remoteUrl: args.remoteUrl,
6754
+ token: args.token,
6755
+ namespace: args.namespace,
6756
+ includeTranscripts: args.includeTranscripts,
6757
+ paths: batch
6758
+ });
6759
+ for (const file of partial.files) {
6760
+ const expected = expectedByPath.get(file.path);
6761
+ if (!expected) continue;
6762
+ if (file.sha256 !== expected.sha256 || file.bytes !== expected.bytes) {
6763
+ throw new Error(`remote file changed while fetching offline content: ${file.path}`);
6764
+ }
6765
+ if (typeof file.contentBase64 !== "string") {
6766
+ throw new Error(`remote offline content response omitted contentBase64 for ${file.path}`);
6767
+ }
6768
+ contentByPath.set(file.path, file.contentBase64);
6769
+ }
6770
+ }
6771
+ } catch (error) {
6772
+ if (!isOfflineFilesUnsupportedError(error)) throw error;
6773
+ return fetchOfflineSnapshot({
6774
+ remoteUrl: args.remoteUrl,
6775
+ token: args.token,
6776
+ namespace: args.namespace,
6777
+ includeTranscripts: args.includeTranscripts,
6778
+ includeContent: true
6779
+ });
6780
+ }
6781
+ const missing = neededPaths.filter((relPath) => !contentByPath.has(relPath));
6782
+ if (missing.length > 0) {
6783
+ throw new Error(
6784
+ `remote offline content response omitted ${missing.length} changed file${missing.length === 1 ? "" : "s"}; retry sync`
6785
+ );
6786
+ }
6787
+ return {
6788
+ ...args.snapshot,
6789
+ files: args.snapshot.files.map((file) => {
6790
+ const contentBase64 = contentByPath.get(file.path);
6791
+ return contentBase64 === void 0 ? file : { ...file, contentBase64 };
6792
+ })
6793
+ };
6794
+ }
6687
6795
  async function pushOfflineChanges(args) {
6688
6796
  return fetchOfflineJson(
6689
6797
  offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/apply"),
@@ -6746,7 +6854,8 @@ async function runOfflineSyncOnce(options) {
6746
6854
  remoteUrl: options.remoteUrl,
6747
6855
  token: options.token,
6748
6856
  namespace: options.namespace,
6749
- includeTranscripts: options.includeTranscripts
6857
+ includeTranscripts: options.includeTranscripts,
6858
+ includeContent: false
6750
6859
  });
6751
6860
  syncNamespace = resolvedOfflineSnapshotNamespace(namespaceProbe, options.namespace);
6752
6861
  }
@@ -6787,21 +6896,59 @@ async function runOfflineSyncOnce(options) {
6787
6896
  namespace: syncNamespace,
6788
6897
  changeset
6789
6898
  }) : null;
6790
- const remoteSnapshot = await fetchOfflineSnapshot({
6899
+ const remoteSnapshotMetadata = await fetchOfflineSnapshot({
6791
6900
  remoteUrl: options.remoteUrl,
6792
6901
  token: options.token,
6793
6902
  namespace: syncNamespace,
6794
- includeTranscripts: options.includeTranscripts
6903
+ includeTranscripts: options.includeTranscripts,
6904
+ includeContent: false
6795
6905
  });
6796
- const resolvedNamespace = resolvedOfflineSnapshotNamespace(remoteSnapshot, syncNamespace);
6797
- const pull = await applyOfflineSyncSnapshot({
6906
+ const currentSnapshot = await buildOfflineSyncSnapshot({
6798
6907
  root: options.memoryDir,
6799
- snapshot: remoteSnapshot,
6908
+ sourceId: localOfflineSourceId(options.memoryDir),
6909
+ includeContent: false,
6910
+ includeTranscripts: options.includeTranscripts,
6911
+ readFile: storageIo.readFile
6912
+ });
6913
+ const remoteSnapshot = await hydrateOfflineSnapshotContent({
6914
+ remoteUrl: options.remoteUrl,
6915
+ token: options.token,
6916
+ namespace: syncNamespace,
6917
+ includeTranscripts: options.includeTranscripts,
6918
+ snapshot: remoteSnapshotMetadata,
6800
6919
  baseFiles,
6801
- readFile: storageIo.readFile,
6802
- writeFile: storageIo.writeFile,
6803
- deleteFile: storageIo.deleteFile
6920
+ currentFiles: currentSnapshot.files
6804
6921
  });
6922
+ const resolvedNamespace = resolvedOfflineSnapshotNamespace(remoteSnapshot, syncNamespace);
6923
+ let pull;
6924
+ try {
6925
+ pull = await applyOfflineSyncSnapshot({
6926
+ root: options.memoryDir,
6927
+ snapshot: remoteSnapshot,
6928
+ baseFiles,
6929
+ readFile: storageIo.readFile,
6930
+ writeFile: storageIo.writeFile,
6931
+ deleteFile: storageIo.deleteFile
6932
+ });
6933
+ } catch (error) {
6934
+ if (!isMissingOfflineContentError(error)) throw error;
6935
+ const retrySnapshot = await hydrateOfflineSnapshotContent({
6936
+ remoteUrl: options.remoteUrl,
6937
+ token: options.token,
6938
+ namespace: syncNamespace,
6939
+ includeTranscripts: options.includeTranscripts,
6940
+ snapshot: remoteSnapshotMetadata,
6941
+ baseFiles
6942
+ });
6943
+ pull = await applyOfflineSyncSnapshot({
6944
+ root: options.memoryDir,
6945
+ snapshot: retrySnapshot,
6946
+ baseFiles,
6947
+ readFile: storageIo.readFile,
6948
+ writeFile: storageIo.writeFile,
6949
+ deleteFile: storageIo.deleteFile
6950
+ });
6951
+ }
6805
6952
  const state = offlineSyncStateFromSnapshot({
6806
6953
  remoteId: options.remoteUrl,
6807
6954
  namespace: resolvedNamespace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/cli",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "CLI for Remnic memory — init, query, doctor, daemon management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "yaml": "^2.4.2",
29
29
  "@remnic/plugin-pi": "^1.0.0",
30
30
  "@remnic/server": "^1.0.5",
31
- "@remnic/core": "^1.1.15"
31
+ "@remnic/core": "^1.1.16"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@remnic/bench": "^1.0.0",
@@ -75,13 +75,13 @@
75
75
  "typescript": "^5.9.3",
76
76
  "@remnic/bench": "1.0.1",
77
77
  "@remnic/export-weclone": "1.0.1",
78
- "@remnic/import-weclone": "1.0.1",
79
- "@remnic/import-mem0": "0.1.0",
80
- "@remnic/import-lossless-claw": "0.1.1",
81
78
  "@remnic/import-chatgpt": "0.1.0",
82
- "@remnic/import-supermemory": "0.1.2",
79
+ "@remnic/import-weclone": "1.0.1",
80
+ "@remnic/import-claude": "0.1.0",
83
81
  "@remnic/import-gemini": "0.1.0",
84
- "@remnic/import-claude": "0.1.0"
82
+ "@remnic/import-lossless-claw": "0.1.1",
83
+ "@remnic/import-mem0": "0.1.0",
84
+ "@remnic/import-supermemory": "0.1.2"
85
85
  },
86
86
  "license": "MIT",
87
87
  "repository": {