@remnic/cli 9.69.14 → 9.69.16

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 +138 -35
  2. package/package.json +32 -32
package/dist/index.js CHANGED
@@ -1701,6 +1701,65 @@ function secureStoreEnvelopeHeaderAad(salt) {
1701
1701
  return out;
1702
1702
  }
1703
1703
 
1704
+ // src/converge-watch.ts
1705
+ var CONVERGE_WATCH_MIN_INTERVAL_MS = 1e3;
1706
+ var CONVERGE_WATCH_DEFAULT_INTERVAL_MS = 3e5;
1707
+ var CONVERGE_WATCH_MAX_INTERVAL_MS = 2147483647;
1708
+ function sleepAborted(ms, signal) {
1709
+ return new Promise((resolve2) => {
1710
+ const timer = setTimeout(() => {
1711
+ signal.removeEventListener("abort", onAbort);
1712
+ resolve2(true);
1713
+ }, ms);
1714
+ const onAbort = () => {
1715
+ clearTimeout(timer);
1716
+ resolve2(false);
1717
+ };
1718
+ if (signal.aborted) {
1719
+ clearTimeout(timer);
1720
+ resolve2(false);
1721
+ return;
1722
+ }
1723
+ signal.addEventListener("abort", onAbort, { once: true });
1724
+ });
1725
+ }
1726
+ async function convergeWatch(options) {
1727
+ const intervalMs = Math.min(
1728
+ CONVERGE_WATCH_MAX_INTERVAL_MS,
1729
+ Math.max(CONVERGE_WATCH_MIN_INTERVAL_MS, options.intervalMs ?? CONVERGE_WATCH_DEFAULT_INTERVAL_MS)
1730
+ );
1731
+ const { apply, intervalMs: _intervalMs, maxCycles, onCycle, signal, ...applyOptions } = options;
1732
+ const outcome = {
1733
+ cycles: 0,
1734
+ convergedCycles: 0,
1735
+ appliedCycles: 0,
1736
+ failedCycles: 0,
1737
+ lastStatus: "aborted"
1738
+ };
1739
+ while (maxCycles === void 0 || outcome.cycles < maxCycles) {
1740
+ if (signal?.aborted) break;
1741
+ try {
1742
+ const result = await apply(applyOptions);
1743
+ outcome.cycles += 1;
1744
+ if (result.status === "converged") outcome.convergedCycles += 1;
1745
+ else if (result.status === "stopped_unresolved_conflicts" || result.status === "applied" && result.transfers.failed > 0) {
1746
+ outcome.failedCycles += 1;
1747
+ } else outcome.appliedCycles += 1;
1748
+ outcome.lastStatus = result.status;
1749
+ onCycle?.(outcome.cycles, { result });
1750
+ } catch (err) {
1751
+ outcome.cycles += 1;
1752
+ outcome.failedCycles += 1;
1753
+ outcome.lastStatus = "error";
1754
+ onCycle?.(outcome.cycles, { error: err });
1755
+ }
1756
+ if (maxCycles !== void 0 && outcome.cycles >= maxCycles) break;
1757
+ const slept = await sleepAborted(intervalMs, signal ?? new AbortController().signal);
1758
+ if (!slept) break;
1759
+ }
1760
+ return outcome;
1761
+ }
1762
+
1704
1763
  // src/converge.ts
1705
1764
  import { resolveAgentAccessAuthToken } from "@remnic/core/resolve-auth-token.js";
1706
1765
 
