@evident-ai/cli 3.1.1-dev.702ee74 → 3.1.1-dev.716aea1
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 +10 -4
- package/dist/index.js +245 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,7 @@ Options:
|
|
|
85
85
|
|
|
86
86
|
- `-a, --agent [id]` — Runner ID to connect to. Optional when `EVIDENT_AGENT_KEY`
|
|
87
87
|
is set (the runner is then resolved automatically from the key).
|
|
88
|
+
- `--runner [id]` — Alias for `--agent` (preferred name; wins if both are given).
|
|
88
89
|
- `-p, --port <port>` — OpenCode port (default: `4096`).
|
|
89
90
|
- `--log-level <level>` — Log verbosity: `debug | info | warn | error` (default:
|
|
90
91
|
`info`). Env: `EVIDENT_LOG_LEVEL`.
|
|
@@ -105,15 +106,18 @@ targets the **production** Evident platform by default.
|
|
|
105
106
|
|
|
106
107
|
## Environment variables
|
|
107
108
|
|
|
108
|
-
- `
|
|
109
|
-
that runner and resolves the runner ID automatically, so `--agent`
|
|
110
|
-
required. Ideal for CI/CD.
|
|
109
|
+
- `EVIDENT_RUNNER_KEY` — A runner key. When set, `evident run` authenticates as
|
|
110
|
+
that runner and resolves the runner ID automatically, so `--runner`/`--agent`
|
|
111
|
+
is not required. Ideal for CI/CD. Preferred name; wins over `EVIDENT_AGENT_KEY`
|
|
112
|
+
if both are set.
|
|
113
|
+
- `EVIDENT_AGENT_KEY` — Alias for `EVIDENT_RUNNER_KEY` (still fully supported).
|
|
111
114
|
- `EVIDENT_TOKEN` — A user token used for authentication (alternative to a
|
|
112
115
|
keychain login from `evident login`).
|
|
113
116
|
- `EVIDENT_API_URL` — Override the API base URL (equivalent to `--endpoint`).
|
|
114
117
|
- `EVIDENT_TUNNEL_URL` — Override the tunnel relay URL (equivalent to `--tunnel`).
|
|
115
118
|
|
|
116
|
-
Authentication precedence for `run`: `EVIDENT_AGENT_KEY`
|
|
119
|
+
Authentication precedence for `run`: `EVIDENT_RUNNER_KEY`/`EVIDENT_AGENT_KEY`
|
|
120
|
+
(tied; `EVIDENT_RUNNER_KEY` wins if both are set) → `EVIDENT_TOKEN` →
|
|
117
121
|
credentials stored by `evident login`. For the URL flags, an explicit
|
|
118
122
|
`--endpoint` / `--tunnel` flag wins over the matching environment variable, which
|
|
119
123
|
in turn overrides the production default.
|
|
@@ -150,6 +154,8 @@ For CI or unattended use, set `EVIDENT_AGENT_KEY` and omit `--agent`:
|
|
|
150
154
|
EVIDENT_AGENT_KEY=<agent-key> evident run --idle-timeout 30
|
|
151
155
|
```
|
|
152
156
|
|
|
157
|
+
(`EVIDENT_RUNNER_KEY` is equivalent and the preferred name — use whichever you like.)
|
|
158
|
+
|
|
153
159
|
## How it works
|
|
154
160
|
|
|
155
161
|
```
|
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
|
|
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(
|
|
@@ -1370,6 +1394,72 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
1370
1394
|
}
|
|
1371
1395
|
return lastOk ?? last;
|
|
1372
1396
|
}
|
|
1397
|
+
function messageUsage(messages, userMessageId) {
|
|
1398
|
+
if (!messages || messages.length === 0) return null;
|
|
1399
|
+
const byParentAll = messages.filter(
|
|
1400
|
+
(m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
|
|
1401
|
+
);
|
|
1402
|
+
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
1403
|
+
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
1404
|
+
let correlated;
|
|
1405
|
+
if (byParent.length > 0) {
|
|
1406
|
+
correlated = byParent;
|
|
1407
|
+
} else {
|
|
1408
|
+
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
1409
|
+
correlated = reply ? [reply] : [];
|
|
1410
|
+
}
|
|
1411
|
+
if (correlated.length === 0) return null;
|
|
1412
|
+
let sawAnyUsage = false;
|
|
1413
|
+
let inputSum = 0;
|
|
1414
|
+
let outputSum = 0;
|
|
1415
|
+
let reasoningSum = 0;
|
|
1416
|
+
let cacheReadSum = 0;
|
|
1417
|
+
let cacheWriteSum = 0;
|
|
1418
|
+
let costSum = 0;
|
|
1419
|
+
let sawCost = false;
|
|
1420
|
+
let modelId = null;
|
|
1421
|
+
let providerId = null;
|
|
1422
|
+
for (const m of correlated) {
|
|
1423
|
+
const info = m.info;
|
|
1424
|
+
if (!info) continue;
|
|
1425
|
+
const tokens = info.tokens;
|
|
1426
|
+
if (tokens) {
|
|
1427
|
+
sawAnyUsage = true;
|
|
1428
|
+
inputSum += tokens.input ?? 0;
|
|
1429
|
+
outputSum += tokens.output ?? 0;
|
|
1430
|
+
reasoningSum += tokens.reasoning ?? 0;
|
|
1431
|
+
cacheReadSum += tokens.cache?.read ?? 0;
|
|
1432
|
+
cacheWriteSum += tokens.cache?.write ?? 0;
|
|
1433
|
+
}
|
|
1434
|
+
if (typeof info.cost === "number") {
|
|
1435
|
+
sawAnyUsage = true;
|
|
1436
|
+
sawCost = true;
|
|
1437
|
+
costSum += info.cost;
|
|
1438
|
+
}
|
|
1439
|
+
if (typeof info.modelID === "string") {
|
|
1440
|
+
sawAnyUsage = true;
|
|
1441
|
+
modelId = info.modelID;
|
|
1442
|
+
}
|
|
1443
|
+
if (typeof info.providerID === "string") {
|
|
1444
|
+
sawAnyUsage = true;
|
|
1445
|
+
providerId = info.providerID;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
if (!sawAnyUsage) return null;
|
|
1449
|
+
return {
|
|
1450
|
+
usage_provider_id: providerId,
|
|
1451
|
+
usage_model_id: modelId,
|
|
1452
|
+
usage_tokens_input: inputSum,
|
|
1453
|
+
usage_tokens_output: outputSum,
|
|
1454
|
+
usage_tokens_reasoning: reasoningSum,
|
|
1455
|
+
usage_tokens_cache_read: cacheReadSum,
|
|
1456
|
+
usage_tokens_cache_write: cacheWriteSum,
|
|
1457
|
+
// NULL means "OpenCode never reported a cost" (never inferred from
|
|
1458
|
+
// tokens) — distinct from a genuine 0-cost turn, which would set
|
|
1459
|
+
// `sawCost` true with `costSum === 0`.
|
|
1460
|
+
usage_cost_usd: sawCost ? costSum : null
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1373
1463
|
function messageRunState(messages, userMessageId) {
|
|
1374
1464
|
if (!messages || messages.length === 0) return "unknown";
|
|
1375
1465
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -1405,6 +1495,37 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
1405
1495
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
1406
1496
|
);
|
|
1407
1497
|
}
|
|
1498
|
+
async function hasAnyConfiguredProvider(port) {
|
|
1499
|
+
try {
|
|
1500
|
+
const res = await fetch(`${opencodeBase(port)}/config/providers`);
|
|
1501
|
+
if (!res.ok) {
|
|
1502
|
+
console.error(
|
|
1503
|
+
`[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
1504
|
+
);
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
const body = await res.json();
|
|
1508
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
1509
|
+
console.error(
|
|
1510
|
+
`[hasAnyConfiguredProvider] GET /config/providers body was not a plain object (port ${port})`
|
|
1511
|
+
);
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
const defaults2 = body.default;
|
|
1515
|
+
if (!defaults2 || typeof defaults2 !== "object" || Array.isArray(defaults2)) {
|
|
1516
|
+
console.error(
|
|
1517
|
+
`[hasAnyConfiguredProvider] GET /config/providers body had no \`default\` object (port ${port})`
|
|
1518
|
+
);
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
return Object.keys(defaults2).length > 0;
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
console.error(
|
|
1524
|
+
`[hasAnyConfiguredProvider] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
1525
|
+
);
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1408
1529
|
|
|
1409
1530
|
// src/lib/opencode/session-cleanup.ts
|
|
1410
1531
|
var DURATION_UNIT_MS = {
|
|
@@ -1716,7 +1837,6 @@ function connectTunnel(options) {
|
|
|
1716
1837
|
onConnected,
|
|
1717
1838
|
onDisconnected,
|
|
1718
1839
|
onError,
|
|
1719
|
-
onRequest,
|
|
1720
1840
|
onResponse,
|
|
1721
1841
|
onInfo,
|
|
1722
1842
|
onDrainPing
|
|
@@ -1729,18 +1849,8 @@ function connectTunnel(options) {
|
|
|
1729
1849
|
Authorization: authHeader
|
|
1730
1850
|
}
|
|
1731
1851
|
});
|
|
1732
|
-
const streamStartTimes = /* @__PURE__ */ new Map();
|
|
1733
1852
|
const forwarder = new StreamForwarder(ws, port, {
|
|
1734
|
-
|
|
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
|
-
},
|
|
1853
|
+
onHead: () => onResponse?.(),
|
|
1744
1854
|
onDrainPing: () => onDrainPing?.()
|
|
1745
1855
|
});
|
|
1746
1856
|
const connectionTimeout = setTimeout(() => {
|
|
@@ -1816,7 +1926,6 @@ function connectTunnel(options) {
|
|
|
1816
1926
|
ws.on("close", (code, reason) => {
|
|
1817
1927
|
const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
1818
1928
|
forwarder.abortAll();
|
|
1819
|
-
streamStartTimes.clear();
|
|
1820
1929
|
onDisconnected?.(code, reasonStr);
|
|
1821
1930
|
});
|
|
1822
1931
|
});
|
|
@@ -1954,7 +2063,7 @@ function backoffDelay(attempt, policy) {
|
|
|
1954
2063
|
function isRetryableStatus(status) {
|
|
1955
2064
|
return status === 429 || status >= 500 && status <= 599;
|
|
1956
2065
|
}
|
|
1957
|
-
var ChannelDriver = class {
|
|
2066
|
+
var ChannelDriver = class _ChannelDriver {
|
|
1958
2067
|
agentId;
|
|
1959
2068
|
port;
|
|
1960
2069
|
apiUrl;
|
|
@@ -2079,9 +2188,12 @@ var ChannelDriver = class {
|
|
|
2079
2188
|
sessionParents = /* @__PURE__ */ new Map();
|
|
2080
2189
|
/**
|
|
2081
2190
|
* Per-session OpenCode title cache (#310), keyed by sessionId. Only a resolved
|
|
2082
|
-
* NON-EMPTY name is stored (terminal — a real session name
|
|
2083
|
-
* so we do NOT re-GET `/session/:id` every tick.
|
|
2084
|
-
*
|
|
2191
|
+
* NON-EMPTY, non-placeholder name is stored (terminal — a real session name
|
|
2192
|
+
* won't later un-name), so we do NOT re-GET `/session/:id` every tick. "Non-empty"
|
|
2193
|
+
* excludes OpenCode's synchronous default title (see
|
|
2194
|
+
* `OPENCODE_DEFAULT_TITLE_PREFIX`, #549) — that placeholder is treated the same
|
|
2195
|
+
* as an empty title so it never latches. A missing entry = not yet resolved OR
|
|
2196
|
+
* resolved-but-still-empty/placeholder → re-fetch on next need, since OpenCode
|
|
2085
2197
|
* names sessions asynchronously mid-turn. Driver-level (not per-watcher) so both
|
|
2086
2198
|
* the watcher completion path AND the restart-recovery re-adopt path (which has
|
|
2087
2199
|
* no watcher) can resolve the title.
|
|
@@ -2445,7 +2557,7 @@ var ChannelDriver = class {
|
|
|
2445
2557
|
}
|
|
2446
2558
|
/**
|
|
2447
2559
|
* Fetch ONE inbound image's bytes through Evident's WI-6 endpoint
|
|
2448
|
-
* (`GET {apiUrl}/
|
|
2560
|
+
* (`GET {apiUrl}/runners/{agentId}/attachments/{messageId}/{index}`) using the
|
|
2449
2561
|
* existing authenticated fetch, and base64-encode into a
|
|
2450
2562
|
* `data:<mime>;base64,<…>` URL for the opencode `file` part's `url`.
|
|
2451
2563
|
*
|
|
@@ -2458,7 +2570,7 @@ var ChannelDriver = class {
|
|
|
2458
2570
|
async fetchAttachmentDataUrl(messageId, index, mime) {
|
|
2459
2571
|
try {
|
|
2460
2572
|
const res = await this.fetchImpl(
|
|
2461
|
-
`${this.apiUrl}/
|
|
2573
|
+
`${this.apiUrl}/runners/${this.agentId}/attachments/${messageId}/${index}`,
|
|
2462
2574
|
{ headers: { Authorization: this.getAuthHeader() } }
|
|
2463
2575
|
);
|
|
2464
2576
|
if (!res.ok) {
|
|
@@ -2790,13 +2902,15 @@ var ChannelDriver = class {
|
|
|
2790
2902
|
message_id: inFlight.evidentMessageId
|
|
2791
2903
|
});
|
|
2792
2904
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
2905
|
+
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
2793
2906
|
try {
|
|
2794
2907
|
await this.markDone(
|
|
2795
2908
|
conv.id,
|
|
2796
2909
|
inFlight.evidentMessageId,
|
|
2797
2910
|
sessionId,
|
|
2798
2911
|
inFlight.opencodeMessageId,
|
|
2799
|
-
title
|
|
2912
|
+
title,
|
|
2913
|
+
usage
|
|
2800
2914
|
);
|
|
2801
2915
|
} catch (err) {
|
|
2802
2916
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -2843,8 +2957,9 @@ var ChannelDriver = class {
|
|
|
2843
2957
|
conversation_id: conv.id,
|
|
2844
2958
|
message_id: inFlight.evidentMessageId
|
|
2845
2959
|
});
|
|
2960
|
+
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
2846
2961
|
try {
|
|
2847
|
-
await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
|
|
2962
|
+
await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2, usage);
|
|
2848
2963
|
} catch (err) {
|
|
2849
2964
|
if (err instanceof ChannelAuthError) throw err;
|
|
2850
2965
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -3081,7 +3196,8 @@ var ChannelDriver = class {
|
|
|
3081
3196
|
});
|
|
3082
3197
|
try {
|
|
3083
3198
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
3084
|
-
|
|
3199
|
+
const usage = messageUsage(messages, ocId ?? "");
|
|
3200
|
+
await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
|
|
3085
3201
|
} catch (err) {
|
|
3086
3202
|
if (err instanceof ChannelAuthError) throw err;
|
|
3087
3203
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -3109,6 +3225,7 @@ var ChannelDriver = class {
|
|
|
3109
3225
|
}
|
|
3110
3226
|
if (state === "failed") {
|
|
3111
3227
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
3228
|
+
const usage = messageUsage(messages, ocId ?? "");
|
|
3112
3229
|
this.log({
|
|
3113
3230
|
level: "error",
|
|
3114
3231
|
message: `Re-adopt: message ${row.id.slice(0, 8)} errored while unwatched \u2014 marking failed: ${error2 ?? "(no error text)"}`,
|
|
@@ -3116,7 +3233,7 @@ var ChannelDriver = class {
|
|
|
3116
3233
|
message_id: row.id
|
|
3117
3234
|
});
|
|
3118
3235
|
try {
|
|
3119
|
-
await this.markFailed(row.conversation_id, row.id, sessionId, error2);
|
|
3236
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, error2, usage);
|
|
3120
3237
|
} catch (err) {
|
|
3121
3238
|
if (err instanceof ChannelAuthError) throw err;
|
|
3122
3239
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -3544,19 +3661,36 @@ var ChannelDriver = class {
|
|
|
3544
3661
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
3545
3662
|
return parent;
|
|
3546
3663
|
}
|
|
3664
|
+
/**
|
|
3665
|
+
* OpenCode's synchronous default session title (e.g.
|
|
3666
|
+
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
3667
|
+
* created — before OpenCode's async LLM-based auto-titling later renames it
|
|
3668
|
+
* mid-turn (#549). Matched by this literal, case-sensitive prefix only; the
|
|
3669
|
+
* timestamp suffix's exact format is deliberately NOT matched, since the prefix
|
|
3670
|
+
* alone is the stable, cheap signal and over-anchoring on the timestamp
|
|
3671
|
+
* representation risks silently breaking if OpenCode ever changes it. Accepted
|
|
3672
|
+
* trade-off: a genuine LLM-assigned title that happens to literally start with
|
|
3673
|
+
* this prefix would also fail to latch (see `resolveSessionTitle`) —
|
|
3674
|
+
* vanishingly unlikely in practice, and deliberately not engineered around.
|
|
3675
|
+
*/
|
|
3676
|
+
static OPENCODE_DEFAULT_TITLE_PREFIX = /^New session - /;
|
|
3547
3677
|
/**
|
|
3548
3678
|
* Resolve (and cache in `sessionTitles`) the OpenCode session TITLE (#310) so the
|
|
3549
3679
|
* status PATCH can carry it into the "Live sessions" list. Driver-level cache so
|
|
3550
3680
|
* BOTH the watcher completion path and the restart-recovery re-adopt path (which
|
|
3551
3681
|
* has no watcher) can use it. `conversationId` is passed only for log context.
|
|
3552
3682
|
* Best-effort:
|
|
3553
|
-
* - a resolved NON-EMPTY title
|
|
3683
|
+
* - a resolved NON-EMPTY title that does NOT match
|
|
3684
|
+
* `OPENCODE_DEFAULT_TITLE_PREFIX` is cached and terminal (a real session name
|
|
3554
3685
|
* won't later un-name), so we do NOT re-GET `/session/:id` every tick;
|
|
3555
|
-
* - while the title is still absent
|
|
3556
|
-
*
|
|
3557
|
-
*
|
|
3558
|
-
*
|
|
3559
|
-
*
|
|
3686
|
+
* - while the title is still absent, empty, or matches the OpenCode
|
|
3687
|
+
* placeholder prefix (#549) we do NOT latch it — OpenCode names sessions
|
|
3688
|
+
* asynchronously mid-turn, so an early call (e.g. at `processing`) must leave
|
|
3689
|
+
* the cache unresolved and re-fetch on the next need so a later call (e.g. at
|
|
3690
|
+
* `done`) picks up the name assigned in the meantime. Such a call returns
|
|
3691
|
+
* `null` (omit the title on THIS PATCH) without caching. If a session is
|
|
3692
|
+
* never renamed, the title is omitted forever rather than ever persisting
|
|
3693
|
+
* the placeholder as a last resort;
|
|
3560
3694
|
* - a failed request likewise leaves the cache unresolved (retry next need)
|
|
3561
3695
|
* and returns `null` — it must NEVER throw or block completion.
|
|
3562
3696
|
* A failure is logged with agent/session context (no silent catch).
|
|
@@ -3569,7 +3703,7 @@ var ChannelDriver = class {
|
|
|
3569
3703
|
if (res.ok) {
|
|
3570
3704
|
const body = await res.json();
|
|
3571
3705
|
const title = body && typeof body.title === "string" ? body.title.trim() : "";
|
|
3572
|
-
if (title.length > 0) {
|
|
3706
|
+
if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
|
|
3573
3707
|
this.sessionTitles.set(sessionId, title);
|
|
3574
3708
|
return title;
|
|
3575
3709
|
}
|
|
@@ -3719,7 +3853,7 @@ var ChannelDriver = class {
|
|
|
3719
3853
|
// Evident API calls (combinedAuth thread routes)
|
|
3720
3854
|
async getPendingConversations() {
|
|
3721
3855
|
const res = await this.fetchImpl(
|
|
3722
|
-
`${this.apiUrl}/
|
|
3856
|
+
`${this.apiUrl}/runners/${this.agentId}/conversations/pending`,
|
|
3723
3857
|
{
|
|
3724
3858
|
headers: { Authorization: this.getAuthHeader() }
|
|
3725
3859
|
}
|
|
@@ -3737,7 +3871,7 @@ var ChannelDriver = class {
|
|
|
3737
3871
|
}
|
|
3738
3872
|
async getPendingMessages(conversationId) {
|
|
3739
3873
|
const res = await this.fetchImpl(
|
|
3740
|
-
`${this.apiUrl}/
|
|
3874
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages?status=pending`,
|
|
3741
3875
|
{ headers: { Authorization: this.getAuthHeader() } }
|
|
3742
3876
|
);
|
|
3743
3877
|
this.assertAuth(res, "fetching pending messages");
|
|
@@ -3761,7 +3895,7 @@ var ChannelDriver = class {
|
|
|
3761
3895
|
*/
|
|
3762
3896
|
async getProcessingMessages() {
|
|
3763
3897
|
const res = await this.fetchImpl(
|
|
3764
|
-
`${this.apiUrl}/
|
|
3898
|
+
`${this.apiUrl}/runners/${this.agentId}/conversations/processing`,
|
|
3765
3899
|
{ headers: { Authorization: this.getAuthHeader() } }
|
|
3766
3900
|
);
|
|
3767
3901
|
this.assertAuth(res, "fetching processing messages");
|
|
@@ -3798,7 +3932,7 @@ var ChannelDriver = class {
|
|
|
3798
3932
|
*/
|
|
3799
3933
|
async markProcessing(conversationId, messageId, sessionId, opencodeMessageId, title) {
|
|
3800
3934
|
const res = await this.fetchImpl(
|
|
3801
|
-
`${this.apiUrl}/
|
|
3935
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3802
3936
|
{
|
|
3803
3937
|
method: "PATCH",
|
|
3804
3938
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -3845,9 +3979,9 @@ var ChannelDriver = class {
|
|
|
3845
3979
|
* watcher retries next tick within the
|
|
3846
3980
|
* deadline, Finding 4).
|
|
3847
3981
|
*/
|
|
3848
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title) {
|
|
3982
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
3849
3983
|
const res = await this.fetchImpl(
|
|
3850
|
-
`${this.apiUrl}/
|
|
3984
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3851
3985
|
{
|
|
3852
3986
|
method: "PATCH",
|
|
3853
3987
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -3855,7 +3989,8 @@ var ChannelDriver = class {
|
|
|
3855
3989
|
status: "done",
|
|
3856
3990
|
opencode_session_id: sessionId,
|
|
3857
3991
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
3858
|
-
...title ? { title } : {}
|
|
3992
|
+
...title ? { title } : {},
|
|
3993
|
+
...usage ? usage : {}
|
|
3859
3994
|
})
|
|
3860
3995
|
}
|
|
3861
3996
|
);
|
|
@@ -3873,14 +4008,15 @@ var ChannelDriver = class {
|
|
|
3873
4008
|
* OpenCode turn sends `{status:'failed', opencode_session_id, error}` so the
|
|
3874
4009
|
* failure reason reaches the channel.
|
|
3875
4010
|
*/
|
|
3876
|
-
async markFailed(conversationId, messageId, sessionId, error2) {
|
|
4011
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage) {
|
|
3877
4012
|
const body = { status: "failed" };
|
|
3878
4013
|
if (sessionId !== void 0) body.opencode_session_id = sessionId;
|
|
3879
4014
|
if (error2 !== void 0) body.error = error2;
|
|
4015
|
+
if (usage) Object.assign(body, usage);
|
|
3880
4016
|
await this.callWithRetry(
|
|
3881
4017
|
"marking message as failed",
|
|
3882
4018
|
() => this.fetchImpl(
|
|
3883
|
-
`${this.apiUrl}/
|
|
4019
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3884
4020
|
{
|
|
3885
4021
|
method: "PATCH",
|
|
3886
4022
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -3907,7 +4043,7 @@ var ChannelDriver = class {
|
|
|
3907
4043
|
async postSignal(conversationId, messageId, signal, extra) {
|
|
3908
4044
|
try {
|
|
3909
4045
|
const res = await this.fetchImpl(
|
|
3910
|
-
`${this.apiUrl}/
|
|
4046
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}/signal`,
|
|
3911
4047
|
{
|
|
3912
4048
|
method: "POST",
|
|
3913
4049
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -3936,7 +4072,7 @@ var ChannelDriver = class {
|
|
|
3936
4072
|
}
|
|
3937
4073
|
async persistSession(conversationId, sessionId) {
|
|
3938
4074
|
const res = await this.fetchImpl(
|
|
3939
|
-
`${this.apiUrl}/
|
|
4075
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}`,
|
|
3940
4076
|
{
|
|
3941
4077
|
method: "PATCH",
|
|
3942
4078
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -3962,7 +4098,7 @@ var ChannelDriver = class {
|
|
|
3962
4098
|
await this.callWithRetry(
|
|
3963
4099
|
"reporting interactive event",
|
|
3964
4100
|
() => this.fetchImpl(
|
|
3965
|
-
`${this.apiUrl}/
|
|
4101
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/interactive-event`,
|
|
3966
4102
|
{
|
|
3967
4103
|
method: "POST",
|
|
3968
4104
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -4205,7 +4341,7 @@ async function resolveAgentIdFromKey(authHeader) {
|
|
|
4205
4341
|
async function notifyAgentDisconnected(agentId, authHeader) {
|
|
4206
4342
|
const apiUrl = getApiUrlConfig();
|
|
4207
4343
|
try {
|
|
4208
|
-
const response = await fetch(`${apiUrl}/
|
|
4344
|
+
const response = await fetch(`${apiUrl}/runners/${agentId}/disconnect`, {
|
|
4209
4345
|
method: "POST",
|
|
4210
4346
|
headers: { Authorization: authHeader }
|
|
4211
4347
|
});
|
|
@@ -4224,7 +4360,7 @@ async function notifyAgentDisconnected(agentId, authHeader) {
|
|
|
4224
4360
|
async function getAgentInfo(agentId, authHeader) {
|
|
4225
4361
|
const apiUrl = getApiUrlConfig();
|
|
4226
4362
|
try {
|
|
4227
|
-
const response = await fetch(`${apiUrl}/
|
|
4363
|
+
const response = await fetch(`${apiUrl}/runners/${agentId}`, {
|
|
4228
4364
|
headers: { Authorization: authHeader }
|
|
4229
4365
|
});
|
|
4230
4366
|
if (response.status === 401) {
|
|
@@ -4611,7 +4747,7 @@ async function run(options) {
|
|
|
4611
4747
|
return;
|
|
4612
4748
|
}
|
|
4613
4749
|
const state = {
|
|
4614
|
-
agentId: options.agent || "",
|
|
4750
|
+
agentId: options.runner || options.agent || "",
|
|
4615
4751
|
agentName: null,
|
|
4616
4752
|
port: options.port ?? 4096,
|
|
4617
4753
|
conversationFilter: options.conversation ?? null,
|
|
@@ -4633,6 +4769,19 @@ async function run(options) {
|
|
|
4633
4769
|
sessionCleanupTimers: [],
|
|
4634
4770
|
authHeader: ""
|
|
4635
4771
|
};
|
|
4772
|
+
if (!options.runner && options.agent) {
|
|
4773
|
+
telemetry.info(
|
|
4774
|
+
EventTypes.DEPRECATED_AGENT_FLAG_USED,
|
|
4775
|
+
"Deprecated --agent flag used instead of --runner",
|
|
4776
|
+
{ command: "run" },
|
|
4777
|
+
state.agentId
|
|
4778
|
+
);
|
|
4779
|
+
const agentFlagNotice = "--agent is deprecated, use --runner instead; will be removed in a future release.";
|
|
4780
|
+
log2(state, agentFlagNotice, "warn");
|
|
4781
|
+
if (state.interactive && !state.json) {
|
|
4782
|
+
logActivity(state, { type: "info", level: "warn", message: agentFlagNotice });
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4636
4785
|
if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
|
|
4637
4786
|
log2(
|
|
4638
4787
|
state,
|
|
@@ -4661,7 +4810,9 @@ async function run(options) {
|
|
|
4661
4810
|
if (!interactive) {
|
|
4662
4811
|
printError("Authentication required");
|
|
4663
4812
|
blank();
|
|
4664
|
-
console.log(
|
|
4813
|
+
console.log(
|
|
4814
|
+
chalk6.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
4815
|
+
);
|
|
4665
4816
|
console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
|
|
4666
4817
|
blank();
|
|
4667
4818
|
process.exit(1);
|
|
@@ -4675,6 +4826,25 @@ async function run(options) {
|
|
|
4675
4826
|
);
|
|
4676
4827
|
}
|
|
4677
4828
|
state.authHeader = getAuthHeader(credentials2);
|
|
4829
|
+
if (credentials2.notice) {
|
|
4830
|
+
log2(state, credentials2.notice, "warn");
|
|
4831
|
+
if (state.interactive && !state.json) {
|
|
4832
|
+
logActivity(state, { type: "info", level: "warn", message: credentials2.notice });
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4835
|
+
if (credentials2.keySource === "agent_key") {
|
|
4836
|
+
telemetry.info(
|
|
4837
|
+
EventTypes.DEPRECATED_AGENT_KEY_ENV_USED,
|
|
4838
|
+
"Deprecated EVIDENT_AGENT_KEY env var used instead of EVIDENT_RUNNER_KEY",
|
|
4839
|
+
{ command: "run" },
|
|
4840
|
+
state.agentId
|
|
4841
|
+
);
|
|
4842
|
+
const agentKeyNotice = "EVIDENT_AGENT_KEY is deprecated, use EVIDENT_RUNNER_KEY instead; will be removed in a future release.";
|
|
4843
|
+
log2(state, agentKeyNotice, "warn");
|
|
4844
|
+
if (state.interactive && !state.json) {
|
|
4845
|
+
logActivity(state, { type: "info", level: "warn", message: agentKeyNotice });
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4678
4848
|
if (!state.agentId) {
|
|
4679
4849
|
if (credentials2.authType === "agent_key") {
|
|
4680
4850
|
const resolved = await resolveAgentIdFromKey(state.authHeader);
|
|
@@ -4692,9 +4862,15 @@ async function run(options) {
|
|
|
4692
4862
|
process.exit(1);
|
|
4693
4863
|
}
|
|
4694
4864
|
} else {
|
|
4695
|
-
printError(
|
|
4865
|
+
printError(
|
|
4866
|
+
"--runner (or --agent) is required when not using EVIDENT_RUNNER_KEY or EVIDENT_AGENT_KEY"
|
|
4867
|
+
);
|
|
4696
4868
|
blank();
|
|
4697
|
-
console.log(
|
|
4869
|
+
console.log(
|
|
4870
|
+
chalk6.dim(
|
|
4871
|
+
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
4872
|
+
)
|
|
4873
|
+
);
|
|
4698
4874
|
blank();
|
|
4699
4875
|
process.exit(1);
|
|
4700
4876
|
}
|
|
@@ -4758,6 +4934,21 @@ async function run(options) {
|
|
|
4758
4934
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
4759
4935
|
}
|
|
4760
4936
|
}
|
|
4937
|
+
const noProviderWarning = buildNoProviderWarning(await hasAnyConfiguredProvider(state.port));
|
|
4938
|
+
if (noProviderWarning) {
|
|
4939
|
+
log2(state, noProviderWarning, "warn");
|
|
4940
|
+
if (state.interactive && !state.json) {
|
|
4941
|
+
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
4942
|
+
blank();
|
|
4943
|
+
console.log(chalk6.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
4944
|
+
console.log(
|
|
4945
|
+
chalk6.dim(
|
|
4946
|
+
`Run ${chalk6.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
4947
|
+
)
|
|
4948
|
+
);
|
|
4949
|
+
blank();
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4761
4952
|
} catch (error2) {
|
|
4762
4953
|
ocSpinner?.fail(error2.message);
|
|
4763
4954
|
throw error2;
|
|
@@ -4909,7 +5100,7 @@ async function run(options) {
|
|
|
4909
5100
|
}
|
|
4910
5101
|
telemetry.error(EventTypes.CLI_ERROR, `Run command failed: ${message}`, {
|
|
4911
5102
|
command: "run",
|
|
4912
|
-
agentId: options.agent
|
|
5103
|
+
agentId: options.runner || options.agent
|
|
4913
5104
|
});
|
|
4914
5105
|
await shutdownTelemetry();
|
|
4915
5106
|
process.exit(1);
|
|
@@ -4934,7 +5125,7 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
|
|
|
4934
5125
|
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
5126
|
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
5127
|
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(
|
|
5128
|
+
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
5129
|
"--log-level <level>",
|
|
4939
5130
|
"Log verbosity: debug | info | warn | error (default: info). Env: EVIDENT_LOG_LEVEL"
|
|
4940
5131
|
).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 +5141,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
4950
5141
|
(options) => {
|
|
4951
5142
|
run({
|
|
4952
5143
|
agent: options.agent,
|
|
5144
|
+
runner: options.runner,
|
|
4953
5145
|
port: parseInt(options.port, 10),
|
|
4954
5146
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
4955
5147
|
// resolveLogLevel (flag > -v > EVIDENT_LOG_LEVEL > info).
|