@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/README.md +41 -0
- package/dist/index.js +701 -256
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
542
|
-
import { isAbsolute as isAbsolute2, join as
|
|
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
|
-
|
|
1259
|
+
windowCount++;
|
|
1260
|
+
return true;
|
|
855
1261
|
}
|
|
856
|
-
function
|
|
857
|
-
|
|
858
|
-
|
|
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 =
|
|
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("~/") ?
|
|
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 =
|
|
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 =
|
|
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(
|
|
2575
|
-
return
|
|
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,
|
|
2609
|
-
const outcome = `${
|
|
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,
|
|
3134
|
+
constructor(message, status2) {
|
|
2692
3135
|
super(message);
|
|
2693
3136
|
this.name = "ChannelTerminalError";
|
|
2694
|
-
this.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(
|
|
2703
|
-
return
|
|
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(
|
|
2907
|
-
this.agentId =
|
|
2908
|
-
this.port =
|
|
2909
|
-
this.apiUrl =
|
|
2910
|
-
this.getAuthHeader =
|
|
2911
|
-
this.conversationFilter =
|
|
2912
|
-
this.retry = { ...DEFAULT_RETRY_POLICY, ...
|
|
2913
|
-
this.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 =
|
|
2916
|
-
this.sleep =
|
|
2917
|
-
this.pausedPollIntervalMs =
|
|
2918
|
-
this.pausedMaxWaitMs =
|
|
2919
|
-
this.stuckQueuedMs =
|
|
2920
|
-
this.now =
|
|
2921
|
-
this.fileSyncDirectories =
|
|
2922
|
-
this.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,
|
|
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 '${
|
|
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("~/") ?
|
|
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,
|
|
5882
|
-
const mode = `age=${
|
|
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:
|
|
5896
|
-
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
|
|
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
|
|
6277
|
+
for (const warning2 of config.warnings) {
|
|
5941
6278
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
5942
6279
|
}
|
|
5943
|
-
if (!
|
|
6280
|
+
if (!config.enabled) return;
|
|
5944
6281
|
logActivity(state, {
|
|
5945
6282
|
type: "info",
|
|
5946
|
-
message: `Session cleanup enabled (age=${
|
|
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,
|
|
6285
|
+
const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);
|
|
5949
6286
|
const firstSweep = setTimeout(
|
|
5950
|
-
() => void runSweep(state, driver,
|
|
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,
|
|
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:
|
|
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,
|