@remnic/cli 1.0.15 → 1.0.17
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 +383 -8
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs7 from "fs";
|
|
3
3
|
import os from "os";
|
|
4
4
|
import path11 from "path";
|
|
5
|
-
import { createHash } from "crypto";
|
|
5
|
+
import { createDecipheriv, createHash } from "crypto";
|
|
6
6
|
import * as childProcess2 from "child_process";
|
|
7
7
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8
8
|
import {
|
|
@@ -95,8 +95,8 @@ import {
|
|
|
95
95
|
defaultOfflineSyncStatePath,
|
|
96
96
|
normalizeOfflineSyncSnapshot,
|
|
97
97
|
offlineSyncStateFromSnapshot,
|
|
98
|
+
readOfflineSyncFileContentChunk,
|
|
98
99
|
readOfflineSyncState,
|
|
99
|
-
summarizeOfflineSyncChangeset,
|
|
100
100
|
summarizeOfflineSyncPendingChanges,
|
|
101
101
|
writeOfflineSyncState,
|
|
102
102
|
buildActionConfidenceInputFromOptions,
|
|
@@ -106,7 +106,24 @@ import {
|
|
|
106
106
|
forkCapsule,
|
|
107
107
|
readForkLineage
|
|
108
108
|
} from "@remnic/core";
|
|
109
|
-
import {
|
|
109
|
+
import {
|
|
110
|
+
AUTH_TAG_LENGTH,
|
|
111
|
+
ENVELOPE_HEADER_SIZE,
|
|
112
|
+
ENVELOPE_LAYOUT,
|
|
113
|
+
ENVELOPE_SALT_LENGTH,
|
|
114
|
+
ENVELOPE_VERSION,
|
|
115
|
+
FILE_FORMAT_FLAGS,
|
|
116
|
+
FILE_FORMAT_VERSION,
|
|
117
|
+
IV_LENGTH,
|
|
118
|
+
MAGIC_BYTES,
|
|
119
|
+
MAGIC_HEADER_SIZE,
|
|
120
|
+
SecureStoreLockedError,
|
|
121
|
+
filePathAad,
|
|
122
|
+
isEncryptedFile,
|
|
123
|
+
keyring,
|
|
124
|
+
readHeader,
|
|
125
|
+
secureStoreDir
|
|
126
|
+
} from "@remnic/core/secure-store";
|
|
110
127
|
|
|
111
128
|
// src/optional-module-loader.ts
|
|
112
129
|
function isSpecifierNotFoundError(err, specifier) {
|
|
@@ -6728,6 +6745,41 @@ async function fetchOfflineFileContentChunk(args) {
|
|
|
6728
6745
|
content
|
|
6729
6746
|
};
|
|
6730
6747
|
}
|
|
6748
|
+
async function postOfflineFileContentChunk(args) {
|
|
6749
|
+
const response = await fetch(
|
|
6750
|
+
offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/apply-file-content", {
|
|
6751
|
+
namespace: args.namespace
|
|
6752
|
+
}),
|
|
6753
|
+
{
|
|
6754
|
+
method: "POST",
|
|
6755
|
+
headers: {
|
|
6756
|
+
authorization: `Bearer ${args.token}`,
|
|
6757
|
+
"content-type": "application/octet-stream",
|
|
6758
|
+
"x-remnic-include-transcripts": args.includeTranscripts ? "true" : "false",
|
|
6759
|
+
"x-remnic-source-id": encodeURIComponent(args.sourceId),
|
|
6760
|
+
"x-remnic-file-path": encodeURIComponent(args.file.path),
|
|
6761
|
+
"x-remnic-file-sha256": args.file.sha256,
|
|
6762
|
+
"x-remnic-file-bytes": String(args.file.bytes),
|
|
6763
|
+
"x-remnic-file-mtime-ms": String(args.file.mtimeMs),
|
|
6764
|
+
"x-remnic-chunk-offset": String(args.offset),
|
|
6765
|
+
...args.baseSha256 ? { "x-remnic-base-sha256": args.baseSha256 } : {}
|
|
6766
|
+
},
|
|
6767
|
+
body: new Blob([new Uint8Array(args.content)])
|
|
6768
|
+
}
|
|
6769
|
+
);
|
|
6770
|
+
if (!response.ok) {
|
|
6771
|
+
let detail = "";
|
|
6772
|
+
try {
|
|
6773
|
+
detail = await response.text();
|
|
6774
|
+
} catch {
|
|
6775
|
+
detail = "";
|
|
6776
|
+
}
|
|
6777
|
+
throw new Error(
|
|
6778
|
+
`offline sync apply-file-content request failed: ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 500)}` : ""}`
|
|
6779
|
+
);
|
|
6780
|
+
}
|
|
6781
|
+
return await response.json();
|
|
6782
|
+
}
|
|
6731
6783
|
function resolvedOfflineSnapshotNamespace(snapshot, requestedNamespace) {
|
|
6732
6784
|
const resolved = typeof snapshot.namespace === "string" && snapshot.namespace.trim().length > 0 ? snapshot.namespace.trim() : void 0;
|
|
6733
6785
|
return resolved ?? requestedNamespace;
|
|
@@ -6782,6 +6834,13 @@ function shouldDirectHydrateOfflineFile(options) {
|
|
|
6782
6834
|
}
|
|
6783
6835
|
return !options.current && !options.base;
|
|
6784
6836
|
}
|
|
6837
|
+
function offlineDirectPushFiles(options) {
|
|
6838
|
+
const base = offlineFileStateMap(options.baseFiles);
|
|
6839
|
+
return options.currentFiles.filter((current) => {
|
|
6840
|
+
if (current.bytes < OFFLINE_SYNC_DIRECT_HYDRATE_MIN_BYTES) return false;
|
|
6841
|
+
return current.sha256 !== base.get(current.path)?.sha256;
|
|
6842
|
+
}).sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path));
|
|
6843
|
+
}
|
|
6785
6844
|
function resolveOfflineDirectHydrationPath(memoryDir, relPath) {
|
|
6786
6845
|
const base = path11.resolve(memoryDir);
|
|
6787
6846
|
const target = path11.resolve(base, relPath);
|
|
@@ -6791,6 +6850,133 @@ function resolveOfflineDirectHydrationPath(memoryDir, relPath) {
|
|
|
6791
6850
|
}
|
|
6792
6851
|
return target;
|
|
6793
6852
|
}
|
|
6853
|
+
async function pushOfflineFileContent(args) {
|
|
6854
|
+
if (args.readFileChunks) {
|
|
6855
|
+
return pushOfflineFileContentFromChunkReader(args);
|
|
6856
|
+
}
|
|
6857
|
+
let offset = 0;
|
|
6858
|
+
let finalResult = null;
|
|
6859
|
+
while (offset < args.file.bytes || args.file.bytes === 0 && offset === 0) {
|
|
6860
|
+
const chunk = await readOfflineSyncFileContentChunk({
|
|
6861
|
+
root: args.memoryDir,
|
|
6862
|
+
path: args.file.path,
|
|
6863
|
+
offset,
|
|
6864
|
+
length: Math.min(
|
|
6865
|
+
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
6866
|
+
Math.max(1, args.file.bytes - offset)
|
|
6867
|
+
),
|
|
6868
|
+
includeTranscripts: args.includeTranscripts,
|
|
6869
|
+
readFile: args.readFile
|
|
6870
|
+
});
|
|
6871
|
+
if (chunk.path !== args.file.path || chunk.sha256 !== void 0 && chunk.sha256 !== args.file.sha256 || chunk.bytes !== args.file.bytes || chunk.mtimeMs !== args.file.mtimeMs || chunk.offset !== offset) {
|
|
6872
|
+
throw new Error(`local file changed while pushing offline content: ${args.file.path}`);
|
|
6873
|
+
}
|
|
6874
|
+
if (chunk.chunkBytes === 0 && args.file.bytes > 0) {
|
|
6875
|
+
throw new Error(`local offline content chunk was empty before EOF: ${args.file.path}`);
|
|
6876
|
+
}
|
|
6877
|
+
finalResult = await postOfflineFileContentChunk({
|
|
6878
|
+
remoteUrl: args.remoteUrl,
|
|
6879
|
+
token: args.token,
|
|
6880
|
+
namespace: args.namespace,
|
|
6881
|
+
includeTranscripts: args.includeTranscripts,
|
|
6882
|
+
sourceId: args.sourceId,
|
|
6883
|
+
file: args.file,
|
|
6884
|
+
baseSha256: args.baseSha256,
|
|
6885
|
+
offset,
|
|
6886
|
+
content: chunk.content
|
|
6887
|
+
});
|
|
6888
|
+
if (finalResult.conflict) {
|
|
6889
|
+
return finalResult;
|
|
6890
|
+
}
|
|
6891
|
+
offset += chunk.chunkBytes;
|
|
6892
|
+
if (args.file.bytes === 0) break;
|
|
6893
|
+
}
|
|
6894
|
+
if (!finalResult?.done) {
|
|
6895
|
+
throw new Error(`offline sync large-file push did not finish for ${args.file.path}`);
|
|
6896
|
+
}
|
|
6897
|
+
return finalResult;
|
|
6898
|
+
}
|
|
6899
|
+
async function pushOfflineFileContentFromChunkReader(args) {
|
|
6900
|
+
const filePath = resolveOfflineDirectHydrationPath(args.memoryDir, args.file.path);
|
|
6901
|
+
const stat = fs7.statSync(filePath);
|
|
6902
|
+
if (stat.mtimeMs !== args.file.mtimeMs) {
|
|
6903
|
+
throw new Error(`local file changed while pushing offline content: ${args.file.path}`);
|
|
6904
|
+
}
|
|
6905
|
+
const hash = createHash("sha256");
|
|
6906
|
+
const chunks = args.readFileChunks({
|
|
6907
|
+
root: path11.resolve(args.memoryDir),
|
|
6908
|
+
path: args.file.path,
|
|
6909
|
+
filePath,
|
|
6910
|
+
chunkSize: OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES
|
|
6911
|
+
});
|
|
6912
|
+
let offset = 0;
|
|
6913
|
+
let pending = null;
|
|
6914
|
+
let finalResult = null;
|
|
6915
|
+
for await (const rawChunk of chunks) {
|
|
6916
|
+
const chunk = Buffer.from(rawChunk);
|
|
6917
|
+
if (chunk.length === 0) continue;
|
|
6918
|
+
if (chunk.length > OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES) {
|
|
6919
|
+
throw new Error(`local offline content chunk exceeds max size: ${args.file.path}`);
|
|
6920
|
+
}
|
|
6921
|
+
if (pending) {
|
|
6922
|
+
hash.update(pending);
|
|
6923
|
+
finalResult = await postOfflineFileContentChunk({
|
|
6924
|
+
remoteUrl: args.remoteUrl,
|
|
6925
|
+
token: args.token,
|
|
6926
|
+
namespace: args.namespace,
|
|
6927
|
+
includeTranscripts: args.includeTranscripts,
|
|
6928
|
+
sourceId: args.sourceId,
|
|
6929
|
+
file: args.file,
|
|
6930
|
+
baseSha256: args.baseSha256,
|
|
6931
|
+
offset,
|
|
6932
|
+
content: pending
|
|
6933
|
+
});
|
|
6934
|
+
if (finalResult.conflict) {
|
|
6935
|
+
return finalResult;
|
|
6936
|
+
}
|
|
6937
|
+
offset += pending.length;
|
|
6938
|
+
}
|
|
6939
|
+
pending = chunk;
|
|
6940
|
+
}
|
|
6941
|
+
if (pending) hash.update(pending);
|
|
6942
|
+
const finalBytes = offset + (pending?.length ?? 0);
|
|
6943
|
+
const digest = hash.digest("hex");
|
|
6944
|
+
if (digest !== args.file.sha256 || finalBytes !== args.file.bytes) {
|
|
6945
|
+
throw new Error(`local file changed while pushing offline content: ${args.file.path}`);
|
|
6946
|
+
}
|
|
6947
|
+
if (pending) {
|
|
6948
|
+
finalResult = await postOfflineFileContentChunk({
|
|
6949
|
+
remoteUrl: args.remoteUrl,
|
|
6950
|
+
token: args.token,
|
|
6951
|
+
namespace: args.namespace,
|
|
6952
|
+
includeTranscripts: args.includeTranscripts,
|
|
6953
|
+
sourceId: args.sourceId,
|
|
6954
|
+
file: args.file,
|
|
6955
|
+
baseSha256: args.baseSha256,
|
|
6956
|
+
offset,
|
|
6957
|
+
content: pending
|
|
6958
|
+
});
|
|
6959
|
+
if (finalResult.conflict) {
|
|
6960
|
+
return finalResult;
|
|
6961
|
+
}
|
|
6962
|
+
} else if (args.file.bytes === 0) {
|
|
6963
|
+
finalResult = await postOfflineFileContentChunk({
|
|
6964
|
+
remoteUrl: args.remoteUrl,
|
|
6965
|
+
token: args.token,
|
|
6966
|
+
namespace: args.namespace,
|
|
6967
|
+
includeTranscripts: args.includeTranscripts,
|
|
6968
|
+
sourceId: args.sourceId,
|
|
6969
|
+
file: args.file,
|
|
6970
|
+
baseSha256: args.baseSha256,
|
|
6971
|
+
offset: 0,
|
|
6972
|
+
content: Buffer.alloc(0)
|
|
6973
|
+
});
|
|
6974
|
+
}
|
|
6975
|
+
if (!finalResult?.done) {
|
|
6976
|
+
throw new Error(`offline sync large-file push did not finish for ${args.file.path}`);
|
|
6977
|
+
}
|
|
6978
|
+
return finalResult;
|
|
6979
|
+
}
|
|
6794
6980
|
async function fetchOfflineFileContent(args) {
|
|
6795
6981
|
const chunks = [];
|
|
6796
6982
|
const hash = createHash("sha256");
|
|
@@ -6978,19 +7164,137 @@ function waitForOfflineInterval(ms, setCancel) {
|
|
|
6978
7164
|
async function createOfflineStorageIo(memoryDir) {
|
|
6979
7165
|
const storage = new StorageManager(memoryDir);
|
|
6980
7166
|
const header = await readHeader(memoryDir);
|
|
7167
|
+
let secureStoreKey = null;
|
|
6981
7168
|
if (header) {
|
|
6982
7169
|
storage.setSecureStoreRequired(true);
|
|
6983
7170
|
const key = keyring.getKey(secureStoreDir(memoryDir));
|
|
6984
7171
|
if (key) {
|
|
6985
7172
|
storage.setSecureStoreKey(key);
|
|
7173
|
+
secureStoreKey = key;
|
|
6986
7174
|
}
|
|
6987
7175
|
}
|
|
6988
7176
|
return {
|
|
6989
7177
|
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
7178
|
+
readFileChunks: ({ filePath, chunkSize }) => readOfflineSyncFileChunks({
|
|
7179
|
+
filePath,
|
|
7180
|
+
memoryDir,
|
|
7181
|
+
secureStoreKey,
|
|
7182
|
+
chunkSize
|
|
7183
|
+
}),
|
|
6990
7184
|
writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
|
|
6991
7185
|
deleteFile: async ({ filePath }) => storage.deleteOfflineSyncFile(filePath)
|
|
6992
7186
|
};
|
|
6993
7187
|
}
|
|
7188
|
+
async function* readOfflineSyncFileChunks(options) {
|
|
7189
|
+
const header = await readFilePrefix(options.filePath, MAGIC_HEADER_SIZE);
|
|
7190
|
+
if (!isEncryptedFile(header)) {
|
|
7191
|
+
yield* readPlainOfflineFileChunks(options.filePath, options.chunkSize);
|
|
7192
|
+
return;
|
|
7193
|
+
}
|
|
7194
|
+
if (!options.secureStoreKey) {
|
|
7195
|
+
throw new SecureStoreLockedError(
|
|
7196
|
+
`secure-store is locked \u2014 cannot read encrypted file at ${options.filePath}. Run \`remnic secure-store unlock\` to decrypt.`
|
|
7197
|
+
);
|
|
7198
|
+
}
|
|
7199
|
+
yield* readEncryptedOfflineFileChunks({
|
|
7200
|
+
filePath: options.filePath,
|
|
7201
|
+
memoryDir: options.memoryDir,
|
|
7202
|
+
key: options.secureStoreKey,
|
|
7203
|
+
chunkSize: options.chunkSize
|
|
7204
|
+
});
|
|
7205
|
+
}
|
|
7206
|
+
async function readFilePrefix(filePath, length) {
|
|
7207
|
+
const handle = await fs7.promises.open(filePath, "r");
|
|
7208
|
+
try {
|
|
7209
|
+
const out = Buffer.alloc(length);
|
|
7210
|
+
const { bytesRead } = await handle.read(out, 0, length, 0);
|
|
7211
|
+
return out.subarray(0, bytesRead);
|
|
7212
|
+
} finally {
|
|
7213
|
+
await handle.close();
|
|
7214
|
+
}
|
|
7215
|
+
}
|
|
7216
|
+
async function* readPlainOfflineFileChunks(filePath, chunkSize) {
|
|
7217
|
+
const stream = fs7.createReadStream(filePath, { highWaterMark: chunkSize });
|
|
7218
|
+
for await (const chunk of stream) {
|
|
7219
|
+
yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
7222
|
+
async function* readEncryptedOfflineFileChunks(options) {
|
|
7223
|
+
const header = await readFilePrefix(options.filePath, MAGIC_HEADER_SIZE + ENVELOPE_HEADER_SIZE);
|
|
7224
|
+
if (header.length < MAGIC_HEADER_SIZE + ENVELOPE_HEADER_SIZE || !isEncryptedFile(header)) {
|
|
7225
|
+
throw new Error(`secure-store encrypted file is truncated: ${options.filePath}`);
|
|
7226
|
+
}
|
|
7227
|
+
const version = header.readUInt8(MAGIC_BYTES.length);
|
|
7228
|
+
const flags = header.readUInt8(MAGIC_BYTES.length + 1);
|
|
7229
|
+
if (version !== FILE_FORMAT_VERSION) {
|
|
7230
|
+
throw new Error(`secure-store file has unsupported version ${version}: ${options.filePath}`);
|
|
7231
|
+
}
|
|
7232
|
+
if (flags !== FILE_FORMAT_FLAGS) {
|
|
7233
|
+
throw new Error(`secure-store file has unsupported flags 0x${flags.toString(16)}: ${options.filePath}`);
|
|
7234
|
+
}
|
|
7235
|
+
const envelopeHeader = header.subarray(MAGIC_HEADER_SIZE);
|
|
7236
|
+
const envelopeVersion = envelopeHeader.readUInt8(ENVELOPE_LAYOUT.version);
|
|
7237
|
+
if (envelopeVersion !== ENVELOPE_VERSION) {
|
|
7238
|
+
throw new Error(`secure-store envelope has unsupported version ${envelopeVersion}: ${options.filePath}`);
|
|
7239
|
+
}
|
|
7240
|
+
const salt = envelopeHeader.subarray(
|
|
7241
|
+
ENVELOPE_LAYOUT.salt,
|
|
7242
|
+
ENVELOPE_LAYOUT.salt + ENVELOPE_SALT_LENGTH
|
|
7243
|
+
);
|
|
7244
|
+
const iv = envelopeHeader.subarray(ENVELOPE_LAYOUT.iv, ENVELOPE_LAYOUT.iv + IV_LENGTH);
|
|
7245
|
+
const authTag = envelopeHeader.subarray(
|
|
7246
|
+
ENVELOPE_LAYOUT.authTag,
|
|
7247
|
+
ENVELOPE_LAYOUT.authTag + AUTH_TAG_LENGTH
|
|
7248
|
+
);
|
|
7249
|
+
const decipher = createDecipheriv("aes-256-gcm", options.key, iv, {
|
|
7250
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
7251
|
+
});
|
|
7252
|
+
decipher.setAuthTag(authTag);
|
|
7253
|
+
decipher.setAAD(Buffer.concat([secureStoreEnvelopeHeaderAad(salt), filePathAad(options.filePath, options.memoryDir)]));
|
|
7254
|
+
let pending = Buffer.alloc(0);
|
|
7255
|
+
const stream = fs7.createReadStream(options.filePath, {
|
|
7256
|
+
start: MAGIC_HEADER_SIZE + ENVELOPE_HEADER_SIZE,
|
|
7257
|
+
highWaterMark: options.chunkSize
|
|
7258
|
+
});
|
|
7259
|
+
for await (const encryptedChunk of stream) {
|
|
7260
|
+
const plain = decipher.update(Buffer.isBuffer(encryptedChunk) ? encryptedChunk : Buffer.from(encryptedChunk));
|
|
7261
|
+
if (plain.length > 0) {
|
|
7262
|
+
pending = Buffer.concat([pending, plain], pending.length + plain.length);
|
|
7263
|
+
}
|
|
7264
|
+
while (pending.length >= options.chunkSize) {
|
|
7265
|
+
yield pending.subarray(0, options.chunkSize);
|
|
7266
|
+
pending = pending.subarray(options.chunkSize);
|
|
7267
|
+
}
|
|
7268
|
+
}
|
|
7269
|
+
const finalPlain = decipher.final();
|
|
7270
|
+
if (finalPlain.length > 0) {
|
|
7271
|
+
pending = Buffer.concat([pending, finalPlain], pending.length + finalPlain.length);
|
|
7272
|
+
}
|
|
7273
|
+
while (pending.length >= options.chunkSize) {
|
|
7274
|
+
yield pending.subarray(0, options.chunkSize);
|
|
7275
|
+
pending = pending.subarray(options.chunkSize);
|
|
7276
|
+
}
|
|
7277
|
+
if (pending.length > 0) yield pending;
|
|
7278
|
+
}
|
|
7279
|
+
function secureStoreEnvelopeHeaderAad(salt) {
|
|
7280
|
+
const out = Buffer.alloc(1 + ENVELOPE_SALT_LENGTH);
|
|
7281
|
+
out.writeUInt8(ENVELOPE_VERSION, 0);
|
|
7282
|
+
Buffer.from(salt).copy(out, 1);
|
|
7283
|
+
return out;
|
|
7284
|
+
}
|
|
7285
|
+
function formatOfflineLargeFilePushFailureMessage(failures) {
|
|
7286
|
+
const paths = failures.slice(0, 5).map((failure) => `${failure.path}: ${failure.error}`).join("; ");
|
|
7287
|
+
const suffix = failures.length > 5 ? `; +${failures.length - 5} more` : "";
|
|
7288
|
+
return `offline sync large-file push failed for ${failures.length} file${failures.length === 1 ? "" : "s"}: ${paths}${suffix}`;
|
|
7289
|
+
}
|
|
7290
|
+
var OfflineLargeFilePushError = class extends Error {
|
|
7291
|
+
failures;
|
|
7292
|
+
constructor(failures) {
|
|
7293
|
+
super(formatOfflineLargeFilePushFailureMessage(failures));
|
|
7294
|
+
this.name = "OfflineLargeFilePushError";
|
|
7295
|
+
this.failures = failures;
|
|
7296
|
+
}
|
|
7297
|
+
};
|
|
6994
7298
|
async function runOfflineSyncOnce(options) {
|
|
6995
7299
|
fs7.mkdirSync(options.memoryDir, { recursive: true });
|
|
6996
7300
|
let activeStatePath = options.statePath;
|
|
@@ -7030,20 +7334,90 @@ async function runOfflineSyncOnce(options) {
|
|
|
7030
7334
|
}
|
|
7031
7335
|
const baseFiles = priorState?.baseFiles ?? [];
|
|
7032
7336
|
const storageIo = await createOfflineStorageIo(options.memoryDir);
|
|
7337
|
+
const localSourceId = localOfflineSourceId(options.memoryDir);
|
|
7338
|
+
const pendingSummary = await summarizeOfflineSyncPendingChanges({
|
|
7339
|
+
root: options.memoryDir,
|
|
7340
|
+
sourceId: localSourceId,
|
|
7341
|
+
baseFiles,
|
|
7342
|
+
includeTranscripts: options.includeTranscripts,
|
|
7343
|
+
readFile: storageIo.readFile
|
|
7344
|
+
});
|
|
7345
|
+
const currentSnapshotForPush = await buildOfflineSyncSnapshot({
|
|
7346
|
+
root: options.memoryDir,
|
|
7347
|
+
sourceId: localSourceId,
|
|
7348
|
+
includeContent: false,
|
|
7349
|
+
includeTranscripts: options.includeTranscripts,
|
|
7350
|
+
readFile: storageIo.readFile
|
|
7351
|
+
});
|
|
7352
|
+
const baseByPath = offlineFileStateMap(baseFiles);
|
|
7353
|
+
let directPushAppliedUpserts = 0;
|
|
7354
|
+
let directPushSkipped = 0;
|
|
7355
|
+
let directPushNamespace;
|
|
7356
|
+
const directPushConflicts = [];
|
|
7357
|
+
const directPushedPaths = /* @__PURE__ */ new Set();
|
|
7358
|
+
const directPushFailures = [];
|
|
7359
|
+
for (const file of offlineDirectPushFiles({
|
|
7360
|
+
currentFiles: currentSnapshotForPush.files,
|
|
7361
|
+
baseFiles
|
|
7362
|
+
})) {
|
|
7363
|
+
let result;
|
|
7364
|
+
try {
|
|
7365
|
+
result = await pushOfflineFileContent({
|
|
7366
|
+
remoteUrl: options.remoteUrl,
|
|
7367
|
+
token: options.token,
|
|
7368
|
+
namespace: syncNamespace,
|
|
7369
|
+
includeTranscripts: options.includeTranscripts,
|
|
7370
|
+
memoryDir: options.memoryDir,
|
|
7371
|
+
sourceId: localSourceId,
|
|
7372
|
+
file,
|
|
7373
|
+
baseSha256: baseByPath.get(file.path)?.sha256,
|
|
7374
|
+
readFile: storageIo.readFile,
|
|
7375
|
+
readFileChunks: storageIo.readFileChunks
|
|
7376
|
+
});
|
|
7377
|
+
} catch (error) {
|
|
7378
|
+
directPushFailures.push({
|
|
7379
|
+
path: file.path,
|
|
7380
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7381
|
+
});
|
|
7382
|
+
continue;
|
|
7383
|
+
}
|
|
7384
|
+
directPushNamespace = result.namespace ?? directPushNamespace;
|
|
7385
|
+
directPushedPaths.add(file.path);
|
|
7386
|
+
if (result.conflict) {
|
|
7387
|
+
directPushConflicts.push({
|
|
7388
|
+
path: result.conflict.path,
|
|
7389
|
+
reason: result.conflict.reason,
|
|
7390
|
+
...result.conflict.conflictPath ? { conflictPath: result.conflict.conflictPath } : {}
|
|
7391
|
+
});
|
|
7392
|
+
} else {
|
|
7393
|
+
if (result.applied) directPushAppliedUpserts += 1;
|
|
7394
|
+
if (result.skipped) directPushSkipped += 1;
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
if (directPushFailures.length > 0) {
|
|
7398
|
+
throw new OfflineLargeFilePushError(directPushFailures);
|
|
7399
|
+
}
|
|
7033
7400
|
const changeset = await buildOfflineSyncChangeset({
|
|
7034
7401
|
root: options.memoryDir,
|
|
7035
|
-
sourceId:
|
|
7402
|
+
sourceId: localSourceId,
|
|
7036
7403
|
baseFiles,
|
|
7404
|
+
excludePaths: [...directPushedPaths],
|
|
7037
7405
|
includeTranscripts: options.includeTranscripts,
|
|
7038
7406
|
readFile: storageIo.readFile
|
|
7039
7407
|
});
|
|
7040
|
-
const
|
|
7041
|
-
const pushed = changeset.changes.length > 0 ? await pushOfflineChanges({
|
|
7408
|
+
const pushedInline = changeset.changes.length > 0 ? await pushOfflineChanges({
|
|
7042
7409
|
remoteUrl: options.remoteUrl,
|
|
7043
7410
|
token: options.token,
|
|
7044
7411
|
namespace: syncNamespace,
|
|
7045
7412
|
changeset
|
|
7046
7413
|
}) : null;
|
|
7414
|
+
const pushed = directPushedPaths.size > 0 || pushedInline ? {
|
|
7415
|
+
namespace: pushedInline?.namespace ?? directPushNamespace ?? syncNamespace ?? "",
|
|
7416
|
+
appliedUpserts: (pushedInline?.appliedUpserts ?? 0) + directPushAppliedUpserts,
|
|
7417
|
+
appliedDeletes: pushedInline?.appliedDeletes ?? 0,
|
|
7418
|
+
skipped: (pushedInline?.skipped ?? 0) + directPushSkipped,
|
|
7419
|
+
conflicts: [...directPushConflicts, ...pushedInline?.conflicts ?? []]
|
|
7420
|
+
} : null;
|
|
7047
7421
|
const remoteSnapshotMetadata = await fetchOfflineSnapshot({
|
|
7048
7422
|
remoteUrl: options.remoteUrl,
|
|
7049
7423
|
token: options.token,
|
|
@@ -7053,7 +7427,7 @@ async function runOfflineSyncOnce(options) {
|
|
|
7053
7427
|
});
|
|
7054
7428
|
const currentSnapshot = await buildOfflineSyncSnapshot({
|
|
7055
7429
|
root: options.memoryDir,
|
|
7056
|
-
sourceId:
|
|
7430
|
+
sourceId: localSourceId,
|
|
7057
7431
|
includeContent: false,
|
|
7058
7432
|
includeTranscripts: options.includeTranscripts,
|
|
7059
7433
|
readFile: storageIo.readFile
|
|
@@ -7071,7 +7445,7 @@ async function runOfflineSyncOnce(options) {
|
|
|
7071
7445
|
});
|
|
7072
7446
|
const applyCurrentSnapshot = directHydratedPaths.size > 0 ? await buildOfflineSyncSnapshot({
|
|
7073
7447
|
root: options.memoryDir,
|
|
7074
|
-
sourceId:
|
|
7448
|
+
sourceId: localSourceId,
|
|
7075
7449
|
includeContent: false,
|
|
7076
7450
|
includeTranscripts: options.includeTranscripts,
|
|
7077
7451
|
readFile: storageIo.readFile
|
|
@@ -10238,6 +10612,7 @@ export {
|
|
|
10238
10612
|
buildQueryRecallRequest,
|
|
10239
10613
|
chunkOfflineFileContentBatches,
|
|
10240
10614
|
extractXrayRawArgs,
|
|
10615
|
+
formatOfflineLargeFilePushFailureMessage,
|
|
10241
10616
|
getBenchUsageText,
|
|
10242
10617
|
hasFlag,
|
|
10243
10618
|
main,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
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.
|
|
30
|
-
"@remnic/
|
|
31
|
-
"@remnic/
|
|
29
|
+
"@remnic/plugin-pi": "^1.0.3",
|
|
30
|
+
"@remnic/core": "^1.1.23",
|
|
31
|
+
"@remnic/server": "^1.0.5"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@remnic/bench": "^1.0.0",
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"@remnic/export-weclone": "1.0.1",
|
|
78
78
|
"@remnic/import-weclone": "1.0.1",
|
|
79
79
|
"@remnic/import-chatgpt": "0.1.0",
|
|
80
|
-
"@remnic/import-claude": "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"
|