@remnic/cli 9.69.16 → 9.69.18

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 +106 -58
  2. package/package.json +32 -32
package/dist/index.js CHANGED
@@ -1401,6 +1401,8 @@ import {
1401
1401
  CONVERGE_CONFLICT_POLICIES,
1402
1402
  DEFAULT_CONVERGE_CONFLICT_POLICY,
1403
1403
  parseConfig as parseConfig13,
1404
+ envConvergePeerRequestTimeoutMs as envConvergePeerRequestTimeoutMs2,
1405
+ normalizeConvergePeerRequestTimeoutMs,
1404
1406
  buildOfflineSyncSnapshotFromBase,
1405
1407
  applyOfflineSyncFileContentChunk,
1406
1408
  isInternalRemnicStatePath as isInternalRemnicStatePath3,
@@ -1921,6 +1923,7 @@ async function parsePeerManifestStream(response, expectedNamespace) {
1921
1923
  }
1922
1924
 
1923
1925
  // src/converge-peer-transport.ts
1926
+ import { envConvergePeerRequestTimeoutMs } from "@remnic/core";
1924
1927
  var DEFAULT_PEER_REQUEST_TIMEOUT_MS = 3e4;
1925
1928
  function normalizePeerBaseUrl(peerUrl) {
1926
1929
  const normalized = normalizeConvergePeerUrl(peerUrl);
@@ -1943,10 +1946,7 @@ async function fetchPeerRequest(fetchImpl, input, init, timeoutMs) {
1943
1946
  async function fetchPeerSyncCapabilities(peerUrl, token, fetchImpl, timeoutMs) {
1944
1947
  const base = normalizePeerBaseUrl(peerUrl);
1945
1948
  const headers = token ? { authorization: `Bearer ${token}` } : {};
1946
- const routes = [
1947
- "/remnic/v1/offline-sync/capabilities",
1948
- "/engram/v1/offline-sync/capabilities"
1949
- ];
1949
+ const routes = ["/remnic/v1/offline-sync/capabilities", "/engram/v1/offline-sync/capabilities"];
1950
1950
  for (const route of routes) {
1951
1951
  const response = await fetchPeerRequest(fetchImpl, `${base}${route}`, { headers }, timeoutMs);
1952
1952
  if (response.status === 404 || response.status === 405) continue;
@@ -1960,9 +1960,11 @@ async function fetchPeerSyncCapabilities(peerUrl, token, fetchImpl, timeoutMs) {
1960
1960
  if (!payload || typeof payload !== "object" || !("convergenceFinalization" in payload) || typeof payload.convergenceFinalization !== "boolean" || !("manifestStream" in payload) || typeof payload.manifestStream !== "boolean") {
1961
1961
  throw new Error("peer capability response was malformed");
1962
1962
  }
1963
+ const platform = "platform" in payload && typeof payload.platform === "string" ? payload.platform : void 0;
1963
1964
  return {
1964
1965
  convergenceFinalization: payload.convergenceFinalization,
1965
- manifestStream: payload.manifestStream
1966
+ manifestStream: payload.manifestStream,
1967
+ ...platform !== void 0 ? { platform } : {}
1966
1968
  };
1967
1969
  }
1968
1970
  return null;
@@ -2051,10 +2053,7 @@ function requiredResponseNumber(response, name) {
2051
2053
  async function streamPeerFileContent(peerUrl, namespace, filePath, onChunk, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS) {
2052
2054
  assertTransferablePeerPath(filePath);
2053
2055
  const base = normalizePeerBaseUrl(peerUrl);
2054
- const routes = [
2055
- "/remnic/v1/offline-sync/file-content",
2056
- "/engram/v1/offline-sync/file-content"
2057
- ];
2056
+ const routes = ["/remnic/v1/offline-sync/file-content", "/engram/v1/offline-sync/file-content"];
2058
2057
  const headers = {
2059
2058
  "content-type": "application/json",
2060
2059
  ...token ? { authorization: `Bearer ${token}` } : {}
@@ -2069,17 +2068,22 @@ async function streamPeerFileContent(peerUrl, namespace, filePath, onChunk, toke
2069
2068
  do {
2070
2069
  let response;
2071
2070
  try {
2072
- response = await fetchPeerRequest(fetchImpl, `${base}${route}`, {
2073
- method: "POST",
2074
- headers,
2075
- body: JSON.stringify({
2076
- namespace,
2077
- includeTranscripts: false,
2078
- path: filePath,
2079
- offset,
2080
- length: OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES
2081
- })
2082
- }, timeoutMs);
2071
+ response = await fetchPeerRequest(
2072
+ fetchImpl,
2073
+ `${base}${route}`,
2074
+ {
2075
+ method: "POST",
2076
+ headers,
2077
+ body: JSON.stringify({
2078
+ namespace,
2079
+ includeTranscripts: false,
2080
+ path: filePath,
2081
+ offset,
2082
+ length: OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES
2083
+ })
2084
+ },
2085
+ timeoutMs
2086
+ );
2083
2087
  if (!response.ok) throw new Error(`offline file content request failed: ${response.status}`);
2084
2088
  } catch {
2085
2089
  routeFailed = true;
@@ -2177,11 +2181,16 @@ async function postPeerFileContent(peerUrl, namespace, filePath, source, token,
2177
2181
  };
2178
2182
  let response;
2179
2183
  try {
2180
- response = await fetchPeerRequest(fetchImpl, `${base}${route}`, {
2181
- method: "POST",
2182
- headers,
2183
- body: new Uint8Array(chunk)
2184
- }, timeoutMs);
2184
+ response = await fetchPeerRequest(
2185
+ fetchImpl,
2186
+ `${base}${route}`,
2187
+ {
2188
+ method: "POST",
2189
+ headers,
2190
+ body: new Uint8Array(chunk)
2191
+ },
2192
+ timeoutMs
2193
+ );
2185
2194
  if (!response.ok) throw new Error(`offline apply-file-content request failed: ${response.status}`);
2186
2195
  } catch {
2187
2196
  if (previousAttemptFailed && offset > 0 && !restartedRoute) {
@@ -2209,18 +2218,20 @@ async function postPeerFileContent(peerUrl, namespace, filePath, source, token,
2209
2218
  async function postPeerConvergenceComplete(peerUrl, namespaces, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS) {
2210
2219
  const base = normalizePeerBaseUrl(peerUrl);
2211
2220
  const query = namespaces.map((namespace) => `namespace=${encodeURIComponent(namespace)}`).join("&");
2212
- const routes = [
2213
- "/remnic/v1/offline-sync/convergence-complete",
2214
- "/engram/v1/offline-sync/convergence-complete"
2215
- ];
2221
+ const routes = ["/remnic/v1/offline-sync/convergence-complete", "/engram/v1/offline-sync/convergence-complete"];
2216
2222
  for (const route of routes) {
2217
- const response = await fetchPeerRequest(fetchImpl, `${base}${route}?${query}`, {
2218
- method: "POST",
2219
- headers: {
2220
- "x-remnic-source-id": encodeURIComponent("remnic-converge"),
2221
- ...token ? { authorization: `Bearer ${token}` } : {}
2222
- }
2223
- }, timeoutMs).catch(() => null);
2223
+ const response = await fetchPeerRequest(
2224
+ fetchImpl,
2225
+ `${base}${route}?${query}`,
2226
+ {
2227
+ method: "POST",
2228
+ headers: {
2229
+ "x-remnic-source-id": encodeURIComponent("remnic-converge"),
2230
+ ...token ? { authorization: `Bearer ${token}` } : {}
2231
+ }
2232
+ },
2233
+ timeoutMs
2234
+ ).catch(() => null);
2224
2235
  if (!response?.ok) continue;
2225
2236
  const result = await response.json().catch(() => null);
2226
2237
  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) {
@@ -2240,21 +2251,26 @@ async function postPeerFileDeletion(peerUrl, namespace, filePath, baseSha256, to
2240
2251
  let previousAttemptFailed = false;
2241
2252
  for (const route of routes) {
2242
2253
  try {
2243
- const response = await fetchPeerRequest(fetchImpl, `${base}${route}`, {
2244
- method: "POST",
2245
- headers,
2246
- body: JSON.stringify({
2247
- namespace,
2248
- changeset: {
2249
- format: OFFLINE_SYNC_CHANGESET_FORMAT,
2250
- schemaVersion: 1,
2251
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2252
- sourceId: "remnic-converge",
2253
- includeTranscripts: false,
2254
- changes: [{ type: "delete", path: filePath, baseSha256 }]
2255
- }
2256
- })
2257
- }, timeoutMs);
2254
+ const response = await fetchPeerRequest(
2255
+ fetchImpl,
2256
+ `${base}${route}`,
2257
+ {
2258
+ method: "POST",
2259
+ headers,
2260
+ body: JSON.stringify({
2261
+ namespace,
2262
+ changeset: {
2263
+ format: OFFLINE_SYNC_CHANGESET_FORMAT,
2264
+ schemaVersion: 1,
2265
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2266
+ sourceId: "remnic-converge",
2267
+ includeTranscripts: false,
2268
+ changes: [{ type: "delete", path: filePath, baseSha256 }]
2269
+ }
2270
+ })
2271
+ },
2272
+ timeoutMs
2273
+ );
2258
2274
  if (!response.ok) throw new Error(`offline apply request failed: ${response.status}`);
2259
2275
  const result = await response.json().catch(() => null);
2260
2276
  if (!result || typeof result !== "object" || !("appliedDeletes" in result) || typeof result.appliedDeletes !== "number" || !("skipped" in result) || typeof result.skipped !== "number" || !("conflicts" in result) || !Array.isArray(result.conflicts) || result.conflicts.length > 0) {
@@ -2348,6 +2364,7 @@ async function computeConvergePlan(options = {}) {
2348
2364
  const peerDeletionMtimeMs = /* @__PURE__ */ new Map();
2349
2365
  const localManifests = /* @__PURE__ */ new Map();
2350
2366
  const peerManifests = /* @__PURE__ */ new Map();
2367
+ let peerPlatform;
2351
2368
  if (options.baseFilesByNamespace) {
2352
2369
  for (const [ns, files] of options.baseFilesByNamespace) {
2353
2370
  namespacesToPlan.add(ns);
@@ -2485,8 +2502,9 @@ async function computeConvergePlan(options = {}) {
2485
2502
  }
2486
2503
  }
2487
2504
  const fetchFn = options.fetchImpl ?? globalThis.fetch;
2488
- const timeoutMs = options.peerRequestTimeoutMs ?? DEFAULT_PEER_REQUEST_TIMEOUT_MS;
2505
+ const timeoutMs = options.peerRequestTimeoutMs ?? config?.converge.peerRequestTimeoutMs ?? envConvergePeerRequestTimeoutMs2() ?? DEFAULT_PEER_REQUEST_TIMEOUT_MS;
2489
2506
  const capabilities = await fetchPeerSyncCapabilities(peerUrl, resolvedToken, fetchFn, timeoutMs);
2507
+ peerPlatform = capabilities?.platform;
2490
2508
  for (const ns of namespacesToPlan) {
2491
2509
  const peerData = await fetchPeerSnapshot(peerUrl, ns, resolvedToken, fetchFn, timeoutMs);
2492
2510
  const streamedManifest = capabilities?.manifestStream ? await fetchPeerManifestStream(peerUrl, ns, resolvedToken, fetchFn, timeoutMs) : null;
@@ -2523,9 +2541,16 @@ async function computeConvergePlan(options = {}) {
2523
2541
  const state = peerFiles.find((file) => file.path === tombstonePath);
2524
2542
  if (!state) continue;
2525
2543
  const remote = await fetchPeerFileContent(peerUrl, ns, tombstonePath, resolvedToken, fetchFn, timeoutMs);
2526
- if (!remote || remote.sha256.toLowerCase() !== state.sha256.toLowerCase()) {
2544
+ if (!remote) {
2527
2545
  throw new Error(`failed to read peer tombstone evidence: ${tombstonePath}`);
2528
2546
  }
2547
+ if (remote.sha256.toLowerCase() !== state.sha256.toLowerCase()) {
2548
+ const listedBytes = typeof state.bytes === "number" ? state.bytes : -1;
2549
+ const prefixMatches = listedBytes >= 0 && remote.content.length >= listedBytes && createHash3("sha256").update(remote.content.subarray(0, listedBytes)).digest("hex") === state.sha256.toLowerCase();
2550
+ if (!prefixMatches) {
2551
+ throw new Error(`failed to read peer tombstone evidence: ${tombstonePath}`);
2552
+ }
2553
+ }
2529
2554
  const parsed = parseTombstoneEvidence(remote.content.toString("utf8"));
2530
2555
  for (const value of parsed.contentHashes) evidence.contentHashes.add(value);
2531
2556
  for (const value of parsed.fileSha256) evidence.fileSha256.add(value);
@@ -2574,7 +2599,7 @@ async function computeConvergePlan(options = {}) {
2574
2599
  });
2575
2600
  }
2576
2601
  const conflictPolicy = options.conflictPolicy ?? config?.converge.conflictPolicy ?? DEFAULT_CONVERGE_CONFLICT_POLICY;
2577
- const plan = planReconciliation(inputs, { conflictPolicy });
2602
+ const plan = planReconciliation(inputs, { conflictPolicy, peerPlatform });
2578
2603
  return collapseActiveFactDuplicates(plan, localManifests, peerManifests, semanticAgreementMap);
2579
2604
  }
2580
2605
  async function executeConvergeApply(options = {}) {
@@ -2640,8 +2665,6 @@ async function executeConvergeApply(options = {}) {
2640
2665
  resolvedToken = options.peerToken;
2641
2666
  }
2642
2667
  }
2643
- const fetchFn = options.fetchImpl ?? globalThis.fetch;
2644
- const timeoutMs = options.peerRequestTimeoutMs ?? DEFAULT_PEER_REQUEST_TIMEOUT_MS;
2645
2668
  let config = options.config;
2646
2669
  if (!config) {
2647
2670
  try {
@@ -2649,6 +2672,8 @@ async function executeConvergeApply(options = {}) {
2649
2672
  } catch {
2650
2673
  }
2651
2674
  }
2675
+ const fetchFn = options.fetchImpl ?? globalThis.fetch;
2676
+ const timeoutMs = options.peerRequestTimeoutMs ?? config?.converge.peerRequestTimeoutMs ?? envConvergePeerRequestTimeoutMs2() ?? DEFAULT_PEER_REQUEST_TIMEOUT_MS;
2652
2677
  const rootMap = /* @__PURE__ */ new Map();
2653
2678
  if (config) {
2654
2679
  try {
@@ -3092,6 +3117,9 @@ Options:
3092
3117
  Default: converge.conflictPolicy (newest-wins)
3093
3118
  --interval <seconds>
3094
3119
  Watch cadence in seconds (watch only; default 300, min 1)
3120
+ --timeout <seconds>
3121
+ Per-request peer HTTP timeout (default 30; use 300+ for
3122
+ boot-scale namespaces of ~100k files)
3095
3123
  --dry-run Simulate transfers without mutating disk or remote peer
3096
3124
  --json Output detailed JSON plan report
3097
3125
  `);
@@ -3108,6 +3136,7 @@ Options:
3108
3136
  let dryRun = false;
3109
3137
  let conflictPolicy;
3110
3138
  let intervalSeconds;
3139
+ let timeoutSeconds;
3111
3140
  for (let i = 0; i < rest.length; i += 1) {
3112
3141
  const arg = rest[i];
3113
3142
  if ((arg === "--peer" || arg === "--remote-url" || arg === "--remote") && rest[i + 1]) {
@@ -3128,6 +3157,17 @@ Options:
3128
3157
  }
3129
3158
  intervalSeconds = parsed;
3130
3159
  i += 1;
3160
+ } else if (arg === "--timeout") {
3161
+ const raw = rest[i + 1];
3162
+ const parsed = raw === void 0 ? Number.NaN : Number(raw);
3163
+ try {
3164
+ timeoutSeconds = normalizeConvergePeerRequestTimeoutMs(parsed, "--timeout") / 1e3;
3165
+ } catch {
3166
+ process.stderr.write("converge: --timeout must be a positive number of seconds.\n");
3167
+ process.exitCode = 2;
3168
+ return;
3169
+ }
3170
+ i += 1;
3131
3171
  } else if (arg === "--conflict-policy") {
3132
3172
  const policy = rest[i + 1];
3133
3173
  if (typeof policy !== "string" || !CONVERGE_CONFLICT_POLICIES.includes(policy)) {
@@ -3152,6 +3192,7 @@ Options:
3152
3192
  peerToken,
3153
3193
  conflictPolicy,
3154
3194
  intervalMs: intervalSeconds !== void 0 ? intervalSeconds * 1e3 : void 0,
3195
+ peerRequestTimeoutMs: timeoutSeconds !== void 0 ? timeoutSeconds * 1e3 : void 0,
3155
3196
  signal: controller.signal,
3156
3197
  onCycle: json ? void 0 : (cycle, event) => {
3157
3198
  if (event.error !== void 0) {
@@ -3183,7 +3224,13 @@ Options:
3183
3224
  return;
3184
3225
  }
3185
3226
  if (action === "plan") {
3186
- const plan = await computeConvergePlan({ config, peerUrl, peerToken, conflictPolicy });
3227
+ const plan = await computeConvergePlan({
3228
+ config,
3229
+ peerUrl,
3230
+ peerToken,
3231
+ conflictPolicy,
3232
+ ...timeoutSeconds !== void 0 ? { peerRequestTimeoutMs: timeoutSeconds * 1e3 } : {}
3233
+ });
3187
3234
  if (json) {
3188
3235
  console.log(JSON.stringify(plan, null, 2));
3189
3236
  } else {
@@ -3196,7 +3243,8 @@ Options:
3196
3243
  peerToken,
3197
3244
  dryRun,
3198
3245
  conflictPolicy,
3199
- config
3246
+ config,
3247
+ ...timeoutSeconds !== void 0 ? { peerRequestTimeoutMs: timeoutSeconds * 1e3 } : {}
3200
3248
  });
3201
3249
  if (json) {
3202
3250
  console.log(JSON.stringify(result, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/cli",
3
- "version": "9.69.16",
3
+ "version": "9.69.18",
4
4
  "description": "CLI for Remnic memory — init, query, doctor, daemon management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,26 +26,26 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "yaml": "^2.4.2",
29
- "@remnic/plugin-pi": "^9.69.16",
30
- "@remnic/core": "^9.69.16",
31
- "@remnic/server": "^9.69.16"
29
+ "@remnic/plugin-pi": "^9.69.18",
30
+ "@remnic/server": "^9.69.18",
31
+ "@remnic/core": "^9.69.18"
32
32
  },
33
33
  "peerDependencies": {
34
- "@remnic/bench": "^9.69.16",
35
- "@remnic/plugin-openclaw": "^9.69.16",
36
- "@remnic/export-weclone": "^9.69.16",
37
- "@remnic/import-weclone": "^9.69.16",
38
- "@remnic/import-chatgpt": "^9.69.16",
39
- "@remnic/import-claude": "^9.69.16",
40
- "@remnic/import-gemini": "^9.69.16",
41
- "@remnic/import-lossless-claw": "^9.69.16",
42
- "@remnic/import-mem0": "^9.69.16",
43
- "@remnic/import-supermemory": "^9.69.16",
44
- "@remnic/import-okf": "^9.69.16",
45
- "@remnic/connector-limitless": "^9.69.16",
46
- "@remnic/connector-bee": "^9.69.16",
47
- "@remnic/connector-omi": "^9.69.16",
48
- "@remnic/capture-audio": "^9.69.16"
34
+ "@remnic/bench": "^9.69.18",
35
+ "@remnic/plugin-openclaw": "^9.69.18",
36
+ "@remnic/export-weclone": "^9.69.18",
37
+ "@remnic/import-weclone": "^9.69.18",
38
+ "@remnic/import-chatgpt": "^9.69.18",
39
+ "@remnic/import-claude": "^9.69.18",
40
+ "@remnic/import-gemini": "^9.69.18",
41
+ "@remnic/import-lossless-claw": "^9.69.18",
42
+ "@remnic/import-mem0": "^9.69.18",
43
+ "@remnic/import-supermemory": "^9.69.18",
44
+ "@remnic/import-okf": "^9.69.18",
45
+ "@remnic/connector-limitless": "^9.69.18",
46
+ "@remnic/connector-bee": "^9.69.18",
47
+ "@remnic/connector-omi": "^9.69.18",
48
+ "@remnic/capture-audio": "^9.69.18"
49
49
  },
50
50
  "peerDependenciesMeta": {
51
51
  "@remnic/bench": {
@@ -97,19 +97,19 @@
97
97
  "devDependencies": {
98
98
  "tsup": "^8.5.1",
99
99
  "typescript": "^5.9.3",
100
- "@remnic/bench": "9.69.16",
101
- "@remnic/plugin-openclaw": "9.69.16",
102
- "@remnic/import-weclone": "9.69.16",
103
- "@remnic/export-weclone": "9.69.16",
104
- "@remnic/import-chatgpt": "9.69.16",
105
- "@remnic/import-lossless-claw": "9.69.16",
106
- "@remnic/import-gemini": "9.69.16",
107
- "@remnic/import-claude": "9.69.16",
108
- "@remnic/import-mem0": "9.69.16",
109
- "@remnic/connector-limitless": "9.69.16",
110
- "@remnic/connector-bee": "9.69.16",
111
- "@remnic/import-supermemory": "9.69.16",
112
- "@remnic/connector-omi": "9.69.16"
100
+ "@remnic/bench": "9.69.18",
101
+ "@remnic/plugin-openclaw": "9.69.18",
102
+ "@remnic/import-weclone": "9.69.18",
103
+ "@remnic/import-chatgpt": "9.69.18",
104
+ "@remnic/export-weclone": "9.69.18",
105
+ "@remnic/import-claude": "9.69.18",
106
+ "@remnic/import-gemini": "9.69.18",
107
+ "@remnic/import-lossless-claw": "9.69.18",
108
+ "@remnic/import-mem0": "9.69.18",
109
+ "@remnic/connector-limitless": "9.69.18",
110
+ "@remnic/connector-bee": "9.69.18",
111
+ "@remnic/connector-omi": "9.69.18",
112
+ "@remnic/import-supermemory": "9.69.18"
113
113
  },
114
114
  "license": "MIT",
115
115
  "repository": {