@evident-ai/cli 3.4.1-dev.d8ffc5f → 3.4.1-dev.f505f12

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/README.md CHANGED
@@ -98,6 +98,10 @@ Options:
98
98
  healthy when the runner starts it itself (default: `180`). On expiry the runner
99
99
  warns and comes online anyway rather than failing. Env:
100
100
  `EVIDENT_OPENCODE_START_TIMEOUT` (seconds).
101
+ - `--litestream-config <path>` — Start `litestream replicate` for the OpenCode
102
+ session database with this config file. Use this for runner images that persist
103
+ the session database; the file is produced by `runner-synchroniser litestream-config`.
104
+ Omit it to disable replication.
101
105
  - `--claude-usage-reporting <mode>` — Whether to report the local Claude Code
102
106
  subscription's rate-limit usage to Evident, so it shows on the runner page:
103
107
  `auto` (default) reports it when a usable Claude Code login is found on this
package/dist/index.js CHANGED
@@ -1681,6 +1681,62 @@ function buildOpenCodeVersionWarning(version2) {
1681
1681
 
1682
1682
  // src/lib/opencode/process.ts
1683
1683
  import { execSync, spawn } from "child_process";
1684
+
1685
+ // src/lib/process-stop.ts
1686
+ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
1687
+ if (!child.pid) {
1688
+ return { outcome: "not-running", code: child.exitCode, signal: child.signalCode };
1689
+ }
1690
+ if (child.exitCode !== null || child.signalCode !== null) {
1691
+ return { outcome: "exited", code: child.exitCode, signal: child.signalCode };
1692
+ }
1693
+ return new Promise((resolve3, reject) => {
1694
+ let forced = false;
1695
+ let settled = false;
1696
+ const timer = setTimeout(() => {
1697
+ forced = true;
1698
+ try {
1699
+ sendKill();
1700
+ } catch (error2) {
1701
+ if (error2.code === "ESRCH") {
1702
+ finish({ outcome: "exited", code: child.exitCode, signal: child.signalCode });
1703
+ } else {
1704
+ fail(error2);
1705
+ }
1706
+ }
1707
+ }, timeoutMs);
1708
+ const finish = (result) => {
1709
+ if (settled) return;
1710
+ settled = true;
1711
+ clearTimeout(timer);
1712
+ child.removeListener("exit", onExit);
1713
+ resolve3(result);
1714
+ };
1715
+ const fail = (error2) => {
1716
+ if (settled) return;
1717
+ settled = true;
1718
+ clearTimeout(timer);
1719
+ child.removeListener("exit", onExit);
1720
+ reject(error2);
1721
+ };
1722
+ const onExit = (code, signal) => {
1723
+ finish({ outcome: forced ? "killed" : "exited", code, signal });
1724
+ };
1725
+ child.once("exit", onExit);
1726
+ try {
1727
+ sendTerm();
1728
+ } catch (error2) {
1729
+ if (error2.code === "ESRCH") {
1730
+ finish({ outcome: "exited", code: child.exitCode, signal: child.signalCode });
1731
+ } else {
1732
+ fail(error2);
1733
+ }
1734
+ return;
1735
+ }
1736
+ });
1737
+ }
1738
+
1739
+ // src/lib/opencode/process.ts
1684
1740
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
1685
1741
  function getProcessCwd(pid) {
1686
1742
  const platform = process.platform;
@@ -1852,23 +1908,20 @@ async function startOpenCode(port) {
1852
1908
  });
1853
1909
  return child;
1854
1910
  }
