@evident-ai/cli 3.1.1-dev.dcb2f6d → 3.1.1-dev.f24491c

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 CHANGED
@@ -645,14 +645,27 @@ var EventTypes = {
645
645
  // CLI lifecycle
646
646
  CLI_STARTED: "cli.started",
647
647
  CLI_COMMAND: "cli.command",
648
- CLI_ERROR: "cli.error"
648
+ CLI_ERROR: "cli.error",
649
+ // Deprecation telemetry (#412) — usage of the old `--agent`/`EVIDENT_AGENT_KEY`
650
+ // names instead of the preferred `--runner`/`EVIDENT_RUNNER_KEY` (#409).
651
+ DEPRECATED_AGENT_FLAG_USED: "cli.deprecated_agent_flag_used",
652
+ DEPRECATED_AGENT_KEY_ENV_USED: "cli.deprecated_agent_key_env_used"
649
653
  };
650
654
 
651
655
  // src/lib/auth.ts
652
656
  async function getAuthCredentials() {
657
+ const runnerKey = process.env.EVIDENT_RUNNER_KEY;
653
658
  const agentKey = process.env.EVIDENT_AGENT_KEY;
659
+ if (runnerKey) {
660
+ return {
661
+ token: runnerKey,
662
+ authType: "agent_key",
663
+ keySource: "runner_key",
664
+ notice: agentKey ? "Both EVIDENT_RUNNER_KEY and EVIDENT_AGENT_KEY are set; using EVIDENT_RUNNER_KEY." : void 0
665
+ };
666
+ }
654
667
  if (agentKey) {
655
- return { token: agentKey, authType: "agent_key" };
668
+ return { token: agentKey, authType: "agent_key", keySource: "agent_key" };
656
669
  }
657
670
  const userToken = process.env.EVIDENT_TOKEN;
658
671
  if (userToken) {
@@ -721,7 +734,7 @@ function buildOpenCodeVersionWarning(version2) {
721
734
  if (isQueueValidatedVersion(version2)) return null;
722
735
  const detected = version2 ? `v${version2}` : "unknown";
723
736
  const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
724
- return `Warning: opencode ${detected} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack/WhatsApp) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
737
+ return `Warning: opencode ${detected} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
725
738
  }
726
739
 
727
740
  // src/lib/opencode/process.ts
@@ -1013,6 +1026,12 @@ async function promptOpenCodeInstall(interactive) {
1013
1026
  return action;
1014
1027
  }
1015
1028
 
1029
+ // src/lib/opencode/provider-check.ts
1030
+ function buildNoProviderWarning(hasProvider) {
1031
+ if (hasProvider !== false) return null;
1032
+ return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
1033
+ }
1034
+
1016
1035
  // src/lib/opencode/session.ts
1017
1036
  function opencodeBase(port) {
1018
1037
  return `http://127.0.0.1:${port}`;
@@ -1215,6 +1234,11 @@ async function getModelAttachmentCapability(port, model) {
1215
1234
  }
1216
1235
  const entry = provider.models[modelId];
1217
1236
  if (!entry || typeof entry !== "object") return null;
1237
+ if (entry.capabilities && typeof entry.capabilities === "object") {
1238
+ if (typeof entry.capabilities.attachment === "boolean") {
1239
+ return entry.capabilities.attachment;
1240
+ }
1241
+ }
1218
1242
  return typeof entry.attachment === "boolean" ? entry.attachment : null;
1219
1243
  } catch (err) {
1220
1244
  console.error(
@@ -1243,6 +1267,16 @@ async function buildFileParts(attachments, capable) {
1243
1267
  );
1244
1268
  dataUrl = null;
1245
1269
  }
1270
+ if (dataUrl !== null && typeof dataUrl === "object") {
1271
+ outcomes.push({
1272
+ index: a.index,
1273
+ mime: a.mime,
1274
+ filename: a.filename,
1275
+ status: "failed",
1276
+ reason: "needs_reauth"
1277
+ });
1278
+ continue;
1279
+ }
1246
1280
  if (dataUrl == null) {
1247
1281
  outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "failed" });
1248
1282
  continue;
@@ -1370,6 +1404,72 @@ function findLastAssistantReplyFor(messages, userMessageId) {
1370
1404
  }
1371
1405
  return lastOk ?? last;
1372
1406
  }
1407
+ function messageUsage(messages, userMessageId) {
1408
+ if (!messages || messages.length === 0) return null;
1409
+ const byParentAll = messages.filter(
1410
+ (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
1411
+ );
1412
+ const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
1413
+ const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
1414
+ let correlated;
1415
+ if (byParent.length > 0) {
1416
+ correlated = byParent;
1417
+ } else {
1418
+ const reply = findAssistantReplyAfter(messages, userMessageId);
1419
+ correlated = reply ? [reply] : [];
1420
+ }
1421
+ if (correlated.length === 0) return null;
1422
+ let sawAnyUsage = false;
1423
+ let inputSum = 0;
1424
+ let outputSum = 0;
1425
+ let reasoningSum = 0;
1426
+ let cacheReadSum = 0;
1427
+ let cacheWriteSum = 0;
1428
+ let costSum = 0;
1429
+ let sawCost = false;
1430
+ let modelId = null;
1431
+ let providerId = null;
1432
+ for (const m of correlated) {
1433
+ const info = m.info;
1434
+ if (!info) continue;
1435
+ const tokens = info.tokens;
1436
+ if (tokens) {
1437
+ sawAnyUsage = true;
1438
+ inputSum += tokens.input ?? 0;
1439
+ outputSum += tokens.output ?? 0;
1440
+ reasoningSum += tokens.reasoning ?? 0;
1441
+ cacheReadSum += tokens.cache?.read ?? 0;
1442
+ cacheWriteSum += tokens.cache?.write ?? 0;
1443
+ }
1444
+ if (typeof info.cost === "number") {
1445
+ sawAnyUsage = true;
1446
+ sawCost = true;
1447
+ costSum += info.cost;
1448
+ }
1449
+ if (typeof info.modelID === "string") {
1450
+ sawAnyUsage = true;
1451
+ modelId = info.modelID;
1452
+ }
1453
+ if (typeof info.providerID === "string") {
1454
+ sawAnyUsage = true;
1455
+ providerId = info.providerID;
1456
+ }
1457
+ }
1458
+ if (!sawAnyUsage) return null;
1459
+ return {
1460
+ usage_provider_id: providerId,
1461
+ usage_model_id: modelId,
1462
+ usage_tokens_input: inputSum,
1463
+ usage_tokens_output: outputSum,
1464
+ usage_tokens_reasoning: reasoningSum,
1465
+ usage_tokens_cache_read: cacheReadSum,
1466
+ usage_tokens_cache_write: cacheWriteSum,
1467
+ // NULL means "OpenCode never reported a cost" (never inferred from
1468
+ // tokens) — distinct from a genuine 0-cost turn, which would set
1469
+ // `sawCost` true with `costSum === 0`.
1470
+ usage_cost_usd: sawCost ? costSum : null
1471
+ };
1472
+ }
1373
1473
  function messageRunState(messages, userMessageId) {
1374
1474
  if (!messages || messages.length === 0) return "unknown";
1375
1475
  const hasUser = messages.some((m) => idOf(m) === userMessageId);
@@ -1405,6 +1505,37 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
1405
1505
  (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
1406
1506
  );
1407
1507
  }
1508
+ async function hasAnyConfiguredProvider(port) {
1509
+ try {
1510
+ const res = await fetch(`${opencodeBase(port)}/config/providers`);
1511
+ if (!res.ok) {
1512
+ console.error(
1513
+ `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
1514
+ );
1515
+ return null;
1516
+ }
1517
+ const body = await res.json();
1518
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
1519
+ console.error(
1520
+ `[hasAnyConfiguredProvider] GET /config/providers body was not a plain object (port ${port})`
1521
+ );
1522
+ return null;
1523
+ }
1524
+ const defaults2 = body.default;
1525
+ if (!defaults2 || typeof defaults2 !== "object" || Array.isArray(defaults2)) {
1526
+ console.error(
1527
+ `[hasAnyConfiguredProvider] GET /config/providers body had no \`default\` object (port ${port})`
1528
+ );
1529
+ return null;
1530
+ }
1531
+ return Object.keys(defaults2).length > 0;
1532
+ } catch (err) {
1533
+ console.error(
1534
+ `[hasAnyConfiguredProvider] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
1535
+ );
1536
+ return null;
1537
+ }
1538
+ }
1408
1539
 
1409
1540
  // src/lib/opencode/session-cleanup.ts
1410
1541
  var DURATION_UNIT_MS = {
@@ -1716,7 +1847,6 @@ function connectTunnel(options) {
1716
1847
  onConnected,
1717
1848
  onDisconnected,
1718
1849
  onError,
1719
- onRequest,
1720
1850
  onResponse,
1721
1851
  onInfo,
1722
1852
  onDrainPing
@@ -1729,18 +1859,8 @@ function connectTunnel(options) {
1729
1859
  Authorization: authHeader
1730
1860
  }
1731
1861
  });
1732
- const streamStartTimes = /* @__PURE__ */ new Map();
1733
1862
  const forwarder = new StreamForwarder(ws, port, {
1734
- onOpen: (sid, method, path) => {
1735
- if (path === TUNNEL_DRAIN_PING_PATH) return;
1736
- streamStartTimes.set(sid, Date.now());
1737
- onRequest?.(method, path, sid);
1738
- },
1739
- onHead: (sid, status) => {
1740
- const startedAt = streamStartTimes.get(sid);
1741
- streamStartTimes.delete(sid);
1742
- onResponse?.(status, startedAt ? Date.now() - startedAt : 0, sid);
1743
- },
1863
+ onHead: () => onResponse?.(),
1744
1864
  onDrainPing: () => onDrainPing?.()
1745
1865
  });
1746
1866
  const connectionTimeout = setTimeout(() => {
@@ -1816,7 +1936,6 @@ function connectTunnel(options) {
1816
1936
  ws.on("close", (code, reason) => {
1817
1937
  const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
1818
1938
  forwarder.abortAll();
1819
- streamStartTimes.clear();
1820
1939
  onDisconnected?.(code, reasonStr);
1821
1940
  });
1822
1941
  });
@@ -1954,7 +2073,7 @@ function backoffDelay(attempt, policy) {
1954
2073
  function isRetryableStatus(status) {
1955
2074
  return status === 429 || status >= 500 && status <= 599;
1956
2075
  }
1957
- var ChannelDriver = class {
2076
+ var ChannelDriver = class _ChannelDriver {
1958
2077
  agentId;
1959
2078
  port;
1960
2079
  apiUrl;
@@ -2079,9 +2198,12 @@ var ChannelDriver = class {
2079
2198
  sessionParents = /* @__PURE__ */ new Map();
2080
2199
  /**
2081
2200
  * Per-session OpenCode title cache (#310), keyed by sessionId. Only a resolved
2082
- * NON-EMPTY name is stored (terminal — a real session name won't later un-name),
2083
- * so we do NOT re-GET `/session/:id` every tick. A missing entry = not yet
2084
- * resolved OR resolved-but-still-empty re-fetch on next need, since OpenCode
2201
+ * NON-EMPTY, non-placeholder name is stored (terminal — a real session name
2202
+ * won't later un-name), so we do NOT re-GET `/session/:id` every tick. "Non-empty"
2203
+ * excludes OpenCode's synchronous default title (see
2204
+ * `OPENCODE_DEFAULT_TITLE_PREFIX`, #549) — that placeholder is treated the same
2205
+ * as an empty title so it never latches. A missing entry = not yet resolved OR
2206
+ * resolved-but-still-empty/placeholder → re-fetch on next need, since OpenCode
2085
2207
  * names sessions asynchronously mid-turn. Driver-level (not per-watcher) so both
2086
2208
  * the watcher completion path AND the restart-recovery re-adopt path (which has
2087
2209
  * no watcher) can resolve the title.
@@ -2445,7 +2567,7 @@ var ChannelDriver = class {
2445
2567
  }
2446
2568
  /**
2447
2569
  * Fetch ONE inbound image's bytes through Evident's WI-6 endpoint
2448
- * (`GET {apiUrl}/agents/{agentId}/attachments/{messageId}/{index}`) using the
2570
+ * (`GET {apiUrl}/runners/{agentId}/attachments/{messageId}/{index}`) using the
2449
2571
  * existing authenticated fetch, and base64-encode into a
2450
2572
  * `data:<mime>;base64,<…>` URL for the opencode `file` part's `url`.
2451
2573
  *
@@ -2453,15 +2575,38 @@ var ChannelDriver = class {
2453
2575
  * (not-owned / out-of-range / deleted-at-source / workspace gone) / 413
2454
2576
  * (over-cap). On ANY non-2xx or thrown failure we return `null` so the caller
2455
2577
  * OMITS that one image and the text turn still sends — NEVER throws the turn.
2456
- * Failures are logged with context (no silent swallow).
2578
+ * A 404 body carrying `{ reason: 'needs_reauth' }` (#547 the server CONFIRMED
2579
+ * a Slack `files:read` scope problem via `files.info`) instead resolves the
2580
+ * `AttachmentFetchNeedsReauth` sentinel, so the in-thread note can steer the
2581
+ * user to reconnect Slack instead of a generic "unavailable". Failures are
2582
+ * logged with context (no silent swallow).
2457
2583
  */
2458
2584
  async fetchAttachmentDataUrl(messageId, index, mime) {
2459
2585
  try {
2460
2586
  const res = await this.fetchImpl(
2461
- `${this.apiUrl}/agents/${this.agentId}/attachments/${messageId}/${index}`,
2587
+ `${this.apiUrl}/runners/${this.agentId}/attachments/${messageId}/${index}`,
2462
2588
  { headers: { Authorization: this.getAuthHeader() } }
2463
2589
  );
2464
2590
  if (!res.ok) {
2591
+ let reason;
2592
+ try {
2593
+ const body = await res.json();
2594
+ if (body && typeof body.reason === "string") reason = body.reason;
2595
+ } catch (parseErr) {
2596
+ this.log({
2597
+ level: "debug",
2598
+ message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index}: error body was not JSON (${parseErr instanceof Error ? parseErr.message : String(parseErr)}) \u2014 treating as a plain failure`,
2599
+ message_id: messageId
2600
+ });
2601
+ }
2602
+ if (reason === "needs_reauth") {
2603
+ this.log({
2604
+ level: "error",
2605
+ message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} returned HTTP ${res.status} \u2014 server confirmed a Slack reauth/scope problem \u2014 omitting this image (text turn proceeds)`,
2606
+ message_id: messageId
2607
+ });
2608
+ return { needsReauth: true };
2609
+ }
2465
2610
  this.log({
2466
2611
  level: "error",
2467
2612
  message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} returned HTTP ${res.status} \u2014 omitting this image (text turn proceeds)`,
@@ -2501,6 +2646,9 @@ var ChannelDriver = class {
2501
2646
  if (this.attachmentsSkippedSignalled.has(messageId)) return;
2502
2647
  this.attachmentsSkippedSignalled.add(messageId);
2503
2648
  const skippedReason = capabilityUnknown ? "unknown" : "unsupported";
2649
+ const failedReason = outcomes.some(
2650
+ (o) => o.status === "failed" && o.reason === "needs_reauth"
2651
+ ) ? "needs_reauth" : void 0;
2504
2652
  this.log({
2505
2653
  level: "info",
2506
2654
  message: `Message ${messageId.slice(0, 8)}: ${skipped} image(s) skipped (${capabilityUnknown ? "capability was unreadable \u2014 failed open to text-only" : "model not attachment-capable"}), ${failed} image(s) unavailable (deleted-at-source or fetch failure) \u2014 noting to Evident`,
@@ -2510,7 +2658,8 @@ var ChannelDriver = class {
2510
2658
  void this.postSignal(conversationId, messageId, "attachments_skipped", {
2511
2659
  skipped,
2512
2660
  failed,
2513
- ...skipped > 0 ? { skipped_reason: skippedReason } : {}
2661
+ ...skipped > 0 ? { skipped_reason: skippedReason } : {},
2662
+ ...failedReason ? { failed_reason: failedReason } : {}
2514
2663
  });
2515
2664
  }
2516
2665
  /** Register a freshly-dispatched message with its session's watcher state. */
@@ -2790,13 +2939,15 @@ var ChannelDriver = class {
2790
2939
  message_id: inFlight.evidentMessageId
2791
2940
  });
2792
2941
  const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
2942
+ const usage = messageUsage(messages, inFlight.opencodeMessageId);
2793
2943
  try {
2794
2944
  await this.markDone(
2795
2945
  conv.id,
2796
2946
  inFlight.evidentMessageId,
2797
2947
  sessionId,
2798
2948
  inFlight.opencodeMessageId,
2799
- title
2949
+ title,
2950
+ usage
2800
2951
  );
2801
2952
  } catch (err) {
2802
2953
  if (err instanceof ChannelAuthError) throw err;
@@ -2843,8 +2994,9 @@ var ChannelDriver = class {
2843
2994
  conversation_id: conv.id,
2844
2995
  message_id: inFlight.evidentMessageId
2845
2996
  });
2997
+ const usage = messageUsage(messages, inFlight.opencodeMessageId);
2846
2998
  try {
2847
- await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
2999
+ await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2, usage);
2848
3000
  } catch (err) {
2849
3001
  if (err instanceof ChannelAuthError) throw err;
2850
3002
  if (err instanceof ChannelTerminalError) {
@@ -3081,7 +3233,8 @@ var ChannelDriver = class {
3081
3233
  });
3082
3234
  try {
3083
3235
  const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
3084
- await this.markDone(row.conversation_id, row.id, sessionId, ocId, title);
3236
+ const usage = messageUsage(messages, ocId ?? "");
3237
+ await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
3085
3238
  } catch (err) {
3086
3239
  if (err instanceof ChannelAuthError) throw err;
3087
3240
  if (err instanceof ChannelTerminalError) {
@@ -3109,6 +3262,7 @@ var ChannelDriver = class {
3109
3262
  }
3110
3263
  if (state === "failed") {
3111
3264
  const error2 = messageError(messages, ocId ?? "") ?? void 0;
3265
+ const usage = messageUsage(messages, ocId ?? "");
3112
3266
  this.log({
3113
3267
  level: "error",
3114
3268
  message: `Re-adopt: message ${row.id.slice(0, 8)} errored while unwatched \u2014 marking failed: ${error2 ?? "(no error text)"}`,
@@ -3116,7 +3270,7 @@ var ChannelDriver = class {
3116
3270
  message_id: row.id
3117
3271
  });
3118
3272
  try {
3119
- await this.markFailed(row.conversation_id, row.id, sessionId, error2);
3273
+ await this.markFailed(row.conversation_id, row.id, sessionId, error2, usage);
3120
3274
  } catch (err) {
3121
3275
  if (err instanceof ChannelAuthError) throw err;
3122
3276
  if (err instanceof ChannelTerminalError) {
@@ -3544,19 +3698,36 @@ var ChannelDriver = class {
3544
3698
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
3545
3699
  return parent;
3546
3700
  }
3701
+ /**
3702
+ * OpenCode's synchronous default session title (e.g.
3703
+ * `"New session - 1737800000000"`), assigned immediately when a session is
3704
+ * created — before OpenCode's async LLM-based auto-titling later renames it
3705
+ * mid-turn (#549). Matched by this literal, case-sensitive prefix only; the
3706
+ * timestamp suffix's exact format is deliberately NOT matched, since the prefix
3707
+ * alone is the stable, cheap signal and over-anchoring on the timestamp
3708
+ * representation risks silently breaking if OpenCode ever changes it. Accepted
3709
+ * trade-off: a genuine LLM-assigned title that happens to literally start with
3710
+ * this prefix would also fail to latch (see `resolveSessionTitle`) —
3711
+ * vanishingly unlikely in practice, and deliberately not engineered around.
3712
+ */
3713
+ static OPENCODE_DEFAULT_TITLE_PREFIX = /^New session - /;
3547
3714
  /**
3548
3715
  * Resolve (and cache in `sessionTitles`) the OpenCode session TITLE (#310) so the
3549
3716
  * status PATCH can carry it into the "Live sessions" list. Driver-level cache so
3550
3717
  * BOTH the watcher completion path and the restart-recovery re-adopt path (which
3551
3718
  * has no watcher) can use it. `conversationId` is passed only for log context.
3552
3719
  * Best-effort:
3553
- * - a resolved NON-EMPTY title is cached and terminal (a real session name
3720
+ * - a resolved NON-EMPTY title that does NOT match
3721
+ * `OPENCODE_DEFAULT_TITLE_PREFIX` is cached and terminal (a real session name
3554
3722
  * won't later un-name), so we do NOT re-GET `/session/:id` every tick;
3555
- * - while the title is still absent/empty we do NOT latch it — OpenCode names
3556
- * sessions asynchronously mid-turn, so an early call (e.g. at `processing`)
3557
- * must leave the cache unresolved and re-fetch on the next need so a later
3558
- * call (e.g. at `done`) picks up the name assigned in the meantime. Such a
3559
- * call returns `null` (omit the title on THIS PATCH) without caching;
3723
+ * - while the title is still absent, empty, or matches the OpenCode
3724
+ * placeholder prefix (#549) we do NOT latch it OpenCode names sessions
3725
+ * asynchronously mid-turn, so an early call (e.g. at `processing`) must leave
3726
+ * the cache unresolved and re-fetch on the next need so a later call (e.g. at
3727
+ * `done`) picks up the name assigned in the meantime. Such a call returns
3728
+ * `null` (omit the title on THIS PATCH) without caching. If a session is
3729
+ * never renamed, the title is omitted forever rather than ever persisting
3730
+ * the placeholder as a last resort;
3560
3731
  * - a failed request likewise leaves the cache unresolved (retry next need)
3561
3732
  * and returns `null` — it must NEVER throw or block completion.
3562
3733
  * A failure is logged with agent/session context (no silent catch).
@@ -3569,7 +3740,7 @@ var ChannelDriver = class {
3569
3740
  if (res.ok) {
3570
3741
  const body = await res.json();
3571
3742
  const title = body && typeof body.title === "string" ? body.title.trim() : "";
3572
- if (title.length > 0) {
3743
+ if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
3573
3744
  this.sessionTitles.set(sessionId, title);
3574
3745
  return title;
3575
3746
  }
@@ -3719,7 +3890,7 @@ var ChannelDriver = class {
3719
3890
  // Evident API calls (combinedAuth thread routes)
3720
3891
  async getPendingConversations() {
3721
3892
  const res = await this.fetchImpl(
3722
- `${this.apiUrl}/agents/${this.agentId}/conversations/pending`,
3893
+ `${this.apiUrl}/runners/${this.agentId}/conversations/pending`,
3723
3894
  {
3724
3895
  headers: { Authorization: this.getAuthHeader() }
3725
3896
  }
@@ -3737,7 +3908,7 @@ var ChannelDriver = class {
3737
3908
  }
3738
3909
  async getPendingMessages(conversationId) {
3739
3910
  const res = await this.fetchImpl(
3740
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages?status=pending`,
3911
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages?status=pending`,
3741
3912
  { headers: { Authorization: this.getAuthHeader() } }
3742
3913
  );
3743
3914
  this.assertAuth(res, "fetching pending messages");
@@ -3761,7 +3932,7 @@ var ChannelDriver = class {
3761
3932
  */
3762
3933
  async getProcessingMessages() {
3763
3934
  const res = await this.fetchImpl(
3764
- `${this.apiUrl}/agents/${this.agentId}/conversations/processing`,
3935
+ `${this.apiUrl}/runners/${this.agentId}/conversations/processing`,
3765
3936
  { headers: { Authorization: this.getAuthHeader() } }
3766
3937
  );
3767
3938
  this.assertAuth(res, "fetching processing messages");
@@ -3798,7 +3969,7 @@ var ChannelDriver = class {
3798
3969
  */
3799
3970
  async markProcessing(conversationId, messageId, sessionId, opencodeMessageId, title) {
3800
3971
  const res = await this.fetchImpl(
3801
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
3972
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
3802
3973
  {
3803
3974
  method: "PATCH",
3804
3975
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -3845,9 +4016,9 @@ var ChannelDriver = class {
3845
4016
  * watcher retries next tick within the
3846
4017
  * deadline, Finding 4).
3847
4018
  */
3848
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title) {
4019
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
3849
4020
  const res = await this.fetchImpl(
3850
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
4021
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
3851
4022
  {
3852
4023
  method: "PATCH",
3853
4024
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -3855,7 +4026,8 @@ var ChannelDriver = class {
3855
4026
  status: "done",
3856
4027
  opencode_session_id: sessionId,
3857
4028
  ...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
3858
- ...title ? { title } : {}
4029
+ ...title ? { title } : {},
4030
+ ...usage ? usage : {}
3859
4031
  })
3860
4032
  }
3861
4033
  );
@@ -3873,14 +4045,15 @@ var ChannelDriver = class {
3873
4045
  * OpenCode turn sends `{status:'failed', opencode_session_id, error}` so the
3874
4046
  * failure reason reaches the channel.
3875
4047
  */
3876
- async markFailed(conversationId, messageId, sessionId, error2) {
4048
+ async markFailed(conversationId, messageId, sessionId, error2, usage) {
3877
4049
  const body = { status: "failed" };
3878
4050
  if (sessionId !== void 0) body.opencode_session_id = sessionId;
3879
4051
  if (error2 !== void 0) body.error = error2;
4052
+ if (usage) Object.assign(body, usage);
3880
4053
  await this.callWithRetry(
3881
4054
  "marking message as failed",
3882
4055
  () => this.fetchImpl(
3883
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
4056
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
3884
4057
  {
3885
4058
  method: "PATCH",
3886
4059
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -3907,7 +4080,7 @@ var ChannelDriver = class {
3907
4080
  async postSignal(conversationId, messageId, signal, extra) {
3908
4081
  try {
3909
4082
  const res = await this.fetchImpl(
3910
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}/signal`,
4083
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}/signal`,
3911
4084
  {
3912
4085
  method: "POST",
3913
4086
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -3936,7 +4109,7 @@ var ChannelDriver = class {
3936
4109
  }
3937
4110
  async persistSession(conversationId, sessionId) {
3938
4111
  const res = await this.fetchImpl(
3939
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}`,
4112
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}`,
3940
4113
  {
3941
4114
  method: "PATCH",
3942
4115
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -3962,7 +4135,7 @@ var ChannelDriver = class {
3962
4135
  await this.callWithRetry(
3963
4136
  "reporting interactive event",
3964
4137
  () => this.fetchImpl(
3965
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/interactive-event`,
4138
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/interactive-event`,
3966
4139
  {
3967
4140
  method: "POST",
3968
4141
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -4205,7 +4378,7 @@ async function resolveAgentIdFromKey(authHeader) {
4205
4378
  async function notifyAgentDisconnected(agentId, authHeader) {
4206
4379
  const apiUrl = getApiUrlConfig();
4207
4380
  try {
4208
- const response = await fetch(`${apiUrl}/agents/${agentId}/disconnect`, {
4381
+ const response = await fetch(`${apiUrl}/runners/${agentId}/disconnect`, {
4209
4382
  method: "POST",
4210
4383
  headers: { Authorization: authHeader }
4211
4384
  });
@@ -4224,7 +4397,7 @@ async function notifyAgentDisconnected(agentId, authHeader) {
4224
4397
  async function getAgentInfo(agentId, authHeader) {
4225
4398
  const apiUrl = getApiUrlConfig();
4226
4399
  try {
4227
- const response = await fetch(`${apiUrl}/agents/${agentId}`, {
4400
+ const response = await fetch(`${apiUrl}/runners/${agentId}`, {
4228
4401
  headers: { Authorization: authHeader }
4229
4402
  });
4230
4403
  if (response.status === 401) {
@@ -4611,7 +4784,7 @@ async function run(options) {
4611
4784
  return;
4612
4785
  }
4613
4786
  const state = {
4614
- agentId: options.agent || "",
4787
+ agentId: options.runner || options.agent || "",
4615
4788
  agentName: null,
4616
4789
  port: options.port ?? 4096,
4617
4790
  conversationFilter: options.conversation ?? null,
@@ -4633,6 +4806,19 @@ async function run(options) {
4633
4806
  sessionCleanupTimers: [],
4634
4807
  authHeader: ""
4635
4808
  };
4809
+ if (!options.runner && options.agent) {
4810
+ telemetry.info(
4811
+ EventTypes.DEPRECATED_AGENT_FLAG_USED,
4812
+ "Deprecated --agent flag used instead of --runner",
4813
+ { command: "run" },
4814
+ state.agentId
4815
+ );
4816
+ const agentFlagNotice = "--agent is deprecated, use --runner instead; will be removed in a future release.";
4817
+ log2(state, agentFlagNotice, "warn");
4818
+ if (state.interactive && !state.json) {
4819
+ logActivity(state, { type: "info", level: "warn", message: agentFlagNotice });
4820
+ }
4821
+ }
4636
4822
  if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
4637
4823
  log2(
4638
4824
  state,
@@ -4661,7 +4847,9 @@ async function run(options) {
4661
4847
  if (!interactive) {
4662
4848
  printError("Authentication required");
4663
4849
  blank();
4664
- console.log(chalk6.dim("Set EVIDENT_AGENT_KEY environment variable for CI"));
4850
+ console.log(
4851
+ chalk6.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
4852
+ );
4665
4853
  console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
4666
4854
  blank();
4667
4855
  process.exit(1);
@@ -4675,6 +4863,25 @@ async function run(options) {
4675
4863
  );
4676
4864
  }
4677
4865
  state.authHeader = getAuthHeader(credentials2);
4866
+ if (credentials2.notice) {
4867
+ log2(state, credentials2.notice, "warn");
4868
+ if (state.interactive && !state.json) {
4869
+ logActivity(state, { type: "info", level: "warn", message: credentials2.notice });
4870
+ }
4871
+ }
4872
+ if (credentials2.keySource === "agent_key") {
4873
+ telemetry.info(
4874
+ EventTypes.DEPRECATED_AGENT_KEY_ENV_USED,
4875
+ "Deprecated EVIDENT_AGENT_KEY env var used instead of EVIDENT_RUNNER_KEY",
4876
+ { command: "run" },
4877
+ state.agentId
4878
+ );
4879
+ const agentKeyNotice = "EVIDENT_AGENT_KEY is deprecated, use EVIDENT_RUNNER_KEY instead; will be removed in a future release.";
4880
+ log2(state, agentKeyNotice, "warn");
4881
+ if (state.interactive && !state.json) {
4882
+ logActivity(state, { type: "info", level: "warn", message: agentKeyNotice });
4883
+ }
4884
+ }
4678
4885
  if (!state.agentId) {
4679
4886
  if (credentials2.authType === "agent_key") {
4680
4887
  const resolved = await resolveAgentIdFromKey(state.authHeader);
@@ -4692,9 +4899,15 @@ async function run(options) {
4692
4899
  process.exit(1);
4693
4900
  }
4694
4901
  } else {
4695
- printError("--agent is required when not using EVIDENT_AGENT_KEY");
4902
+ printError(
4903
+ "--runner (or --agent) is required when not using EVIDENT_RUNNER_KEY or EVIDENT_AGENT_KEY"
4904
+ );
4696
4905
  blank();
4697
- console.log(chalk6.dim("Either provide --agent <id> or set EVIDENT_AGENT_KEY"));
4906
+ console.log(
4907
+ chalk6.dim(
4908
+ "Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
4909
+ )
4910
+ );
4698
4911
  blank();
4699
4912
  process.exit(1);
4700
4913
  }
@@ -4758,6 +4971,21 @@ async function run(options) {
4758
4971
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
4759
4972
  }
4760
4973
  }
4974
+ const noProviderWarning = buildNoProviderWarning(await hasAnyConfiguredProvider(state.port));
4975
+ if (noProviderWarning) {
4976
+ log2(state, noProviderWarning, "warn");
4977
+ if (state.interactive && !state.json) {
4978
+ logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
4979
+ blank();
4980
+ console.log(chalk6.yellow("\u26A0 No OpenCode model provider is configured."));
4981
+ console.log(
4982
+ chalk6.dim(
4983
+ `Run ${chalk6.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
4984
+ )
4985
+ );
4986
+ blank();
4987
+ }
4988
+ }
4761
4989
  } catch (error2) {
4762
4990
  ocSpinner?.fail(error2.message);
4763
4991
  throw error2;
@@ -4909,7 +5137,7 @@ async function run(options) {
4909
5137
  }
4910
5138
  telemetry.error(EventTypes.CLI_ERROR, `Run command failed: ${message}`, {
4911
5139
  command: "run",
4912
- agentId: options.agent
5140
+ agentId: options.runner || options.agent
4913
5141
  });
4914
5142
  await shutdownTelemetry();
4915
5143
  process.exit(1);
@@ -4934,7 +5162,7 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
4934
5162
  program.command("login").description("Authenticate with Evident").option("--token", "Use token-based authentication (for CI/CD)").option("--no-browser", "Do not open the browser automatically").action(login);
4935
5163
  program.command("logout").description("Remove stored credentials for the current endpoint").option("--all", "Remove stored credentials for all endpoints").action((options) => logout({ all: options.all }));
4936
5164
  program.command("whoami").description("Show the currently logged in user").action(whoami);
4937
- program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "Runner ID to connect to (optional when EVIDENT_AGENT_KEY is set)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option(
5165
+ program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "Runner ID to connect to (optional when EVIDENT_AGENT_KEY is set)").option("--runner [id]", "Alias for --agent (preferred name; wins if both are given)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option(
4938
5166
  "--log-level <level>",
4939
5167
  "Log verbosity: debug | info | warn | error (default: info). Env: EVIDENT_LOG_LEVEL"
4940
5168
  ).option("-v, --verbose", "Alias for --log-level debug (ignored if --log-level is set)").option("-c, --conversation <id>", "Process only this specific conversation").option("--idle-timeout <seconds>", "Exit after N seconds idle").option("--json", "Output in JSON format").option(
@@ -4950,6 +5178,7 @@ program.command("run").description("Connect to Evident and process messages").op
4950
5178
  (options) => {
4951
5179
  run({
4952
5180
  agent: options.agent,
5181
+ runner: options.runner,
4953
5182
  port: parseInt(options.port, 10),
4954
5183
  // Raw string — validation/precedence is single-sourced in run.ts's
4955
5184
  // resolveLogLevel (flag > -v > EVIDENT_LOG_LEVEL > info).