@evident-ai/cli 3.1.1-dev.caa3277 → 3.1.1-dev.db6b362

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
@@ -38,11 +38,6 @@ function getApiUrl() {
38
38
  function getTunnelUrl() {
39
39
  return tunnelOverride ?? process.env.EVIDENT_TUNNEL_URL ?? defaults.tunnelUrl;
40
40
  }
41
- var config = new Conf({
42
- projectName: "evident",
43
- projectSuffix: "",
44
- defaults
45
- });
46
41
  var credentials = new Conf({
47
42
  projectName: "evident",
48
43
  projectSuffix: "",
@@ -537,9 +532,469 @@ async function whoami() {
537
532
  blank();
538
533
  }
539
534
 
535
+ // src/lib/auth.ts
536
+ async function getAuthCredentials() {
537
+ const runnerKey = process.env.EVIDENT_RUNNER_KEY;
538
+ const agentKey = process.env.EVIDENT_AGENT_KEY;
539
+ if (runnerKey) {
540
+ return {
541
+ token: runnerKey,
542
+ authType: "agent_key",
543
+ keySource: "runner_key",
544
+ notice: agentKey ? "Both EVIDENT_RUNNER_KEY and EVIDENT_AGENT_KEY are set; using EVIDENT_RUNNER_KEY." : void 0
545
+ };
546
+ }
547
+ if (agentKey) {
548
+ return { token: agentKey, authType: "agent_key", keySource: "agent_key" };
549
+ }
550
+ const userToken = process.env.EVIDENT_TOKEN;
551
+ if (userToken) {
552
+ return { token: userToken, authType: "bearer" };
553
+ }
554
+ const keychainCreds = await getToken();
555
+ if (keychainCreds) {
556
+ return {
557
+ token: keychainCreds.token,
558
+ authType: "bearer",
559
+ user: keychainCreds.user
560
+ };
561
+ }
562
+ return null;
563
+ }
564
+ function getAuthHeader(credentials2) {
565
+ if (credentials2.authType === "agent_key") {
566
+ return `SandboxKey ${credentials2.token}`;
567
+ }
568
+ return `Bearer ${credentials2.token}`;
569
+ }
570
+ function isInteractive(jsonOutput) {
571
+ if (jsonOutput) return false;
572
+ if (process.env.CI) return false;
573
+ if (process.env.GITHUB_ACTIONS) return false;
574
+ if (!process.stdin.isTTY) return false;
575
+ return true;
576
+ }
577
+
578
+ // src/commands/agent-lookup.ts
579
+ async function readErrorMessage(response) {
580
+ const text = await response.text().catch(() => "");
581
+ if (!text) return response.statusText || void 0;
582
+ try {
583
+ const data = JSON.parse(text);
584
+ const message = data.message ?? data.error;
585
+ if (typeof message === "string" && message.trim()) {
586
+ return message;
587
+ }
588
+ } catch {
589
+ }
590
+ return text.trim() || response.statusText || void 0;
591
+ }
592
+ function authFailureHint(apiUrl, serverMessage) {
593
+ const reason = serverMessage ? `: ${serverMessage}` : "";
594
+ return `Authentication failed${reason}. Your credentials were rejected by ${apiUrl}. This usually means you logged in against a different environment, or your session expired \u2014 log in again pointing at this endpoint and retry.`;
595
+ }
596
+ async function resolveAgentIdFromKey(authHeader) {
597
+ const apiUrl = getApiUrlConfig();
598
+ try {
599
+ const response = await fetch(`${apiUrl}/me`, {
600
+ headers: { Authorization: authHeader }
601
+ });
602
+ if (response.status === 401) {
603
+ const serverMessage = await readErrorMessage(response);
604
+ return { error: authFailureHint(apiUrl, serverMessage), authFailed: true };
605
+ }
606
+ if (!response.ok) {
607
+ const serverMessage = await readErrorMessage(response);
608
+ return {
609
+ error: `Failed to resolve runner from key (HTTP ${response.status})${serverMessage ? `: ${serverMessage}` : ""}`
610
+ };
611
+ }
612
+ const data = await response.json();
613
+ if (data.auth_type === "agent_key" && data.agent_id) {
614
+ return { agent_id: data.agent_id };
615
+ }
616
+ return {
617
+ error: "Cannot resolve runner ID: auth type is not agent_key. Please provide --runner explicitly."
618
+ };
619
+ } catch (error2) {
620
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
621
+ return { error: `Failed to resolve runner from key: ${message}` };
622
+ }
623
+ }
624
+ var BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2e3;
625
+ async function notifyAgentDisconnected(agentId, authHeader) {
626
+ const apiUrl = getApiUrlConfig();
627
+ try {
628
+ const response = await fetch(`${apiUrl}/runners/${agentId}/disconnect`, {
629
+ method: "POST",
630
+ headers: { Authorization: authHeader },
631
+ signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
632
+ });
633
+ if (!response.ok) {
634
+ const serverMessage = await readErrorMessage(response);
635
+ return {
636
+ ok: false,
637
+ error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
638
+ };
639
+ }
640
+ return { ok: true };
641
+ } catch (error2) {
642
+ return { ok: false, error: describeBestEffortError(error2) };
643
+ }
644
+ }
645
+ function describeBestEffortError(error2) {
646
+ const name = error2?.name;
647
+ if (name === "TimeoutError" || name === "AbortError") {
648
+ return `timed out after ${BEST_EFFORT_NOTIFY_TIMEOUT_MS}ms`;
649
+ }
650
+ return error2 instanceof Error ? error2.message : String(error2);
651
+ }
652
+ async function reportMicrovmId(agentId, authHeader, microvmId) {
653
+ try {
654
+ const apiUrl = getApiUrlConfig();
655
+ const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {
656
+ method: "POST",
657
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
658
+ body: JSON.stringify({ microvm_id: microvmId }),
659
+ signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
660
+ });
661
+ if (!response.ok) {
662
+ const serverMessage = await readErrorMessage(response);
663
+ return {
664
+ ok: false,
665
+ error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
666
+ };
667
+ }
668
+ return { ok: true };
669
+ } catch (error2) {
670
+ return { ok: false, error: describeBestEffortError(error2) };
671
+ }
672
+ }
673
+ function toReportedWindow(window) {
674
+ if (!window) return null;
675
+ return { utilization: window.utilization, resets_at: window.resetsAt };
676
+ }
677
+ async function reportClaudeUsage(agentId, authHeader, snapshot) {
678
+ try {
679
+ const apiUrl = getApiUrlConfig();
680
+ const response = await fetch(`${apiUrl}/runners/${agentId}/claude-usage`, {
681
+ method: "POST",
682
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
683
+ body: JSON.stringify({
684
+ five_hour: toReportedWindow(snapshot.fiveHour),
685
+ seven_day: toReportedWindow(snapshot.sevenDay)
686
+ }),
687
+ signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
688
+ });
689
+ if (!response.ok) {
690
+ const serverMessage = await readErrorMessage(response);
691
+ return {
692
+ ok: false,
693
+ error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
694
+ };
695
+ }
696
+ return { ok: true };
697
+ } catch (error2) {
698
+ return { ok: false, error: describeBestEffortError(error2) };
699
+ }
700
+ }
701
+ async function getAgentInfo(agentId, authHeader) {
702
+ const apiUrl = getApiUrlConfig();
703
+ try {
704
+ const response = await fetch(`${apiUrl}/runners/${agentId}`, {
705
+ headers: { Authorization: authHeader }
706
+ });
707
+ if (response.status === 401) {
708
+ const serverMessage = await readErrorMessage(response);
709
+ return { valid: false, error: authFailureHint(apiUrl, serverMessage), authFailed: true };
710
+ }
711
+ if (response.status === 403) {
712
+ const serverMessage = await readErrorMessage(response);
713
+ return {
714
+ valid: false,
715
+ error: serverMessage ?? "You do not have access to this runner (it may belong to a different team or organization)."
716
+ };
717
+ }
718
+ if (response.status === 404) {
719
+ const serverMessage = await readErrorMessage(response);
720
+ return { valid: false, error: serverMessage ?? `Runner ${agentId} not found` };
721
+ }
722
+ if (!response.ok) {
723
+ const serverMessage = await readErrorMessage(response);
724
+ return {
725
+ valid: false,
726
+ error: `API error (HTTP ${response.status})${serverMessage ? `: ${serverMessage}` : ""}`
727
+ };
728
+ }
729
+ const agent = await response.json();
730
+ if (agent.agent_type !== "local") {
731
+ return {
732
+ valid: false,
733
+ error: `Runner is type '${agent.agent_type}', must be 'local' for CLI connection`
734
+ };
735
+ }
736
+ return { valid: true, agent };
737
+ } catch (error2) {
738
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
739
+ return { valid: false, error: `Failed to validate runner: ${message}` };
740
+ }
741
+ }
742
+
743
+ // src/commands/status.ts
744
+ var STATUS_TIMEOUT_MS = 1e4;
745
+ function authLabelFor(credentials2) {
746
+ if (credentials2.authType === "agent_key") {
747
+ return credentials2.keySource === "agent_key" ? "runner key (EVIDENT_AGENT_KEY)" : "runner key (EVIDENT_RUNNER_KEY)";
748
+ }
749
+ return "user token";
750
+ }
751
+ function describeFetchError(error2) {
752
+ const name = error2?.name;
753
+ if (name === "TimeoutError" || name === "AbortError") {
754
+ return `timed out after ${STATUS_TIMEOUT_MS}ms waiting for a response`;
755
+ }
756
+ return error2 instanceof Error ? error2.message : String(error2);
757
+ }
758
+ async function checkStatus(jsonMode) {
759
+ const apiUrl = getApiUrlConfig();
760
+ const credentials2 = await getAuthCredentials();
761
+ if (!credentials2) {
762
+ return {
763
+ ok: false,
764
+ endpoint: apiUrl,
765
+ reason: "no_credentials",
766
+ error: "No credentials configured. Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY), or EVIDENT_TOKEN, or run `evident login`.",
767
+ exitCode: 1
768
+ };
769
+ }
770
+ if (credentials2.notice && !jsonMode) {
771
+ printWarning(credentials2.notice);
772
+ }
773
+ let response;
774
+ try {
775
+ response = await fetch(`${apiUrl}/me`, {
776
+ headers: { Authorization: getAuthHeader(credentials2) },
777
+ signal: AbortSignal.timeout(STATUS_TIMEOUT_MS)
778
+ });
779
+ } catch (error2) {
780
+ return {
781
+ ok: false,
782
+ endpoint: apiUrl,
783
+ authLabel: authLabelFor(credentials2),
784
+ reason: "unreachable",
785
+ error: `Could not reach ${apiUrl}: ${describeFetchError(error2)}. The credentials were NOT validated.`,
786
+ exitCode: 75
787
+ };
788
+ }
789
+ if (response.status === 401) {
790
+ const serverMessage = await readErrorMessage(response);
791
+ return {
792
+ ok: false,
793
+ endpoint: apiUrl,
794
+ authLabel: authLabelFor(credentials2),
795
+ reason: "unauthorized",
796
+ error: authFailureHint(apiUrl, serverMessage),
797
+ exitCode: 1
798
+ };
799
+ }
800
+ if (response.status >= 500) {
801
+ const serverMessage = await readErrorMessage(response);
802
+ return {
803
+ ok: false,
804
+ endpoint: apiUrl,
805
+ authLabel: authLabelFor(credentials2),
806
+ reason: "unreachable",
807
+ error: `${apiUrl} returned HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}. The credentials were NOT validated.`,
808
+ exitCode: 75
809
+ };
810
+ }
811
+ if (!response.ok) {
812
+ const serverMessage = await readErrorMessage(response);
813
+ return {
814
+ ok: false,
815
+ endpoint: apiUrl,
816
+ authLabel: authLabelFor(credentials2),
817
+ reason: "http_error",
818
+ error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`,
819
+ exitCode: 1
820
+ };
821
+ }
822
+ const data = await response.json();
823
+ return {
824
+ ok: true,
825
+ endpoint: apiUrl,
826
+ authType: data.auth_type,
827
+ authLabel: authLabelFor(credentials2),
828
+ runnerId: data.auth_type === "agent_key" ? data.agent_id : void 0,
829
+ reason: "ok",
830
+ exitCode: 0
831
+ };
832
+ }
833
+ function printJson(result) {
834
+ const payload = {
835
+ ok: result.ok,
836
+ endpoint: result.endpoint
837
+ };
838
+ if (result.authType) payload.auth_type = result.authType;
839
+ if (result.runnerId) payload.runner_id = result.runnerId;
840
+ if (result.reason) payload.reason = result.reason;
841
+ if (result.error) payload.error = result.error;
842
+ console.log(JSON.stringify(payload));
843
+ }
844
+ function printHuman(result) {
845
+ blank();
846
+ console.log(keyValue("Endpoint", result.endpoint));
847
+ if (result.ok) {
848
+ console.log(keyValue("Auth", result.authLabel ?? "\u2014"));
849
+ if (result.runnerId) {
850
+ console.log(keyValue("Runner", result.runnerId));
851
+ }
852
+ console.log(keyValue("Status", "OK \u2014 credentials accepted"));
853
+ blank();
854
+ return;
855
+ }
856
+ if (result.authLabel) {
857
+ console.log(keyValue("Auth", result.authLabel));
858
+ }
859
+ blank();
860
+ printError(result.error ?? "Unknown error");
861
+ }
862
+ async function status(options = {}) {
863
+ const result = await checkStatus(Boolean(options.json));
864
+ if (options.json) {
865
+ printJson(result);
866
+ } else {
867
+ printHuman(result);
868
+ }
869
+ process.exit(result.exitCode);
870
+ }
871
+
872
+ // src/lib/claude-usage.ts
873
+ import { execFileSync } from "child_process";
874
+ import { readFileSync } from "fs";
875
+ import { homedir } from "os";
876
+ import { join } from "path";
877
+ var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
878
+ var KEYCHAIN_SERVICE = "Claude Code-credentials";
879
+ function parseClaudeCliCredentials(raw) {
880
+ let parsed;
881
+ try {
882
+ parsed = JSON.parse(raw);
883
+ } catch {
884
+ return null;
885
+ }
886
+ const data = parsed.claudeAiOauth ?? parsed;
887
+ const creds = data;
888
+ if (typeof creds.accessToken !== "string" || typeof creds.expiresAt !== "number") {
889
+ return null;
890
+ }
891
+ return { accessToken: creds.accessToken, expiresAt: creds.expiresAt };
892
+ }
893
+ function readClaudeCliCredentials() {
894
+ if (process.platform === "darwin") {
895
+ try {
896
+ const raw = execFileSync(
897
+ "/usr/bin/security",
898
+ ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"],
899
+ { encoding: "utf-8", timeout: 2e3, stdio: ["pipe", "pipe", "ignore"] }
900
+ );
901
+ return parseClaudeCliCredentials(raw);
902
+ } catch {
903
+ return null;
904
+ }
905
+ }
906
+ try {
907
+ const raw = readFileSync(join(homedir(), ".claude", ".credentials.json"), "utf-8");
908
+ return parseClaudeCliCredentials(raw);
909
+ } catch {
910
+ return null;
911
+ }
912
+ }
913
+ var ClaudeUsageError = class extends Error {
914
+ constructor(message, reason) {
915
+ super(message);
916
+ this.reason = reason;
917
+ }
918
+ };
919
+ function isLocalCredentialProblem(err) {
920
+ return err instanceof ClaudeUsageError && (err.reason === "no_credentials" || err.reason === "credentials_expired");
921
+ }
922
+ function normalizeResetsAt(value) {
923
+ const ms = Date.parse(value);
924
+ return Number.isNaN(ms) ? null : new Date(ms).toISOString();
925
+ }
926
+ function toWindow(value) {
927
+ if (!value || typeof value !== "object") {
928
+ return null;
929
+ }
930
+ const window = value;
931
+ if (typeof window.utilization !== "number" || typeof window.resets_at !== "string") {
932
+ return null;
933
+ }
934
+ const resetsAt = normalizeResetsAt(window.resets_at);
935
+ if (resetsAt === null) {
936
+ return null;
937
+ }
938
+ return { utilization: window.utilization, resetsAt };
939
+ }
940
+ async function getClaudeUsage() {
941
+ const credentials2 = readClaudeCliCredentials();
942
+ if (!credentials2) {
943
+ throw new ClaudeUsageError(
944
+ "No local Claude Code login found. Run `claude` once to sign in with your Claude subscription.",
945
+ "no_credentials"
946
+ );
947
+ }
948
+ if (credentials2.expiresAt < Date.now()) {
949
+ throw new ClaudeUsageError(
950
+ "Claude Code credentials have expired. Run `claude` to refresh them.",
951
+ "credentials_expired"
952
+ );
953
+ }
954
+ const res = await fetch(CLAUDE_USAGE_URL, {
955
+ headers: {
956
+ Authorization: `Bearer ${credentials2.accessToken}`,
957
+ "Content-Type": "application/json",
958
+ "anthropic-version": "2023-06-01"
959
+ }
960
+ });
961
+ if (!res.ok) {
962
+ throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
963
+ }
964
+ const body = await res.json();
965
+ return {
966
+ fiveHour: toWindow(body.five_hour),
967
+ sevenDay: toWindow(body.seven_day)
968
+ };
969
+ }
970
+
971
+ // src/commands/claude-usage.ts
972
+ function formatWindow(label, window) {
973
+ if (!window) {
974
+ return keyValue(label, "not available for this plan");
975
+ }
976
+ const resetsAt = new Date(window.resetsAt);
977
+ return keyValue(label, `${window.utilization}% used, resets ${resetsAt.toLocaleString()}`);
978
+ }
979
+ async function claudeUsage() {
980
+ try {
981
+ const usage = await getClaudeUsage();
982
+ blank();
983
+ console.log(formatWindow("5-hour session", usage.fiveHour));
984
+ console.log(formatWindow("7-day", usage.sevenDay));
985
+ blank();
986
+ } catch (err) {
987
+ if (err instanceof ClaudeUsageError) {
988
+ printError(err.message);
989
+ process.exit(1);
990
+ }
991
+ throw err;
992
+ }
993
+ }
994
+
540
995
  // src/commands/run.ts
541
- import { homedir as homedir2 } from "os";
542
- import { isAbsolute as isAbsolute2, join as join2, parse, resolve as resolvePath } from "path";
996
+ import { homedir as homedir3 } from "os";
997
+ import { isAbsolute as isAbsolute2, join as join3, parse, resolve as resolvePath } from "path";
543
998
  import chalk6 from "chalk";
544
999
 
545
1000
  // ../../packages/types/src/telemetry/index.ts
@@ -797,74 +1252,31 @@ function admitUnderRateLimit(now) {
797
1252
  if (windowDroppedCount === 1) {
798
1253
  console.error(
799
1254
  `[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window`
800
- );
801
- }
802
- return false;
803
- }
804
- windowCount++;
805
- return true;
806
- }
807
- function forwardRunnerActivity(entry, context) {
808
- try {
809
- if (!FORWARDED_LEVELS.has(entry.level)) return;
810
- if (!context.agentId || !context.authHeader) return;
811
- if (!admitUnderRateLimit(Date.now())) return;
812
- const rawMessage = entry.error ?? entry.message ?? "";
813
- const message = truncate(redact(rawMessage));
814
- logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
815
- severity: SEVERITY_BY_LEVEL[entry.level],
816
- message,
817
- metadata: { source: "cli.run" },
818
- agentId: context.agentId
819
- });
820
- } catch (err) {
821
- console.error(
822
- `[runner-activity-telemetry] failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
823
- );
824
- }
825
- }
826
-
827
- // src/lib/auth.ts
828
- async function getAuthCredentials() {
829
- const runnerKey = process.env.EVIDENT_RUNNER_KEY;
830
- const agentKey = process.env.EVIDENT_AGENT_KEY;
831
- if (runnerKey) {
832
- return {
833
- token: runnerKey,
834
- authType: "agent_key",
835
- keySource: "runner_key",
836
- notice: agentKey ? "Both EVIDENT_RUNNER_KEY and EVIDENT_AGENT_KEY are set; using EVIDENT_RUNNER_KEY." : void 0
837
- };
838
- }
839
- if (agentKey) {
840
- return { token: agentKey, authType: "agent_key", keySource: "agent_key" };
841
- }
842
- const userToken = process.env.EVIDENT_TOKEN;
843
- if (userToken) {
844
- return { token: userToken, authType: "bearer" };
845
- }
846
- const keychainCreds = await getToken();
847
- if (keychainCreds) {
848
- return {
849
- token: keychainCreds.token,
850
- authType: "bearer",
851
- user: keychainCreds.user
852
- };
1255
+ );
1256
+ }
1257
+ return false;
853
1258
  }
854
- return null;
1259
+ windowCount++;
1260
+ return true;
855
1261
  }
856
- function getAuthHeader(credentials2) {
857
- if (credentials2.authType === "agent_key") {
858
- return `SandboxKey ${credentials2.token}`;
1262
+ function forwardRunnerActivity(entry, context) {
1263
+ try {
1264
+ if (!FORWARDED_LEVELS.has(entry.level)) return;
1265
+ if (!context.agentId || !context.authHeader) return;
1266
+ if (!admitUnderRateLimit(Date.now())) return;
1267
+ const rawMessage = entry.error ?? entry.message ?? "";
1268
+ const message = truncate(redact(rawMessage));
1269
+ logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1270
+ severity: SEVERITY_BY_LEVEL[entry.level],
1271
+ message,
1272
+ metadata: { source: "cli.run" },
1273
+ agentId: context.agentId
1274
+ });
1275
+ } catch (err) {
1276
+ console.error(
1277
+ `[runner-activity-telemetry] failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
1278
+ );
859
1279
  }
860
- return `Bearer ${credentials2.token}`;
861
- }
862
- function isInteractive(jsonOutput) {
863
- if (jsonOutput) return false;
864
- if (process.env.CI) return false;
865
- if (process.env.GITHUB_ACTIONS) return false;
866
- if (!process.stdin.isTTY) return false;
867
- return true;
868
1280
  }
869
1281
 
870
1282
  // src/lib/opencode/health.ts
@@ -2250,13 +2662,44 @@ function writeTunnelReadyMarker(path, agentId) {
2250
2662
  }
2251
2663
  }
2252
2664
 
2665
+ // src/lib/claude-usage-reporting.ts
2666
+ var VALID_MODES = ["auto", "on", "off"];
2667
+ function resolveClaudeUsageReportingMode(flagValue, env) {
2668
+ const raw = flagValue ?? env.EVIDENT_CLAUDE_USAGE_REPORTING;
2669
+ if (raw === void 0 || raw === "") {
2670
+ return { mode: "auto", warnings: [] };
2671
+ }
2672
+ const normalized = raw.trim().toLowerCase();
2673
+ if (VALID_MODES.includes(normalized)) {
2674
+ return { mode: normalized, warnings: [] };
2675
+ }
2676
+ const source = flagValue !== void 0 ? "--claude-usage-reporting" : "EVIDENT_CLAUDE_USAGE_REPORTING";
2677
+ return {
2678
+ mode: "auto",
2679
+ warnings: [
2680
+ `Ignoring invalid ${source} "${raw}": expected one of ${VALID_MODES.join(", ")}; using auto`
2681
+ ]
2682
+ };
2683
+ }
2684
+ var BASE_REPORT_DELAY_MS = 10 * 6e4;
2685
+ var REPORT_DELAY_JITTER_FRACTION = 0.2;
2686
+ function nextReportDelayMs(random = Math.random) {
2687
+ const jitterRangeMs = BASE_REPORT_DELAY_MS * REPORT_DELAY_JITTER_FRACTION;
2688
+ return BASE_REPORT_DELAY_MS - jitterRangeMs + random() * (2 * jitterRangeMs);
2689
+ }
2690
+ var FIRST_REPORT_DELAY_MS = 5e3 + Math.random() * 1e4;
2691
+ var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
2692
+ function claudeUsageFailureLogLevel(consecutiveFailures) {
2693
+ return consecutiveFailures === 1 || consecutiveFailures % CLAUDE_USAGE_FAILURE_REESCALATION_TICKS === 0 ? "warn" : "debug";
2694
+ }
2695
+
2253
2696
  // src/lib/channels/driver.ts
2254
- import { homedir } from "os";
2697
+ import { homedir as homedir2 } from "os";
2255
2698
 
2256
2699
  // src/lib/file-push.ts
2257
2700
  import { randomUUID } from "crypto";
2258
2701
  import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
2259
- import { basename, dirname as dirname2, isAbsolute, join, relative, resolve as resolve2, sep } from "path";
2702
+ import { basename, dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2, sep } from "path";
2260
2703
  var FILE_MODE = 384;
2261
2704
  var DIRECTORY_MODE = 448;
2262
2705
  async function writePushedFile(request) {
@@ -2289,7 +2732,7 @@ async function writePushedFile(request) {
2289
2732
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
2290
2733
  dirname2(candidate)
2291
2734
  );
2292
- const realTarget = join(existingAncestor, ...missingSegments, basename(candidate));
2735
+ const realTarget = join2(existingAncestor, ...missingSegments, basename(candidate));
2293
2736
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
2294
2737
  if (allowedDirectory === null) {
2295
2738
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -2325,7 +2768,7 @@ function expandAndValidate(requestedPath, homeDir) {
2325
2768
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
2326
2769
  return null;
2327
2770
  }
2328
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join(homeDir, requestedPath.slice(2)) : requestedPath;
2771
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join2(homeDir, requestedPath.slice(2)) : requestedPath;
2329
2772
  if (expanded.split(/[/\\]/).includes("..")) {
2330
2773
  return null;
2331
2774
  }
@@ -2398,13 +2841,13 @@ function contains(realDirectory, realTarget) {
2398
2841
  async function createMissingDirectories(existingAncestor, missingSegments) {
2399
2842
  let current = existingAncestor;
2400
2843
  for (const segment of missingSegments) {
2401
- current = join(current, segment);
2844
+ current = join2(current, segment);
2402
2845
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
2403
2846
  await chmod(current, DIRECTORY_MODE);
2404
2847
  }
2405
2848
  }
2406
2849
  async function writeAtomically(realTarget, content) {
2407
- const temporaryPath = join(dirname2(realTarget), `.evident-push-${randomUUID()}.tmp`);
2850
+ const temporaryPath = join2(dirname2(realTarget), `.evident-push-${randomUUID()}.tmp`);
2408
2851
  let handle;
2409
2852
  try {
2410
2853
  handle = await open2(temporaryPath, "wx", FILE_MODE);
@@ -2571,8 +3014,8 @@ async function applyOne(options, file) {
2571
3014
  await ack(options, file, "applied");
2572
3015
  return true;
2573
3016
  }
2574
- function durableDownloadCode(status) {
2575
- return status === 413 ? "file_too_large" : "write_failed";
3017
+ function durableDownloadCode(status2) {
3018
+ return status2 === 413 ? "file_too_large" : "write_failed";
2576
3019
  }
2577
3020
  async function downloadContent(options, file, label) {
2578
3021
  try {
@@ -2605,8 +3048,8 @@ async function downloadContent(options, file, label) {
2605
3048
  return { ok: false, terminal: false };
2606
3049
  }
2607
3050
  }
2608
- async function ack(options, file, status, reason) {
2609
- const outcome = `${status}${reason ? ` (${reason})` : ""}`;
3051
+ async function ack(options, file, status2, reason) {
3052
+ const outcome = `${status2}${reason ? ` (${reason})` : ""}`;
2610
3053
  try {
2611
3054
  const res = await options.fetchImpl(
2612
3055
  `${options.apiUrl}/runners/${options.agentId}/files/${file.id}/ack`,
@@ -2616,7 +3059,7 @@ async function ack(options, file, status, reason) {
2616
3059
  Authorization: options.getAuthHeader(),
2617
3060
  "Content-Type": "application/json"
2618
3061
  },
2619
- body: JSON.stringify(reason ? { status, reason } : { status })
3062
+ body: JSON.stringify(reason ? { status: status2, reason } : { status: status2 })
2620
3063
  }
2621
3064
  );
2622
3065
  if (!res.ok) {
@@ -2688,10 +3131,10 @@ var ChannelAuthError = class extends Error {
2688
3131
  };
2689
3132
  var ChannelTerminalError = class extends Error {
2690
3133
  status;
2691
- constructor(message, status) {
3134
+ constructor(message, status2) {
2692
3135
  super(message);
2693
3136
  this.name = "ChannelTerminalError";
2694
- this.status = status;
3137
+ this.status = status2;
2695
3138
  }
2696
3139
  };
2697
3140
  function backoffDelay(attempt, policy) {
@@ -2699,8 +3142,8 @@ function backoffDelay(attempt, policy) {
2699
3142
  const capped = Math.min(policy.maxDelayMs, exp);
2700
3143
  return Math.floor(Math.random() * capped);
2701
3144
  }
2702
- function isRetryableStatus(status) {
2703
- return status === 429 || status >= 500 && status <= 599;
3145
+ function isRetryableStatus(status2) {
3146
+ return status2 === 429 || status2 >= 500 && status2 <= 599;
2704
3147
  }
2705
3148
  var ChannelDriver = class _ChannelDriver {
2706
3149
  agentId;
@@ -2903,23 +3346,23 @@ var ChannelDriver = class _ChannelDriver {
2903
3346
  * and stops opencode.
2904
3347
  */
2905
3348
  stopped = false;
2906
- constructor(config2) {
2907
- this.agentId = config2.agentId;
2908
- this.port = config2.port;
2909
- this.apiUrl = config2.apiUrl.replace(/\/$/, "");
2910
- this.getAuthHeader = config2.getAuthHeader;
2911
- this.conversationFilter = config2.conversationFilter ?? null;
2912
- this.retry = { ...DEFAULT_RETRY_POLICY, ...config2.retry };
2913
- this.log = config2.log ?? (() => {
3349
+ constructor(config) {
3350
+ this.agentId = config.agentId;
3351
+ this.port = config.port;
3352
+ this.apiUrl = config.apiUrl.replace(/\/$/, "");
3353
+ this.getAuthHeader = config.getAuthHeader;
3354
+ this.conversationFilter = config.conversationFilter ?? null;
3355
+ this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
3356
+ this.log = config.log ?? (() => {
2914
3357
  });
2915
- this.fetchImpl = config2.fetchImpl ?? fetch;
2916
- this.sleep = config2.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2917
- this.pausedPollIntervalMs = config2.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
2918
- this.pausedMaxWaitMs = config2.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
2919
- this.stuckQueuedMs = config2.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
2920
- this.now = config2.now ?? (() => Date.now());
2921
- this.fileSyncDirectories = config2.fileSyncDirectories ?? [];
2922
- this.homeDir = config2.homeDir ?? homedir();
3358
+ this.fetchImpl = config.fetchImpl ?? fetch;
3359
+ this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3360
+ this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
3361
+ this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
3362
+ this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
3363
+ this.now = config.now ?? (() => Date.now());
3364
+ this.fileSyncDirectories = config.fileSyncDirectories ?? [];
3365
+ this.homeDir = config.homeDir ?? homedir2();
2923
3366
  }
2924
3367
  /** The IPv4-loopback base URL for the local `opencode serve`. */
2925
3368
  get opencodeBase() {
@@ -3304,7 +3747,12 @@ var ChannelDriver = class _ChannelDriver {
3304
3747
  const directory = await this.resolveOpenCodeDirectory();
3305
3748
  const sessionId = await createOpenCodeSession(this.port, directory);
3306
3749
  this.sessions.set(conversationId, sessionId);
3307
- await this.persistSession(conversationId, sessionId).catch(() => {
3750
+ await this.persistSession(conversationId, sessionId).catch((err) => {
3751
+ this.log({
3752
+ level: "warn",
3753
+ message: `Persisting the OpenCode session binding ${sessionId.slice(0, 8)} for conversation ${conversationId.slice(0, 8)} failed (best-effort, not retried) \u2014 the completion PATCH also carries opencode_session_id, so the binding is repaired when the turn finishes: ${err instanceof Error ? err.message : String(err)}`,
3754
+ conversation_id: conversationId
3755
+ });
3308
3756
  });
3309
3757
  return sessionId;
3310
3758
  }
@@ -5017,11 +5465,11 @@ var ChannelDriver = class _ChannelDriver {
5017
5465
  * `ensureSession` guard, not this suppression, is what makes the self-heal
5018
5466
  * stick.
5019
5467
  */
5020
- sessionIdBody(sessionId, conversationId, messageId, status) {
5468
+ sessionIdBody(sessionId, conversationId, messageId, status2) {
5021
5469
  if (!this.isSuperseded(conversationId, sessionId)) return { opencode_session_id: sessionId };
5022
5470
  this.log({
5023
5471
  level: "debug",
5024
- message: `Omitting the abandoned OpenCode session ${sessionId.slice(0, 8)} from the '${status}' update for message ${messageId.slice(0, 8)} so it is not re-bound to conversation ${conversationId.slice(0, 8)}`,
5472
+ message: `Omitting the abandoned OpenCode session ${sessionId.slice(0, 8)} from the '${status2}' update for message ${messageId.slice(0, 8)} so it is not re-bound to conversation ${conversationId.slice(0, 8)}`,
5025
5473
  conversation_id: conversationId,
5026
5474
  message_id: messageId
5027
5475
  });
@@ -5467,143 +5915,6 @@ Port ${port} is already in use.`));
5467
5915
  return { port, process: null, version: null, notReadyReason: "you chose to continue without it" };
5468
5916
  }
5469
5917
 
5470
- // src/commands/agent-lookup.ts
5471
- async function readErrorMessage(response) {
5472
- const text = await response.text().catch(() => "");
5473
- if (!text) return response.statusText || void 0;
5474
- try {
5475
- const data = JSON.parse(text);
5476
- const message = data.message ?? data.error;
5477
- if (typeof message === "string" && message.trim()) {
5478
- return message;
5479
- }
5480
- } catch {
5481
- }
5482
- return text.trim() || response.statusText || void 0;
5483
- }
5484
- function authFailureHint(apiUrl, serverMessage) {
5485
- const reason = serverMessage ? `: ${serverMessage}` : "";
5486
- return `Authentication failed${reason}. Your credentials were rejected by ${apiUrl}. This usually means you logged in against a different environment, or your session expired \u2014 log in again pointing at this endpoint and retry.`;
5487
- }
5488
- async function resolveAgentIdFromKey(authHeader) {
5489
- const apiUrl = getApiUrlConfig();
5490
- try {
5491
- const response = await fetch(`${apiUrl}/me`, {
5492
- headers: { Authorization: authHeader }
5493
- });
5494
- if (response.status === 401) {
5495
- const serverMessage = await readErrorMessage(response);
5496
- return { error: authFailureHint(apiUrl, serverMessage), authFailed: true };
5497
- }
5498
- if (!response.ok) {
5499
- const serverMessage = await readErrorMessage(response);
5500
- return {
5501
- error: `Failed to resolve runner from key (HTTP ${response.status})${serverMessage ? `: ${serverMessage}` : ""}`
5502
- };
5503
- }
5504
- const data = await response.json();
5505
- if (data.auth_type === "agent_key" && data.agent_id) {
5506
- return { agent_id: data.agent_id };
5507
- }
5508
- return {
5509
- error: "Cannot resolve runner ID: auth type is not agent_key. Please provide --runner explicitly."
5510
- };
5511
- } catch (error2) {
5512
- const message = error2 instanceof Error ? error2.message : "Unknown error";
5513
- return { error: `Failed to resolve runner from key: ${message}` };
5514
- }
5515
- }
5516
- var BEST_EFFORT_NOTIFY_TIMEOUT_MS = 2e3;
5517
- async function notifyAgentDisconnected(agentId, authHeader) {
5518
- const apiUrl = getApiUrlConfig();
5519
- try {
5520
- const response = await fetch(`${apiUrl}/runners/${agentId}/disconnect`, {
5521
- method: "POST",
5522
- headers: { Authorization: authHeader },
5523
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
5524
- });
5525
- if (!response.ok) {
5526
- const serverMessage = await readErrorMessage(response);
5527
- return {
5528
- ok: false,
5529
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
5530
- };
5531
- }
5532
- return { ok: true };
5533
- } catch (error2) {
5534
- return { ok: false, error: describeBestEffortError(error2) };
5535
- }
5536
- }
5537
- function describeBestEffortError(error2) {
5538
- const name = error2?.name;
5539
- if (name === "TimeoutError" || name === "AbortError") {
5540
- return `timed out after ${BEST_EFFORT_NOTIFY_TIMEOUT_MS}ms`;
5541
- }
5542
- return error2 instanceof Error ? error2.message : String(error2);
5543
- }
5544
- async function reportMicrovmId(agentId, authHeader, microvmId) {
5545
- try {
5546
- const apiUrl = getApiUrlConfig();
5547
- const response = await fetch(`${apiUrl}/runners/${agentId}/microvm`, {
5548
- method: "POST",
5549
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
5550
- body: JSON.stringify({ microvm_id: microvmId }),
5551
- signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
5552
- });
5553
- if (!response.ok) {
5554
- const serverMessage = await readErrorMessage(response);
5555
- return {
5556
- ok: false,
5557
- error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
5558
- };
5559
- }
5560
- return { ok: true };
5561
- } catch (error2) {
5562
- return { ok: false, error: describeBestEffortError(error2) };
5563
- }
5564
- }
5565
- async function getAgentInfo(agentId, authHeader) {
5566
- const apiUrl = getApiUrlConfig();
5567
- try {
5568
- const response = await fetch(`${apiUrl}/runners/${agentId}`, {
5569
- headers: { Authorization: authHeader }
5570
- });
5571
- if (response.status === 401) {
5572
- const serverMessage = await readErrorMessage(response);
5573
- return { valid: false, error: authFailureHint(apiUrl, serverMessage), authFailed: true };
5574
- }
5575
- if (response.status === 403) {
5576
- const serverMessage = await readErrorMessage(response);
5577
- return {
5578
- valid: false,
5579
- error: serverMessage ?? "You do not have access to this runner (it may belong to a different team or organization)."
5580
- };
5581
- }
5582
- if (response.status === 404) {
5583
- const serverMessage = await readErrorMessage(response);
5584
- return { valid: false, error: serverMessage ?? `Runner ${agentId} not found` };
5585
- }
5586
- if (!response.ok) {
5587
- const serverMessage = await readErrorMessage(response);
5588
- return {
5589
- valid: false,
5590
- error: `API error (HTTP ${response.status})${serverMessage ? `: ${serverMessage}` : ""}`
5591
- };
5592
- }
5593
- const agent = await response.json();
5594
- if (agent.agent_type !== "local") {
5595
- return {
5596
- valid: false,
5597
- error: `Runner is type '${agent.agent_type}', must be 'local' for CLI connection`
5598
- };
5599
- }
5600
- return { valid: true, agent };
5601
- } catch (error2) {
5602
- const message = error2 instanceof Error ? error2.message : "Unknown error";
5603
- return { valid: false, error: `Failed to validate runner: ${message}` };
5604
- }
5605
- }
5606
-
5607
5918
  // src/commands/run.ts
5608
5919
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
5609
5920
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
@@ -5640,7 +5951,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
5640
5951
  if (trimmed === "") {
5641
5952
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
5642
5953
  }
5643
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join2(homeDir, trimmed.slice(2)) : trimmed;
5954
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join3(homeDir, trimmed.slice(2)) : trimmed;
5644
5955
  if (!isAbsolute2(expanded)) {
5645
5956
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
5646
5957
  }
@@ -5814,6 +6125,7 @@ async function handleAuthError(state, error2) {
5814
6125
  }
5815
6126
  async function driveChannels(state, driver) {
5816
6127
  let idlePolls = 0;
6128
+ let consecutiveDrainFailures = 0;
5817
6129
  let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
5818
6130
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
5819
6131
  while (state.running) {
@@ -5831,6 +6143,7 @@ async function driveChannels(state, driver) {
5831
6143
  );
5832
6144
  try {
5833
6145
  const processed = await driver.drainPending();
6146
+ consecutiveDrainFailures = 0;
5834
6147
  state.messageCount += processed;
5835
6148
  const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
5836
6149
  lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
@@ -5865,8 +6178,32 @@ async function driveChannels(state, driver) {
5865
6178
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
5866
6179
  logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage}` });
5867
6180
  if (state.interactive) displayStatus(state);
6181
+ if (driver.hasInFlightWatchers()) {
6182
+ consecutiveDrainFailures = 0;
6183
+ } else if (state.idleTimeout !== null) {
6184
+ consecutiveDrainFailures++;
6185
+ if (consecutiveDrainFailures === 1) {
6186
+ logActivity(state, {
6187
+ type: "info",
6188
+ message: `Cannot reach Evident, will exit if this persists past the idle timeout (timeout: ${state.idleTimeout}s)...`
6189
+ });
6190
+ if (state.interactive) displayStatus(state);
6191
+ }
6192
+ }
5868
6193
  }
5869
6194
  await new Promise((resolve3) => setTimeout(resolve3, CHANNEL_POLL_INTERVAL_MS));
6195
+ if (state.idleTimeout !== null && consecutiveDrainFailures >= 2) {
6196
+ const unreachableMs = consecutiveDrainFailures * CHANNEL_POLL_INTERVAL_MS;
6197
+ if (unreachableMs > state.idleTimeout * 1e3) {
6198
+ logActivity(state, {
6199
+ type: "info",
6200
+ level: "warn",
6201
+ message: `Exiting: could not reach Evident for ${consecutiveDrainFailures} consecutive polls (${Math.round(unreachableMs / 1e3)}s)`
6202
+ });
6203
+ if (state.interactive) displayStatus(state);
6204
+ break;
6205
+ }
6206
+ }
5870
6207
  if (state.idleTimeout !== null && idlePolls >= 2) {
5871
6208
  const idleMs = idlePolls * CHANNEL_POLL_INTERVAL_MS;
5872
6209
  if (idleMs > state.idleTimeout * 1e3) {
@@ -5878,8 +6215,8 @@ async function driveChannels(state, driver) {
5878
6215
  }
5879
6216
  }
5880
6217
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
5881
- async function runSweep(state, driver, config2) {
5882
- const mode = `age=${config2.maxAgeMs ?? "\u2014"} count=${config2.maxCount ?? "\u2014"}`;
6218
+ async function runSweep(state, driver, config) {
6219
+ const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
5883
6220
  try {
5884
6221
  const sessions = await listSessions(state.port);
5885
6222
  if (sessions === null) {
@@ -5892,8 +6229,8 @@ async function runSweep(state, driver, config2) {
5892
6229
  const toDelete = selectSessionsToDelete(
5893
6230
  sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
5894
6231
  {
5895
- maxAgeMs: config2.maxAgeMs,
5896
- maxCount: config2.maxCount,
6232
+ maxAgeMs: config.maxAgeMs,
6233
+ maxCount: config.maxCount,
5897
6234
  nowMs: Date.now(),
5898
6235
  protectedIds: driver.protectedSessionIds()
5899
6236
  }
@@ -5929,7 +6266,7 @@ async function runSweep(state, driver, config2) {
5929
6266
  }
5930
6267
  }
5931
6268
  function scheduleSessionCleanup(state, driver, options) {
5932
- const config2 = resolveSessionCleanupConfig(
6269
+ const config = resolveSessionCleanupConfig(
5933
6270
  {
5934
6271
  maxAge: options.sessionCleanupMaxAge,
5935
6272
  maxCount: options.sessionCleanupMaxCount,
@@ -5937,21 +6274,112 @@ function scheduleSessionCleanup(state, driver, options) {
5937
6274
  },
5938
6275
  process.env
5939
6276
  );
5940
- for (const warning2 of config2.warnings) {
6277
+ for (const warning2 of config.warnings) {
5941
6278
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
5942
6279
  }
5943
- if (!config2.enabled) return;
6280
+ if (!config.enabled) return;
5944
6281
  logActivity(state, {
5945
6282
  type: "info",
5946
- message: `Session cleanup enabled (age=${config2.maxAgeMs ?? "\u2014"}, count=${config2.maxCount ?? "\u2014"}, interval=${config2.intervalMs}ms)`
6283
+ message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)`
5947
6284
  });
5948
- const interval = setInterval(() => void runSweep(state, driver, config2), config2.intervalMs);
6285
+ const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);
5949
6286
  const firstSweep = setTimeout(
5950
- () => void runSweep(state, driver, config2),
6287
+ () => void runSweep(state, driver, config),
5951
6288
  SESSION_CLEANUP_FIRST_SWEEP_MS
5952
6289
  );
5953
6290
  state.sessionCleanupTimers.push(interval, firstSweep);
5954
6291
  }
