@evident-ai/cli 3.4.1-dev.8fa4d29 → 3.4.1-dev.b1bf8c5

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
@@ -746,6 +746,40 @@ async function reportClaudeUsage(agentId, authHeader, snapshot) {
746
746
  return { ok: false, error: describeBestEffortError(error2) };
747
747
  }
748
748
  }
749
+ function toReportedOpenAiWindow(window) {
750
+ if (!window) return null;
751
+ return {
752
+ utilization: window.utilization,
753
+ window_minutes: window.windowMinutes,
754
+ resets_at: window.resetsAt
755
+ };
756
+ }
757
+ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
758
+ try {
759
+ const apiUrl = getApiUrlConfig();
760
+ const response = await fetch(`${apiUrl}/runners/${agentId}/openai-usage`, {
761
+ method: "POST",
762
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
763
+ body: JSON.stringify({
764
+ primary: toReportedOpenAiWindow(snapshot.primary),
765
+ secondary: toReportedOpenAiWindow(snapshot.secondary),
766
+ has_credits: snapshot.hasCredits,
767
+ credits_unlimited: snapshot.creditsUnlimited
768
+ }),
769
+ signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
770
+ });
771
+ if (!response.ok) {
772
+ const serverMessage = await readErrorMessage(response);
773
+ return {
774
+ ok: false,
775
+ error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
776
+ };
777
+ }
778
+ return { ok: true };
779
+ } catch (error2) {
780
+ return { ok: false, error: describeBestEffortError(error2) };
781
+ }
782
+ }
749
783
  async function reportResourceUsage(agentId, authHeader, usage) {
750
784
  try {
751
785
  const apiUrl = getApiUrlConfig();
@@ -1092,8 +1126,8 @@ async function claudeUsage() {
1092
1126
  }
1093
1127
 
1094
1128
  // src/commands/run.ts
1095
- import { homedir as homedir3 } from "os";
1096
- import { isAbsolute as isAbsolute2, join as join5, parse, resolve as resolvePath } from "path";
1129
+ import { homedir as homedir4 } from "os";
1130
+ import { isAbsolute as isAbsolute2, join as join6, parse, resolve as resolvePath } from "path";
1097
1131
  import chalk6 from "chalk";
1098
1132
 
1099
1133
  // ../../packages/types/src/agents/index.ts
@@ -2991,6 +3025,177 @@ function writeTunnelReadyMarker(path, agentId) {
2991
3025
  }
2992
3026
  }
2993
3027
 
3028
+ // src/lib/openai-usage.ts
3029
+ import { readFileSync as readFileSync2 } from "fs";
3030
+ import { homedir as homedir2 } from "os";
3031
+ import { join as join3 } from "path";
3032
+ var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
3033
+ var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
3034
+ var OpenAiUsageError = class extends Error {
3035
+ constructor(message, reason) {
3036
+ super(message);
3037
+ this.reason = reason;
3038
+ }
3039
+ };
3040
+ function isLocalCredentialProblem2(err) {
3041
+ return err instanceof OpenAiUsageError && (err.reason === "no_credentials" || err.reason === "credentials_expired");
3042
+ }
3043
+ function readOpenCodeChatGptCredentials() {
3044
+ try {
3045
+ const raw = readFileSync2(join3(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3046
+ let parsed;
3047
+ try {
3048
+ parsed = JSON.parse(raw);
3049
+ } catch {
3050
+ return null;
3051
+ }
3052
+ const entry = parsed.openai;
3053
+ if (entry?.type !== "oauth" || typeof entry.access !== "string" || !entry.access || typeof entry.expires !== "number") {
3054
+ return null;
3055
+ }
3056
+ return { accessToken: entry.access, expiresAt: entry.expires };
3057
+ } catch (err) {
3058
+ const code = err.code;
3059
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
3060
+ console.warn(
3061
+ `readOpenCodeChatGptCredentials: reading auth.json failed (${code ?? "unknown"})`
3062
+ );
3063
+ }
3064
+ return null;
3065
+ }
3066
+ }
3067
+ function toWindow2(headers, name) {
3068
+ const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
3069
+ const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
3070
+ if (utilizationHeader == null || utilizationHeader === "" || windowMinutesHeader == null) {
3071
+ return null;
3072
+ }
3073
+ const utilization = Number(utilizationHeader);
3074
+ const windowMinutes = Number(windowMinutesHeader);
3075
+ if (!Number.isFinite(utilization) || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
3076
+ return null;
3077
+ }
3078
+ const resetAtHeader = headers.get(`x-codex-${name}-reset-at`);
3079
+ const resetSeconds = resetAtHeader == null || resetAtHeader === "" ? NaN : Number(resetAtHeader);
3080
+ const resetsAt = Number.isFinite(resetSeconds) ? new Date(resetSeconds * 1e3).toISOString() : null;
3081
+ return { utilization: Math.min(100, Math.max(0, utilization)), windowMinutes, resetsAt };
3082
+ }
3083
+ function parseCodexUsageHeaders(headers) {
3084
+ return {
3085
+ primary: toWindow2(headers, "primary"),
3086
+ secondary: toWindow2(headers, "secondary"),
3087
+ hasCredits: headers.has("x-codex-credits-has-credits") ? headers.get("x-codex-credits-has-credits")?.toLowerCase() === "true" : null,
3088
+ creditsUnlimited: headers.has("x-codex-credits-unlimited") ? headers.get("x-codex-credits-unlimited")?.toLowerCase() === "true" : null
3089
+ };
3090
+ }
3091
+ function normalizeProbeModel(model) {
3092
+ return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
3093
+ }
3094
+ async function resolveProbeModels(port) {
3095
+ try {
3096
+ const res = await withRequestTimeout(
3097
+ fetch,
3098
+ REQUEST_TIMEOUT_MS
3099
+ )(`${opencodeBase(port)}/config/providers`);
3100
+ if (!res.ok) {
3101
+ console.error(
3102
+ `[resolveProbeModels] GET /config/providers returned HTTP ${res.status} (port ${port})`
3103
+ );
3104
+ return [];
3105
+ }
3106
+ const body = await res.json();
3107
+ const provider = body?.providers?.find((candidate) => candidate?.id === "openai");
3108
+ if (!provider || !provider.models || typeof provider.models !== "object") return [];
3109
+ const candidates = [
3110
+ ...typeof body?.default?.openai === "string" ? [body.default.openai] : [],
3111
+ ...Object.keys(provider.models)
3112
+ ].map(normalizeProbeModel);
3113
+ return [...new Set(candidates)].slice(0, 4);
3114
+ } catch (err) {
3115
+ console.error(
3116
+ `[resolveProbeModels] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
3117
+ );
3118
+ return [];
3119
+ }
3120
+ }
3121
+ function hasPrimaryHeaders(headers) {
3122
+ return [
3123
+ "x-codex-primary-used-percent",
3124
+ "x-codex-primary-window-minutes",
3125
+ "x-codex-primary-reset-at"
3126
+ ].some((name) => headers.has(name));
3127
+ }
3128
+ async function getOpenAiUsage(port) {
3129
+ const credentials2 = readOpenCodeChatGptCredentials();
3130
+ if (!credentials2) {
3131
+ throw new OpenAiUsageError(
3132
+ "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
3133
+ "no_credentials"
3134
+ );
3135
+ }
3136
+ if (credentials2.expiresAt < Date.now()) {
3137
+ throw new OpenAiUsageError(
3138
+ "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
3139
+ "credentials_expired"
3140
+ );
3141
+ }
3142
+ const models = await resolveProbeModels(port);
3143
+ if (models.length === 0) {
3144
+ throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
3145
+ }
3146
+ let lastStatus;
3147
+ for (const model of models) {
3148
+ let res;
3149
+ try {
3150
+ res = await withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(CODEX_RESPONSES_URL, {
3151
+ method: "POST",
3152
+ headers: {
3153
+ Authorization: `Bearer ${credentials2.accessToken}`,
3154
+ "Content-Type": "application/json"
3155
+ },
3156
+ body: JSON.stringify({ model, store: false, stream: true })
3157
+ });
3158
+ } catch (err) {
3159
+ throw new OpenAiUsageError(
3160
+ `OpenAI usage probe request failed: ${err instanceof Error ? err.message : String(err)}.`,
3161
+ "request_failed"
3162
+ );
3163
+ }
3164
+ try {
3165
+ lastStatus = res.status;
3166
+ if (hasPrimaryHeaders(res.headers)) {
3167
+ const usage = parseCodexUsageHeaders(res.headers);
3168
+ if (!usage.primary && !usage.secondary) {
3169
+ throw new OpenAiUsageError(
3170
+ `OpenAI usage probe returned no usable window (HTTP ${res.status}).`,
3171
+ "no_usable_window"
3172
+ );
3173
+ }
3174
+ return usage;
3175
+ }
3176
+ if (res.status === 401) {
3177
+ throw new OpenAiUsageError(
3178
+ "ChatGPT credentials have expired (HTTP 401).",
3179
+ "credentials_expired"
3180
+ );
3181
+ }
3182
+ if (res.status === 403 || res.status === 429) {
3183
+ throw new OpenAiUsageError(
3184
+ `OpenAI usage probe was blocked (HTTP ${res.status}).`,
3185
+ "probe_blocked"
3186
+ );
3187
+ }
3188
+ } finally {
3189
+ await res.body?.cancel().catch(() => {
3190
+ });
3191
+ }
3192
+ }
3193
+ throw new OpenAiUsageError(
3194
+ `OpenAI usage probe failed: HTTP ${lastStatus ?? "unknown"}.`,
3195
+ "request_failed"
3196
+ );
3197
+ }
3198
+
2994
3199
  // src/lib/reporting-schedule.ts
2995
3200
  function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
2996
3201
  const jitterRangeMs = baseMs * jitterFraction;
@@ -2999,6 +3204,31 @@ function jitteredDelayMs(baseMs, jitterFraction, random = Math.random) {
2999
3204
  function firstReportDelayMs(random = Math.random) {
3000
3205
  return 5e3 + random() * 1e4;
3001
3206
  }
3207
+ var VALID_USAGE_REPORTING_MODES = ["auto", "on", "off"];
3208
+ function resolveUsageReportingMode(flagValue, env, names) {
3209
+ const raw = flagValue ?? env[names.envVar];
3210
+ if (raw === void 0 || raw === "") return { mode: "auto", warnings: [] };
3211
+ const normalized = raw.trim().toLowerCase();
3212
+ if (VALID_USAGE_REPORTING_MODES.includes(normalized)) {
3213
+ return { mode: normalized, warnings: [] };
3214
+ }
3215
+ const source = flagValue !== void 0 ? names.flagName : names.envVar;
3216
+ return {
3217
+ mode: "auto",
3218
+ warnings: [
3219
+ `Ignoring invalid ${source} "${raw}": expected one of ${VALID_USAGE_REPORTING_MODES.join(", ")}; using auto`
3220
+ ]
3221
+ };
3222
+ }
3223
+ var BASE_USAGE_REPORT_DELAY_MS = 10 * 6e4;
3224
+ var USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
3225
+ function usageReportDelayMs(random = Math.random) {
3226
+ return jitteredDelayMs(BASE_USAGE_REPORT_DELAY_MS, USAGE_REPORT_DELAY_JITTER_FRACTION, random);
3227
+ }
3228
+ var USAGE_REPORT_FAILURE_REESCALATION_TICKS = 6;
3229
+ function usageReportFailureLogLevel(consecutiveFailures) {
3230
+ return reportFailureLogLevel(consecutiveFailures, USAGE_REPORT_FAILURE_REESCALATION_TICKS);
3231
+ }
3002
3232
  function reportFailureLogLevel(consecutiveFailures, reescalationTicks) {
3003
3233
  return consecutiveFailures === 1 || consecutiveFailures % reescalationTicks === 0 ? "warn" : "debug";
3004
3234
  }
@@ -3007,33 +3237,26 @@ function failureStreakSuffix(consecutiveFailures) {
3007
3237
  }
3008
3238
 
3009
3239
  // src/lib/claude-usage-reporting.ts
3010
- var VALID_MODES = ["auto", "on", "off"];
3011
3240
  function resolveClaudeUsageReportingMode(flagValue, env) {
3012
- const raw = flagValue ?? env.EVIDENT_CLAUDE_USAGE_REPORTING;
3013
- if (raw === void 0 || raw === "") {
3014
- return { mode: "auto", warnings: [] };
3015
- }
3016
- const normalized = raw.trim().toLowerCase();
3017
- if (VALID_MODES.includes(normalized)) {
3018
- return { mode: normalized, warnings: [] };
3019
- }
3020
- const source = flagValue !== void 0 ? "--claude-usage-reporting" : "EVIDENT_CLAUDE_USAGE_REPORTING";
3021
- return {
3022
- mode: "auto",
3023
- warnings: [
3024
- `Ignoring invalid ${source} "${raw}": expected one of ${VALID_MODES.join(", ")}; using auto`
3025
- ]
3026
- };
3241
+ return resolveUsageReportingMode(flagValue, env, {
3242
+ flagName: "--claude-usage-reporting",
3243
+ envVar: "EVIDENT_CLAUDE_USAGE_REPORTING"
3244
+ });
3027
3245
  }
3028
- var BASE_REPORT_DELAY_MS = 10 * 6e4;
3029
- var REPORT_DELAY_JITTER_FRACTION = 0.2;
3030
3246
  function nextReportDelayMs(random = Math.random) {
3031
- return jitteredDelayMs(BASE_REPORT_DELAY_MS, REPORT_DELAY_JITTER_FRACTION, random);
3247
+ return usageReportDelayMs(random);
3032
3248
  }
3033
3249
  var FIRST_REPORT_DELAY_MS = firstReportDelayMs();
3034
- var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
3035
3250
  function claudeUsageFailureLogLevel(consecutiveFailures) {
3036
- return reportFailureLogLevel(consecutiveFailures, CLAUDE_USAGE_FAILURE_REESCALATION_TICKS);
3251
+ return usageReportFailureLogLevel(consecutiveFailures);
3252
+ }
3253
+
3254
+ // src/lib/openai-usage-reporting.ts
3255
+ function resolveOpenAiUsageReportingMode(flagValue, env) {
3256
+ return resolveUsageReportingMode(flagValue, env, {
3257
+ flagName: "--openai-usage-reporting",
3258
+ envVar: "EVIDENT_OPENAI_USAGE_REPORTING"
3259
+ });
3037
3260
  }
3038
3261
 
3039
3262
  // src/lib/resource-usage-reporting.ts
@@ -3192,15 +3415,15 @@ function createResourceUsageCollector(homeDir) {
3192
3415
  }
3193
3416
 
3194
3417
  // src/lib/channels/driver.ts
3195
- import { homedir as homedir2 } from "os";
3418
+ import { homedir as homedir3 } from "os";
3196
3419
 
3197
3420
  // src/lib/runner-file-sync.ts
3198
- import { join as join4 } from "path";
3421
+ import { join as join5 } from "path";
3199
3422
 
3200
3423
  // src/lib/file-push.ts
3201
3424
  import { randomUUID } from "crypto";
3202
3425
  import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
3203
- import { basename, dirname as dirname3, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
3426
+ import { basename, dirname as dirname3, isAbsolute, join as join4, relative, resolve as resolve2, sep } from "path";
3204
3427
  var FILE_MODE = 384;
3205
3428
  var DIRECTORY_MODE = 448;
3206
3429
  async function writePushedFile(request) {
@@ -3233,7 +3456,7 @@ async function writePushedFile(request) {
3233
3456
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
3234
3457
  dirname3(candidate)
3235
3458
  );
3236
- const realTarget = join3(existingAncestor, ...missingSegments, basename(candidate));
3459
+ const realTarget = join4(existingAncestor, ...missingSegments, basename(candidate));
3237
3460
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
3238
3461
  if (allowedDirectory === null) {
3239
3462
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -3269,7 +3492,7 @@ function expandAndValidate(requestedPath, homeDir) {
3269
3492
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
3270
3493
  return null;
3271
3494
  }
3272
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join3(homeDir, requestedPath.slice(2)) : requestedPath;
3495
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join4(homeDir, requestedPath.slice(2)) : requestedPath;
3273
3496
  if (expanded.split(/[/\\]/).includes("..")) {
3274
3497
  return null;
3275
3498
  }
@@ -3342,13 +3565,13 @@ function contains(realDirectory, realTarget) {
3342
3565
  async function createMissingDirectories(existingAncestor, missingSegments) {
3343
3566
  let current = existingAncestor;
3344
3567
  for (const segment of missingSegments) {
3345
- current = join3(current, segment);
3568
+ current = join4(current, segment);
3346
3569
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
3347
3570
  await chmod(current, DIRECTORY_MODE);
3348
3571
  }
3349
3572
  }
3350
3573
  async function writeAtomically(realTarget, content) {
3351
- const temporaryPath = join3(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
3574
+ const temporaryPath = join4(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
3352
3575
  let handle;
3353
3576
  try {
3354
3577
  handle = await open2(temporaryPath, "wx", FILE_MODE);
@@ -3390,20 +3613,28 @@ async function syncPendingRunnerFiles(options) {
3390
3613
  for (const id of options.ackFailures.keys()) {
3391
3614
  if (!pendingIds.has(id)) options.ackFailures.delete(id);
3392
3615
  }
3393
- if (pending.length === 0) return { applied: 0, claudeCredentialApplied: false };
3616
+ if (pending.length === 0) {
3617
+ return { applied: 0, claudeCredentialApplied: false, opencodeAuthApplied: false };
3618
+ }
3394
3619
  options.log({
3395
3620
  level: "info",
3396
3621
  message: `Runner file sync: ${pending.length} file(s) queued for this runner`
3397
3622
  });
3398
3623
  let applied = 0;
3399
3624
  let claudeCredentialApplied = false;
3625
+ let opencodeAuthApplied = false;
3400
3626
  for (const file of pending) {
3401
3627
  if ((options.ackFailures.get(file.id) ?? 0) >= MAX_ACK_ATTEMPTS) continue;
3402
3628
  const outcome = await applyOne(options, file);
3403
3629
  if (outcome.applied) applied += 1;
3404
3630
  if (outcome.claudeCredentialApplied) claudeCredentialApplied = true;
3631
+ if (outcome.opencodeAuthApplied) opencodeAuthApplied = true;
3405
3632
  }
3406
- return { applied, claudeCredentialApplied };
3633
+ return {
3634
+ applied,
3635
+ claudeCredentialApplied,
3636
+ opencodeAuthApplied
3637
+ };
3407
3638
  }
3408
3639
  async function listPendingFiles(options) {
3409
3640
  let res;
@@ -3464,10 +3695,18 @@ function asPendingFile(entry) {
3464
3695
  if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
3465
3696
  return { id, path, size };
3466
3697
  }
3467
- var NOT_APPLIED = { applied: false, claudeCredentialApplied: false };
3698
+ var NOT_APPLIED = {
3699
+ applied: false,
3700
+ claudeCredentialApplied: false,
3701
+ opencodeAuthApplied: false
3702
+ };
3468
3703
  function isClaudeCredentialPath(requestedPath, homeDir) {
3469
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join4(homeDir, requestedPath.slice(2)) : requestedPath;
3470
- return expanded === join4(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
3704
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join5(homeDir, requestedPath.slice(2)) : requestedPath;
3705
+ return expanded === join5(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
3706
+ }
3707
+ function isOpenCodeAuthPath(requestedPath, homeDir) {
3708
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join5(homeDir, requestedPath.slice(2)) : requestedPath;
3709
+ return expanded === join5(homeDir, ...OPENCODE_AUTH_SEGMENTS);
3471
3710
  }
3472
3711
  async function applyOne(options, file) {
3473
3712
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -3523,7 +3762,8 @@ async function applyOne(options, file) {
3523
3762
  await ack(options, file, "applied");
3524
3763
  return {
3525
3764
  applied: true,
3526
- claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir)
3765
+ claudeCredentialApplied: isClaudeCredentialPath(file.path, options.homeDir),
3766
+ opencodeAuthApplied: isOpenCodeAuthPath(file.path, options.homeDir)
3527
3767
  };
3528
3768
  }
3529
3769
  function durableDownloadCode(status2) {
@@ -3977,6 +4217,7 @@ var ChannelDriver = class _ChannelDriver {
3977
4217
  * that way rather than "fixing" it into a count.
3978
4218
  */
3979
4219
  claudeCredentialApplyCount = 0;
4220
+ opencodeAuthApplyCount = 0;
3980
4221
  /**
3981
4222
  * The currently-executing `drainPending()` promise, or null when idle. Lets a
3982
4223
  * graceful shutdown (`waitForInFlight`) await an in-progress drain so a turn it
@@ -4011,7 +4252,7 @@ var ChannelDriver = class _ChannelDriver {
4011
4252
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
4012
4253
  this.now = config.now ?? (() => Date.now());
4013
4254
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
4014
- this.homeDir = config.homeDir ?? homedir2();
4255
+ this.homeDir = config.homeDir ?? homedir3();
4015
4256
  this.maxActiveSessions = config.maxActiveSessions;
4016
4257
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
4017
4258
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
@@ -4081,6 +4322,7 @@ var ChannelDriver = class _ChannelDriver {
4081
4322
  });
4082
4323
  this.appliedFileCount += result.applied;
4083
4324
  if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
4325
+ if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
4084
4326
  return result.applied;
4085
4327
  } catch (err) {
4086
4328
  this.log({
@@ -4188,7 +4430,8 @@ var ChannelDriver = class _ChannelDriver {
4188
4430
  return {
4189
4431
  appliedFiles: this.appliedFileCount,
4190
4432
  inFlight: this.syncingFiles,
4191
- claudeCredentialApplies: this.claudeCredentialApplyCount
4433
+ claudeCredentialApplies: this.claudeCredentialApplyCount,
4434
+ opencodeAuthApplyCount: this.opencodeAuthApplyCount
4192
4435
  };
4193
4436
  }
4194
4437
  /**
@@ -7525,7 +7768,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
7525
7768
  if (trimmed === "") {
7526
7769
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
7527
7770
  }
7528
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join5(homeDir, trimmed.slice(2)) : trimmed;
7771
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join6(homeDir, trimmed.slice(2)) : trimmed;
7529
7772
  if (!isAbsolute2(expanded)) {
7530
7773
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
7531
7774
  }
@@ -7733,6 +7976,7 @@ async function driveChannels(state, driver) {
7733
7976
  let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
7734
7977
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
7735
7978
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
7979
+ let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
7736
7980
  while (state.running) {
7737
7981
  const cycleStartedAtMs = performance.now();
7738
7982
  let idleThisCycle = false;
@@ -7765,6 +8009,10 @@ async function driveChannels(state, driver) {
7765
8009
  const claudeCredentialApplied = claudeCredentialApplies !== lastSeenClaudeApplies;
7766
8010
  lastSeenClaudeApplies = claudeCredentialApplies;
7767
8011
  if (claudeCredentialApplied) state.claudeUsageRearm?.();
8012
+ const opencodeAuthApplies = fileActivitySnapshot.opencodeAuthApplyCount;
8013
+ const opencodeAuthApplied = opencodeAuthApplies !== lastSeenOpencodeAuthApplies;
8014
+ lastSeenOpencodeAuthApplies = opencodeAuthApplies;
8015
+ if (opencodeAuthApplied) state.openaiUsageRearm?.();
7768
8016
  if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
7769
8017
  idlePolls = 0;
7770
8018
  idleMs = 0;
@@ -7833,7 +8081,7 @@ async function driveChannels(state, driver) {
7833
8081
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
7834
8082
  var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
7835
8083
  function sessionDbPath() {
7836
- return join5(homedir3(), ".local", "share", "opencode", "opencode.db");
8084
+ return join6(homedir4(), ".local", "share", "opencode", "opencode.db");
7837
8085
  }
7838
8086
  async function runSweep(state, driver, config) {
7839
8087
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
@@ -7916,7 +8164,7 @@ function scheduleSessionCleanup(state, driver, options) {
7916
8164
  for (const warning2 of config.warnings) {
7917
8165
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
7918
8166
  }
7919
- const dbBytes = statSessionDbBytes(homedir3());
8167
+ const dbBytes = statSessionDbBytes(homedir4());
7920
8168
  void (async () => {
7921
8169
  const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
7922
8170
  const sizeWarning = buildSessionStoreSizeWarning({
@@ -7944,23 +8192,20 @@ function scheduleSessionCleanup(state, driver, options) {
7944
8192
  );
7945
8193
  state.sessionCleanupTimers.push(interval, firstSweep);
7946
8194
  }
7947
- function scheduleClaudeUsageReporting(state, options) {
7948
- const { mode, warnings } = resolveClaudeUsageReportingMode(
7949
- options.claudeUsageReporting,
7950
- process.env
7951
- );
8195
+ function scheduleUsageReporting(state, params) {
8196
+ const { mode, warnings } = params.resolved;
7952
8197
  for (const warning2 of warnings) {
7953
8198
  logActivity(state, {
7954
8199
  type: "info",
7955
8200
  level: "warn",
7956
- message: `Claude usage reporting: ${warning2}`
8201
+ message: `${params.label} usage reporting: ${warning2}`
7957
8202
  });
7958
8203
  }
7959
8204
  if (mode === "off") {
7960
8205
  logActivity(state, {
7961
8206
  type: "info",
7962
8207
  level: "debug",
7963
- message: "Claude usage reporting is off (--claude-usage-reporting off)"
8208
+ message: `${params.label} usage reporting is off (${params.offFlagHint})`
7964
8209
  });
7965
8210
  return null;
7966
8211
  }
@@ -7969,7 +8214,7 @@ function scheduleClaudeUsageReporting(state, options) {
7969
8214
  let rearmRequested = false;
7970
8215
  const armProbe = () => {
7971
8216
  phase = "probe-pending";
7972
- state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
8217
+ params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
7973
8218
  };
7974
8219
  const scheduleNextTick = () => {
7975
8220
  if (rearmRequested) {
@@ -7978,7 +8223,7 @@ function scheduleClaudeUsageReporting(state, options) {
7978
8223
  return;
7979
8224
  }
7980
8225
  phase = "steady-pending";
7981
- state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());
8226
+ params.setTimer(setTimeout(() => void tick(false), params.nextDelayMs()));
7982
8227
  };
7983
8228
  const rearm = () => {
7984
8229
  switch (phase) {
@@ -7988,9 +8233,9 @@ function scheduleClaudeUsageReporting(state, options) {
7988
8233
  case "probe-pending":
7989
8234
  return;
7990
8235
  case "steady-pending":
7991
- if (state.claudeUsageTimer) {
7992
- clearTimeout(state.claudeUsageTimer);
7993
- state.claudeUsageTimer = null;
8236
+ if (params.getTimer()) {
8237
+ clearTimeout(params.getTimer());
8238
+ params.setTimer(null);
7994
8239
  }
7995
8240
  rearmRequested = false;
7996
8241
  armProbe();
@@ -8004,45 +8249,45 @@ function scheduleClaudeUsageReporting(state, options) {
8004
8249
  const tick = async (isProbe) => {
8005
8250
  phase = "tick-in-flight";
8006
8251
  try {
8007
- const usage = await getClaudeUsage();
8008
- const result = await reportClaudeUsage(state.agentId, state.authHeader, usage);
8252
+ const usage = await params.fetchUsage();
8253
+ const result = await params.report(usage);
8009
8254
  if (result.ok) {
8010
8255
  if (consecutiveFailures > 0) {
8011
8256
  logActivity(state, {
8012
8257
  type: "info",
8013
8258
  level: "info",
8014
- message: "Claude usage reporting recovered"
8259
+ message: `${params.label} usage reporting recovered`
8015
8260
  });
8016
8261
  }
8017
8262
  consecutiveFailures = 0;
8018
8263
  logActivity(state, {
8019
8264
  type: "info",
8020
8265
  level: "debug",
8021
- message: "Reported Claude usage to Evident"
8266
+ message: `Reported ${params.label} usage to Evident`
8022
8267
  });
8023
8268
  } else {
8024
8269
  consecutiveFailures++;
8025
8270
  logActivity(state, {
8026
8271
  type: "info",
8027
- level: claudeUsageFailureLogLevel(consecutiveFailures),
8028
- message: `Failed to report Claude usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
8272
+ level: params.failureLogLevel(consecutiveFailures),
8273
+ message: `Failed to report ${params.label} usage: ${result.error}${failureStreakSuffix(consecutiveFailures)}`
8029
8274
  });
8030
8275
  }
8031
8276
  scheduleNextTick();
8032
8277
  } catch (error2) {
8033
- if (error2 instanceof ClaudeUsageError && isLocalCredentialProblem(error2)) {
8278
+ if (params.isLocalCredentialProblem(error2)) {
8034
8279
  if (mode === "on") {
8035
8280
  logActivity(state, {
8036
8281
  type: "info",
8037
8282
  level: "warn",
8038
- message: "Claude usage reporting is forced on but no usable Claude Code login was found \u2014 run `claude` to sign in; reporting will keep retrying"
8283
+ message: `${params.label} usage reporting is forced on but no usable login was found \u2014 ${params.forcedOnHint}; reporting will keep retrying`
8039
8284
  });
8040
8285
  scheduleNextTick();
8041
8286
  } else if (isProbe) {
8042
8287
  logActivity(state, {
8043
8288
  type: "info",
8044
8289
  level: "debug",
8045
- message: `Claude usage reporting: ${error2.message}`
8290
+ message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
8046
8291
  });
8047
8292
  phase = "dormant";
8048
8293
  if (rearmRequested) rearm();
@@ -8050,7 +8295,7 @@ function scheduleClaudeUsageReporting(state, options) {
8050
8295
  logActivity(state, {
8051
8296
  type: "info",
8052
8297
  level: "debug",
8053
- message: `Claude usage reporting: ${error2.message}`
8298
+ message: `${params.label} usage reporting: ${error2 instanceof Error ? error2.message : String(error2)}`
8054
8299
  });
8055
8300
  scheduleNextTick();
8056
8301
  }
@@ -8059,8 +8304,8 @@ function scheduleClaudeUsageReporting(state, options) {
8059
8304
  const message = error2 instanceof Error ? error2.message : String(error2);
8060
8305
  logActivity(state, {
8061
8306
  type: "info",
8062
- level: claudeUsageFailureLogLevel(consecutiveFailures),
8063
- message: `Claude usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
8307
+ level: params.failureLogLevel(consecutiveFailures),
8308
+ message: `${params.label} usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
8064
8309
  });
8065
8310
  scheduleNextTick();
8066
8311
  }
@@ -8069,6 +8314,24 @@ function scheduleClaudeUsageReporting(state, options) {
8069
8314
  armProbe();
8070
8315
  return rearm;
8071
8316
  }
8317
+ function scheduleClaudeUsageReporting(state, options) {
8318
+ return scheduleUsageReporting(state, {
8319
+ label: "Claude",
8320
+ resolved: resolveClaudeUsageReportingMode(options.claudeUsageReporting, process.env),
8321
+ offFlagHint: "--claude-usage-reporting off",
8322
+ getTimer: () => state.claudeUsageTimer,
8323
+ setTimer: (timer) => {
8324
+ state.claudeUsageTimer = timer;
8325
+ },
8326
+ fetchUsage: getClaudeUsage,
8327
+ report: (usage) => reportClaudeUsage(state.agentId, state.authHeader, usage),
8328
+ isLocalCredentialProblem,
8329
+ forcedOnHint: "run `claude` to sign in",
8330
+ firstDelayMs: () => FIRST_REPORT_DELAY_MS,
8331
+ nextDelayMs: nextReportDelayMs,
8332
+ failureLogLevel: claudeUsageFailureLogLevel
8333
+ });
8334
+ }
8072
8335
  var RESOURCE_USAGE_BASE_REPORT_DELAY_MS = 10 * 6e4;
8073
8336
  var RESOURCE_USAGE_REPORT_DELAY_JITTER_FRACTION = 0.2;
8074
8337
  var RESOURCE_USAGE_FAILURE_REESCALATION_TICKS = 6;
@@ -8092,7 +8355,7 @@ function scheduleResourceUsageReporting(state, options) {
8092
8355
  });
8093
8356
  return;
8094
8357
  }
8095
- const collect = createResourceUsageCollector(homedir3());
8358
+ const collect = createResourceUsageCollector(homedir4());
8096
8359
  let consecutiveFailures = 0;
8097
8360
  const tick = async () => {
8098
8361
  try {
@@ -8193,6 +8456,11 @@ async function cleanup(state, opts = {}) {
8193
8456
  state.claudeUsageTimer = null;
8194
8457
  }
8195
8458
  state.claudeUsageRearm = null;
8459
+ if (state.openaiUsageTimer) {
8460
+ clearTimeout(state.openaiUsageTimer);
8461
+ state.openaiUsageTimer = null;
8462
+ }
8463
+ state.openaiUsageRearm = null;
8196
8464
  if (state.resourceUsageTimer) {
8197
8465
  clearTimeout(state.resourceUsageTimer);
8198
8466
  state.resourceUsageTimer = null;
@@ -8244,7 +8512,7 @@ async function run(options) {
8244
8512
  let fileSyncDirectories;
8245
8513
  try {
8246
8514
  logLevel = resolveLogLevel(options);
8247
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir3());
8515
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir4());
8248
8516
  } catch (error2) {
8249
8517
  const message = error2 instanceof Error ? error2.message : String(error2);
8250
8518
  if (options.json) {
@@ -8279,6 +8547,8 @@ async function run(options) {
8279
8547
  sessionCleanupTimers: [],
8280
8548
  claudeUsageTimer: null,
8281
8549
  claudeUsageRearm: null,
8550
+ openaiUsageTimer: null,
8551
+ openaiUsageRearm: null,
8282
8552
  resourceUsageTimer: null,
8283
8553
  authHeader: ""
8284
8554
  };
@@ -8541,7 +8811,7 @@ async function run(options) {
8541
8811
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
8542
8812
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
8543
8813
  fileSyncDirectories,
8544
- homeDir: homedir3(),
8814
+ homeDir: homedir4(),
8545
8815
  maxActiveSessions,
8546
8816
  log: (entry) => (
8547
8817
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -8679,6 +8949,22 @@ async function run(options) {
8679
8949
  }
8680
8950
  scheduleSessionCleanup(state, channelDriver, options);
8681
8951
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
8952
+ state.openaiUsageRearm = scheduleUsageReporting(state, {
8953
+ label: "OpenAI",
8954
+ resolved: resolveOpenAiUsageReportingMode(options.openaiUsageReporting, process.env),
8955
+ offFlagHint: "--openai-usage-reporting off",
8956
+ getTimer: () => state.openaiUsageTimer,
8957
+ setTimer: (timer) => {
8958
+ state.openaiUsageTimer = timer;
8959
+ },
8960
+ fetchUsage: () => getOpenAiUsage(state.port),
8961
+ report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
8962
+ isLocalCredentialProblem: isLocalCredentialProblem2,
8963
+ forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
8964
+ firstDelayMs: firstReportDelayMs,
8965
+ nextDelayMs: usageReportDelayMs,
8966
+ failureLogLevel: usageReportFailureLogLevel
8967
+ });
8682
8968
  scheduleResourceUsageReporting(state, options);
8683
8969
  if (!interactive || state.json) {
8684
8970
  log2(state, "Driving channel messages...");
@@ -8760,6 +9046,9 @@ program.command("run").description("Connect to Evident and process messages").op
8760
9046
  ).option(
8761
9047
  "--claude-usage-reporting <mode>",
8762
9048
  "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
9049
+ ).option(
9050
+ "--openai-usage-reporting <mode>",
9051
+ "Report OpenAI plan usage to Evident: auto | on | off (default: auto). Env: EVIDENT_OPENAI_USAGE_REPORTING"
8763
9052
  ).option(
8764
9053
  "--no-resource-usage-reporting",
8765
9054
  "Don't report this machine's CPU and memory usage to Evident (reporting is on by default). Env: EVIDENT_RESOURCE_USAGE_REPORTING=off"
@@ -8795,6 +9084,7 @@ program.command("run").description("Connect to Evident and process messages").op
8795
9084
  // Raw string — the resolver in run.ts single-sources parsing
8796
9085
  // (resolveClaudeUsageReportingMode).
8797
9086
  claudeUsageReporting: options.claudeUsageReporting,
9087
+ openaiUsageReporting: options.openaiUsageReporting,
8798
9088
  // Raw value — resolution is single-sourced in run.ts's
8799
9089
  // resolveResourceUsageReportingEnabled.
8800
9090
  resourceUsageReporting: options.resourceUsageReporting,