1855
- function stopOpenCode(opencodeProcess) {
1856
- if (!opencodeProcess || !opencodeProcess.pid) {
1857
- return;
1858
- }
1859
- try {
1911
+ function stopOpenCodeAndWait(opencodeProcess, timeoutMs) {
1912
+ const sendSignal = (signal) => {
1860
1913
  if (process.platform === "win32") {
1861
- opencodeProcess.kill("SIGTERM");
1914
+ opencodeProcess.kill(signal);
1862
1915
  } else {
1863
- process.kill(-opencodeProcess.pid, "SIGTERM");
1916
+ process.kill(-opencodeProcess.pid, signal);
1864
1917
  }
1865
- } catch (err) {
1866
- if (err.code !== "ESRCH") {
1867
- console.warn(
1868
- `stopOpenCode: kill failed: ${err instanceof Error ? err.message : String(err)}`
1869
- );
1870
- }
1871
- }
1918
+ };
1919
+ return stopProcessAndWait(
1920
+ opencodeProcess,
1921
+ timeoutMs,
1922
+ () => sendSignal("SIGTERM"),
1923
+ () => sendSignal("SIGKILL")
1924
+ );
1872
1925
  }
1873
1926
 
1874
1927
  // src/lib/opencode/install.ts
@@ -2155,6 +2208,7 @@ async function createOpenCodeSession(port, directory) {
2155
2208
  return data.id;
2156
2209
  }
2157
2210
  async function getModelAttachmentCapability(port, model) {
2211
+ const { model: baseModel } = splitModelVariant(model);
2158
2212
  try {
2159
2213
  const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
2160
2214
  if (!res.ok) {
@@ -2171,9 +2225,9 @@ async function getModelAttachmentCapability(port, model) {
2171
2225
  );
2172
2226
  return null;
2173
2227
  }
2174
- const slash = model ? model.indexOf("/") : -1;
2175
- const providerId = slash > 0 ? model.slice(0, slash) : void 0;
2176
- let modelId = slash > 0 ? model.slice(slash + 1) : void 0;
2228
+ const slash = baseModel ? baseModel.indexOf("/") : -1;
2229
+ const providerId = slash > 0 ? baseModel.slice(0, slash) : void 0;
2230
+ let modelId = slash > 0 ? baseModel.slice(slash + 1) : void 0;
2177
2231
  const defaults2 = body?.default && typeof body.default === "object" ? body.default : void 0;
2178
2232
  let provider = providerId ? providers.find((p) => p?.id === providerId) : void 0;
2179
2233
  if (!provider && !providerId) {
@@ -2253,6 +2307,29 @@ async function buildFileParts(attachments, capable) {
2253
2307
  }
2254
2308
  return { parts, outcomes, capabilityUnknown };
2255
2309
  }
2310
+ function splitModelVariant(raw) {
2311
+ const value = raw?.trim();
2312
+ if (!value) return {};
2313
+ const hashIndex = value.indexOf("#");
2314
+ if (hashIndex === -1) return { model: value };
2315
+ const model = value.slice(0, hashIndex).trim() || void 0;
2316
+ const variant = value.slice(hashIndex + 1).trim() || void 0;
2317
+ return { model, variant };
2318
+ }
2319
+ function applyModelOptions(body, options) {
2320
+ if (options?.agent) body.agent = options.agent;
2321
+ const { model, variant } = splitModelVariant(options?.model);
2322
+ if (model) {
2323
+ const slashIndex = model.indexOf("/");
2324
+ if (slashIndex !== -1) {
2325
+ body.model = {
2326
+ providerID: model.substring(0, slashIndex),
2327
+ modelID: model.substring(slashIndex + 1)
2328
+ };
2329
+ }
2330
+ }
2331
+ if (variant) body.variant = variant;
2332
+ }
2256
2333
  function messageText(m) {
2257
2334
  if (!m || !Array.isArray(m.parts)) return "";
2258
2335
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
@@ -2277,18 +2354,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
2277
2354
  const body = {
2278
2355
  parts
2279
2356
  };
2280
- if (options?.agent) {
2281
- body.agent = options.agent;
2282
- }
2283
- if (options?.model) {
2284
- const slashIndex = options.model.indexOf("/");
2285
- if (slashIndex !== -1) {
2286
- body.model = {
2287
- providerID: options.model.substring(0, slashIndex),
2288
- modelID: options.model.substring(slashIndex + 1)
2289
- };
2290
- }
2291
- }
2357
+ applyModelOptions(body, options);
2292
2358
  const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
2293
2359
  method: "POST",
2294
2360
  headers: { "Content-Type": "application/json" },
@@ -2296,7 +2362,10 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
2296
2362
  });
2297
2363
  if (res.status < 200 || res.status >= 300) {
2298
2364
  const text = await res.text().catch(() => "");
2299
- throw new Error(`OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ""}`);
2365
+ const { variant } = splitModelVariant(options?.model);
2366
+ throw new Error(
2367
+ `OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ""}${variant ? ` (variant: ${variant})` : ""}`
2368
+ );
2300
2369
  }
2301
2370
  const READ_BACK_ATTEMPTS = 5;
2302
2371
  const READ_BACK_DELAY_MS = 150;
@@ -3246,6 +3315,22 @@ function writeTunnelReadyMarker(path, agentId) {
3246
3315
  }
3247
3316
  }
3248
3317
 
3318
+ // src/lib/replication.ts
3319
+ import { spawn as spawn2 } from "child_process";
3320
+ function startSessionDbReplication(configPath) {
3321
+ return spawn2("litestream", ["replicate", "-config", configPath], {
3322
+ stdio: "inherit"
3323
+ });
3324
+ }
3325
+ async function stopSessionDbReplication(child, timeoutMs) {
3326
+ return stopProcessAndWait(
3327
+ child,
3328
+ timeoutMs,
3329
+ () => child.kill("SIGTERM"),
3330
+ () => child.kill("SIGKILL")
3331
+ );
3332
+ }
3333
+
3249
3334
  // src/lib/openai-usage.ts
3250
3335
  import { readFileSync as readFileSync3 } from "fs";
3251
3336
  import { homedir as homedir2 } from "os";
@@ -7980,6 +8065,7 @@ var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_
7980
8065
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
7981
8066
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
7982
8067
  var TELEMETRY_SHUTDOWN_TIMEOUT_MS = 5e3;
8068
+ var CHILD_STOP_TIMEOUT_MS = 1e4;
7983
8069
  function resolveLogLevel(options) {
7984
8070
  const accepted = Object.keys(LOG_LEVELS);
7985
8071
  const validate = (value, source) => {
@@ -8767,15 +8853,31 @@ async function cleanup(state, opts = {}) {
8767
8853
  }
8768
8854
  if (state.opencodeProcess) {
8769
8855
  const opencodeProcess = state.opencodeProcess;
8770
- await timeShutdownPhase(state, durations, "opencode_stop", () => stopOpenCode(opencodeProcess));
8856
+ const result = await timeShutdownPhase(
8857
+ state,
8858
+ durations,
8859
+ "opencode_stop",
8860
+ () => stopOpenCodeAndWait(opencodeProcess, CHILD_STOP_TIMEOUT_MS)
8861
+ );
8771
8862
  if (state.interactive) {
8772
- logActivity(state, { type: "info", message: "Stopped OpenCode process" });
8863
+ logActivity(state, { type: "info", message: `Stopped OpenCode process (${result.outcome})` });
8773
8864
  displayStatus(state);
8774
8865
  } else {
8775
- log2(state, "Stopped OpenCode process");
8866
+ log2(state, `Stopped OpenCode process (${result.outcome})`);
8776
8867
  }
8777
8868
  state.opencodeProcess = null;
8778
8869
  }
8870
+ if (state.litestreamProcess) {
8871
+ const litestreamProcess = state.litestreamProcess;
8872
+ const result = await timeShutdownPhase(
8873
+ state,
8874
+ durations,
8875
+ "litestream_stop",
8876
+ () => stopSessionDbReplication(litestreamProcess, CHILD_STOP_TIMEOUT_MS)
8877
+ );
8878
+ log2(state, `Stopped litestream replication (${result.outcome})`);
8879
+ state.litestreamProcess = null;
8880
+ }
8779
8881
  return durations;
8780
8882
  }
8781
8883
  async function run(options) {
@@ -8809,6 +8911,7 @@ async function run(options) {
8809
8911
  opencodeConnected: false,
8810
8912
  opencodeVersion: null,
8811
8913
  opencodeProcess: null,
8914
+ litestreamProcess: null,
8812
8915
  connection: null,
8813
8916
  channelDriver: null,
8814
8917
  running: true,
@@ -9073,6 +9176,40 @@ async function run(options) {
9073
9176
  ocSpinner?.fail(error2.message);
9074
9177
  throw error2;
9075
9178
  }
9179
+ if (options.litestreamConfig) {
9180
+ const litestreamProcess = startSessionDbReplication(options.litestreamConfig);
9181
+ state.litestreamProcess = litestreamProcess;
9182
+ let failureHandled = false;
9183
+ const failRunForReplication = (message) => {
9184
+ if (failureHandled || state.shuttingDown || !state.running) return;
9185
+ failureHandled = true;
9186
+ state.shuttingDown = true;
9187
+ logActivity(state, { type: "error", error: message });
9188
+ if (state.interactive) displayStatus(state);
9189
+ void (async () => {
9190
+ try {
9191
+ await cleanup(state);
9192
+ await shutdownTelemetry();
9193
+ } catch (error2) {
9194
+ console.error(
9195
+ `[run] replication failure cleanup failed: ${error2 instanceof Error ? error2.message : String(error2)}`
9196
+ );
9197
+ }
9198
+ process.exit(1);
9199
+ })();
9200
+ };
9201
+ litestreamProcess.on("exit", (code, signal) => {
9202
+ failRunForReplication(
9203
+ `Litestream replication stopped unexpectedly (code: ${code ?? "null"}, signal: ${signal ?? "none"})`
9204
+ );
9205
+ });
9206
+ litestreamProcess.on("error", (error2) => {
9207
+ failRunForReplication(
9208
+ `Litestream replication failed: ${error2 instanceof Error ? error2.message : String(error2)}`
9209
+ );
9210
+ });
9211
+ log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
9212
+ }
9076
9213
  const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
9077
9214
  const channelDriver = new ChannelDriver({
9078
9215
  agentId: state.agentId,
@@ -9333,6 +9470,9 @@ program.command("run").description("Connect to Evident and process messages").op
9333
9470
  ).option(
9334
9471
  "--tunnel-ready-file <path>",
9335
9472
  "Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
9473
+ ).option(
9474
+ "--litestream-config <path>",
9475
+ "Replicate the OpenCode session database with `litestream replicate` using this config file (written by the runner image). Omit to disable replication."
9336
9476
  ).action(
9337
9477
  (options) => {
9338
9478
  run({
@@ -9364,7 +9504,8 @@ program.command("run").description("Connect to Evident and process messages").op
9364
9504
  // Raw values — expansion/validation is single-sourced in run.ts's
9365
9505
  // resolveFileSyncDirectories.
9366
9506
  enableFileSyncTo: options.enableFileSyncTo,
9367
- tunnelReadyFile: options.tunnelReadyFile
9507
+ tunnelReadyFile: options.tunnelReadyFile,
9508
+ litestreamConfig: options.litestreamConfig
9368
9509
  });
9369
9510
  }
9370
9511
  );