@@ -2292,7 +2351,10 @@ async function computeConvergePlan(options = {}) {
2292
2351
  if (options.baseFilesByNamespace) {
2293
2352
  for (const [ns, files] of options.baseFilesByNamespace) {
2294
2353
  namespacesToPlan.add(ns);
2295
- baseMap.set(ns, files.filter((file) => !isInternalRemnicStatePath3(file.path)));
2354
+ baseMap.set(
2355
+ ns,
2356
+ files.filter((file) => !isInternalRemnicStatePath3(file.path))
2357
+ );
2296
2358
  }
2297
2359
  }
2298
2360
  if (options.semanticAgreementsByNamespace) {
@@ -2304,7 +2366,10 @@ async function computeConvergePlan(options = {}) {
2304
2366
  if (options.localFilesByNamespace) {
2305
2367
  for (const [ns, files] of options.localFilesByNamespace) {
2306
2368
  namespacesToPlan.add(ns);
2307
- localMap.set(ns, files.filter((file) => !isInternalRemnicStatePath3(file.path)));
2369
+ localMap.set(
2370
+ ns,
2371
+ files.filter((file) => !isInternalRemnicStatePath3(file.path))
2372
+ );
2308
2373
  }
2309
2374
  }
2310
2375
  if (options.localTombstonesByNamespace) {
@@ -2321,7 +2386,10 @@ async function computeConvergePlan(options = {}) {
2321
2386
  if (options.peerFilesByNamespace) {
2322
2387
  for (const [ns, files] of options.peerFilesByNamespace) {
2323
2388
  namespacesToPlan.add(ns);
2324
- peerMap.set(ns, files.filter((file) => !isInternalRemnicStatePath3(file.path)));
2389
+ peerMap.set(
2390
+ ns,
2391
+ files.filter((file) => !isInternalRemnicStatePath3(file.path))
2392
+ );
2325
2393
  }
2326
2394
  }
2327
2395
  if (options.peerTombstonesByNamespace) {
@@ -2418,12 +2486,7 @@ async function computeConvergePlan(options = {}) {
2418
2486
  }
2419
2487
  const fetchFn = options.fetchImpl ?? globalThis.fetch;
2420
2488
  const timeoutMs = options.peerRequestTimeoutMs ?? DEFAULT_PEER_REQUEST_TIMEOUT_MS;
2421
- const capabilities = await fetchPeerSyncCapabilities(
2422
- peerUrl,
2423
- resolvedToken,
2424
- fetchFn,
2425
- timeoutMs
2426
- );
2489
+ const capabilities = await fetchPeerSyncCapabilities(peerUrl, resolvedToken, fetchFn, timeoutMs);
2427
2490
  for (const ns of namespacesToPlan) {
2428
2491
  const peerData = await fetchPeerSnapshot(peerUrl, ns, resolvedToken, fetchFn, timeoutMs);
2429
2492
  const streamedManifest = capabilities?.manifestStream ? await fetchPeerManifestStream(peerUrl, ns, resolvedToken, fetchFn, timeoutMs) : null;
@@ -2459,14 +2522,7 @@ async function computeConvergePlan(options = {}) {
2459
2522
  for (const tombstonePath of TOMBSTONE_PATHS) {
2460
2523
  const state = peerFiles.find((file) => file.path === tombstonePath);
2461
2524
  if (!state) continue;
2462
- const remote = await fetchPeerFileContent(
2463
- peerUrl,
2464
- ns,
2465
- tombstonePath,
2466
- resolvedToken,
2467
- fetchFn,
2468
- timeoutMs
2469
- );
2525
+ const remote = await fetchPeerFileContent(peerUrl, ns, tombstonePath, resolvedToken, fetchFn, timeoutMs);
2470
2526
  if (!remote || remote.sha256.toLowerCase() !== state.sha256.toLowerCase()) {
2471
2527
  throw new Error(`failed to read peer tombstone evidence: ${tombstonePath}`);
2472
2528
  }
@@ -2939,13 +2995,7 @@ async function executeConvergeApply(options = {}) {
2939
2995
  }
2940
2996
  if (options.peerUrl && peerMutatedNamespaces.size > 0) {
2941
2997
  const namespaces = [...peerMutatedNamespaces].sort();
2942
- if (!await postPeerConvergenceComplete(
2943
- options.peerUrl,
2944
- namespaces,
2945
- resolvedToken,
2946
- fetchFn,
2947
- timeoutMs
2948
- )) {
2998
+ if (!await postPeerConvergenceComplete(options.peerUrl, namespaces, resolvedToken, fetchFn, timeoutMs)) {
2949
2999
  actualTransfers.failed += 1;
2950
3000
  }
2951
3001
  }
@@ -2975,11 +3025,7 @@ async function updateCursorsForPlan(plan, options) {
2975
3025
  for (const ns of namespaces) {
2976
3026
  const cursorPath = defaultConvergeCursorPath(memoryDir, peerUrl, ns);
2977
3027
  const priorSemanticAgreements = options.semanticAgreementsByNamespace?.get(ns) ?? (await readConvergeCursor(cursorPath))?.semanticAgreements ?? [];
2978
- const { baseFiles, semanticAgreements } = deriveConvergeCursorBase(
2979
- plan.entries,
2980
- ns,
2981
- priorSemanticAgreements
2982
- );
3028
+ const { baseFiles, semanticAgreements } = deriveConvergeCursorBase(plan.entries, ns, priorSemanticAgreements);
2983
3029
  const cursorState = {
2984
3030
  version: 1,
2985
3031
  peerUrl,
@@ -3031,11 +3077,12 @@ function formatConvergeApplyReport(result) {
3031
3077
  }
3032
3078
  async function cmdConverge(action, rest, json, config = parseConfig13({})) {
3033
3079
  if (action === "help" || action === "--help" || action === "-h" || rest.includes("--help") || rest.includes("-h")) {
3034
- console.log(`Usage: remnic converge <plan|apply> [options]
3080
+ console.log(`Usage: remnic converge <plan|apply|watch> [options]
3035
3081
 
3036
3082
  Subcommands:
3037
3083
  plan Compute and display reconciliation plan
3038
3084
  apply Execute bidirectional converge transport (alias: transport, sync)
3085
+ watch Run apply on a cadence until stopped (scheduled replication)
3039
3086
 
3040
3087
  Options:
3041
3088
  --peer <url> Peer server URL (or --remote-url / --remote)
@@ -3043,13 +3090,15 @@ Options:
3043
3090
  --conflict-policy <policy>
3044
3091
  Policy override (newest-wins|manual)
3045
3092
  Default: converge.conflictPolicy (newest-wins)
3093
+ --interval <seconds>
3094
+ Watch cadence in seconds (watch only; default 300, min 1)
3046
3095
  --dry-run Simulate transfers without mutating disk or remote peer
3047
3096
  --json Output detailed JSON plan report
3048
3097
  `);
3049
3098
  return;
3050
3099
  }
3051
- if (action !== "plan" && action !== "apply" && action !== "transport" && action !== "sync") {
3052
- process.stderr.write(`converge: unknown action "${action}". Use: plan or apply [options].
3100
+ if (action !== "plan" && action !== "apply" && action !== "transport" && action !== "sync" && action !== "watch") {
3101
+ process.stderr.write(`converge: unknown action "${action}". Use: plan, apply, or watch [options].
3053
3102
  `);
3054
3103
  process.exitCode = 2;
3055
3104
  return;
@@ -3058,6 +3107,7 @@ Options:
3058
3107
  let peerToken;
3059
3108
  let dryRun = false;
3060
3109
  let conflictPolicy;
3110
+ let intervalSeconds;
3061
3111
  for (let i = 0; i < rest.length; i += 1) {
3062
3112
  const arg = rest[i];
3063
3113
  if ((arg === "--peer" || arg === "--remote-url" || arg === "--remote") && rest[i + 1]) {
@@ -3068,17 +3118,70 @@ Options:
3068
3118
  i += 1;
3069
3119
  } else if (arg === "--dry-run") {
3070
3120
  dryRun = true;
3121
+ } else if (arg === "--interval") {
3122
+ const raw = rest[i + 1];
3123
+ const parsed = raw === void 0 ? Number.NaN : Number(raw);
3124
+ if (!Number.isFinite(parsed) || parsed <= 0) {
3125
+ process.stderr.write("converge: --interval must be a positive number of seconds.\n");
3126
+ process.exitCode = 2;
3127
+ return;
3128
+ }
3129
+ intervalSeconds = parsed;
3130
+ i += 1;
3071
3131
  } else if (arg === "--conflict-policy") {
3072
3132
  const policy = rest[i + 1];
3073
3133
  if (typeof policy !== "string" || !CONVERGE_CONFLICT_POLICIES.includes(policy)) {
3074
- throw new Error(
3075
- `converge: --conflict-policy must be one of ${CONVERGE_CONFLICT_POLICIES.join(", ")}`
3076
- );
3134
+ throw new Error(`converge: --conflict-policy must be one of ${CONVERGE_CONFLICT_POLICIES.join(", ")}`);
3077
3135
  }
3078
3136
  conflictPolicy = policy;
3079
3137
  i += 1;
3080
3138
  }
3081
3139
  }
3140
+ if (action === "watch") {
3141
+ const controller = new AbortController();
3142
+ const onSignal = () => controller.abort();
3143
+ process.once("SIGINT", onSignal);
3144
+ process.once("SIGTERM", onSignal);
3145
+ let outcome;
3146
+ try {
3147
+ outcome = await convergeWatch({
3148
+ apply: (applyOptions) => executeConvergeApply(applyOptions),
3149
+ config,
3150
+ peerUrl,
3151
+ dryRun,
3152
+ peerToken,
3153
+ conflictPolicy,
3154
+ intervalMs: intervalSeconds !== void 0 ? intervalSeconds * 1e3 : void 0,
3155
+ signal: controller.signal,
3156
+ onCycle: json ? void 0 : (cycle, event) => {
3157
+ if (event.error !== void 0) {
3158
+ console.error(`converge watch: cycle ${cycle} failed: ${String(event.error)}`);
3159
+ return;
3160
+ }
3161
+ const result2 = event.result;
3162
+ if (!result2) return;
3163
+ const transfers = result2.transfers;
3164
+ console.log(
3165
+ `converge watch: cycle ${cycle} status=${result2.status} pulled=${transfers.pulled} pushed=${transfers.pushed} conflicts=${transfers.conflictsResolved} failed=${transfers.failed}`
3166
+ );
3167
+ }
3168
+ });
3169
+ if (json) {
3170
+ console.log(JSON.stringify(outcome, null, 2));
3171
+ } else {
3172
+ console.log(
3173
+ `converge watch stopped after ${outcome.cycles} cycle(s): ${outcome.convergedCycles} converged, ${outcome.appliedCycles} applied, ${outcome.failedCycles} failed (last: ${outcome.lastStatus}).`
3174
+ );
3175
+ }
3176
+ } finally {
3177
+ process.removeListener("SIGINT", onSignal);
3178
+ process.removeListener("SIGTERM", onSignal);
3179
+ if (outcome && outcome.cycles > 0 && outcome.failedCycles === outcome.cycles) {
3180
+ process.exitCode = 1;
3181
+ }
3182
+ }
3183
+ return;
3184
+ }
3082
3185
  if (action === "plan") {
3083
3186
  const plan = await computeConvergePlan({ config, peerUrl, peerToken, conflictPolicy });
3084
3187
  if (json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/cli",
3
- "version": "9.69.14",
3
+ "version": "9.69.16",
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.14",
30
- "@remnic/core": "^9.69.14",
31
- "@remnic/server": "^9.69.14"
29
+ "@remnic/plugin-pi": "^9.69.16",
30
+ "@remnic/core": "^9.69.16",
31
+ "@remnic/server": "^9.69.16"
32
32
  },
33
33
  "peerDependencies": {
34
- "@remnic/bench": "^9.69.14",
35
- "@remnic/plugin-openclaw": "^9.69.14",
36
- "@remnic/export-weclone": "^9.69.14",
37
- "@remnic/import-weclone": "^9.69.14",
38
- "@remnic/import-chatgpt": "^9.69.14",
39
- "@remnic/import-claude": "^9.69.14",
40
- "@remnic/import-gemini": "^9.69.14",
41
- "@remnic/import-lossless-claw": "^9.69.14",
42
- "@remnic/import-mem0": "^9.69.14",
43
- "@remnic/import-supermemory": "^9.69.14",
44
- "@remnic/import-okf": "^9.69.14",
45
- "@remnic/connector-limitless": "^9.69.14",
46
- "@remnic/connector-bee": "^9.69.14",
47
- "@remnic/connector-omi": "^9.69.14",
48
- "@remnic/capture-audio": "^9.69.14"
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"
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.14",
101
- "@remnic/plugin-openclaw": "9.69.14",
102
- "@remnic/export-weclone": "9.69.14",
103
- "@remnic/import-claude": "9.69.14",
104
- "@remnic/import-chatgpt": "9.69.14",
105
- "@remnic/import-gemini": "9.69.14",
106
- "@remnic/import-lossless-claw": "9.69.14",
107
- "@remnic/import-weclone": "9.69.14",
108
- "@remnic/import-mem0": "9.69.14",
109
- "@remnic/import-supermemory": "9.69.14",
110
- "@remnic/connector-limitless": "9.69.14",
111
- "@remnic/connector-bee": "9.69.14",
112
- "@remnic/connector-omi": "9.69.14"
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"
113
113
  },
114
114
  "license": "MIT",
115
115
  "repository": {