6292
+ function claudeUsageFailureStreakSuffix(consecutiveFailures) {
6293
+ return consecutiveFailures > 1 ? ` (${consecutiveFailures} consecutive failures)` : "";
6294
+ }
6295
+ function scheduleClaudeUsageReporting(state, options) {
6296
+ const { mode, warnings } = resolveClaudeUsageReportingMode(
6297
+ options.claudeUsageReporting,
6298
+ process.env
6299
+ );
6300
+ for (const warning2 of warnings) {
6301
+ logActivity(state, {
6302
+ type: "info",
6303
+ level: "warn",
6304
+ message: `Claude usage reporting: ${warning2}`
6305
+ });
6306
+ }
6307
+ if (mode === "off") {
6308
+ logActivity(state, {
6309
+ type: "info",
6310
+ level: "debug",
6311
+ message: "Claude usage reporting is off (--claude-usage-reporting off)"
6312
+ });
6313
+ return;
6314
+ }
6315
+ let consecutiveFailures = 0;
6316
+ const scheduleNextTick = () => {
6317
+ state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());
6318
+ };
6319
+ const tick = async (isFirst) => {
6320
+ try {
6321
+ const usage = await getClaudeUsage();
6322
+ const result = await reportClaudeUsage(state.agentId, state.authHeader, usage);
6323
+ if (result.ok) {
6324
+ if (consecutiveFailures > 0) {
6325
+ logActivity(state, {
6326
+ type: "info",
6327
+ level: "info",
6328
+ message: "Claude usage reporting recovered"
6329
+ });
6330
+ }
6331
+ consecutiveFailures = 0;
6332
+ logActivity(state, {
6333
+ type: "info",
6334
+ level: "debug",
6335
+ message: "Reported Claude usage to Evident"
6336
+ });
6337
+ } else {
6338
+ consecutiveFailures++;
6339
+ logActivity(state, {
6340
+ type: "info",
6341
+ level: claudeUsageFailureLogLevel(consecutiveFailures),
6342
+ message: `Failed to report Claude usage: ${result.error}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
6343
+ });
6344
+ }
6345
+ scheduleNextTick();
6346
+ } catch (error2) {
6347
+ if (error2 instanceof ClaudeUsageError && isLocalCredentialProblem(error2)) {
6348
+ if (mode === "on") {
6349
+ logActivity(state, {
6350
+ type: "info",
6351
+ level: "warn",
6352
+ 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"
6353
+ });
6354
+ scheduleNextTick();
6355
+ } else if (isFirst) {
6356
+ logActivity(state, {
6357
+ type: "info",
6358
+ level: "debug",
6359
+ message: `Claude usage reporting: ${error2.message}`
6360
+ });
6361
+ } else {
6362
+ logActivity(state, {
6363
+ type: "info",
6364
+ level: "debug",
6365
+ message: `Claude usage reporting: ${error2.message}`
6366
+ });
6367
+ scheduleNextTick();
6368
+ }
6369
+ } else {
6370
+ consecutiveFailures++;
6371
+ const message = error2 instanceof Error ? error2.message : String(error2);
6372
+ logActivity(state, {
6373
+ type: "info",
6374
+ level: claudeUsageFailureLogLevel(consecutiveFailures),
6375
+ message: `Claude usage reporting failed: ${message}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
6376
+ });
6377
+ scheduleNextTick();
6378
+ }
6379
+ }
6380
+ };
6381
+ state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
6382
+ }
5955
6383
  async function notifyOffline(state) {
5956
6384
  if (!state.agentId || !state.authHeader) return;
5957
6385
  if (!state.connected) {
@@ -5987,6 +6415,10 @@ async function cleanup(state, opts = {}) {
5987
6415
  clearTimeout(timer);
5988
6416
  }
5989
6417
  state.sessionCleanupTimers = [];
6418
+ if (state.claudeUsageTimer) {
6419
+ clearTimeout(state.claudeUsageTimer);
6420
+ state.claudeUsageTimer = null;
6421
+ }
5990
6422
  if (opts.graceful && state.channelDriver) {
5991
6423
  state.channelDriver.stop();
5992
6424
  log2(state, "Draining in-flight channel work before shutdown...");
@@ -6034,7 +6466,7 @@ async function run(options) {
6034
6466
  let fileSyncDirectories;
6035
6467
  try {
6036
6468
  logLevel = resolveLogLevel(options);
6037
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir2());
6469
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir3());
6038
6470
  } catch (error2) {
6039
6471
  const message = error2 instanceof Error ? error2.message : String(error2);
6040
6472
  if (options.json) {
@@ -6067,6 +6499,7 @@ async function run(options) {
6067
6499
  messageCount: 0,
6068
6500
  lastProxiedActivityAt: null,
6069
6501
  sessionCleanupTimers: [],
6502
+ claudeUsageTimer: null,
6070
6503
  authHeader: ""
6071
6504
  };
6072
6505
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
@@ -6146,6 +6579,7 @@ async function run(options) {
6146
6579
  console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
6147
6580
  blank();
6148
6581
  process.exit(1);
6582
+ return;
6149
6583
  }
6150
6584
  blank();
6151
6585
  console.log(chalk6.yellow("You are not logged in to Evident."));
@@ -6190,6 +6624,7 @@ async function run(options) {
6190
6624
  } else {
6191
6625
  printError(resolved.error || "Failed to resolve runner ID from key");
6192
6626
  process.exit(1);
6627
+ return;
6193
6628
  }
6194
6629
  } else {
6195
6630
  printError(
@@ -6203,6 +6638,7 @@ async function run(options) {
6203
6638
  );
6204
6639
  blank();
6205
6640
  process.exit(1);
6641
+ return;
6206
6642
  }
6207
6643
  }
6208
6644
  telemetry.info(
@@ -6321,7 +6757,7 @@ async function run(options) {
6321
6757
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
6322
6758
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
6323
6759
  fileSyncDirectories,
6324
- homeDir: homedir2(),
6760
+ homeDir: homedir3(),
6325
6761
  log: (entry) => (
6326
6762
  // Thread the driver's real level straight through so `debug`/`warn`
6327
6763
  // survive the sink filter (they no longer collapse to info). `type`
@@ -6450,6 +6886,7 @@ async function run(options) {
6450
6886
  throw error2;
6451
6887
  }
6452
6888
  scheduleSessionCleanup(state, channelDriver, options);
6889
+ scheduleClaudeUsageReporting(state, options);
6453
6890
  if (!interactive || state.json) {
6454
6891
  log2(state, "Driving channel messages...");
6455
6892
  }
@@ -6504,6 +6941,8 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
6504
6941
  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);
6505
6942
  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 }));
6506
6943
  program.command("whoami").description("Show the currently logged in user").action(whoami);
6944
+ program.command("status").description("Check whether the configured credentials can reach Evident").option("--json", "Output in JSON format").action((options) => status({ json: options.json }));
6945
+ program.command("claude-usage").description("[spike] Show Claude subscription usage (requires a local `claude login`)").action(claudeUsage);
6507
6946
  program.command("run").description("Connect to Evident and process messages").option("--runner [id]", "Runner ID to connect to (optional when EVIDENT_RUNNER_KEY is set)").option(
6508
6947
  "-a, --agent [id]",
6509
6948
  "Deprecated alias for --runner (still supported; --runner wins if both are given)"
@@ -6522,6 +6961,9 @@ program.command("run").description("Connect to Evident and process messages").op
6522
6961
  ).option(
6523
6962
  "--session-cleanup-interval <duration>",
6524
6963
  "How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL"
6964
+ ).option(
6965
+ "--claude-usage-reporting <mode>",
6966
+ "Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
6525
6967
  ).option(
6526
6968
  "--enable-file-sync-to <dir>",
6527
6969
  "Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
@@ -6550,6 +6992,9 @@ program.command("run").description("Connect to Evident and process messages").op
6550
6992
  sessionCleanupMaxAge: options.sessionCleanupMaxAge,
6551
6993
  sessionCleanupMaxCount: options.sessionCleanupMaxCount,
6552
6994
  sessionCleanupInterval: options.sessionCleanupInterval,
6995
+ // Raw string — the resolver in run.ts single-sources parsing
6996
+ // (resolveClaudeUsageReportingMode).
6997
+ claudeUsageReporting: options.claudeUsageReporting,
6553
6998
  // Raw values — expansion/validation is single-sourced in run.ts's
6554
6999
  // resolveFileSyncDirectories.
6555
7000
  enableFileSyncTo: options.enableFileSyncTo,