@mrciphersmith/keryx 0.2.159 → 0.2.161

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.
Files changed (3) hide show
  1. package/dist/cli.js +1916 -1367
  2. package/dist/core.js +33 -20
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -320,7 +320,20 @@ function saveCustomCompatProvider(provider, dir) {
320
320
  return acc;
321
321
  }, {});
322
322
  current[provider.name] = provider;
323
- writeOwnerOnlyFile(llmProvidersConfigPath(dir), `${JSON.stringify({ schemaVersion: 1, providers: current }, null, 2)}
323
+ writeOwnerOnlyFileAtomic(llmProvidersConfigPath(dir), `${JSON.stringify({ schemaVersion: 1, providers: current }, null, 2)}
324
+ `);
325
+ } catch {}
326
+ }
327
+ function removeCustomCompatProvider(name, dir) {
328
+ try {
329
+ const current = loadCustomCompatProviders(dir).reduce((acc, p) => {
330
+ acc[p.name] = p;
331
+ return acc;
332
+ }, {});
333
+ if (!(name in current))
334
+ return;
335
+ delete current[name];
336
+ writeOwnerOnlyFileAtomic(llmProvidersConfigPath(dir), `${JSON.stringify({ schemaVersion: 1, providers: current }, null, 2)}
324
337
  `);
325
338
  } catch {}
326
339
  }
@@ -467,7 +480,7 @@ function saveShellConfig(patch, dir) {
467
480
  try {
468
481
  ensureKeryxConfigDir(dir);
469
482
  const next = { ...loadShellConfig(dir), ...patch };
470
- writeOwnerOnlyFile(shellConfigPath(dir), `${JSON.stringify(next, null, 2)}
483
+ writeOwnerOnlyFileAtomic(shellConfigPath(dir), `${JSON.stringify(next, null, 2)}
471
484
  `);
472
485
  } catch {}
473
486
  }
@@ -479,6 +492,27 @@ function saveProviderBaseUrl(provider, baseUrl, dir) {
479
492
  const existing = loadShellConfig(dir).baseUrls ?? {};
480
493
  saveShellConfig({ baseUrls: { ...existing, [provider]: baseUrl } }, dir);
481
494
  }
495
+ function removeApiKey(envKey, dir) {
496
+ const existing = loadShellConfig(dir).apiKeys ?? {};
497
+ if (!(envKey in existing))
498
+ return;
499
+ const { [envKey]: _removed, ...rest } = existing;
500
+ saveShellConfig({ apiKeys: rest }, dir);
501
+ }
502
+ function removeProviderBaseUrl(provider, dir) {
503
+ const existing = loadShellConfig(dir).baseUrls ?? {};
504
+ if (!(provider in existing))
505
+ return;
506
+ const { [provider]: _removed, ...rest } = existing;
507
+ saveShellConfig({ baseUrls: rest }, dir);
508
+ }
509
+ function removeProviderModelParams(provider, dir) {
510
+ const existing = loadShellConfig(dir).modelParams ?? {};
511
+ if (!(provider in existing))
512
+ return;
513
+ const { [provider]: _removed, ...rest } = existing;
514
+ saveShellConfig({ modelParams: rest }, dir);
515
+ }
482
516
  function envWithSavedApiKeys(env = process.env, dir) {
483
517
  const merged = { ...env };
484
518
  try {
@@ -600,6 +634,9 @@ function grantNeedsRefresh(grant, now = Date.now) {
600
634
  }
601
635
  return grant.expires - now() <= ACCESS_TOKEN_REFRESH_SKEW_MS;
602
636
  }
637
+ function oauthEnvKeyFor(provider) {
638
+ return OAUTH_ENV_KEYS.find((entry) => entry.provider === provider)?.envKey;
639
+ }
603
640
  function envWithOAuthAccess(env = process.env, dir) {
604
641
  const merged = { ...env };
605
642
  for (const { provider, envKey } of OAUTH_ENV_KEYS) {
@@ -647,6 +684,498 @@ var init_grants = __esm(() => {
647
684
  ];
648
685
  });
649
686
 
687
+ // src/lib/oauth/device-code.ts
688
+ function requestInit(base, signal) {
689
+ return signal === undefined ? base : { ...base, signal };
690
+ }
691
+ function formHeaders(extra) {
692
+ return {
693
+ "Content-Type": "application/x-www-form-urlencoded",
694
+ Accept: "application/json",
695
+ ...extra
696
+ };
697
+ }
698
+ function positiveSecondsToMs(value, defaultMs) {
699
+ const seconds = Number(value);
700
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : defaultMs;
701
+ }
702
+ async function defaultSleep(ms) {
703
+ await new Promise((resolve) => setTimeout(resolve, ms));
704
+ }
705
+ function aborted(signal) {
706
+ return signal?.aborted === true;
707
+ }
708
+ function readJsonError(body) {
709
+ if (typeof body !== "object" || body === null) {
710
+ return {};
711
+ }
712
+ const rec = body;
713
+ return {
714
+ ...typeof rec.error === "string" ? { error: rec.error } : {},
715
+ ...typeof rec.error_description === "string" ? { error_description: rec.error_description } : {}
716
+ };
717
+ }
718
+ async function requestDeviceCode(request, http) {
719
+ if (aborted(http.signal)) {
720
+ throw new DeviceCodeError("cancelled", "device authorization was cancelled");
721
+ }
722
+ const body = new URLSearchParams({
723
+ client_id: request.clientId,
724
+ scope: request.scope,
725
+ ...request.extraForm
726
+ });
727
+ const response = await http.fetch(request.deviceAuthorizationEndpoint, requestInit({
728
+ method: "POST",
729
+ headers: formHeaders(request.headers),
730
+ body: body.toString()
731
+ }, http.signal));
732
+ const json = await response.json().catch(() => ({}));
733
+ if (!response.ok) {
734
+ const detail = readJsonError(json).error_description ?? `HTTP ${response.status}`;
735
+ throw new DeviceCodeError("failed", `device code request failed: ${detail}`);
736
+ }
737
+ if (typeof json !== "object" || json === null) {
738
+ throw new DeviceCodeError("failed", "device code response is not an object");
739
+ }
740
+ const rec = json;
741
+ const deviceCode = typeof rec.device_code === "string" ? rec.device_code : "";
742
+ const userCode = typeof rec.user_code === "string" ? rec.user_code : "";
743
+ const verificationUri = typeof rec.verification_uri === "string" ? rec.verification_uri : "";
744
+ if (deviceCode.length === 0 || userCode.length === 0 || verificationUri.length === 0) {
745
+ throw new DeviceCodeError("failed", "device code response is missing device_code / user_code / verification_uri");
746
+ }
747
+ if (![...userCode].every((c) => /[A-Za-z0-9-]/.test(c))) {
748
+ throw new DeviceCodeError("failed", "device code response has an invalid user_code");
749
+ }
750
+ const complete = typeof rec.verification_uri_complete === "string" && rec.verification_uri_complete.length > 0 ? rec.verification_uri_complete : undefined;
751
+ return {
752
+ deviceCode,
753
+ userCode,
754
+ verificationUri,
755
+ ...complete !== undefined ? { verificationUriComplete: complete } : {},
756
+ ...typeof rec.expires_in === "number" ? { expiresInSeconds: rec.expires_in } : {},
757
+ ...typeof rec.interval === "number" ? { intervalSeconds: rec.interval } : {}
758
+ };
759
+ }
760
+ async function pollDeviceCodeToken(challenge, request, http) {
761
+ const sleep = http.sleep ?? defaultSleep;
762
+ const now = http.now ?? Date.now;
763
+ const expiresInMs = positiveSecondsToMs(challenge.expiresInSeconds, DEFAULT_EXPIRES_MS);
764
+ const deadline = now() + expiresInMs;
765
+ let intervalMs = Math.max(positiveSecondsToMs(challenge.intervalSeconds, DEFAULT_INTERVAL_MS), MIN_INTERVAL_MS);
766
+ while (now() < deadline) {
767
+ if (aborted(http.signal)) {
768
+ throw new DeviceCodeError("cancelled", "device authorization was cancelled");
769
+ }
770
+ const response = await http.fetch(request.tokenEndpoint, requestInit({
771
+ method: "POST",
772
+ headers: formHeaders(request.headers),
773
+ body: new URLSearchParams({
774
+ grant_type: DEVICE_CODE_GRANT_TYPE,
775
+ client_id: request.clientId,
776
+ device_code: challenge.deviceCode,
777
+ ...request.extraForm
778
+ }).toString()
779
+ }, http.signal));
780
+ const json = await response.json().catch(() => ({}));
781
+ if (typeof json === "object" && json !== null && typeof json.access_token === "string") {
782
+ const rec = json;
783
+ return {
784
+ accessToken: rec.access_token,
785
+ ...typeof rec.refresh_token === "string" && rec.refresh_token.length > 0 ? { refreshToken: rec.refresh_token } : {},
786
+ ...typeof rec.expires_in === "number" ? { expiresInSeconds: rec.expires_in } : {},
787
+ ...typeof rec.scope === "string" ? { scope: rec.scope } : {},
788
+ ...typeof rec.id_token === "string" ? { idToken: rec.id_token } : {}
789
+ };
790
+ }
791
+ const { error, error_description } = readJsonError(json);
792
+ const remaining = Math.max(0, deadline - now());
793
+ if (error === "authorization_pending") {
794
+ await sleep(Math.min(intervalMs + POLL_SAFETY_MARGIN_MS, remaining));
795
+ continue;
796
+ }
797
+ if (error === "slow_down") {
798
+ intervalMs += SLOW_DOWN_INCREMENT_MS;
799
+ await sleep(Math.min(intervalMs + POLL_SAFETY_MARGIN_MS, remaining));
800
+ continue;
801
+ }
802
+ if (error === "access_denied" || error === "authorization_denied") {
803
+ throw new DeviceCodeError("access_denied", "device authorization was denied");
804
+ }
805
+ if (error === "expired_token") {
806
+ throw new DeviceCodeError("expired_token", "device code expired");
807
+ }
808
+ if (!response.ok) {
809
+ throw new DeviceCodeError("failed", `device token exchange failed (${response.status})${error_description ? `: ${error_description}` : ""}`);
810
+ }
811
+ throw new DeviceCodeError("failed", `device token exchange failed${error_description ? `: ${error_description}` : ""}`);
812
+ }
813
+ throw new DeviceCodeError("timeout", "device authorization timed out");
814
+ }
815
+ async function refreshAccessToken(request, http) {
816
+ const response = await http.fetch(request.tokenEndpoint, requestInit({
817
+ method: "POST",
818
+ headers: formHeaders(request.headers),
819
+ body: new URLSearchParams({
820
+ grant_type: "refresh_token",
821
+ refresh_token: request.refreshToken,
822
+ client_id: request.clientId,
823
+ ...request.extraForm
824
+ }).toString()
825
+ }, http.signal));
826
+ const json = await response.json().catch(() => ({}));
827
+ if (!response.ok || typeof json !== "object" || json === null || typeof json.access_token !== "string") {
828
+ const detail = readJsonError(json).error_description ?? `HTTP ${response.status}`;
829
+ throw new DeviceCodeError("failed", `token refresh failed: ${detail}`);
830
+ }
831
+ const rec = json;
832
+ return {
833
+ accessToken: rec.access_token,
834
+ refreshToken: typeof rec.refresh_token === "string" && rec.refresh_token.length > 0 ? rec.refresh_token : request.refreshToken,
835
+ ...typeof rec.expires_in === "number" ? { expiresInSeconds: rec.expires_in } : {},
836
+ ...typeof rec.scope === "string" ? { scope: rec.scope } : {},
837
+ ...typeof rec.id_token === "string" ? { idToken: rec.id_token } : {}
838
+ };
839
+ }
840
+ var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code", DEFAULT_INTERVAL_MS = 5000, MIN_INTERVAL_MS = 1000, SLOW_DOWN_INCREMENT_MS = 5000, DEFAULT_EXPIRES_MS, POLL_SAFETY_MARGIN_MS = 3000, DeviceCodeError;
841
+ var init_device_code = __esm(() => {
842
+ DEFAULT_EXPIRES_MS = 5 * 60 * 1000;
843
+ DeviceCodeError = class DeviceCodeError extends Error {
844
+ kind;
845
+ constructor(kind, message) {
846
+ super(message);
847
+ this.name = "DeviceCodeError";
848
+ this.kind = kind;
849
+ }
850
+ };
851
+ });
852
+
853
+ // src/lib/oauth/copilot.ts
854
+ function copilotApiBaseFromExchange(raw) {
855
+ if (typeof raw !== "string" || raw.length === 0) {
856
+ return;
857
+ }
858
+ try {
859
+ const url = new URL(raw);
860
+ if (url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0) {
861
+ return;
862
+ }
863
+ const host = url.hostname;
864
+ if (host !== "api.githubcopilot.com" && !host.endsWith(".githubcopilot.com")) {
865
+ return;
866
+ }
867
+ return `https://${host}${url.port.length > 0 ? `:${url.port}` : ""}`;
868
+ } catch {
869
+ return;
870
+ }
871
+ }
872
+ function expiresInSecondsFrom(rec, now) {
873
+ if (typeof rec.expires_at === "string") {
874
+ const expires = Date.parse(rec.expires_at);
875
+ if (Number.isFinite(expires)) {
876
+ return Math.max(1, Math.floor((expires - now) / 1000));
877
+ }
878
+ }
879
+ if (typeof rec.expires_at === "number" && Number.isFinite(rec.expires_at)) {
880
+ const expiresMs = rec.expires_at < 1000000000000 ? rec.expires_at * 1000 : rec.expires_at;
881
+ return Math.max(1, Math.floor((expiresMs - now) / 1000));
882
+ }
883
+ if (typeof rec.refresh_in === "number" && Number.isFinite(rec.refresh_in) && rec.refresh_in > 0) {
884
+ return Math.floor(rec.refresh_in);
885
+ }
886
+ return;
887
+ }
888
+ function describeExchangeFailure(status, json, body) {
889
+ if (typeof json === "object" && json !== null) {
890
+ const rec = json;
891
+ if (rec.error_details?.copilot_access_denied === true) {
892
+ return `GitHub Copilot token exchange failed (HTTP ${status}): Copilot access denied \u2014 check the GitHub Copilot subscription on this account`;
893
+ }
894
+ if (typeof rec.message === "string" && rec.message.length > 0) {
895
+ return `GitHub Copilot token exchange failed (HTTP ${status}): ${rec.message}`;
896
+ }
897
+ }
898
+ if (/zscaler|blocked because it does not comply/i.test(body)) {
899
+ return `GitHub Copilot token exchange failed (HTTP ${status}): request was blocked by a gateway or proxy`;
900
+ }
901
+ return `GitHub Copilot token exchange failed (HTTP ${status})`;
902
+ }
903
+ async function exchangeGithubTokenForCopilot(githubToken, http) {
904
+ const init = {
905
+ method: "GET",
906
+ headers: {
907
+ ...GITHUB_COPILOT_REQUEST_HEADERS,
908
+ Authorization: `token ${githubToken}`
909
+ }
910
+ };
911
+ if (http.signal !== undefined) {
912
+ init.signal = http.signal;
913
+ }
914
+ const response = await http.fetch(COPILOT_TOKEN_URL, init);
915
+ const body = await response.text().catch(() => "");
916
+ let json = {};
917
+ if (body.length > 0) {
918
+ try {
919
+ json = JSON.parse(body);
920
+ } catch {
921
+ json = {};
922
+ }
923
+ }
924
+ if (!response.ok || typeof json !== "object" || json === null) {
925
+ throw new DeviceCodeError("failed", describeExchangeFailure(response.status, json, body));
926
+ }
927
+ const rec = json;
928
+ if (typeof rec.token !== "string" || rec.token.length === 0) {
929
+ throw new DeviceCodeError("failed", "GitHub Copilot token exchange returned no token");
930
+ }
931
+ const expiresInSeconds = expiresInSecondsFrom(rec, Date.now());
932
+ const apiBaseUrl = copilotApiBaseFromExchange(rec.endpoints?.api);
933
+ return {
934
+ accessToken: rec.token,
935
+ ...expiresInSeconds !== undefined ? { expiresInSeconds } : {},
936
+ ...apiBaseUrl !== undefined ? { apiBaseUrl } : {}
937
+ };
938
+ }
939
+ var COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
940
+ var init_copilot = __esm(() => {
941
+ init_catalog();
942
+ init_device_code();
943
+ });
944
+
945
+ // src/lib/oauth/openai-codex.ts
946
+ function withSignal(base, signal) {
947
+ return signal === undefined ? base : { ...base, signal };
948
+ }
949
+ async function requestCodexDeviceCode(http) {
950
+ const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/api/accounts/deviceauth/usercode`, withSignal({
951
+ method: "POST",
952
+ headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "keryx" },
953
+ body: JSON.stringify({ client_id: OPENAI_CODEX_CLIENT_ID })
954
+ }, http.signal));
955
+ const json = await response.json().catch(() => ({}));
956
+ if (!response.ok || typeof json !== "object" || json === null) {
957
+ throw new DeviceCodeError("failed", `ChatGPT device authorization failed (HTTP ${response.status})`);
958
+ }
959
+ const rec = json;
960
+ if (typeof rec.device_auth_id !== "string" || typeof rec.user_code !== "string") {
961
+ throw new DeviceCodeError("failed", "ChatGPT device authorization is missing device_auth_id / user_code");
962
+ }
963
+ const intervalSeconds = Number(rec.interval);
964
+ return {
965
+ deviceAuthId: rec.device_auth_id,
966
+ userCode: rec.user_code,
967
+ verificationUri: `${OPENAI_CODEX_ISSUER}/codex/device`,
968
+ intervalMs: Number.isFinite(intervalSeconds) && intervalSeconds > 0 ? intervalSeconds * 1000 : 5000
969
+ };
970
+ }
971
+ async function pollCodexDeviceToken(challenge, http) {
972
+ const sleep = http.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
973
+ const now = http.now ?? Date.now;
974
+ const deadline = now() + 5 * 60 * 1000;
975
+ while (now() < deadline) {
976
+ if (http.signal?.aborted === true) {
977
+ throw new DeviceCodeError("cancelled", "device authorization was cancelled");
978
+ }
979
+ const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/api/accounts/deviceauth/token`, withSignal({
980
+ method: "POST",
981
+ headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "keryx" },
982
+ body: JSON.stringify({ device_auth_id: challenge.deviceAuthId, user_code: challenge.userCode })
983
+ }, http.signal));
984
+ if (response.ok) {
985
+ const data = await response.json();
986
+ if (typeof data.authorization_code !== "string" || typeof data.code_verifier !== "string") {
987
+ throw new DeviceCodeError("failed", "ChatGPT device token is missing authorization_code");
988
+ }
989
+ return exchangeCodexAuthorizationCode(data.authorization_code, data.code_verifier, http);
990
+ }
991
+ if (response.status !== 403 && response.status !== 404) {
992
+ throw new DeviceCodeError("failed", `ChatGPT device token poll failed (HTTP ${response.status})`);
993
+ }
994
+ await sleep(challenge.intervalMs + POLL_SAFETY_MARGIN_MS2);
995
+ }
996
+ throw new DeviceCodeError("timeout", "ChatGPT device authorization timed out");
997
+ }
998
+ async function exchangeCodexAuthorizationCode(code, codeVerifier, http) {
999
+ const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/oauth/token`, withSignal({
1000
+ method: "POST",
1001
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
1002
+ body: new URLSearchParams({
1003
+ grant_type: "authorization_code",
1004
+ code,
1005
+ redirect_uri: `${OPENAI_CODEX_ISSUER}/deviceauth/callback`,
1006
+ client_id: OPENAI_CODEX_CLIENT_ID,
1007
+ code_verifier: codeVerifier
1008
+ }).toString()
1009
+ }, http.signal));
1010
+ const json = await response.json().catch(() => ({}));
1011
+ if (!response.ok || typeof json !== "object" || json === null || typeof json.access_token !== "string") {
1012
+ throw new DeviceCodeError("failed", `ChatGPT token exchange failed (HTTP ${response.status})`);
1013
+ }
1014
+ const rec = json;
1015
+ return {
1016
+ accessToken: rec.access_token,
1017
+ ...typeof rec.refresh_token === "string" ? { refreshToken: rec.refresh_token } : {},
1018
+ ...typeof rec.expires_in === "number" ? { expiresInSeconds: rec.expires_in } : {}
1019
+ };
1020
+ }
1021
+ var OPENAI_CODEX_ISSUER = "https://auth.openai.com", OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann", POLL_SAFETY_MARGIN_MS2 = 3000;
1022
+ var init_openai_codex = __esm(() => {
1023
+ init_device_code();
1024
+ });
1025
+
1026
+ // src/lib/oauth/login.ts
1027
+ function httpOf(input) {
1028
+ return {
1029
+ fetch: input.fetch,
1030
+ ...input.sleep !== undefined ? { sleep: input.sleep } : {},
1031
+ ...input.now !== undefined ? { now: input.now } : {},
1032
+ ...input.signal !== undefined ? { signal: input.signal } : {}
1033
+ };
1034
+ }
1035
+ function present(challenge, provider) {
1036
+ return {
1037
+ userCode: challenge.userCode,
1038
+ verificationUri: challenge.verificationUri,
1039
+ ...challenge.verificationUriComplete !== undefined ? { verificationUriComplete: challenge.verificationUriComplete } : {},
1040
+ instructions: `Open ${challenge.verificationUri} and enter code: ${challenge.userCode} (${deviceCodeMethodLabel(provider)})`
1041
+ };
1042
+ }
1043
+ async function loginRfc8628(input) {
1044
+ const endpoints = input.provider === "github-copilot" ? GITHUB_COPILOT_DEVICE : XAI_GROK_DEVICE;
1045
+ const http = httpOf(input);
1046
+ const challenge = await requestDeviceCode({
1047
+ deviceAuthorizationEndpoint: endpoints.deviceAuthorizationEndpoint,
1048
+ clientId: endpoints.clientId,
1049
+ scope: endpoints.scope,
1050
+ ...endpoints.extraForm !== undefined ? { extraForm: endpoints.extraForm } : {},
1051
+ ...endpoints.headers !== undefined ? { headers: endpoints.headers } : {}
1052
+ }, http);
1053
+ input.onChallenge(present(challenge, input.provider));
1054
+ const tokens = await pollDeviceCodeToken(challenge, {
1055
+ tokenEndpoint: endpoints.tokenEndpoint,
1056
+ clientId: endpoints.clientId,
1057
+ ...endpoints.headers !== undefined ? { headers: endpoints.headers } : {}
1058
+ }, http);
1059
+ if (endpoints.afterToken === "github-copilot") {
1060
+ const copilot = await exchangeGithubTokenForCopilot(tokens.accessToken, http);
1061
+ if (copilot.apiBaseUrl !== undefined) {
1062
+ saveProviderBaseUrl("github-copilot", copilot.apiBaseUrl, input.dir);
1063
+ }
1064
+ return {
1065
+ accessToken: copilot.accessToken,
1066
+ refreshToken: tokens.accessToken,
1067
+ ...copilot.expiresInSeconds !== undefined ? { expiresInSeconds: copilot.expiresInSeconds } : {}
1068
+ };
1069
+ }
1070
+ return tokens;
1071
+ }
1072
+ async function loginDeviceCode(input) {
1073
+ const entry = authCatalogEntry(input.provider);
1074
+ if (entry === undefined) {
1075
+ return { ok: false, error: `unknown provider: ${input.provider}` };
1076
+ }
1077
+ if (!entry.methods.includes("device-code")) {
1078
+ const refusal = catalogRefusal(input.provider);
1079
+ if (refusal !== undefined) {
1080
+ return { ok: false, error: refusal, refused: true };
1081
+ }
1082
+ return { ok: false, error: `${input.provider} does not support device authorization` };
1083
+ }
1084
+ try {
1085
+ const tokens = input.provider === "openai" ? await loginCodex(input) : await loginRfc8628(input);
1086
+ saveOAuthGrant(input.provider, grantFromTokens("device-code", tokens, input.now), input.dir);
1087
+ return { ok: true, provider: input.provider };
1088
+ } catch (err) {
1089
+ const message = err instanceof DeviceCodeError ? err.message : err instanceof Error ? err.message : "device authorization failed";
1090
+ return { ok: false, error: message };
1091
+ }
1092
+ }
1093
+ async function loginCodex(input) {
1094
+ const http = httpOf(input);
1095
+ const challenge = await requestCodexDeviceCode(http);
1096
+ input.onChallenge(present({
1097
+ userCode: challenge.userCode,
1098
+ verificationUri: challenge.verificationUri
1099
+ }, "openai"));
1100
+ return pollCodexDeviceToken(challenge, http);
1101
+ }
1102
+ async function refreshProviderGrant(provider, http, dir) {
1103
+ const grant = loadOAuthGrant(provider, dir);
1104
+ if (grant === undefined || !grantNeedsRefresh(grant, http.now)) {
1105
+ return grant;
1106
+ }
1107
+ if (grant.refresh === undefined) {
1108
+ return grant;
1109
+ }
1110
+ const endpoints = provider === "github-copilot" ? GITHUB_COPILOT_DEVICE : XAI_GROK_DEVICE;
1111
+ if (provider === "github-copilot") {
1112
+ const copilot = await exchangeGithubTokenForCopilot(grant.refresh, http);
1113
+ if (copilot.apiBaseUrl !== undefined) {
1114
+ saveProviderBaseUrl("github-copilot", copilot.apiBaseUrl, dir);
1115
+ }
1116
+ const next = grantFromTokens("device-code", {
1117
+ accessToken: copilot.accessToken,
1118
+ refreshToken: grant.refresh,
1119
+ ...copilot.expiresInSeconds !== undefined ? { expiresInSeconds: copilot.expiresInSeconds } : {}
1120
+ }, http.now);
1121
+ next.lastRefreshedAt = new Date((http.now ?? Date.now)()).toISOString();
1122
+ saveOAuthGrant(provider, next, dir);
1123
+ return next;
1124
+ }
1125
+ if (provider === "openai") {
1126
+ return grant;
1127
+ }
1128
+ const tokens = await refreshAccessToken({
1129
+ tokenEndpoint: endpoints.tokenEndpoint,
1130
+ clientId: endpoints.clientId,
1131
+ refreshToken: grant.refresh,
1132
+ ...endpoints.headers !== undefined ? { headers: endpoints.headers } : {}
1133
+ }, http);
1134
+ const next = grantFromTokens("device-code", tokens, http.now);
1135
+ next.lastRefreshedAt = new Date((http.now ?? Date.now)()).toISOString();
1136
+ saveOAuthGrant(provider, next, dir);
1137
+ return next;
1138
+ }
1139
+ async function refreshSavedGrants(http, dir, providers = SESSION_REFRESHED_GRANTS) {
1140
+ const warnings = [];
1141
+ const now = http.now ?? Date.now;
1142
+ for (const provider of providers) {
1143
+ if (!SESSION_REFRESHED_GRANTS.includes(provider))
1144
+ continue;
1145
+ const grant = loadOAuthGrant(provider, dir);
1146
+ if (grant === undefined)
1147
+ continue;
1148
+ if (grant.refresh === undefined || grant.refresh.length === 0) {
1149
+ if (grant.expires !== undefined && grant.expires <= now()) {
1150
+ warnings.push(`${provider}: the saved login has expired and holds no refresh token. Run \`keryx auth login ${provider}\`.`);
1151
+ }
1152
+ continue;
1153
+ }
1154
+ if (!grantNeedsRefresh(grant, http.now))
1155
+ continue;
1156
+ try {
1157
+ await refreshProviderGrant(provider, http, dir);
1158
+ } catch (cause) {
1159
+ const reason = cause instanceof Error ? cause.message : String(cause);
1160
+ warnings.push(`${provider}: the saved login is expiring or expired and could not be refreshed (${reason}). ` + `Run \`keryx auth login ${provider}\`.`);
1161
+ }
1162
+ }
1163
+ return warnings;
1164
+ }
1165
+ function logoutProvider(provider, dir) {
1166
+ deleteOAuthGrant(provider, dir);
1167
+ }
1168
+ var SESSION_REFRESHED_GRANTS;
1169
+ var init_login = __esm(() => {
1170
+ init_shell_config();
1171
+ init_catalog();
1172
+ init_copilot();
1173
+ init_device_code();
1174
+ init_grants();
1175
+ init_openai_codex();
1176
+ SESSION_REFRESHED_GRANTS = ["grok", "github-copilot"];
1177
+ });
1178
+
650
1179
  // src/lib/caller-session.ts
651
1180
  function resolveCallerSession(input = {}) {
652
1181
  const pair = (provider, model, source) => ({
@@ -717,6 +1246,827 @@ function shortFlag(flag) {
717
1246
  }
718
1247
  var init_args = () => {};
719
1248
 
1249
+ // src/lib/md-blocks.ts
1250
+ function stripTrailingCr(line) {
1251
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
1252
+ }
1253
+ function splitLines(text) {
1254
+ return text.split(`
1255
+ `).map(stripTrailingCr);
1256
+ }
1257
+ function markdownTableCells(line) {
1258
+ const source = line.trim();
1259
+ if (!source.includes("|")) {
1260
+ return;
1261
+ }
1262
+ const cells = [];
1263
+ let cell = "";
1264
+ let inCode = false;
1265
+ for (let index = 0;index < source.length; index += 1) {
1266
+ const char = source[index] ?? "";
1267
+ if (char === "\\" && source[index + 1] === "|") {
1268
+ cell += "|";
1269
+ index += 1;
1270
+ continue;
1271
+ }
1272
+ if (char === "`") {
1273
+ inCode = !inCode;
1274
+ cell += char;
1275
+ continue;
1276
+ }
1277
+ if (char === "|" && !inCode) {
1278
+ cells.push(cell.trim());
1279
+ cell = "";
1280
+ continue;
1281
+ }
1282
+ cell += char;
1283
+ }
1284
+ cells.push(cell.trim());
1285
+ if (source.startsWith("|")) {
1286
+ cells.shift();
1287
+ }
1288
+ if (source.endsWith("|")) {
1289
+ cells.pop();
1290
+ }
1291
+ return cells.length >= 2 ? cells : undefined;
1292
+ }
1293
+ function tableAlignments(line, columns) {
1294
+ const cells = markdownTableCells(line);
1295
+ if (cells === undefined || cells.length !== columns) {
1296
+ return;
1297
+ }
1298
+ const alignments = [];
1299
+ for (const cell of cells) {
1300
+ const marker = cell.trim();
1301
+ if (!/^:?-{3,}:?$/.test(marker)) {
1302
+ return;
1303
+ }
1304
+ alignments.push(marker.startsWith(":") && marker.endsWith(":") ? "center" : marker.endsWith(":") ? "right" : "left");
1305
+ }
1306
+ return alignments;
1307
+ }
1308
+ function parseMarkdownTables(text) {
1309
+ if (text.length === 0) {
1310
+ return [];
1311
+ }
1312
+ const lines = splitLines(text);
1313
+ const parts = [];
1314
+ let proseStart = 0;
1315
+ const flushProse = (end) => {
1316
+ if (end <= proseStart) {
1317
+ return;
1318
+ }
1319
+ const prose = lines.slice(proseStart, end).join(`
1320
+ `);
1321
+ if (prose.length > 0) {
1322
+ parts.push({ kind: "prose", text: prose });
1323
+ }
1324
+ };
1325
+ for (let index = 0;index + 1 < lines.length; index += 1) {
1326
+ const headers = markdownTableCells(lines[index] ?? "");
1327
+ if (headers === undefined) {
1328
+ continue;
1329
+ }
1330
+ const alignments = tableAlignments(lines[index + 1] ?? "", headers.length);
1331
+ if (alignments === undefined) {
1332
+ continue;
1333
+ }
1334
+ flushProse(index);
1335
+ const rows = [];
1336
+ let cursor = index + 2;
1337
+ while (cursor < lines.length) {
1338
+ const row = markdownTableCells(lines[cursor] ?? "");
1339
+ if (row === undefined || row.length !== headers.length) {
1340
+ break;
1341
+ }
1342
+ rows.push(row);
1343
+ cursor += 1;
1344
+ }
1345
+ parts.push({ kind: "table", table: { headers, alignments, rows } });
1346
+ proseStart = cursor;
1347
+ index = cursor - 1;
1348
+ }
1349
+ flushProse(lines.length);
1350
+ return parts.length > 0 ? parts : [{ kind: "prose", text }];
1351
+ }
1352
+ function fenceInfo(line) {
1353
+ const match = FENCE_LINE.exec(line);
1354
+ if (match === null) {
1355
+ return;
1356
+ }
1357
+ return { marker: match[1] ?? "", lang: (match[2] ?? "").trim().split(/\s+/)[0] ?? "" };
1358
+ }
1359
+ function segmentMarkdown(md) {
1360
+ const segments = [];
1361
+ let text = [];
1362
+ let code;
1363
+ let lang = "";
1364
+ let marker = "";
1365
+ const flushText = () => {
1366
+ const joined = text.join(`
1367
+ `);
1368
+ text = [];
1369
+ if (joined.length > 0) {
1370
+ segments.push({ kind: "text", text: joined });
1371
+ }
1372
+ };
1373
+ for (const line of splitLines(md)) {
1374
+ const fence = fenceInfo(line);
1375
+ if (code === undefined) {
1376
+ if (fence === undefined) {
1377
+ text.push(line);
1378
+ continue;
1379
+ }
1380
+ flushText();
1381
+ marker = fence.marker;
1382
+ lang = fence.lang;
1383
+ code = [];
1384
+ continue;
1385
+ }
1386
+ if (fence?.marker === marker) {
1387
+ segments.push({ kind: "code", lang, body: code.join(`
1388
+ `) });
1389
+ code = undefined;
1390
+ continue;
1391
+ }
1392
+ code.push(line);
1393
+ }
1394
+ if (code === undefined) {
1395
+ flushText();
1396
+ } else {
1397
+ segments.push({ kind: "code", lang, body: code.join(`
1398
+ `) });
1399
+ }
1400
+ return segments;
1401
+ }
1402
+ function classifyDiffLine(line) {
1403
+ if (line.startsWith("@@")) {
1404
+ return "hunk";
1405
+ }
1406
+ if (line.startsWith("---") || line.startsWith("+++")) {
1407
+ return "meta";
1408
+ }
1409
+ if (line.startsWith("+")) {
1410
+ return "add";
1411
+ }
1412
+ if (line.startsWith("-")) {
1413
+ return "del";
1414
+ }
1415
+ return "context";
1416
+ }
1417
+ function looksLikeUnifiedDiff(text) {
1418
+ const lines = splitLines(text);
1419
+ for (const [index, line] of lines.entries()) {
1420
+ if (HUNK_HEADER.test(line)) {
1421
+ return true;
1422
+ }
1423
+ if (line.startsWith("--- ") && (lines[index + 1]?.startsWith("+++ ") ?? false)) {
1424
+ return true;
1425
+ }
1426
+ }
1427
+ return false;
1428
+ }
1429
+ function codeCommentPrefix(normalizedLang) {
1430
+ switch (normalizedLang) {
1431
+ case "javascript":
1432
+ case "typescript":
1433
+ case "go":
1434
+ case "rust":
1435
+ case "java":
1436
+ case "c":
1437
+ case "cpp":
1438
+ case "csharp":
1439
+ case "swift":
1440
+ case "kotlin":
1441
+ case "scala":
1442
+ case "php":
1443
+ return "//";
1444
+ case "python":
1445
+ case "bash":
1446
+ case "ruby":
1447
+ case "yaml":
1448
+ case "toml":
1449
+ case "r":
1450
+ case "perl":
1451
+ return "#";
1452
+ case "sql":
1453
+ case "lua":
1454
+ case "haskell":
1455
+ return "--";
1456
+ default:
1457
+ return "";
1458
+ }
1459
+ }
1460
+ function normalizeCodeLang(lang) {
1461
+ const key = lang.trim().toLowerCase();
1462
+ return CODE_LANG_ALIASES[key] ?? key;
1463
+ }
1464
+ function tokenizeCodeWords(text, keywords) {
1465
+ const tokens = [];
1466
+ let last = 0;
1467
+ CODE_WORD_OR_NUMBER.lastIndex = 0;
1468
+ let m;
1469
+ while ((m = CODE_WORD_OR_NUMBER.exec(text)) !== null) {
1470
+ if (m.index > last) {
1471
+ tokens.push({ kind: "plain", text: text.slice(last, m.index) });
1472
+ }
1473
+ const word = m[0];
1474
+ if (/^[0-9]/.test(word)) {
1475
+ tokens.push({ kind: "number", text: word });
1476
+ } else if (keywords.has(word)) {
1477
+ tokens.push({ kind: "keyword", text: word });
1478
+ } else {
1479
+ tokens.push({ kind: "plain", text: word });
1480
+ }
1481
+ last = m.index + word.length;
1482
+ }
1483
+ if (last < text.length) {
1484
+ tokens.push({ kind: "plain", text: text.slice(last) });
1485
+ }
1486
+ return tokens;
1487
+ }
1488
+ function tokenizeCodeLine(line, lang) {
1489
+ const normalized = normalizeCodeLang(lang);
1490
+ const keywords = CODE_LANG_KEYWORDS[normalized] ?? GENERIC_KEYWORDS;
1491
+ const commentPrefix = codeCommentPrefix(normalized);
1492
+ const tokens = [];
1493
+ let plainBuf = "";
1494
+ const flushPlain = () => {
1495
+ if (plainBuf.length > 0) {
1496
+ tokens.push(...tokenizeCodeWords(plainBuf, keywords));
1497
+ plainBuf = "";
1498
+ }
1499
+ };
1500
+ let i = 0;
1501
+ while (i < line.length) {
1502
+ if (commentPrefix.length > 0 && line.startsWith(commentPrefix, i)) {
1503
+ flushPlain();
1504
+ tokens.push({ kind: "comment", text: line.slice(i) });
1505
+ break;
1506
+ }
1507
+ const ch = line[i];
1508
+ if (ch === '"' || ch === "'" || ch === "`") {
1509
+ flushPlain();
1510
+ let j = i + 1;
1511
+ while (j < line.length && line[j] !== ch) {
1512
+ j += line[j] === "\\" ? 2 : 1;
1513
+ }
1514
+ j = Math.min(j + 1, line.length);
1515
+ tokens.push({ kind: "string", text: line.slice(i, j) });
1516
+ i = j;
1517
+ continue;
1518
+ }
1519
+ plainBuf += ch;
1520
+ i += 1;
1521
+ }
1522
+ flushPlain();
1523
+ return tokens;
1524
+ }
1525
+ function payloadKind(lang, _lineCount) {
1526
+ const normalized = lang.toLowerCase();
1527
+ if (MARKDOWN_LANGS.has(normalized)) {
1528
+ return "markdown";
1529
+ }
1530
+ if (DIFF_LANGS.has(normalized)) {
1531
+ return "diff";
1532
+ }
1533
+ return "code";
1534
+ }
1535
+ function blockLabel({ kind, lineCount, collapsed, hint }) {
1536
+ const marker = collapsed ? "\u25B8" : "\u25BE";
1537
+ const unit = lineCount === 1 ? "line" : "lines";
1538
+ const suffix = hint !== undefined && hint.length > 0 ? ` \xB7 ${hint}` : "";
1539
+ return `${marker} ${kind} (${lineCount} ${unit})${suffix}`;
1540
+ }
1541
+ function summarizeSubmittedLine(line) {
1542
+ const normalized = line.replace(/\r\n/g, `
1543
+ `).replace(/\r/g, `
1544
+ `);
1545
+ const nonEmpty = normalized.split(`
1546
+ `).filter((linePart) => linePart.length > 0);
1547
+ if (nonEmpty.length <= 1) {
1548
+ return line;
1549
+ }
1550
+ const [first, ...rest] = nonEmpty;
1551
+ return `${first} [+ ${rest.length} pasted line${rest.length === 1 ? "" : "s"}]`;
1552
+ }
1553
+ function isZeroWidth(cp) {
1554
+ return cp >= 768 && cp <= 879 || cp >= 8203 && cp <= 8207 || cp >= 65024 && cp <= 65039 || cp >= 8288 && cp <= 8292;
1555
+ }
1556
+ function codePointWidth(cp) {
1557
+ for (const [lo, hi] of WIDE_RANGES) {
1558
+ if (cp >= lo && cp <= hi) {
1559
+ return 2;
1560
+ }
1561
+ }
1562
+ return 1;
1563
+ }
1564
+ function visualWidth(text) {
1565
+ let width = 0;
1566
+ for (const ch of text) {
1567
+ const codePoint = ch.codePointAt(0) ?? 0;
1568
+ if (isZeroWidth(codePoint)) {
1569
+ continue;
1570
+ }
1571
+ width += codePointWidth(codePoint);
1572
+ }
1573
+ return width;
1574
+ }
1575
+ function hugWidth(text, chrome) {
1576
+ const lines = splitLines(text.replace(/\n+$/, ""));
1577
+ let widest = 0;
1578
+ for (const line of lines) {
1579
+ widest = Math.max(widest, visualWidth(line));
1580
+ }
1581
+ return widest + chrome;
1582
+ }
1583
+ var FENCE_LINE, HUNK_HEADER, MARKDOWN_LANGS, DIFF_LANGS, CODE_LANG_ALIASES, GENERIC_KEYWORDS, CODE_LANG_KEYWORDS, CODE_WORD_OR_NUMBER, WIDE_RANGES;
1584
+ var init_md_blocks = __esm(() => {
1585
+ FENCE_LINE = /^[ \t]{0,3}(```|~~~)(.*)\r?$/;
1586
+ HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
1587
+ MARKDOWN_LANGS = new Set(["md", "markdown", "prompt"]);
1588
+ DIFF_LANGS = new Set(["diff", "patch"]);
1589
+ CODE_LANG_ALIASES = {
1590
+ js: "javascript",
1591
+ mjs: "javascript",
1592
+ cjs: "javascript",
1593
+ jsx: "javascript",
1594
+ ts: "typescript",
1595
+ tsx: "typescript",
1596
+ py: "python",
1597
+ py3: "python",
1598
+ sh: "bash",
1599
+ shell: "bash",
1600
+ zsh: "bash",
1601
+ console: "bash",
1602
+ yml: "yaml",
1603
+ rb: "ruby",
1604
+ rs: "rust",
1605
+ kt: "kotlin",
1606
+ cs: "csharp",
1607
+ "c++": "cpp",
1608
+ cc: "cpp",
1609
+ cxx: "cpp",
1610
+ hpp: "cpp"
1611
+ };
1612
+ GENERIC_KEYWORDS = new Set([
1613
+ "if",
1614
+ "else",
1615
+ "for",
1616
+ "while",
1617
+ "return",
1618
+ "function",
1619
+ "class",
1620
+ "const",
1621
+ "let",
1622
+ "var",
1623
+ "import",
1624
+ "export",
1625
+ "from",
1626
+ "true",
1627
+ "false",
1628
+ "null",
1629
+ "new",
1630
+ "this",
1631
+ "try",
1632
+ "catch",
1633
+ "throw",
1634
+ "switch",
1635
+ "case",
1636
+ "break",
1637
+ "continue",
1638
+ "default",
1639
+ "static",
1640
+ "public",
1641
+ "private"
1642
+ ]);
1643
+ CODE_LANG_KEYWORDS = {
1644
+ javascript: new Set([
1645
+ ...GENERIC_KEYWORDS,
1646
+ "async",
1647
+ "await",
1648
+ "yield",
1649
+ "typeof",
1650
+ "instanceof",
1651
+ "in",
1652
+ "of",
1653
+ "void",
1654
+ "undefined",
1655
+ "super",
1656
+ "extends",
1657
+ "finally",
1658
+ "do",
1659
+ "delete"
1660
+ ]),
1661
+ typescript: new Set([
1662
+ ...GENERIC_KEYWORDS,
1663
+ "async",
1664
+ "await",
1665
+ "yield",
1666
+ "typeof",
1667
+ "instanceof",
1668
+ "in",
1669
+ "of",
1670
+ "void",
1671
+ "undefined",
1672
+ "super",
1673
+ "extends",
1674
+ "implements",
1675
+ "interface",
1676
+ "type",
1677
+ "enum",
1678
+ "namespace",
1679
+ "declare",
1680
+ "readonly",
1681
+ "abstract",
1682
+ "as",
1683
+ "satisfies",
1684
+ "finally",
1685
+ "do",
1686
+ "delete"
1687
+ ]),
1688
+ python: new Set([
1689
+ "def",
1690
+ "return",
1691
+ "if",
1692
+ "elif",
1693
+ "else",
1694
+ "for",
1695
+ "while",
1696
+ "break",
1697
+ "continue",
1698
+ "class",
1699
+ "import",
1700
+ "from",
1701
+ "as",
1702
+ "try",
1703
+ "except",
1704
+ "finally",
1705
+ "raise",
1706
+ "with",
1707
+ "lambda",
1708
+ "yield",
1709
+ "pass",
1710
+ "None",
1711
+ "True",
1712
+ "False",
1713
+ "and",
1714
+ "or",
1715
+ "not",
1716
+ "in",
1717
+ "is",
1718
+ "global",
1719
+ "nonlocal",
1720
+ "assert",
1721
+ "async",
1722
+ "await",
1723
+ "del",
1724
+ "self"
1725
+ ]),
1726
+ bash: new Set([
1727
+ "if",
1728
+ "then",
1729
+ "else",
1730
+ "elif",
1731
+ "fi",
1732
+ "for",
1733
+ "while",
1734
+ "do",
1735
+ "done",
1736
+ "case",
1737
+ "esac",
1738
+ "function",
1739
+ "return",
1740
+ "local",
1741
+ "export",
1742
+ "exit",
1743
+ "break",
1744
+ "continue",
1745
+ "in",
1746
+ "select",
1747
+ "until"
1748
+ ]),
1749
+ go: new Set([
1750
+ "func",
1751
+ "return",
1752
+ "if",
1753
+ "else",
1754
+ "for",
1755
+ "range",
1756
+ "switch",
1757
+ "case",
1758
+ "default",
1759
+ "break",
1760
+ "continue",
1761
+ "package",
1762
+ "import",
1763
+ "var",
1764
+ "const",
1765
+ "type",
1766
+ "struct",
1767
+ "interface",
1768
+ "map",
1769
+ "chan",
1770
+ "go",
1771
+ "defer",
1772
+ "select",
1773
+ "fallthrough",
1774
+ "nil",
1775
+ "true",
1776
+ "false"
1777
+ ]),
1778
+ rust: new Set([
1779
+ "fn",
1780
+ "let",
1781
+ "mut",
1782
+ "return",
1783
+ "if",
1784
+ "else",
1785
+ "for",
1786
+ "while",
1787
+ "loop",
1788
+ "match",
1789
+ "struct",
1790
+ "enum",
1791
+ "impl",
1792
+ "trait",
1793
+ "pub",
1794
+ "use",
1795
+ "mod",
1796
+ "crate",
1797
+ "self",
1798
+ "Self",
1799
+ "super",
1800
+ "const",
1801
+ "static",
1802
+ "async",
1803
+ "await",
1804
+ "move",
1805
+ "ref",
1806
+ "dyn",
1807
+ "where",
1808
+ "unsafe",
1809
+ "true",
1810
+ "false",
1811
+ "None",
1812
+ "Some",
1813
+ "Ok",
1814
+ "Err"
1815
+ ]),
1816
+ json: new Set(["true", "false", "null"])
1817
+ };
1818
+ CODE_WORD_OR_NUMBER = /[A-Za-z_$][A-Za-z0-9_$]*|\d+(?:\.\d+)?/g;
1819
+ WIDE_RANGES = [
1820
+ [4352, 4447],
1821
+ [11904, 12350],
1822
+ [12353, 13311],
1823
+ [13312, 19903],
1824
+ [19968, 40959],
1825
+ [40960, 42191],
1826
+ [44032, 55203],
1827
+ [63744, 64255],
1828
+ [65072, 65135],
1829
+ [65280, 65376],
1830
+ [65504, 65510],
1831
+ [127744, 128591],
1832
+ [129280, 129535],
1833
+ [131072, 262141]
1834
+ ];
1835
+ });
1836
+
1837
+ // src/lib/ui.ts
1838
+ import { stdout } from "process";
1839
+ function colorEnabled() {
1840
+ if (process.env.NO_COLOR !== undefined) {
1841
+ return false;
1842
+ }
1843
+ if (process.env.FORCE_COLOR !== undefined) {
1844
+ return true;
1845
+ }
1846
+ return Boolean(stdout.isTTY);
1847
+ }
1848
+ function wrap(open, close) {
1849
+ return (text) => colorEnabled() ? `\x1B[${open}m${text}\x1B[${close}m` : text;
1850
+ }
1851
+ function banner(title, subtitle) {
1852
+ const rule = style.cyan("\u2501".repeat(RULE_WIDTH));
1853
+ console.log(rule);
1854
+ console.log(` ${style.bold(title)}`);
1855
+ if (subtitle) {
1856
+ console.log(` ${style.dim(subtitle)}`);
1857
+ }
1858
+ console.log(rule);
1859
+ }
1860
+ function heading(title, emit = console.log) {
1861
+ emit(`
1862
+ ${style.bold(style.cyan(title))}`);
1863
+ }
1864
+ function statusLine(label, enabled, detail) {
1865
+ const marker = enabled ? style.green(symbols.ok) : style.gray(symbols.off);
1866
+ const name = enabled ? label : style.gray(label);
1867
+ const suffix = detail ? style.dim(` (${detail})`) : "";
1868
+ console.log(` ${marker} ${name}${suffix}`);
1869
+ }
1870
+ function nextSteps(steps) {
1871
+ if (steps.length === 0) {
1872
+ return;
1873
+ }
1874
+ heading("Next steps");
1875
+ for (const step of steps) {
1876
+ console.log(` ${style.cyan(symbols.arrow)} ${step}`);
1877
+ }
1878
+ }
1879
+ function note(message) {
1880
+ console.log(` ${style.dim(message)}`);
1881
+ }
1882
+ function renderInline(text) {
1883
+ const withCode = text.replace(/`([^`]+)`/g, (_match, code) => style.gray(code));
1884
+ return withCode.replace(/\*\*([^*]+)\*\*/g, (_match, bold) => style.bold(bold));
1885
+ }
1886
+ function renderTextLine(line) {
1887
+ const heading = /^#{1,6}\s+(.*)$/.exec(line);
1888
+ if (heading !== null) {
1889
+ return style.bold(style.cyan(heading[1] ?? ""));
1890
+ }
1891
+ const bullet = /^(\s*)[-*]\s+(.*)$/.exec(line);
1892
+ if (bullet !== null) {
1893
+ return `${bullet[1] ?? ""}${style.cyan(symbols.bullet)} ${renderInline(bullet[2] ?? "")}`;
1894
+ }
1895
+ return renderInline(line);
1896
+ }
1897
+ function renderDiff(text) {
1898
+ if (!colorEnabled()) {
1899
+ return text;
1900
+ }
1901
+ return splitLines(text).map((line) => {
1902
+ switch (classifyDiffLine(line)) {
1903
+ case "add":
1904
+ return style.green(line);
1905
+ case "del":
1906
+ return style.red(line);
1907
+ case "hunk":
1908
+ return style.cyan(line);
1909
+ case "meta":
1910
+ return style.dim(line);
1911
+ default:
1912
+ return line;
1913
+ }
1914
+ }).join(`
1915
+ `);
1916
+ }
1917
+ function renderCodeSegment(lang, body) {
1918
+ const lines = body.split(`
1919
+ `);
1920
+ const out = [];
1921
+ if (lang.length > 0) {
1922
+ out.push(style.dim(lang));
1923
+ }
1924
+ if (payloadKind(lang, lines.length) === "diff" || looksLikeUnifiedDiff(body)) {
1925
+ out.push(renderDiff(body));
1926
+ return out;
1927
+ }
1928
+ for (const line of lines) {
1929
+ out.push(style.gray(line));
1930
+ }
1931
+ return out;
1932
+ }
1933
+ function renderMarkdown(md) {
1934
+ if (!colorEnabled()) {
1935
+ return md;
1936
+ }
1937
+ const out = [];
1938
+ for (const segment of segmentMarkdown(md)) {
1939
+ if (segment.kind === "code") {
1940
+ out.push(...renderCodeSegment(segment.lang, segment.body));
1941
+ continue;
1942
+ }
1943
+ for (const line of segment.text.split(`
1944
+ `)) {
1945
+ out.push(renderTextLine(line));
1946
+ }
1947
+ }
1948
+ return out.join(`
1949
+ `);
1950
+ }
1951
+ function collapseToolOutput(text, maxWidth = 100) {
1952
+ const trimmed = text.replace(/\r?\n+$/, "");
1953
+ const lines = trimmed.length === 0 ? [] : splitLines(trimmed);
1954
+ const firstNonEmpty = lines.find((line) => line.trim().length > 0) ?? "";
1955
+ const summary = firstNonEmpty.length > maxWidth ? `${firstNonEmpty.slice(0, maxWidth)}\u2026` : firstNonEmpty;
1956
+ const lineCount = lines.length;
1957
+ return { summary, lineCount, hidden: Math.max(0, lineCount - 1) };
1958
+ }
1959
+ function indentBlock(text, pad) {
1960
+ return text.split(`
1961
+ `).map((line) => line.length > 0 ? pad + line : line).join(`
1962
+ `);
1963
+ }
1964
+ function summarizeToolArgs(input, max = 80) {
1965
+ const clip = (s) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
1966
+ const raw = input.trim();
1967
+ if (raw.length === 0) {
1968
+ return "";
1969
+ }
1970
+ let parsed;
1971
+ try {
1972
+ parsed = JSON.parse(raw);
1973
+ } catch {
1974
+ return clip(raw);
1975
+ }
1976
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1977
+ return clip(raw);
1978
+ }
1979
+ const parts = [];
1980
+ for (const [key, value] of Object.entries(parsed)) {
1981
+ let shown;
1982
+ if (value === null) {
1983
+ shown = "null";
1984
+ } else if (Array.isArray(value)) {
1985
+ shown = "[\u2026]";
1986
+ } else if (typeof value === "object") {
1987
+ shown = "{\u2026}";
1988
+ } else {
1989
+ shown = String(value);
1990
+ }
1991
+ parts.push(`${key}=${shown}`);
1992
+ }
1993
+ return clip(parts.join(", "));
1994
+ }
1995
+ function helpTitle(command, summary) {
1996
+ console.log(`${style.bold(command)} ${style.dim(`\u2014 ${summary}`)}`);
1997
+ }
1998
+ function helpUsage(lines) {
1999
+ heading("Usage");
2000
+ for (const line of lines) {
2001
+ console.log(` ${style.cyan(line)}`);
2002
+ }
2003
+ }
2004
+ function helpOptions(options) {
2005
+ heading("Options");
2006
+ const width = Math.max(...options.map((option) => option.flag.length));
2007
+ for (const option of options) {
2008
+ console.log(` ${style.cyan(option.flag.padEnd(width))} ${style.dim(option.desc)}`);
2009
+ }
2010
+ }
2011
+ var style, symbols, RULE_WIDTH = 52;
2012
+ var init_ui = __esm(() => {
2013
+ init_md_blocks();
2014
+ style = {
2015
+ bold: wrap(1, 22),
2016
+ dim: wrap(2, 22),
2017
+ red: wrap(31, 39),
2018
+ green: wrap(32, 39),
2019
+ yellow: wrap(33, 39),
2020
+ cyan: wrap(36, 39),
2021
+ gray: wrap(90, 39)
2022
+ };
2023
+ symbols = {
2024
+ ok: "\u2713",
2025
+ cross: "\u2717",
2026
+ off: "\xB7",
2027
+ arrow: "\u2192",
2028
+ bullet: "\u2022"
2029
+ };
2030
+ });
2031
+
2032
+ // src/lib/prompt.ts
2033
+ import { stdin as input, stdout as output } from "process";
2034
+ import readline from "readline/promises";
2035
+ async function confirm(question, defaultValue = false) {
2036
+ if (!input.isTTY) {
2037
+ return defaultValue;
2038
+ }
2039
+ const suffix = defaultValue ? "Y/n" : "y/N";
2040
+ const rl = readline.createInterface({ input, output });
2041
+ try {
2042
+ const answer = await rl.question(`${style.cyan(symbols.arrow)} ${question} ${style.dim(`(${suffix})`)} `);
2043
+ const normalized = answer.trim().toLowerCase();
2044
+ if (!normalized) {
2045
+ return defaultValue;
2046
+ }
2047
+ return normalized === "y" || normalized === "yes";
2048
+ } finally {
2049
+ rl.close();
2050
+ }
2051
+ }
2052
+ async function choice(question, choices, defaultValue) {
2053
+ if (!input.isTTY) {
2054
+ return defaultValue;
2055
+ }
2056
+ const rl = readline.createInterface({ input, output });
2057
+ try {
2058
+ const answer = await rl.question(`${style.cyan(symbols.arrow)} ${question} ${style.dim(`(${choices.join("/")}; default: ${defaultValue})`)} `);
2059
+ const normalized = answer.trim().toLowerCase();
2060
+ const match = choices.find((item) => item.toLowerCase() === normalized);
2061
+ return match ?? defaultValue;
2062
+ } finally {
2063
+ rl.close();
2064
+ }
2065
+ }
2066
+ var init_prompt = __esm(() => {
2067
+ init_ui();
2068
+ });
2069
+
720
2070
  // src/commands/providers.ts
721
2071
  function isProviderPlatformSupported(provider, platform = process.platform) {
722
2072
  if (provider.platforms === undefined || provider.platforms.length === 0) {
@@ -987,6 +2337,82 @@ function providerApiKey(provider, env = process.env) {
987
2337
  const raw = env[envKey];
988
2338
  return typeof raw === "string" && raw.length > 0 ? raw : undefined;
989
2339
  }
2340
+ async function testProviderConnection(provider, fetchFn = globalThis.fetch, env = process.env) {
2341
+ const apiKey = providerApiKey(provider, env) ?? provider.apiKey;
2342
+ return fetchOpenAiCompatModelsDetailed(fetchFn, provider, apiKey, { timeoutMs: MODELS_FETCH_TIMEOUT_MS });
2343
+ }
2344
+ function providersSharingEnvKey(envKey, excludeName, providers) {
2345
+ return providers.filter((p) => p.envKey === envKey && p.name !== excludeName).map((p) => p.name).sort();
2346
+ }
2347
+ function sharedCredentialWarning(sharedWith, envKey) {
2348
+ if (sharedWith.length === 0 || envKey === undefined)
2349
+ return;
2350
+ return `this also disconnects ${sharedWith.join(", ")} (same ${envKey})`;
2351
+ }
2352
+ function classifyProviderConnection(name, env = process.env, dir) {
2353
+ if (customCompatProviders(dir).some((p) => p.name === name)) {
2354
+ return { kind: "custom", sharedWith: [] };
2355
+ }
2356
+ if (loadShellConfig(dir).oauthGrants?.[name] !== undefined) {
2357
+ const envKey = oauthEnvKeyFor(name);
2358
+ const sharedWith = envKey === undefined ? [] : providersSharingEnvKey(envKey, name, allOpenAiCompatProviders(dir));
2359
+ return { kind: "oauth-grant", ...envKey !== undefined ? { envKey } : {}, sharedWith };
2360
+ }
2361
+ const registry = providerByName(name, dir);
2362
+ const envKey = registry?.envKey;
2363
+ if (envKey === undefined) {
2364
+ return { kind: "no-credential", sharedWith: [] };
2365
+ }
2366
+ const sharedWith = providersSharingEnvKey(envKey, name, allOpenAiCompatProviders(dir));
2367
+ if (loadShellConfig(dir).apiKeys?.[envKey] !== undefined) {
2368
+ return { kind: "saved-api-key", envKey, sharedWith };
2369
+ }
2370
+ const raw = env[envKey];
2371
+ if (typeof raw === "string" && raw.length > 0) {
2372
+ return { kind: "env-var-only", envKey, sharedWith };
2373
+ }
2374
+ return { kind: "no-credential", sharedWith: [] };
2375
+ }
2376
+ function disconnectProvider(name, env = process.env, dir) {
2377
+ const classification = classifyProviderConnection(name, env, dir);
2378
+ switch (classification.kind) {
2379
+ case "custom":
2380
+ removeCustomCompatProvider(name, dir);
2381
+ removeProviderBaseUrl(name, dir);
2382
+ removeProviderModelParams(name, dir);
2383
+ return { ok: true, kind: "custom", sharedWith: classification.sharedWith };
2384
+ case "oauth-grant": {
2385
+ logoutProvider(name, dir);
2386
+ const envKey = classification.envKey;
2387
+ if (envKey !== undefined && savedCredentialEnvKeys().has(envKey)) {
2388
+ delete process.env[envKey];
2389
+ }
2390
+ return { ok: true, kind: "oauth-grant", sharedWith: classification.sharedWith };
2391
+ }
2392
+ case "saved-api-key": {
2393
+ const envKey = classification.envKey;
2394
+ removeApiKey(envKey, dir);
2395
+ if (savedCredentialEnvKeys().has(envKey)) {
2396
+ delete process.env[envKey];
2397
+ }
2398
+ return { ok: true, kind: "saved-api-key", sharedWith: classification.sharedWith };
2399
+ }
2400
+ case "env-var-only":
2401
+ return {
2402
+ ok: false,
2403
+ kind: "env-var-only",
2404
+ reason: `set via ${classification.envKey} in your environment \u2014 unset ${classification.envKey} in your shell to disconnect`,
2405
+ sharedWith: classification.sharedWith
2406
+ };
2407
+ case "no-credential":
2408
+ return {
2409
+ ok: true,
2410
+ kind: "no-credential",
2411
+ reason: "no saved credential for this provider \u2014 nothing to remove",
2412
+ sharedWith: classification.sharedWith
2413
+ };
2414
+ }
2415
+ }
990
2416
  function configuredProviders(env = process.env, dir) {
991
2417
  const customNames = new Set(customCompatProviders(dir).map((provider) => provider.name));
992
2418
  return allOpenAiCompatProviders(dir).filter((provider) => {
@@ -997,7 +2423,7 @@ function configuredProviders(env = process.env, dir) {
997
2423
  return providerApiKey(provider, env) !== undefined || provider.apiKey !== undefined;
998
2424
  }).filter((provider) => isProviderPlatformSupported(provider)).map((provider) => ({ name: provider.name, models: provider.models }));
999
2425
  }
1000
- function providersCommand(args) {
2426
+ async function providersCommand(args, deps = {}) {
1001
2427
  const command = args[0];
1002
2428
  if (!command || command === "--help" || command === "-h") {
1003
2429
  printProvidersHelp();
@@ -1011,10 +2437,99 @@ function providersCommand(args) {
1011
2437
  runCrossFamily(args.slice(1));
1012
2438
  return;
1013
2439
  }
2440
+ if (command === "test") {
2441
+ await runProvidersTest(args.slice(1), deps);
2442
+ return;
2443
+ }
2444
+ if (command === "remove") {
2445
+ await runProvidersRemove(args.slice(1), deps);
2446
+ return;
2447
+ }
1014
2448
  console.error(`Unknown providers command: ${command}`);
1015
2449
  printProvidersHelp();
1016
2450
  process.exitCode = 1;
1017
2451
  }
2452
+ async function runProvidersTest(args, deps) {
2453
+ const name = args[0];
2454
+ if (name === undefined || name === "--help" || name === "-h") {
2455
+ console.error("Usage: keryx providers test <name> [--json]");
2456
+ process.exitCode = 1;
2457
+ return;
2458
+ }
2459
+ const dir = deps.dir;
2460
+ const provider = providerByName(name, dir);
2461
+ if (provider === undefined) {
2462
+ console.error(`Unknown provider: ${name}`);
2463
+ process.exitCode = 1;
2464
+ return;
2465
+ }
2466
+ const env = deps.env ?? envWithOAuthAccess(envWithSavedApiKeys(process.env, dir));
2467
+ const fetchFn = deps.fetch ?? globalThis.fetch;
2468
+ const result = await testProviderConnection(provider, fetchFn, env);
2469
+ const label = provider.label ?? provider.name;
2470
+ if (args.includes("--json")) {
2471
+ console.log(JSON.stringify({ provider: name, ok: result.source === "live", models: result.models.length, failure: result.failure ?? null }, null, 2));
2472
+ if (result.source !== "live")
2473
+ process.exitCode = 1;
2474
+ return;
2475
+ }
2476
+ if (result.source === "live") {
2477
+ console.log(`${label}: ok \u2014 ${result.models.length} model(s)`);
2478
+ return;
2479
+ }
2480
+ console.log(modelsFailureLine(label, result.failure ?? { kind: "empty" }));
2481
+ process.exitCode = 1;
2482
+ }
2483
+ async function runProvidersRemove(args, deps) {
2484
+ const name = args[0];
2485
+ if (name === undefined || name === "--help" || name === "-h") {
2486
+ console.error("Usage: keryx providers remove <name> [--yes] [--json]");
2487
+ process.exitCode = 1;
2488
+ return;
2489
+ }
2490
+ const dir = deps.dir;
2491
+ const env = deps.env ?? process.env;
2492
+ const json = args.includes("--json");
2493
+ if (providerByName(name, dir) === undefined) {
2494
+ if (json) {
2495
+ console.log(JSON.stringify({ provider: name, ok: false, reason: "unknown provider" }, null, 2));
2496
+ } else {
2497
+ console.error(`Unknown provider: ${name}`);
2498
+ }
2499
+ process.exitCode = 1;
2500
+ return;
2501
+ }
2502
+ const classification = classifyProviderConnection(name, env, dir);
2503
+ const warning = sharedCredentialWarning(classification.sharedWith, classification.envKey);
2504
+ const yes = args.includes("--yes");
2505
+ const confirmFn = deps.confirm ?? confirm;
2506
+ const question = warning === undefined ? `Disconnect provider "${name}" and remove its saved credential?` : `Disconnect provider "${name}" and remove its saved credential? Note: ${warning}.`;
2507
+ const confirmed = yes || await confirmFn(question, false);
2508
+ if (!confirmed) {
2509
+ if (json) {
2510
+ console.log(JSON.stringify({ provider: name, ok: false, reason: "not confirmed", sharedWith: classification.sharedWith }, null, 2));
2511
+ } else {
2512
+ console.log("keryx providers remove: not confirmed \u2014 nothing was changed.");
2513
+ }
2514
+ if (!yes && !process.stdin.isTTY && deps.confirm === undefined)
2515
+ process.exitCode = 1;
2516
+ return;
2517
+ }
2518
+ const result = disconnectProvider(name, env, dir);
2519
+ if (json) {
2520
+ console.log(JSON.stringify({ provider: name, ok: result.ok, kind: result.kind, sharedWith: result.sharedWith, reason: result.reason ?? null }, null, 2));
2521
+ if (!result.ok)
2522
+ process.exitCode = 1;
2523
+ return;
2524
+ }
2525
+ if (!result.ok) {
2526
+ console.log(`keryx providers remove: "${name}" not removed \u2014 ${result.reason}`);
2527
+ process.exitCode = 1;
2528
+ return;
2529
+ }
2530
+ const sharedNote = sharedCredentialWarning(result.sharedWith, classification.envKey);
2531
+ console.log(`keryx providers remove: "${name}" disconnected (${result.kind}).${sharedNote !== undefined ? ` Note: ${sharedNote}.` : ""}${result.reason !== undefined ? ` ${result.reason}` : ""}`);
2532
+ }
1018
2533
  function runProvidersList(args) {
1019
2534
  const env = envWithOAuthAccess(envWithSavedApiKeys());
1020
2535
  const configured = configuredProviders(env).map((provider) => ({
@@ -1098,17 +2613,38 @@ function printProvidersHelp() {
1098
2613
  Usage:
1099
2614
  keryx providers list [--json]
1100
2615
  keryx providers cross-family [--opt-in] [--session-provider <id>] [--session-model <id>] [--from-shell-config] [--json]
2616
+ keryx providers test <name> [--json]
2617
+ keryx providers remove <name> [--yes] [--json]
1101
2618
 
1102
2619
  Commands:
1103
2620
  list Providers this operator has configured, and the family of each
1104
2621
  cross-family Whether review can run on a different model family than authored
1105
2622
  the change, and the record the round should carry
2623
+ test Run this provider's live model-list probe and report ok/count
2624
+ or the failure reason. Makes ONE network call \u2014 unlike list/
2625
+ cross-family, not network-free
2626
+ remove Disconnect a provider: remove its saved API key, OAuth grant,
2627
+ or custom-provider entry. Errors on an unknown name (exit 1)
2628
+ before asking anything. Asks for confirmation on a terminal;
2629
+ refuses without one unless --yes. A provider whose only
2630
+ credential is an environment variable you exported yourself
2631
+ cannot be removed \u2014 the command names the variable to unset.
2632
+ Some built-ins share one env var (e.g. zai/zai-coding both
2633
+ read ZAI_API_KEY): removing either ALSO disconnects the
2634
+ other, and both the confirmation prompt and the result name
2635
+ every provider this affects \u2014 --json lists them under
2636
+ sharedWith
1106
2637
 
1107
2638
  cross-family is OPT-IN: without --opt-in it reports what would happen and
1108
2639
  chooses single-family review. Dispatching to another provider spends tokens and
1109
2640
  sends the change to a second vendor, which is a decision rather than an
1110
2641
  optimisation. With no second family configured it reports single-family review
1111
2642
  with a stated reason and exits 0 \u2014 that is a normal configuration, not an error.
2643
+
2644
+ Disconnecting a provider removes only keryx's LOCAL copy of its credential. It
2645
+ does not revoke anything at the vendor: an OAuth grant is deleted from
2646
+ auth.json only (no revoke call), and a saved API key simply stops being read \u2014
2647
+ the key itself is still valid until you revoke it yourself with the vendor.
1112
2648
  `);
1113
2649
  }
1114
2650
  var DEFAULT_MODELS_PATH = "/v1/models", MINIMAX_REASONING_PRESET_HOSTS, DEEPSEEK_REASONING_PRESET_HOSTS, OPENAI_COMPAT_PROVIDERS, MODELS_FETCH_TIMEOUT_MS = 1e4, MODELS_ERROR_BODY_LIMIT = 4096, BALANCE_FETCH_TIMEOUT_MS = 8000;
@@ -1116,9 +2652,11 @@ var init_providers = __esm(() => {
1116
2652
  init_provider_config();
1117
2653
  init_catalog();
1118
2654
  init_grants();
2655
+ init_login();
1119
2656
  init_caller_session();
1120
2657
  init_shell_config();
1121
2658
  init_args();
2659
+ init_prompt();
1122
2660
  MINIMAX_REASONING_PRESET_HOSTS = new Set(["api.minimax.io", "api.minimaxi.com"]);
1123
2661
  DEEPSEEK_REASONING_PRESET_HOSTS = new Set(["api.deepseek.com"]);
1124
2662
  OPENAI_COMPAT_PROVIDERS = [
@@ -45281,498 +46819,6 @@ var init_debug_log = __esm(() => {
45281
46819
  init_config_dir();
45282
46820
  });
45283
46821
 
45284
- // src/lib/oauth/device-code.ts
45285
- function requestInit(base, signal) {
45286
- return signal === undefined ? base : { ...base, signal };
45287
- }
45288
- function formHeaders(extra) {
45289
- return {
45290
- "Content-Type": "application/x-www-form-urlencoded",
45291
- Accept: "application/json",
45292
- ...extra
45293
- };
45294
- }
45295
- function positiveSecondsToMs(value, defaultMs) {
45296
- const seconds = Number(value);
45297
- return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : defaultMs;
45298
- }
45299
- async function defaultSleep(ms) {
45300
- await new Promise((resolve) => setTimeout(resolve, ms));
45301
- }
45302
- function aborted(signal) {
45303
- return signal?.aborted === true;
45304
- }
45305
- function readJsonError(body) {
45306
- if (typeof body !== "object" || body === null) {
45307
- return {};
45308
- }
45309
- const rec = body;
45310
- return {
45311
- ...typeof rec.error === "string" ? { error: rec.error } : {},
45312
- ...typeof rec.error_description === "string" ? { error_description: rec.error_description } : {}
45313
- };
45314
- }
45315
- async function requestDeviceCode(request, http) {
45316
- if (aborted(http.signal)) {
45317
- throw new DeviceCodeError("cancelled", "device authorization was cancelled");
45318
- }
45319
- const body = new URLSearchParams({
45320
- client_id: request.clientId,
45321
- scope: request.scope,
45322
- ...request.extraForm
45323
- });
45324
- const response = await http.fetch(request.deviceAuthorizationEndpoint, requestInit({
45325
- method: "POST",
45326
- headers: formHeaders(request.headers),
45327
- body: body.toString()
45328
- }, http.signal));
45329
- const json = await response.json().catch(() => ({}));
45330
- if (!response.ok) {
45331
- const detail = readJsonError(json).error_description ?? `HTTP ${response.status}`;
45332
- throw new DeviceCodeError("failed", `device code request failed: ${detail}`);
45333
- }
45334
- if (typeof json !== "object" || json === null) {
45335
- throw new DeviceCodeError("failed", "device code response is not an object");
45336
- }
45337
- const rec = json;
45338
- const deviceCode = typeof rec.device_code === "string" ? rec.device_code : "";
45339
- const userCode = typeof rec.user_code === "string" ? rec.user_code : "";
45340
- const verificationUri = typeof rec.verification_uri === "string" ? rec.verification_uri : "";
45341
- if (deviceCode.length === 0 || userCode.length === 0 || verificationUri.length === 0) {
45342
- throw new DeviceCodeError("failed", "device code response is missing device_code / user_code / verification_uri");
45343
- }
45344
- if (![...userCode].every((c) => /[A-Za-z0-9-]/.test(c))) {
45345
- throw new DeviceCodeError("failed", "device code response has an invalid user_code");
45346
- }
45347
- const complete = typeof rec.verification_uri_complete === "string" && rec.verification_uri_complete.length > 0 ? rec.verification_uri_complete : undefined;
45348
- return {
45349
- deviceCode,
45350
- userCode,
45351
- verificationUri,
45352
- ...complete !== undefined ? { verificationUriComplete: complete } : {},
45353
- ...typeof rec.expires_in === "number" ? { expiresInSeconds: rec.expires_in } : {},
45354
- ...typeof rec.interval === "number" ? { intervalSeconds: rec.interval } : {}
45355
- };
45356
- }
45357
- async function pollDeviceCodeToken(challenge, request, http) {
45358
- const sleep = http.sleep ?? defaultSleep;
45359
- const now = http.now ?? Date.now;
45360
- const expiresInMs = positiveSecondsToMs(challenge.expiresInSeconds, DEFAULT_EXPIRES_MS);
45361
- const deadline = now() + expiresInMs;
45362
- let intervalMs = Math.max(positiveSecondsToMs(challenge.intervalSeconds, DEFAULT_INTERVAL_MS), MIN_INTERVAL_MS);
45363
- while (now() < deadline) {
45364
- if (aborted(http.signal)) {
45365
- throw new DeviceCodeError("cancelled", "device authorization was cancelled");
45366
- }
45367
- const response = await http.fetch(request.tokenEndpoint, requestInit({
45368
- method: "POST",
45369
- headers: formHeaders(request.headers),
45370
- body: new URLSearchParams({
45371
- grant_type: DEVICE_CODE_GRANT_TYPE,
45372
- client_id: request.clientId,
45373
- device_code: challenge.deviceCode,
45374
- ...request.extraForm
45375
- }).toString()
45376
- }, http.signal));
45377
- const json = await response.json().catch(() => ({}));
45378
- if (typeof json === "object" && json !== null && typeof json.access_token === "string") {
45379
- const rec = json;
45380
- return {
45381
- accessToken: rec.access_token,
45382
- ...typeof rec.refresh_token === "string" && rec.refresh_token.length > 0 ? { refreshToken: rec.refresh_token } : {},
45383
- ...typeof rec.expires_in === "number" ? { expiresInSeconds: rec.expires_in } : {},
45384
- ...typeof rec.scope === "string" ? { scope: rec.scope } : {},
45385
- ...typeof rec.id_token === "string" ? { idToken: rec.id_token } : {}
45386
- };
45387
- }
45388
- const { error, error_description } = readJsonError(json);
45389
- const remaining = Math.max(0, deadline - now());
45390
- if (error === "authorization_pending") {
45391
- await sleep(Math.min(intervalMs + POLL_SAFETY_MARGIN_MS, remaining));
45392
- continue;
45393
- }
45394
- if (error === "slow_down") {
45395
- intervalMs += SLOW_DOWN_INCREMENT_MS;
45396
- await sleep(Math.min(intervalMs + POLL_SAFETY_MARGIN_MS, remaining));
45397
- continue;
45398
- }
45399
- if (error === "access_denied" || error === "authorization_denied") {
45400
- throw new DeviceCodeError("access_denied", "device authorization was denied");
45401
- }
45402
- if (error === "expired_token") {
45403
- throw new DeviceCodeError("expired_token", "device code expired");
45404
- }
45405
- if (!response.ok) {
45406
- throw new DeviceCodeError("failed", `device token exchange failed (${response.status})${error_description ? `: ${error_description}` : ""}`);
45407
- }
45408
- throw new DeviceCodeError("failed", `device token exchange failed${error_description ? `: ${error_description}` : ""}`);
45409
- }
45410
- throw new DeviceCodeError("timeout", "device authorization timed out");
45411
- }
45412
- async function refreshAccessToken(request, http) {
45413
- const response = await http.fetch(request.tokenEndpoint, requestInit({
45414
- method: "POST",
45415
- headers: formHeaders(request.headers),
45416
- body: new URLSearchParams({
45417
- grant_type: "refresh_token",
45418
- refresh_token: request.refreshToken,
45419
- client_id: request.clientId,
45420
- ...request.extraForm
45421
- }).toString()
45422
- }, http.signal));
45423
- const json = await response.json().catch(() => ({}));
45424
- if (!response.ok || typeof json !== "object" || json === null || typeof json.access_token !== "string") {
45425
- const detail = readJsonError(json).error_description ?? `HTTP ${response.status}`;
45426
- throw new DeviceCodeError("failed", `token refresh failed: ${detail}`);
45427
- }
45428
- const rec = json;
45429
- return {
45430
- accessToken: rec.access_token,
45431
- refreshToken: typeof rec.refresh_token === "string" && rec.refresh_token.length > 0 ? rec.refresh_token : request.refreshToken,
45432
- ...typeof rec.expires_in === "number" ? { expiresInSeconds: rec.expires_in } : {},
45433
- ...typeof rec.scope === "string" ? { scope: rec.scope } : {},
45434
- ...typeof rec.id_token === "string" ? { idToken: rec.id_token } : {}
45435
- };
45436
- }
45437
- var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code", DEFAULT_INTERVAL_MS = 5000, MIN_INTERVAL_MS = 1000, SLOW_DOWN_INCREMENT_MS = 5000, DEFAULT_EXPIRES_MS, POLL_SAFETY_MARGIN_MS = 3000, DeviceCodeError;
45438
- var init_device_code = __esm(() => {
45439
- DEFAULT_EXPIRES_MS = 5 * 60 * 1000;
45440
- DeviceCodeError = class DeviceCodeError extends Error {
45441
- kind;
45442
- constructor(kind, message) {
45443
- super(message);
45444
- this.name = "DeviceCodeError";
45445
- this.kind = kind;
45446
- }
45447
- };
45448
- });
45449
-
45450
- // src/lib/oauth/copilot.ts
45451
- function copilotApiBaseFromExchange(raw) {
45452
- if (typeof raw !== "string" || raw.length === 0) {
45453
- return;
45454
- }
45455
- try {
45456
- const url = new URL(raw);
45457
- if (url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0) {
45458
- return;
45459
- }
45460
- const host = url.hostname;
45461
- if (host !== "api.githubcopilot.com" && !host.endsWith(".githubcopilot.com")) {
45462
- return;
45463
- }
45464
- return `https://${host}${url.port.length > 0 ? `:${url.port}` : ""}`;
45465
- } catch {
45466
- return;
45467
- }
45468
- }
45469
- function expiresInSecondsFrom(rec, now) {
45470
- if (typeof rec.expires_at === "string") {
45471
- const expires = Date.parse(rec.expires_at);
45472
- if (Number.isFinite(expires)) {
45473
- return Math.max(1, Math.floor((expires - now) / 1000));
45474
- }
45475
- }
45476
- if (typeof rec.expires_at === "number" && Number.isFinite(rec.expires_at)) {
45477
- const expiresMs = rec.expires_at < 1000000000000 ? rec.expires_at * 1000 : rec.expires_at;
45478
- return Math.max(1, Math.floor((expiresMs - now) / 1000));
45479
- }
45480
- if (typeof rec.refresh_in === "number" && Number.isFinite(rec.refresh_in) && rec.refresh_in > 0) {
45481
- return Math.floor(rec.refresh_in);
45482
- }
45483
- return;
45484
- }
45485
- function describeExchangeFailure(status, json, body) {
45486
- if (typeof json === "object" && json !== null) {
45487
- const rec = json;
45488
- if (rec.error_details?.copilot_access_denied === true) {
45489
- return `GitHub Copilot token exchange failed (HTTP ${status}): Copilot access denied \u2014 check the GitHub Copilot subscription on this account`;
45490
- }
45491
- if (typeof rec.message === "string" && rec.message.length > 0) {
45492
- return `GitHub Copilot token exchange failed (HTTP ${status}): ${rec.message}`;
45493
- }
45494
- }
45495
- if (/zscaler|blocked because it does not comply/i.test(body)) {
45496
- return `GitHub Copilot token exchange failed (HTTP ${status}): request was blocked by a gateway or proxy`;
45497
- }
45498
- return `GitHub Copilot token exchange failed (HTTP ${status})`;
45499
- }
45500
- async function exchangeGithubTokenForCopilot(githubToken, http) {
45501
- const init = {
45502
- method: "GET",
45503
- headers: {
45504
- ...GITHUB_COPILOT_REQUEST_HEADERS,
45505
- Authorization: `token ${githubToken}`
45506
- }
45507
- };
45508
- if (http.signal !== undefined) {
45509
- init.signal = http.signal;
45510
- }
45511
- const response = await http.fetch(COPILOT_TOKEN_URL, init);
45512
- const body = await response.text().catch(() => "");
45513
- let json = {};
45514
- if (body.length > 0) {
45515
- try {
45516
- json = JSON.parse(body);
45517
- } catch {
45518
- json = {};
45519
- }
45520
- }
45521
- if (!response.ok || typeof json !== "object" || json === null) {
45522
- throw new DeviceCodeError("failed", describeExchangeFailure(response.status, json, body));
45523
- }
45524
- const rec = json;
45525
- if (typeof rec.token !== "string" || rec.token.length === 0) {
45526
- throw new DeviceCodeError("failed", "GitHub Copilot token exchange returned no token");
45527
- }
45528
- const expiresInSeconds = expiresInSecondsFrom(rec, Date.now());
45529
- const apiBaseUrl = copilotApiBaseFromExchange(rec.endpoints?.api);
45530
- return {
45531
- accessToken: rec.token,
45532
- ...expiresInSeconds !== undefined ? { expiresInSeconds } : {},
45533
- ...apiBaseUrl !== undefined ? { apiBaseUrl } : {}
45534
- };
45535
- }
45536
- var COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
45537
- var init_copilot = __esm(() => {
45538
- init_catalog();
45539
- init_device_code();
45540
- });
45541
-
45542
- // src/lib/oauth/openai-codex.ts
45543
- function withSignal(base, signal) {
45544
- return signal === undefined ? base : { ...base, signal };
45545
- }
45546
- async function requestCodexDeviceCode(http) {
45547
- const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/api/accounts/deviceauth/usercode`, withSignal({
45548
- method: "POST",
45549
- headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "keryx" },
45550
- body: JSON.stringify({ client_id: OPENAI_CODEX_CLIENT_ID })
45551
- }, http.signal));
45552
- const json = await response.json().catch(() => ({}));
45553
- if (!response.ok || typeof json !== "object" || json === null) {
45554
- throw new DeviceCodeError("failed", `ChatGPT device authorization failed (HTTP ${response.status})`);
45555
- }
45556
- const rec = json;
45557
- if (typeof rec.device_auth_id !== "string" || typeof rec.user_code !== "string") {
45558
- throw new DeviceCodeError("failed", "ChatGPT device authorization is missing device_auth_id / user_code");
45559
- }
45560
- const intervalSeconds = Number(rec.interval);
45561
- return {
45562
- deviceAuthId: rec.device_auth_id,
45563
- userCode: rec.user_code,
45564
- verificationUri: `${OPENAI_CODEX_ISSUER}/codex/device`,
45565
- intervalMs: Number.isFinite(intervalSeconds) && intervalSeconds > 0 ? intervalSeconds * 1000 : 5000
45566
- };
45567
- }
45568
- async function pollCodexDeviceToken(challenge, http) {
45569
- const sleep = http.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
45570
- const now = http.now ?? Date.now;
45571
- const deadline = now() + 5 * 60 * 1000;
45572
- while (now() < deadline) {
45573
- if (http.signal?.aborted === true) {
45574
- throw new DeviceCodeError("cancelled", "device authorization was cancelled");
45575
- }
45576
- const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/api/accounts/deviceauth/token`, withSignal({
45577
- method: "POST",
45578
- headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "keryx" },
45579
- body: JSON.stringify({ device_auth_id: challenge.deviceAuthId, user_code: challenge.userCode })
45580
- }, http.signal));
45581
- if (response.ok) {
45582
- const data = await response.json();
45583
- if (typeof data.authorization_code !== "string" || typeof data.code_verifier !== "string") {
45584
- throw new DeviceCodeError("failed", "ChatGPT device token is missing authorization_code");
45585
- }
45586
- return exchangeCodexAuthorizationCode(data.authorization_code, data.code_verifier, http);
45587
- }
45588
- if (response.status !== 403 && response.status !== 404) {
45589
- throw new DeviceCodeError("failed", `ChatGPT device token poll failed (HTTP ${response.status})`);
45590
- }
45591
- await sleep(challenge.intervalMs + POLL_SAFETY_MARGIN_MS2);
45592
- }
45593
- throw new DeviceCodeError("timeout", "ChatGPT device authorization timed out");
45594
- }
45595
- async function exchangeCodexAuthorizationCode(code, codeVerifier, http) {
45596
- const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/oauth/token`, withSignal({
45597
- method: "POST",
45598
- headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
45599
- body: new URLSearchParams({
45600
- grant_type: "authorization_code",
45601
- code,
45602
- redirect_uri: `${OPENAI_CODEX_ISSUER}/deviceauth/callback`,
45603
- client_id: OPENAI_CODEX_CLIENT_ID,
45604
- code_verifier: codeVerifier
45605
- }).toString()
45606
- }, http.signal));
45607
- const json = await response.json().catch(() => ({}));
45608
- if (!response.ok || typeof json !== "object" || json === null || typeof json.access_token !== "string") {
45609
- throw new DeviceCodeError("failed", `ChatGPT token exchange failed (HTTP ${response.status})`);
45610
- }
45611
- const rec = json;
45612
- return {
45613
- accessToken: rec.access_token,
45614
- ...typeof rec.refresh_token === "string" ? { refreshToken: rec.refresh_token } : {},
45615
- ...typeof rec.expires_in === "number" ? { expiresInSeconds: rec.expires_in } : {}
45616
- };
45617
- }
45618
- var OPENAI_CODEX_ISSUER = "https://auth.openai.com", OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann", POLL_SAFETY_MARGIN_MS2 = 3000;
45619
- var init_openai_codex = __esm(() => {
45620
- init_device_code();
45621
- });
45622
-
45623
- // src/lib/oauth/login.ts
45624
- function httpOf(input) {
45625
- return {
45626
- fetch: input.fetch,
45627
- ...input.sleep !== undefined ? { sleep: input.sleep } : {},
45628
- ...input.now !== undefined ? { now: input.now } : {},
45629
- ...input.signal !== undefined ? { signal: input.signal } : {}
45630
- };
45631
- }
45632
- function present(challenge, provider) {
45633
- return {
45634
- userCode: challenge.userCode,
45635
- verificationUri: challenge.verificationUri,
45636
- ...challenge.verificationUriComplete !== undefined ? { verificationUriComplete: challenge.verificationUriComplete } : {},
45637
- instructions: `Open ${challenge.verificationUri} and enter code: ${challenge.userCode} (${deviceCodeMethodLabel(provider)})`
45638
- };
45639
- }
45640
- async function loginRfc8628(input) {
45641
- const endpoints = input.provider === "github-copilot" ? GITHUB_COPILOT_DEVICE : XAI_GROK_DEVICE;
45642
- const http = httpOf(input);
45643
- const challenge = await requestDeviceCode({
45644
- deviceAuthorizationEndpoint: endpoints.deviceAuthorizationEndpoint,
45645
- clientId: endpoints.clientId,
45646
- scope: endpoints.scope,
45647
- ...endpoints.extraForm !== undefined ? { extraForm: endpoints.extraForm } : {},
45648
- ...endpoints.headers !== undefined ? { headers: endpoints.headers } : {}
45649
- }, http);
45650
- input.onChallenge(present(challenge, input.provider));
45651
- const tokens = await pollDeviceCodeToken(challenge, {
45652
- tokenEndpoint: endpoints.tokenEndpoint,
45653
- clientId: endpoints.clientId,
45654
- ...endpoints.headers !== undefined ? { headers: endpoints.headers } : {}
45655
- }, http);
45656
- if (endpoints.afterToken === "github-copilot") {
45657
- const copilot = await exchangeGithubTokenForCopilot(tokens.accessToken, http);
45658
- if (copilot.apiBaseUrl !== undefined) {
45659
- saveProviderBaseUrl("github-copilot", copilot.apiBaseUrl, input.dir);
45660
- }
45661
- return {
45662
- accessToken: copilot.accessToken,
45663
- refreshToken: tokens.accessToken,
45664
- ...copilot.expiresInSeconds !== undefined ? { expiresInSeconds: copilot.expiresInSeconds } : {}
45665
- };
45666
- }
45667
- return tokens;
45668
- }
45669
- async function loginDeviceCode(input) {
45670
- const entry = authCatalogEntry(input.provider);
45671
- if (entry === undefined) {
45672
- return { ok: false, error: `unknown provider: ${input.provider}` };
45673
- }
45674
- if (!entry.methods.includes("device-code")) {
45675
- const refusal = catalogRefusal(input.provider);
45676
- if (refusal !== undefined) {
45677
- return { ok: false, error: refusal, refused: true };
45678
- }
45679
- return { ok: false, error: `${input.provider} does not support device authorization` };
45680
- }
45681
- try {
45682
- const tokens = input.provider === "openai" ? await loginCodex(input) : await loginRfc8628(input);
45683
- saveOAuthGrant(input.provider, grantFromTokens("device-code", tokens, input.now), input.dir);
45684
- return { ok: true, provider: input.provider };
45685
- } catch (err) {
45686
- const message = err instanceof DeviceCodeError ? err.message : err instanceof Error ? err.message : "device authorization failed";
45687
- return { ok: false, error: message };
45688
- }
45689
- }
45690
- async function loginCodex(input) {
45691
- const http = httpOf(input);
45692
- const challenge = await requestCodexDeviceCode(http);
45693
- input.onChallenge(present({
45694
- userCode: challenge.userCode,
45695
- verificationUri: challenge.verificationUri
45696
- }, "openai"));
45697
- return pollCodexDeviceToken(challenge, http);
45698
- }
45699
- async function refreshProviderGrant(provider, http, dir) {
45700
- const grant = loadOAuthGrant(provider, dir);
45701
- if (grant === undefined || !grantNeedsRefresh(grant, http.now)) {
45702
- return grant;
45703
- }
45704
- if (grant.refresh === undefined) {
45705
- return grant;
45706
- }
45707
- const endpoints = provider === "github-copilot" ? GITHUB_COPILOT_DEVICE : XAI_GROK_DEVICE;
45708
- if (provider === "github-copilot") {
45709
- const copilot = await exchangeGithubTokenForCopilot(grant.refresh, http);
45710
- if (copilot.apiBaseUrl !== undefined) {
45711
- saveProviderBaseUrl("github-copilot", copilot.apiBaseUrl, dir);
45712
- }
45713
- const next = grantFromTokens("device-code", {
45714
- accessToken: copilot.accessToken,
45715
- refreshToken: grant.refresh,
45716
- ...copilot.expiresInSeconds !== undefined ? { expiresInSeconds: copilot.expiresInSeconds } : {}
45717
- }, http.now);
45718
- next.lastRefreshedAt = new Date((http.now ?? Date.now)()).toISOString();
45719
- saveOAuthGrant(provider, next, dir);
45720
- return next;
45721
- }
45722
- if (provider === "openai") {
45723
- return grant;
45724
- }
45725
- const tokens = await refreshAccessToken({
45726
- tokenEndpoint: endpoints.tokenEndpoint,
45727
- clientId: endpoints.clientId,
45728
- refreshToken: grant.refresh,
45729
- ...endpoints.headers !== undefined ? { headers: endpoints.headers } : {}
45730
- }, http);
45731
- const next = grantFromTokens("device-code", tokens, http.now);
45732
- next.lastRefreshedAt = new Date((http.now ?? Date.now)()).toISOString();
45733
- saveOAuthGrant(provider, next, dir);
45734
- return next;
45735
- }
45736
- async function refreshSavedGrants(http, dir, providers = SESSION_REFRESHED_GRANTS) {
45737
- const warnings = [];
45738
- const now = http.now ?? Date.now;
45739
- for (const provider of providers) {
45740
- if (!SESSION_REFRESHED_GRANTS.includes(provider))
45741
- continue;
45742
- const grant = loadOAuthGrant(provider, dir);
45743
- if (grant === undefined)
45744
- continue;
45745
- if (grant.refresh === undefined || grant.refresh.length === 0) {
45746
- if (grant.expires !== undefined && grant.expires <= now()) {
45747
- warnings.push(`${provider}: the saved login has expired and holds no refresh token. Run \`keryx auth login ${provider}\`.`);
45748
- }
45749
- continue;
45750
- }
45751
- if (!grantNeedsRefresh(grant, http.now))
45752
- continue;
45753
- try {
45754
- await refreshProviderGrant(provider, http, dir);
45755
- } catch (cause) {
45756
- const reason = cause instanceof Error ? cause.message : String(cause);
45757
- warnings.push(`${provider}: the saved login is expiring or expired and could not be refreshed (${reason}). ` + `Run \`keryx auth login ${provider}\`.`);
45758
- }
45759
- }
45760
- return warnings;
45761
- }
45762
- function logoutProvider(provider, dir) {
45763
- deleteOAuthGrant(provider, dir);
45764
- }
45765
- var SESSION_REFRESHED_GRANTS;
45766
- var init_login = __esm(() => {
45767
- init_shell_config();
45768
- init_catalog();
45769
- init_copilot();
45770
- init_device_code();
45771
- init_grants();
45772
- init_openai_codex();
45773
- SESSION_REFRESHED_GRANTS = ["grok", "github-copilot"];
45774
- });
45775
-
45776
46822
  // src/tui/debug-watcher.ts
45777
46823
  import { spawn as spawn6, spawnSync as spawnSync4 } from "child_process";
45778
46824
  import { closeSync as closeSync5, constants as constants4, openSync as openSync6, readdirSync as readdirSync5, readFileSync as readFileSync15, readlinkSync as readlinkSync3, statSync as statSync12 } from "fs";
@@ -46531,786 +47577,8 @@ function sanitizeForDisplay(value) {
46531
47577
  return out;
46532
47578
  }
46533
47579
 
46534
- // src/lib/ui.ts
46535
- import { stdout } from "process";
46536
-
46537
- // src/lib/md-blocks.ts
46538
- var FENCE_LINE = /^[ \t]{0,3}(```|~~~)(.*)\r?$/;
46539
- var HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
46540
- var MARKDOWN_LANGS = new Set(["md", "markdown", "prompt"]);
46541
- var DIFF_LANGS = new Set(["diff", "patch"]);
46542
- function stripTrailingCr(line) {
46543
- return line.endsWith("\r") ? line.slice(0, -1) : line;
46544
- }
46545
- function splitLines(text) {
46546
- return text.split(`
46547
- `).map(stripTrailingCr);
46548
- }
46549
- function markdownTableCells(line) {
46550
- const source = line.trim();
46551
- if (!source.includes("|")) {
46552
- return;
46553
- }
46554
- const cells = [];
46555
- let cell = "";
46556
- let inCode = false;
46557
- for (let index = 0;index < source.length; index += 1) {
46558
- const char = source[index] ?? "";
46559
- if (char === "\\" && source[index + 1] === "|") {
46560
- cell += "|";
46561
- index += 1;
46562
- continue;
46563
- }
46564
- if (char === "`") {
46565
- inCode = !inCode;
46566
- cell += char;
46567
- continue;
46568
- }
46569
- if (char === "|" && !inCode) {
46570
- cells.push(cell.trim());
46571
- cell = "";
46572
- continue;
46573
- }
46574
- cell += char;
46575
- }
46576
- cells.push(cell.trim());
46577
- if (source.startsWith("|")) {
46578
- cells.shift();
46579
- }
46580
- if (source.endsWith("|")) {
46581
- cells.pop();
46582
- }
46583
- return cells.length >= 2 ? cells : undefined;
46584
- }
46585
- function tableAlignments(line, columns) {
46586
- const cells = markdownTableCells(line);
46587
- if (cells === undefined || cells.length !== columns) {
46588
- return;
46589
- }
46590
- const alignments = [];
46591
- for (const cell of cells) {
46592
- const marker = cell.trim();
46593
- if (!/^:?-{3,}:?$/.test(marker)) {
46594
- return;
46595
- }
46596
- alignments.push(marker.startsWith(":") && marker.endsWith(":") ? "center" : marker.endsWith(":") ? "right" : "left");
46597
- }
46598
- return alignments;
46599
- }
46600
- function parseMarkdownTables(text) {
46601
- if (text.length === 0) {
46602
- return [];
46603
- }
46604
- const lines = splitLines(text);
46605
- const parts = [];
46606
- let proseStart = 0;
46607
- const flushProse = (end) => {
46608
- if (end <= proseStart) {
46609
- return;
46610
- }
46611
- const prose = lines.slice(proseStart, end).join(`
46612
- `);
46613
- if (prose.length > 0) {
46614
- parts.push({ kind: "prose", text: prose });
46615
- }
46616
- };
46617
- for (let index = 0;index + 1 < lines.length; index += 1) {
46618
- const headers = markdownTableCells(lines[index] ?? "");
46619
- if (headers === undefined) {
46620
- continue;
46621
- }
46622
- const alignments = tableAlignments(lines[index + 1] ?? "", headers.length);
46623
- if (alignments === undefined) {
46624
- continue;
46625
- }
46626
- flushProse(index);
46627
- const rows = [];
46628
- let cursor = index + 2;
46629
- while (cursor < lines.length) {
46630
- const row = markdownTableCells(lines[cursor] ?? "");
46631
- if (row === undefined || row.length !== headers.length) {
46632
- break;
46633
- }
46634
- rows.push(row);
46635
- cursor += 1;
46636
- }
46637
- parts.push({ kind: "table", table: { headers, alignments, rows } });
46638
- proseStart = cursor;
46639
- index = cursor - 1;
46640
- }
46641
- flushProse(lines.length);
46642
- return parts.length > 0 ? parts : [{ kind: "prose", text }];
46643
- }
46644
- function fenceInfo(line) {
46645
- const match = FENCE_LINE.exec(line);
46646
- if (match === null) {
46647
- return;
46648
- }
46649
- return { marker: match[1] ?? "", lang: (match[2] ?? "").trim().split(/\s+/)[0] ?? "" };
46650
- }
46651
- function segmentMarkdown(md) {
46652
- const segments = [];
46653
- let text = [];
46654
- let code;
46655
- let lang = "";
46656
- let marker = "";
46657
- const flushText = () => {
46658
- const joined = text.join(`
46659
- `);
46660
- text = [];
46661
- if (joined.length > 0) {
46662
- segments.push({ kind: "text", text: joined });
46663
- }
46664
- };
46665
- for (const line of splitLines(md)) {
46666
- const fence = fenceInfo(line);
46667
- if (code === undefined) {
46668
- if (fence === undefined) {
46669
- text.push(line);
46670
- continue;
46671
- }
46672
- flushText();
46673
- marker = fence.marker;
46674
- lang = fence.lang;
46675
- code = [];
46676
- continue;
46677
- }
46678
- if (fence?.marker === marker) {
46679
- segments.push({ kind: "code", lang, body: code.join(`
46680
- `) });
46681
- code = undefined;
46682
- continue;
46683
- }
46684
- code.push(line);
46685
- }
46686
- if (code === undefined) {
46687
- flushText();
46688
- } else {
46689
- segments.push({ kind: "code", lang, body: code.join(`
46690
- `) });
46691
- }
46692
- return segments;
46693
- }
46694
- function classifyDiffLine(line) {
46695
- if (line.startsWith("@@")) {
46696
- return "hunk";
46697
- }
46698
- if (line.startsWith("---") || line.startsWith("+++")) {
46699
- return "meta";
46700
- }
46701
- if (line.startsWith("+")) {
46702
- return "add";
46703
- }
46704
- if (line.startsWith("-")) {
46705
- return "del";
46706
- }
46707
- return "context";
46708
- }
46709
- function looksLikeUnifiedDiff(text) {
46710
- const lines = splitLines(text);
46711
- for (const [index, line] of lines.entries()) {
46712
- if (HUNK_HEADER.test(line)) {
46713
- return true;
46714
- }
46715
- if (line.startsWith("--- ") && (lines[index + 1]?.startsWith("+++ ") ?? false)) {
46716
- return true;
46717
- }
46718
- }
46719
- return false;
46720
- }
46721
- var CODE_LANG_ALIASES = {
46722
- js: "javascript",
46723
- mjs: "javascript",
46724
- cjs: "javascript",
46725
- jsx: "javascript",
46726
- ts: "typescript",
46727
- tsx: "typescript",
46728
- py: "python",
46729
- py3: "python",
46730
- sh: "bash",
46731
- shell: "bash",
46732
- zsh: "bash",
46733
- console: "bash",
46734
- yml: "yaml",
46735
- rb: "ruby",
46736
- rs: "rust",
46737
- kt: "kotlin",
46738
- cs: "csharp",
46739
- "c++": "cpp",
46740
- cc: "cpp",
46741
- cxx: "cpp",
46742
- hpp: "cpp"
46743
- };
46744
- var GENERIC_KEYWORDS = new Set([
46745
- "if",
46746
- "else",
46747
- "for",
46748
- "while",
46749
- "return",
46750
- "function",
46751
- "class",
46752
- "const",
46753
- "let",
46754
- "var",
46755
- "import",
46756
- "export",
46757
- "from",
46758
- "true",
46759
- "false",
46760
- "null",
46761
- "new",
46762
- "this",
46763
- "try",
46764
- "catch",
46765
- "throw",
46766
- "switch",
46767
- "case",
46768
- "break",
46769
- "continue",
46770
- "default",
46771
- "static",
46772
- "public",
46773
- "private"
46774
- ]);
46775
- var CODE_LANG_KEYWORDS = {
46776
- javascript: new Set([
46777
- ...GENERIC_KEYWORDS,
46778
- "async",
46779
- "await",
46780
- "yield",
46781
- "typeof",
46782
- "instanceof",
46783
- "in",
46784
- "of",
46785
- "void",
46786
- "undefined",
46787
- "super",
46788
- "extends",
46789
- "finally",
46790
- "do",
46791
- "delete"
46792
- ]),
46793
- typescript: new Set([
46794
- ...GENERIC_KEYWORDS,
46795
- "async",
46796
- "await",
46797
- "yield",
46798
- "typeof",
46799
- "instanceof",
46800
- "in",
46801
- "of",
46802
- "void",
46803
- "undefined",
46804
- "super",
46805
- "extends",
46806
- "implements",
46807
- "interface",
46808
- "type",
46809
- "enum",
46810
- "namespace",
46811
- "declare",
46812
- "readonly",
46813
- "abstract",
46814
- "as",
46815
- "satisfies",
46816
- "finally",
46817
- "do",
46818
- "delete"
46819
- ]),
46820
- python: new Set([
46821
- "def",
46822
- "return",
46823
- "if",
46824
- "elif",
46825
- "else",
46826
- "for",
46827
- "while",
46828
- "break",
46829
- "continue",
46830
- "class",
46831
- "import",
46832
- "from",
46833
- "as",
46834
- "try",
46835
- "except",
46836
- "finally",
46837
- "raise",
46838
- "with",
46839
- "lambda",
46840
- "yield",
46841
- "pass",
46842
- "None",
46843
- "True",
46844
- "False",
46845
- "and",
46846
- "or",
46847
- "not",
46848
- "in",
46849
- "is",
46850
- "global",
46851
- "nonlocal",
46852
- "assert",
46853
- "async",
46854
- "await",
46855
- "del",
46856
- "self"
46857
- ]),
46858
- bash: new Set([
46859
- "if",
46860
- "then",
46861
- "else",
46862
- "elif",
46863
- "fi",
46864
- "for",
46865
- "while",
46866
- "do",
46867
- "done",
46868
- "case",
46869
- "esac",
46870
- "function",
46871
- "return",
46872
- "local",
46873
- "export",
46874
- "exit",
46875
- "break",
46876
- "continue",
46877
- "in",
46878
- "select",
46879
- "until"
46880
- ]),
46881
- go: new Set([
46882
- "func",
46883
- "return",
46884
- "if",
46885
- "else",
46886
- "for",
46887
- "range",
46888
- "switch",
46889
- "case",
46890
- "default",
46891
- "break",
46892
- "continue",
46893
- "package",
46894
- "import",
46895
- "var",
46896
- "const",
46897
- "type",
46898
- "struct",
46899
- "interface",
46900
- "map",
46901
- "chan",
46902
- "go",
46903
- "defer",
46904
- "select",
46905
- "fallthrough",
46906
- "nil",
46907
- "true",
46908
- "false"
46909
- ]),
46910
- rust: new Set([
46911
- "fn",
46912
- "let",
46913
- "mut",
46914
- "return",
46915
- "if",
46916
- "else",
46917
- "for",
46918
- "while",
46919
- "loop",
46920
- "match",
46921
- "struct",
46922
- "enum",
46923
- "impl",
46924
- "trait",
46925
- "pub",
46926
- "use",
46927
- "mod",
46928
- "crate",
46929
- "self",
46930
- "Self",
46931
- "super",
46932
- "const",
46933
- "static",
46934
- "async",
46935
- "await",
46936
- "move",
46937
- "ref",
46938
- "dyn",
46939
- "where",
46940
- "unsafe",
46941
- "true",
46942
- "false",
46943
- "None",
46944
- "Some",
46945
- "Ok",
46946
- "Err"
46947
- ]),
46948
- json: new Set(["true", "false", "null"])
46949
- };
46950
- function codeCommentPrefix(normalizedLang) {
46951
- switch (normalizedLang) {
46952
- case "javascript":
46953
- case "typescript":
46954
- case "go":
46955
- case "rust":
46956
- case "java":
46957
- case "c":
46958
- case "cpp":
46959
- case "csharp":
46960
- case "swift":
46961
- case "kotlin":
46962
- case "scala":
46963
- case "php":
46964
- return "//";
46965
- case "python":
46966
- case "bash":
46967
- case "ruby":
46968
- case "yaml":
46969
- case "toml":
46970
- case "r":
46971
- case "perl":
46972
- return "#";
46973
- case "sql":
46974
- case "lua":
46975
- case "haskell":
46976
- return "--";
46977
- default:
46978
- return "";
46979
- }
46980
- }
46981
- function normalizeCodeLang(lang) {
46982
- const key = lang.trim().toLowerCase();
46983
- return CODE_LANG_ALIASES[key] ?? key;
46984
- }
46985
- var CODE_WORD_OR_NUMBER = /[A-Za-z_$][A-Za-z0-9_$]*|\d+(?:\.\d+)?/g;
46986
- function tokenizeCodeWords(text, keywords) {
46987
- const tokens = [];
46988
- let last = 0;
46989
- CODE_WORD_OR_NUMBER.lastIndex = 0;
46990
- let m;
46991
- while ((m = CODE_WORD_OR_NUMBER.exec(text)) !== null) {
46992
- if (m.index > last) {
46993
- tokens.push({ kind: "plain", text: text.slice(last, m.index) });
46994
- }
46995
- const word = m[0];
46996
- if (/^[0-9]/.test(word)) {
46997
- tokens.push({ kind: "number", text: word });
46998
- } else if (keywords.has(word)) {
46999
- tokens.push({ kind: "keyword", text: word });
47000
- } else {
47001
- tokens.push({ kind: "plain", text: word });
47002
- }
47003
- last = m.index + word.length;
47004
- }
47005
- if (last < text.length) {
47006
- tokens.push({ kind: "plain", text: text.slice(last) });
47007
- }
47008
- return tokens;
47009
- }
47010
- function tokenizeCodeLine(line, lang) {
47011
- const normalized = normalizeCodeLang(lang);
47012
- const keywords = CODE_LANG_KEYWORDS[normalized] ?? GENERIC_KEYWORDS;
47013
- const commentPrefix = codeCommentPrefix(normalized);
47014
- const tokens = [];
47015
- let plainBuf = "";
47016
- const flushPlain = () => {
47017
- if (plainBuf.length > 0) {
47018
- tokens.push(...tokenizeCodeWords(plainBuf, keywords));
47019
- plainBuf = "";
47020
- }
47021
- };
47022
- let i = 0;
47023
- while (i < line.length) {
47024
- if (commentPrefix.length > 0 && line.startsWith(commentPrefix, i)) {
47025
- flushPlain();
47026
- tokens.push({ kind: "comment", text: line.slice(i) });
47027
- break;
47028
- }
47029
- const ch = line[i];
47030
- if (ch === '"' || ch === "'" || ch === "`") {
47031
- flushPlain();
47032
- let j = i + 1;
47033
- while (j < line.length && line[j] !== ch) {
47034
- j += line[j] === "\\" ? 2 : 1;
47035
- }
47036
- j = Math.min(j + 1, line.length);
47037
- tokens.push({ kind: "string", text: line.slice(i, j) });
47038
- i = j;
47039
- continue;
47040
- }
47041
- plainBuf += ch;
47042
- i += 1;
47043
- }
47044
- flushPlain();
47045
- return tokens;
47046
- }
47047
- function payloadKind(lang, _lineCount) {
47048
- const normalized = lang.toLowerCase();
47049
- if (MARKDOWN_LANGS.has(normalized)) {
47050
- return "markdown";
47051
- }
47052
- if (DIFF_LANGS.has(normalized)) {
47053
- return "diff";
47054
- }
47055
- return "code";
47056
- }
47057
- function blockLabel({ kind, lineCount, collapsed, hint }) {
47058
- const marker = collapsed ? "\u25B8" : "\u25BE";
47059
- const unit = lineCount === 1 ? "line" : "lines";
47060
- const suffix = hint !== undefined && hint.length > 0 ? ` \xB7 ${hint}` : "";
47061
- return `${marker} ${kind} (${lineCount} ${unit})${suffix}`;
47062
- }
47063
- function summarizeSubmittedLine(line) {
47064
- const normalized = line.replace(/\r\n/g, `
47065
- `).replace(/\r/g, `
47066
- `);
47067
- const nonEmpty = normalized.split(`
47068
- `).filter((linePart) => linePart.length > 0);
47069
- if (nonEmpty.length <= 1) {
47070
- return line;
47071
- }
47072
- const [first, ...rest] = nonEmpty;
47073
- return `${first} [+ ${rest.length} pasted line${rest.length === 1 ? "" : "s"}]`;
47074
- }
47075
- function isZeroWidth(cp) {
47076
- return cp >= 768 && cp <= 879 || cp >= 8203 && cp <= 8207 || cp >= 65024 && cp <= 65039 || cp >= 8288 && cp <= 8292;
47077
- }
47078
- var WIDE_RANGES = [
47079
- [4352, 4447],
47080
- [11904, 12350],
47081
- [12353, 13311],
47082
- [13312, 19903],
47083
- [19968, 40959],
47084
- [40960, 42191],
47085
- [44032, 55203],
47086
- [63744, 64255],
47087
- [65072, 65135],
47088
- [65280, 65376],
47089
- [65504, 65510],
47090
- [127744, 128591],
47091
- [129280, 129535],
47092
- [131072, 262141]
47093
- ];
47094
- function codePointWidth(cp) {
47095
- for (const [lo, hi] of WIDE_RANGES) {
47096
- if (cp >= lo && cp <= hi) {
47097
- return 2;
47098
- }
47099
- }
47100
- return 1;
47101
- }
47102
- function visualWidth(text) {
47103
- let width = 0;
47104
- for (const ch of text) {
47105
- const codePoint = ch.codePointAt(0) ?? 0;
47106
- if (isZeroWidth(codePoint)) {
47107
- continue;
47108
- }
47109
- width += codePointWidth(codePoint);
47110
- }
47111
- return width;
47112
- }
47113
- function hugWidth(text, chrome) {
47114
- const lines = splitLines(text.replace(/\n+$/, ""));
47115
- let widest = 0;
47116
- for (const line of lines) {
47117
- widest = Math.max(widest, visualWidth(line));
47118
- }
47119
- return widest + chrome;
47120
- }
47121
-
47122
- // src/lib/ui.ts
47123
- function colorEnabled() {
47124
- if (process.env.NO_COLOR !== undefined) {
47125
- return false;
47126
- }
47127
- if (process.env.FORCE_COLOR !== undefined) {
47128
- return true;
47129
- }
47130
- return Boolean(stdout.isTTY);
47131
- }
47132
- function wrap(open, close) {
47133
- return (text) => colorEnabled() ? `\x1B[${open}m${text}\x1B[${close}m` : text;
47134
- }
47135
- var style = {
47136
- bold: wrap(1, 22),
47137
- dim: wrap(2, 22),
47138
- red: wrap(31, 39),
47139
- green: wrap(32, 39),
47140
- yellow: wrap(33, 39),
47141
- cyan: wrap(36, 39),
47142
- gray: wrap(90, 39)
47143
- };
47144
- var symbols = {
47145
- ok: "\u2713",
47146
- cross: "\u2717",
47147
- off: "\xB7",
47148
- arrow: "\u2192",
47149
- bullet: "\u2022"
47150
- };
47151
- var RULE_WIDTH = 52;
47152
- function banner(title, subtitle) {
47153
- const rule = style.cyan("\u2501".repeat(RULE_WIDTH));
47154
- console.log(rule);
47155
- console.log(` ${style.bold(title)}`);
47156
- if (subtitle) {
47157
- console.log(` ${style.dim(subtitle)}`);
47158
- }
47159
- console.log(rule);
47160
- }
47161
- function heading(title, emit = console.log) {
47162
- emit(`
47163
- ${style.bold(style.cyan(title))}`);
47164
- }
47165
- function statusLine(label, enabled, detail) {
47166
- const marker = enabled ? style.green(symbols.ok) : style.gray(symbols.off);
47167
- const name = enabled ? label : style.gray(label);
47168
- const suffix = detail ? style.dim(` (${detail})`) : "";
47169
- console.log(` ${marker} ${name}${suffix}`);
47170
- }
47171
- function nextSteps(steps) {
47172
- if (steps.length === 0) {
47173
- return;
47174
- }
47175
- heading("Next steps");
47176
- for (const step of steps) {
47177
- console.log(` ${style.cyan(symbols.arrow)} ${step}`);
47178
- }
47179
- }
47180
- function note(message) {
47181
- console.log(` ${style.dim(message)}`);
47182
- }
47183
- function renderInline(text) {
47184
- const withCode = text.replace(/`([^`]+)`/g, (_match, code) => style.gray(code));
47185
- return withCode.replace(/\*\*([^*]+)\*\*/g, (_match, bold) => style.bold(bold));
47186
- }
47187
- function renderTextLine(line) {
47188
- const heading = /^#{1,6}\s+(.*)$/.exec(line);
47189
- if (heading !== null) {
47190
- return style.bold(style.cyan(heading[1] ?? ""));
47191
- }
47192
- const bullet = /^(\s*)[-*]\s+(.*)$/.exec(line);
47193
- if (bullet !== null) {
47194
- return `${bullet[1] ?? ""}${style.cyan(symbols.bullet)} ${renderInline(bullet[2] ?? "")}`;
47195
- }
47196
- return renderInline(line);
47197
- }
47198
- function renderDiff(text) {
47199
- if (!colorEnabled()) {
47200
- return text;
47201
- }
47202
- return splitLines(text).map((line) => {
47203
- switch (classifyDiffLine(line)) {
47204
- case "add":
47205
- return style.green(line);
47206
- case "del":
47207
- return style.red(line);
47208
- case "hunk":
47209
- return style.cyan(line);
47210
- case "meta":
47211
- return style.dim(line);
47212
- default:
47213
- return line;
47214
- }
47215
- }).join(`
47216
- `);
47217
- }
47218
- function renderCodeSegment(lang, body) {
47219
- const lines = body.split(`
47220
- `);
47221
- const out = [];
47222
- if (lang.length > 0) {
47223
- out.push(style.dim(lang));
47224
- }
47225
- if (payloadKind(lang, lines.length) === "diff" || looksLikeUnifiedDiff(body)) {
47226
- out.push(renderDiff(body));
47227
- return out;
47228
- }
47229
- for (const line of lines) {
47230
- out.push(style.gray(line));
47231
- }
47232
- return out;
47233
- }
47234
- function renderMarkdown(md) {
47235
- if (!colorEnabled()) {
47236
- return md;
47237
- }
47238
- const out = [];
47239
- for (const segment of segmentMarkdown(md)) {
47240
- if (segment.kind === "code") {
47241
- out.push(...renderCodeSegment(segment.lang, segment.body));
47242
- continue;
47243
- }
47244
- for (const line of segment.text.split(`
47245
- `)) {
47246
- out.push(renderTextLine(line));
47247
- }
47248
- }
47249
- return out.join(`
47250
- `);
47251
- }
47252
- function collapseToolOutput(text, maxWidth = 100) {
47253
- const trimmed = text.replace(/\r?\n+$/, "");
47254
- const lines = trimmed.length === 0 ? [] : splitLines(trimmed);
47255
- const firstNonEmpty = lines.find((line) => line.trim().length > 0) ?? "";
47256
- const summary = firstNonEmpty.length > maxWidth ? `${firstNonEmpty.slice(0, maxWidth)}\u2026` : firstNonEmpty;
47257
- const lineCount = lines.length;
47258
- return { summary, lineCount, hidden: Math.max(0, lineCount - 1) };
47259
- }
47260
- function indentBlock(text, pad) {
47261
- return text.split(`
47262
- `).map((line) => line.length > 0 ? pad + line : line).join(`
47263
- `);
47264
- }
47265
- function summarizeToolArgs(input, max = 80) {
47266
- const clip = (s) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
47267
- const raw = input.trim();
47268
- if (raw.length === 0) {
47269
- return "";
47270
- }
47271
- let parsed;
47272
- try {
47273
- parsed = JSON.parse(raw);
47274
- } catch {
47275
- return clip(raw);
47276
- }
47277
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
47278
- return clip(raw);
47279
- }
47280
- const parts = [];
47281
- for (const [key, value] of Object.entries(parsed)) {
47282
- let shown;
47283
- if (value === null) {
47284
- shown = "null";
47285
- } else if (Array.isArray(value)) {
47286
- shown = "[\u2026]";
47287
- } else if (typeof value === "object") {
47288
- shown = "{\u2026}";
47289
- } else {
47290
- shown = String(value);
47291
- }
47292
- parts.push(`${key}=${shown}`);
47293
- }
47294
- return clip(parts.join(", "));
47295
- }
47296
- function helpTitle(command, summary) {
47297
- console.log(`${style.bold(command)} ${style.dim(`\u2014 ${summary}`)}`);
47298
- }
47299
- function helpUsage(lines) {
47300
- heading("Usage");
47301
- for (const line of lines) {
47302
- console.log(` ${style.cyan(line)}`);
47303
- }
47304
- }
47305
- function helpOptions(options) {
47306
- heading("Options");
47307
- const width = Math.max(...options.map((option) => option.flag.length));
47308
- for (const option of options) {
47309
- console.log(` ${style.cyan(option.flag.padEnd(width))} ${style.dim(option.desc)}`);
47310
- }
47311
- }
47312
-
47313
47580
  // src/commands/projects.ts
47581
+ init_ui();
47314
47582
  async function projectsCommand(args = []) {
47315
47583
  const sub = args[0];
47316
47584
  if (args.includes("--help") || args.includes("-h") || sub === "help") {
@@ -47512,40 +47780,9 @@ var GDGRAPH_CORE_SOURCES = [
47512
47780
  "types.ts"
47513
47781
  ];
47514
47782
 
47515
- // src/lib/prompt.ts
47516
- import { stdin as input, stdout as output } from "process";
47517
- import readline from "readline/promises";
47518
- async function confirm(question, defaultValue = false) {
47519
- if (!input.isTTY) {
47520
- return defaultValue;
47521
- }
47522
- const suffix = defaultValue ? "Y/n" : "y/N";
47523
- const rl = readline.createInterface({ input, output });
47524
- try {
47525
- const answer = await rl.question(`${style.cyan(symbols.arrow)} ${question} ${style.dim(`(${suffix})`)} `);
47526
- const normalized = answer.trim().toLowerCase();
47527
- if (!normalized) {
47528
- return defaultValue;
47529
- }
47530
- return normalized === "y" || normalized === "yes";
47531
- } finally {
47532
- rl.close();
47533
- }
47534
- }
47535
- async function choice(question, choices, defaultValue) {
47536
- if (!input.isTTY) {
47537
- return defaultValue;
47538
- }
47539
- const rl = readline.createInterface({ input, output });
47540
- try {
47541
- const answer = await rl.question(`${style.cyan(symbols.arrow)} ${question} ${style.dim(`(${choices.join("/")}; default: ${defaultValue})`)} `);
47542
- const normalized = answer.trim().toLowerCase();
47543
- const match = choices.find((item) => item.toLowerCase() === normalized);
47544
- return match ?? defaultValue;
47545
- } finally {
47546
- rl.close();
47547
- }
47548
- }
47783
+ // src/commands/init.ts
47784
+ init_prompt();
47785
+ init_ui();
47549
47786
 
47550
47787
  // src/lib/project-sandbox-policy.ts
47551
47788
  init_paths();
@@ -53009,7 +53246,7 @@ import path47 from "path";
53009
53246
  // package.json
53010
53247
  var package_default = {
53011
53248
  name: "@mrciphersmith/keryx",
53012
- version: "0.2.159",
53249
+ version: "0.2.161",
53013
53250
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
53014
53251
  private: false,
53015
53252
  publishConfig: {
@@ -61452,6 +61689,9 @@ function normalizeRouteText(value) {
61452
61689
  return value.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
61453
61690
  }
61454
61691
 
61692
+ // src/commands/skills.ts
61693
+ init_ui();
61694
+
61455
61695
  // src/gdskills/learn.ts
61456
61696
  init_fs();
61457
61697
  init_json();
@@ -66038,9 +66278,10 @@ init_github();
66038
66278
  init_flow_move();
66039
66279
  init_service7();
66040
66280
  init_guard2();
66281
+ init_ui();
66282
+ init_machine();
66041
66283
  import { randomInt } from "crypto";
66042
66284
  import path155 from "path";
66043
- init_machine();
66044
66285
 
66045
66286
  // src/flow/types.ts
66046
66287
  var ATTEMPT_CLI_OUTCOMES = ["started", "failed", "blocked"];
@@ -67281,6 +67522,7 @@ function createJobService(deps) {
67281
67522
  }
67282
67523
 
67283
67524
  // src/commands/job.ts
67525
+ init_ui();
67284
67526
  function positional2(args, index) {
67285
67527
  const value = args[index];
67286
67528
  return value === undefined || value.startsWith("--") ? undefined : value;
@@ -73906,6 +74148,7 @@ Options:
73906
74148
  }
73907
74149
 
73908
74150
  // src/commands/standard.ts
74151
+ init_ui();
73909
74152
  import { mkdir as mkdir59, writeFile as writeFile49 } from "fs/promises";
73910
74153
  import path166 from "path";
73911
74154
 
@@ -74308,15 +74551,15 @@ function extractCapabilities(manifest) {
74308
74551
 
74309
74552
  // src/standard/help-groups.ts
74310
74553
  var HELP_GROUP_ORDER = [
74311
- { name: "Start here", slug: "start-here" },
74312
- { name: "Connect a model provider", slug: "connect" },
74313
- { name: "Look and feel", slug: "look-and-feel" },
74314
- { name: "Working in keryx shell", slug: "shell-work" },
74315
- { name: "Project knowledge", slug: "project-knowledge" },
74316
- { name: "Managed work", slug: "managed-work" },
74317
- { name: "Automation", slug: "automation" },
74318
- { name: "External agents, ACP and MCP", slug: "external-agents" },
74319
- { name: "Maintenance and diagnostics", slug: "maintenance" }
74554
+ { name: "Start here", slug: "start-here", tab: "Start", tabShort: "Start" },
74555
+ { name: "Connect a model provider", slug: "connect", tab: "Connect", tabShort: "Conn" },
74556
+ { name: "Look and feel", slug: "look-and-feel", tab: "Look", tabShort: "Look" },
74557
+ { name: "Working in keryx shell", slug: "shell-work", tab: "Shell", tabShort: "Shell" },
74558
+ { name: "Project knowledge", slug: "project-knowledge", tab: "Knowledge", tabShort: "Know" },
74559
+ { name: "Managed work", slug: "managed-work", tab: "Work", tabShort: "Work" },
74560
+ { name: "Automation", slug: "automation", tab: "Automate", tabShort: "Auto" },
74561
+ { name: "External agents, ACP and MCP", slug: "external-agents", tab: "Agents", tabShort: "Agents" },
74562
+ { name: "Maintenance and diagnostics", slug: "maintenance", tab: "Maintain", tabShort: "Maint" }
74320
74563
  ];
74321
74564
  var HELP_GROUPS = [
74322
74565
  { kind: "cli", name: "init", group: "Start here", summary: "Initialize .metaproject in the current project." },
@@ -74346,7 +74589,7 @@ var HELP_GROUPS = [
74346
74589
  group: "Connect a model provider",
74347
74590
  summary: "Providers this operator has configured, and cross-family review eligibility."
74348
74591
  },
74349
- { kind: "slash", name: "/connect", group: "Connect a model provider", summary: "Switch provider / API key." },
74592
+ { kind: "slash", name: "/connect", group: "Connect a model provider", summary: "Switch provider; row buttons test/disconnect it." },
74350
74593
  {
74351
74594
  kind: "slash",
74352
74595
  name: "/search-provider",
@@ -74762,15 +75005,29 @@ function renderEntryRows(entries, maxColumns) {
74762
75005
  }
74763
75006
  return rows;
74764
75007
  }
75008
+ var SHELL_SUBHEADING = " in keryx shell:";
75009
+ function renderGroupLines(group, maxColumns) {
75010
+ const cliEntries = entriesInGroup(group.name, "cli");
75011
+ const slashEntries = entriesInGroup(group.name, "slash");
75012
+ if (cliEntries.length === 0 && slashEntries.length === 0) {
75013
+ return [];
75014
+ }
75015
+ const lines = [`${group.name}:`];
75016
+ lines.push(...renderEntryRows(cliEntries, maxColumns));
75017
+ if (slashEntries.length > 0) {
75018
+ lines.push(SHELL_SUBHEADING);
75019
+ lines.push(...renderEntryRows(slashEntries, maxColumns));
75020
+ }
75021
+ return lines;
75022
+ }
74765
75023
  function renderGroupedCliHelp(maxColumns = HELP_DEFAULT_COLUMNS) {
74766
75024
  const lines = [];
74767
75025
  for (const group of HELP_GROUP_ORDER) {
74768
- const entries = entriesInGroup(group.name, "cli");
74769
- if (entries.length === 0) {
75026
+ const groupLines = renderGroupLines(group, maxColumns);
75027
+ if (groupLines.length === 0) {
74770
75028
  continue;
74771
75029
  }
74772
- lines.push(`${group.name}:`);
74773
- lines.push(...renderEntryRows(entries, maxColumns));
75030
+ lines.push(...groupLines);
74774
75031
  lines.push("");
74775
75032
  }
74776
75033
  return lines.join(`
@@ -74778,8 +75035,7 @@ function renderGroupedCliHelp(maxColumns = HELP_DEFAULT_COLUMNS) {
74778
75035
  `);
74779
75036
  }
74780
75037
  function renderCliGroupHelp(group, maxColumns = HELP_DEFAULT_COLUMNS) {
74781
- const entries = entriesInGroup(group.name, "cli");
74782
- const lines = [`${group.name}:`, ...renderEntryRows(entries, maxColumns)];
75038
+ const lines = renderGroupLines(group, maxColumns);
74783
75039
  return lines.join(`
74784
75040
  `) + `
74785
75041
  `;
@@ -74798,12 +75054,12 @@ function renderGroupedNamedHelp(options, maxColumns) {
74798
75054
  `) + `
74799
75055
  `;
74800
75056
  }
74801
- function renderEntryDetail(entry) {
74802
- const lines = [`${entry.name}`, "", entry.summary, ""];
75057
+ function renderEntryDetail(entry, maxColumns = HELP_DEFAULT_COLUMNS) {
75058
+ const lines = [`${entry.name}`, "", ...wrapWords(entry.summary, maxColumns), ""];
74803
75059
  if (entry.kind === "slash") {
74804
- lines.push("This is a keryx shell command \u2014 type it inside `keryx shell` (readline chat/agent mode, or the", "TUI's `/`-menu). It has no standalone CLI form.");
75060
+ lines.push(...wrapWords("This is a keryx shell command \u2014 type it inside `keryx shell` (readline chat/agent mode, or " + "the TUI's `/`-menu). It has no standalone CLI form.", maxColumns));
74805
75061
  } else {
74806
- lines.push(`Run \`keryx ${entry.name} --help\` (or \`keryx help ${entry.name}\`) for its full usage.`);
75062
+ lines.push(...wrapWords(`Run \`keryx ${entry.name} --help\` (or \`keryx help ${entry.name}\`) for its full usage.`, maxColumns));
74807
75063
  }
74808
75064
  return lines.join(`
74809
75065
  `) + `
@@ -76561,13 +76817,14 @@ Usage:
76561
76817
  }
76562
76818
 
76563
76819
  // src/commands/security.ts
76564
- import { mkdir as mkdir60, readdir as readdir33, readFile as readFile96, writeFile as writeFile50 } from "fs/promises";
76565
- import path168 from "path";
76566
- import { fileURLToPath as fileURLToPath7 } from "url";
76820
+ init_ui();
76567
76821
  init_args();
76568
76822
  init_contained_path();
76569
76823
  init_fs();
76570
76824
  init_json();
76825
+ import { mkdir as mkdir60, readdir as readdir33, readFile as readFile96, writeFile as writeFile50 } from "fs/promises";
76826
+ import path168 from "path";
76827
+ import { fileURLToPath as fileURLToPath7 } from "url";
76571
76828
 
76572
76829
  // src/security/detect/mcp.ts
76573
76830
  import { createHash as createHash31 } from "crypto";
@@ -77906,8 +78163,10 @@ async function sandboxCommand(args = [], deps = {}) {
77906
78163
 
77907
78164
  // src/commands/mcp.ts
77908
78165
  init_args();
78166
+ init_ui();
77909
78167
 
77910
78168
  // src/commands/integrate.ts
78169
+ init_ui();
77911
78170
  import path170 from "path";
77912
78171
  var EDITOR_USAGE = `<cursor|claude|opencode|vscode|generic|all>`;
77913
78172
  var DEFAULT_EDITORS = ["all"];
@@ -81231,6 +81490,7 @@ function positional3(args) {
81231
81490
 
81232
81491
  // src/commands/serve-mcp.ts
81233
81492
  init_args();
81493
+ init_ui();
81234
81494
  import path183 from "path";
81235
81495
 
81236
81496
  // src/mcp/discovery.ts
@@ -99820,6 +100080,7 @@ async function approveExternalSpawn(request) {
99820
100080
  init_caller_session();
99821
100081
 
99822
100082
  // src/lib/statusbar.ts
100083
+ init_ui();
99823
100084
  import { homedir as homedir10 } from "os";
99824
100085
  var ESC = "\x1B";
99825
100086
  var CSI = `${ESC}[`;
@@ -100821,6 +101082,7 @@ function boldChunk(otui, text) {
100821
101082
  }
100822
101083
 
100823
101084
  // src/tui/transcript-blocks.ts
101085
+ init_md_blocks();
100824
101086
  var EVICTED_BLOCK_TEXT = "(output no longer retained)";
100825
101087
  var UNKNOWN_BLOCK_TEXT = "(no such block)";
100826
101088
  var TRUNCATED_BLOCK_NOTICE = "\u2026 (output truncated at the retention cap)";
@@ -109997,7 +110259,32 @@ function openSessionInfo(otui, chrome, options) {
109997
110259
  // src/tui/help-modal.ts
109998
110260
  var HELP_MODAL_TITLE = "/help";
109999
110261
  var HELP_MODAL_KEYS = "keys: \u2191/\u2193 select \xB7 enter details \xB7 \u2190/\u2192 tabs \xB7 esc close";
110000
- var HELP_MODAL_TABS = HELP_GROUP_ORDER.map((g) => ({ id: g.slug, label: g.name }));
110262
+ var HELP_MODAL_TABS = HELP_GROUP_ORDER.map((g) => ({ id: g.slug, label: g.tab }));
110263
+ var HELP_MODAL_TABS_SHORT = HELP_GROUP_ORDER.map((g) => ({ id: g.slug, label: g.tabShort }));
110264
+ function tabStripWidth(labels) {
110265
+ return labels.reduce((total, label, i) => total + label.length + 2 + (i === 0 ? 0 : 1), 0);
110266
+ }
110267
+ var READABLE_TAB_STRIP_WIDTH = tabStripWidth(HELP_MODAL_TABS.map((t) => t.label));
110268
+ function pickTabs(chrome, options) {
110269
+ let cols;
110270
+ let rows;
110271
+ if (options.renderer !== undefined) {
110272
+ cols = options.renderer.width;
110273
+ rows = options.renderer.height;
110274
+ } else {
110275
+ const c = chrome;
110276
+ if (c?.renderer !== undefined) {
110277
+ cols = resolveModalAvailableWidth(c);
110278
+ rows = c.renderer.height;
110279
+ }
110280
+ }
110281
+ if (typeof cols !== "number" || typeof rows !== "number") {
110282
+ return HELP_MODAL_TABS_SHORT;
110283
+ }
110284
+ const panelWidth = Math.min(cols, resolveModalPanelSize(cols, rows).width);
110285
+ const innerWidth = panelWidth - MODAL_PANEL_CHROME_X;
110286
+ return READABLE_TAB_STRIP_WIDTH <= innerWidth ? HELP_MODAL_TABS : HELP_MODAL_TABS_SHORT;
110287
+ }
110001
110288
  function entriesForGroup(group) {
110002
110289
  return [...entriesInGroup(group.name, "cli"), ...entriesInGroup(group.name, "slash")];
110003
110290
  }
@@ -110010,9 +110297,10 @@ function openHelpModal(otui, chrome, options) {
110010
110297
  const core = otui;
110011
110298
  const r = chrome?.renderer;
110012
110299
  const bodyRows = bodyRowsFor2(chrome, options);
110300
+ const tabs = pickTabs(chrome, options);
110013
110301
  const states = new Map(HELP_GROUP_ORDER.map((group) => [
110014
110302
  group.slug,
110015
- { entries: entriesForGroup(group), selected: 0, detail: false, scroll: 0 }
110303
+ { groupName: group.name, entries: entriesForGroup(group), selected: 0, detail: false, scroll: 0 }
110016
110304
  ]));
110017
110305
  let width;
110018
110306
  let bodyNode;
@@ -110020,23 +110308,29 @@ function openHelpModal(otui, chrome, options) {
110020
110308
  const keys = {};
110021
110309
  const host = {};
110022
110310
  const activeState = () => {
110023
- const id = host.handle?.activeTab() ?? (HELP_MODAL_TABS[0]?.id ?? "");
110311
+ const id = host.handle?.activeTab() ?? (tabs[0]?.id ?? "");
110024
110312
  const existing = states.get(id);
110025
110313
  if (existing !== undefined) {
110026
110314
  return existing;
110027
110315
  }
110028
- const fallback = { entries: [], selected: 0, detail: false, scroll: 0 };
110316
+ const fallback = { groupName: "", entries: [], selected: 0, detail: false, scroll: 0 };
110029
110317
  states.set(id, fallback);
110030
110318
  return fallback;
110031
110319
  };
110032
110320
  const tabLines = (state) => {
110321
+ const heading = `${state.groupName}:`;
110033
110322
  if (state.entries.length === 0) {
110034
- return [HELP_MODAL_KEYS, "", "(no commands in this group)"];
110323
+ return [heading, HELP_MODAL_KEYS, "", "(no commands in this group)"];
110035
110324
  }
110036
110325
  if (state.detail) {
110037
110326
  const entry = state.entries[state.selected];
110038
- return [HELP_MODAL_KEYS, "", ...entry === undefined ? ["(nothing selected)"] : wrapLines(renderEntryDetail(entry), width).split(`
110039
- `)];
110327
+ return [
110328
+ heading,
110329
+ HELP_MODAL_KEYS,
110330
+ "",
110331
+ ...entry === undefined ? ["(nothing selected)"] : wrapLines(renderEntryDetail(entry), width).split(`
110332
+ `)
110333
+ ];
110040
110334
  }
110041
110335
  const rows = state.entries.map((entry, i) => {
110042
110336
  const marker = i === state.selected ? ">" : " ";
@@ -110045,7 +110339,7 @@ function openHelpModal(otui, chrome, options) {
110045
110339
  `).join(`
110046
110340
  `);
110047
110341
  });
110048
- return [HELP_MODAL_KEYS, "", ...rows];
110342
+ return [heading, HELP_MODAL_KEYS, "", ...rows];
110049
110343
  };
110050
110344
  const paint = () => {
110051
110345
  if (closed) {
@@ -110054,7 +110348,7 @@ function openHelpModal(otui, chrome, options) {
110054
110348
  const state = activeState();
110055
110349
  const lines = tabLines(state);
110056
110350
  if (!state.detail) {
110057
- state.scroll = scrollToReveal(state.selected + 2, state.scroll, bodyRows);
110351
+ state.scroll = scrollToReveal(state.selected + 3, state.scroll, bodyRows);
110058
110352
  }
110059
110353
  state.scroll = clampScroll(state.scroll, lines.length, bodyRows);
110060
110354
  if (bodyNode !== undefined) {
@@ -110065,7 +110359,7 @@ function openHelpModal(otui, chrome, options) {
110065
110359
  const initialTab = options.initialGroupSlug !== undefined && groupBySlug(options.initialGroupSlug) !== undefined ? options.initialGroupSlug : undefined;
110066
110360
  const handle = openModal(core, chrome, {
110067
110361
  title: HELP_MODAL_TITLE,
110068
- tabs: HELP_MODAL_TABS,
110362
+ tabs,
110069
110363
  ...initialTab !== undefined ? { initialTab } : {},
110070
110364
  footer: [
110071
110365
  { key: "\u2191/\u2193", label: "select" },
@@ -111179,6 +111473,8 @@ function openGamesModal(otui, chrome, options = {}) {
111179
111473
  }
111180
111474
  // src/tui/tui-shell.ts
111181
111475
  init_providers();
111476
+ init_ui();
111477
+ init_md_blocks();
111182
111478
  init_patch_risk();
111183
111479
  init_catalog();
111184
111480
  init_grants();
@@ -112083,6 +112379,13 @@ function stepQueueNavAction(selected, direction) {
112083
112379
  const nextIdx = direction === "left" ? idx > 0 ? idx - 1 : count - 1 : idx < count - 1 ? idx + 1 : 0;
112084
112380
  return QUEUE_NAV_ACTIONS[nextIdx];
112085
112381
  }
112382
+ var CONNECT_NAV_ACTIONS = ["label", "test", "disconnect"];
112383
+ function stepConnectNavAction(selected, direction) {
112384
+ const idx = CONNECT_NAV_ACTIONS.indexOf(selected);
112385
+ const count = CONNECT_NAV_ACTIONS.length;
112386
+ const nextIdx = direction === "left" ? idx > 0 ? idx - 1 : count - 1 : idx < count - 1 ? idx + 1 : 0;
112387
+ return CONNECT_NAV_ACTIONS[nextIdx];
112388
+ }
112086
112389
 
112087
112390
  // src/tui/worker-fleet.ts
112088
112391
  var STATUS_GLYPH2 = {
@@ -117653,6 +117956,28 @@ function modelPickerNotice(label, result) {
117653
117956
  }
117654
117957
  return modelsFailureLine(label, result.failure);
117655
117958
  }
117959
+ function smallActionButton(otui, r, label, id, color, onMouseDown) {
117960
+ const box = new otui.BoxRenderable(r, {
117961
+ id,
117962
+ flexShrink: 0,
117963
+ marginLeft: 1,
117964
+ paddingLeft: 1,
117965
+ paddingRight: 1,
117966
+ onMouseDown: (event) => {
117967
+ event.stopPropagation();
117968
+ onMouseDown();
117969
+ }
117970
+ });
117971
+ const text = new otui.TextRenderable(r, { id: `${id}-t`, content: `[${label}]` });
117972
+ text.fg = color;
117973
+ box.add(text);
117974
+ const setActive = (active) => {
117975
+ box.backgroundColor = active ? getTheme().highlight : undefined;
117976
+ text.content = active ? otui.t`${boldChunk(otui, `[${label}]`)}` : `[${label}]`;
117977
+ text.fg = color;
117978
+ };
117979
+ return { box, setActive };
117980
+ }
117656
117981
  function pickProviderStep(otui, target, detected) {
117657
117982
  const r = stepRenderer(target);
117658
117983
  return new Promise((resolve) => {
@@ -117691,6 +118016,221 @@ function pickProviderStep(otui, target, detected) {
117691
118016
  });
117692
118017
  });
117693
118018
  }
118019
+ function pickConnectedProviderStep(otui, target, detected, opts) {
118020
+ const r = stepRenderer(target);
118021
+ return new Promise((resolve) => {
118022
+ let rows = [...detected];
118023
+ let selectedIndex = 0;
118024
+ let selectedAction = "label";
118025
+ let pendingDisconnect;
118026
+ let resolved = false;
118027
+ let rowBlocks = [];
118028
+ const labelOf = (d) => d.label ?? d.name;
118029
+ const noteOf = (d) => d.note ?? `${d.models.length} model(s)`;
118030
+ const unsubEscapeGuard = onKeypress4(r, (key) => {
118031
+ if (resolved || key.name !== "escape" || pendingDisconnect === undefined)
118032
+ return;
118033
+ clearPending();
118034
+ applyHighlight();
118035
+ key.preventDefault();
118036
+ key.stopPropagation();
118037
+ });
118038
+ const surface = openStepSurface(otui, target, {
118039
+ id: "connect-picker",
118040
+ title: "Connected providers",
118041
+ tab: "Providers",
118042
+ hint: "(\u2191/\u2193 row \xB7 \u2190/\u2192 Label/Test/Disconnect \xB7 Enter \xB7 Esc to cancel)",
118043
+ footer: [
118044
+ { key: "\u2191/\u2193", label: "row" },
118045
+ { key: "\u2190/\u2192", label: "Label/Test/Disconnect" },
118046
+ { key: "Enter", label: "select/run" },
118047
+ { key: "esc", label: "cancel" }
118048
+ ],
118049
+ contentRows: selectBoxHeight(Math.max(rows.length, 1), true),
118050
+ onEscape: () => finish(undefined)
118051
+ });
118052
+ const body = surface.body;
118053
+ function finish(value) {
118054
+ if (resolved)
118055
+ return;
118056
+ resolved = true;
118057
+ unsubEscapeGuard();
118058
+ unsub();
118059
+ surface.close();
118060
+ resolve(value);
118061
+ }
118062
+ function clearPending() {
118063
+ if (pendingDisconnect === undefined)
118064
+ return;
118065
+ const armed = rowBlocks.find((b) => b.name === pendingDisconnect);
118066
+ pendingDisconnect = undefined;
118067
+ if (armed !== undefined)
118068
+ armed.status.content = "";
118069
+ }
118070
+ function applyHighlight() {
118071
+ for (let i = 0;i < rowBlocks.length; i++) {
118072
+ const block = rowBlocks[i];
118073
+ if (block === undefined)
118074
+ continue;
118075
+ const active = i === selectedIndex;
118076
+ block.outer.backgroundColor = active ? getTheme().highlight : undefined;
118077
+ block.labelText.content = active && selectedAction === "label" ? otui.t`${boldChunk(otui, `${labelOf(rows[i])}`)} ${dimChunk(otui, noteOf(rows[i]))}` : otui.t`${labelOf(rows[i])} ${dimChunk(otui, noteOf(rows[i]))}`;
118078
+ block.test.setActive(active && selectedAction === "test");
118079
+ block.disconnect.setActive(active && selectedAction === "disconnect");
118080
+ }
118081
+ }
118082
+ async function runTest(name) {
118083
+ const block = rowBlocks.find((b) => b.name === name);
118084
+ const provider = providerByName(name, opts.configDir);
118085
+ if (block === undefined)
118086
+ return;
118087
+ if (provider === undefined) {
118088
+ block.status.content = otui.t`${roleChunk(otui, "error", "\u2717")} unknown provider \u2014 cannot test`;
118089
+ return;
118090
+ }
118091
+ block.status.content = otui.t`${dimChunk(otui, "testing\u2026")}`;
118092
+ const result = await testProviderConnection(provider, opts.fetch ?? globalThis.fetch, opts.env ?? process.env);
118093
+ const current = rowBlocks.find((b) => b.name === name);
118094
+ if (current === undefined || resolved)
118095
+ return;
118096
+ current.status.content = result.source === "live" ? otui.t`${roleChunk(otui, "ok", "\u2713")} ok \u2014 ${String(result.models.length)} model(s)` : otui.t`${roleChunk(otui, "error", "\u2717")} ${modelsFailureLine(labelOf(rows.find((d) => d.name === name) ?? { name, models: [] }), result.failure ?? { kind: "empty" })}`;
118097
+ }
118098
+ function armOrConfirmDisconnect(name) {
118099
+ const block = rowBlocks.find((b) => b.name === name);
118100
+ if (block === undefined)
118101
+ return;
118102
+ if (pendingDisconnect !== name) {
118103
+ clearPending();
118104
+ pendingDisconnect = name;
118105
+ const env = opts.env ?? process.env;
118106
+ const classification = classifyProviderConnection(name, env, opts.configDir);
118107
+ const warning = sharedCredentialWarning(classification.sharedWith, classification.envKey);
118108
+ const label = labelOf(rows.find((d) => d.name === name) ?? { name, models: [] });
118109
+ block.status.content = warning === undefined ? otui.t`${roleChunk(otui, "attention", "?")} disconnect '${label}'? ${dimChunk(otui, "click/Enter Disconnect again to confirm, Esc to cancel")}` : otui.t`${roleChunk(otui, "attention", "?")} disconnect '${label}'? ${roleChunk(otui, "attention", `Note: ${warning}.`)} ${dimChunk(otui, "click/Enter Disconnect again to confirm, Esc to cancel")}`;
118110
+ return;
118111
+ }
118112
+ pendingDisconnect = undefined;
118113
+ const result = disconnectProvider(name, opts.env ?? process.env, opts.configDir);
118114
+ if (!result.ok) {
118115
+ block.status.content = otui.t`${roleChunk(otui, "error", "\u2717")} not removed \u2014 ${result.reason ?? "unknown reason"}`;
118116
+ return;
118117
+ }
118118
+ opts.onDisconnected?.(name, result.sharedWith);
118119
+ rows = rows.filter((d) => d.name !== name);
118120
+ paint();
118121
+ }
118122
+ function paint() {
118123
+ for (const block of rowBlocks) {
118124
+ try {
118125
+ body.remove(block.outer);
118126
+ } catch {}
118127
+ }
118128
+ rowBlocks = [];
118129
+ if (rows.length === 0) {
118130
+ finish(undefined);
118131
+ return;
118132
+ }
118133
+ selectedIndex = clampQueueNavIndex(selectedIndex, rows.length);
118134
+ for (let i = 0;i < rows.length; i++) {
118135
+ const d = rows[i];
118136
+ const outer = new otui.BoxRenderable(r, { id: `cp-${d.name}`, width: "100%", flexDirection: "column" });
118137
+ const row = new otui.BoxRenderable(r, { id: `cp-${d.name}-row`, width: "100%", flexDirection: "row" });
118138
+ const labelText = new otui.TextRenderable(r, {
118139
+ id: `cp-${d.name}-label-t`,
118140
+ content: otui.t`${labelOf(d)} ${dimChunk(otui, noteOf(d))}`
118141
+ });
118142
+ const label = new otui.BoxRenderable(r, {
118143
+ id: `cp-${d.name}-label`,
118144
+ flexGrow: 1,
118145
+ minWidth: 0,
118146
+ onMouseDown: (event) => {
118147
+ event.stopPropagation();
118148
+ clearPending();
118149
+ selectedIndex = i;
118150
+ selectedAction = "label";
118151
+ finish(d);
118152
+ }
118153
+ });
118154
+ label.add(labelText);
118155
+ row.add(label);
118156
+ const test = smallActionButton(otui, r, "Test", `cp-test-${d.name}`, getTheme().focus, () => {
118157
+ clearPending();
118158
+ selectedIndex = i;
118159
+ selectedAction = "test";
118160
+ applyHighlight();
118161
+ runTest(d.name);
118162
+ });
118163
+ const disconnect = smallActionButton(otui, r, "Disconnect", `cp-disc-${d.name}`, getTheme().error, () => {
118164
+ selectedIndex = i;
118165
+ selectedAction = "disconnect";
118166
+ applyHighlight();
118167
+ armOrConfirmDisconnect(d.name);
118168
+ });
118169
+ row.add(test.box);
118170
+ row.add(disconnect.box);
118171
+ outer.add(row);
118172
+ const status = new otui.TextRenderable(r, { id: `cp-${d.name}-status`, content: "", marginLeft: 1 });
118173
+ outer.add(status);
118174
+ body.add(outer);
118175
+ rowBlocks.push({ name: d.name, outer, label, labelText, status, test, disconnect });
118176
+ }
118177
+ applyHighlight();
118178
+ }
118179
+ const unsub = onKeypress4(r, (key) => {
118180
+ if (resolved)
118181
+ return;
118182
+ if (key.name === "up") {
118183
+ clearPending();
118184
+ selectedIndex = stepQueueNavIndex(selectedIndex, rows.length, "up");
118185
+ applyHighlight();
118186
+ key.preventDefault();
118187
+ key.stopPropagation();
118188
+ return;
118189
+ }
118190
+ if (key.name === "down") {
118191
+ clearPending();
118192
+ selectedIndex = stepQueueNavIndex(selectedIndex, rows.length, "down");
118193
+ applyHighlight();
118194
+ key.preventDefault();
118195
+ key.stopPropagation();
118196
+ return;
118197
+ }
118198
+ if (key.name === "left") {
118199
+ clearPending();
118200
+ selectedAction = stepConnectNavAction(selectedAction, "left");
118201
+ applyHighlight();
118202
+ key.preventDefault();
118203
+ key.stopPropagation();
118204
+ return;
118205
+ }
118206
+ if (key.name === "right") {
118207
+ clearPending();
118208
+ selectedAction = stepConnectNavAction(selectedAction, "right");
118209
+ applyHighlight();
118210
+ key.preventDefault();
118211
+ key.stopPropagation();
118212
+ return;
118213
+ }
118214
+ if (key.name === "return" || key.name === "linefeed" || key.name === "kpenter") {
118215
+ const row = rows[selectedIndex];
118216
+ if (row === undefined)
118217
+ return;
118218
+ if (selectedAction === "label") {
118219
+ clearPending();
118220
+ finish(row);
118221
+ } else if (selectedAction === "test") {
118222
+ clearPending();
118223
+ runTest(row.name);
118224
+ } else {
118225
+ armOrConfirmDisconnect(row.name);
118226
+ }
118227
+ key.preventDefault();
118228
+ key.stopPropagation();
118229
+ }
118230
+ });
118231
+ paint();
118232
+ });
118233
+ }
117694
118234
  function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
117695
118235
  return new Promise((resolve) => {
117696
118236
  if (detected.length === 0) {
@@ -117709,7 +118249,12 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
117709
118249
  }
117710
118250
  const allCandidates = options.onlyConnected ? candidates : [...candidates, CUSTOM_ADD_ENTRY];
117711
118251
  while (true) {
117712
- const prov = await pickProviderStep(otui, rOrChrome, allCandidates);
118252
+ const prov = options.onlyConnected ? await pickConnectedProviderStep(otui, rOrChrome, allCandidates, {
118253
+ ...options.fetch !== undefined ? { fetch: options.fetch } : {},
118254
+ ...options.env !== undefined ? { env: options.env } : {},
118255
+ ...options.configDir !== undefined ? { configDir: options.configDir } : {},
118256
+ ...options.onDisconnected !== undefined ? { onDisconnected: options.onDisconnected } : {}
118257
+ }) : await pickProviderStep(otui, rOrChrome, allCandidates);
117713
118258
  if (prov === undefined) {
117714
118259
  resolve(undefined);
117715
118260
  return;
@@ -119793,28 +120338,7 @@ Staying in the current session.
119793
120338
  let selectedQueueAction = "force";
119794
120339
  chrome.addOverlaySource(() => queueNavActive);
119795
120340
  let mainQueueBlocks = [];
119796
- const mainQueueButton = (label, id, color, onMouseDown) => {
119797
- const box = new otui.BoxRenderable(r, {
119798
- id,
119799
- flexShrink: 0,
119800
- marginLeft: 1,
119801
- paddingLeft: 1,
119802
- paddingRight: 1,
119803
- onMouseDown: (event) => {
119804
- event.stopPropagation();
119805
- onMouseDown();
119806
- }
119807
- });
119808
- const text = new otui.TextRenderable(r, { id: `${id}-t`, content: `[${label}]` });
119809
- text.fg = color;
119810
- box.add(text);
119811
- const setActive = (active) => {
119812
- box.backgroundColor = active ? getTheme().highlight : undefined;
119813
- text.content = active ? otui.t`${boldChunk(otui, `[${label}]`)}` : `[${label}]`;
119814
- text.fg = color;
119815
- };
119816
- return { box, setActive };
119817
- };
120341
+ const mainQueueButton = (label, id, color, onMouseDown) => smallActionButton(otui, r, label, id, color, onMouseDown);
119818
120342
  const applyQueueNavHighlight = () => {
119819
120343
  for (let i = 0;i < mainQueueBlocks.length; i++) {
119820
120344
  const entry = mainQueueBlocks[i];
@@ -120711,7 +121235,20 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
120711
121235
  if (command.name === "/connect" || command.name === "/provider") {
120712
121236
  (async () => {
120713
121237
  const detected = opts.redetect !== undefined ? await opts.redetect() : opts.detected;
120714
- const ns = await chrome.withOverlay(() => command.name === "/connect" ? selectProviderModelInTui(otui, chrome, detected, { onlyConnected: true, env: process.env }) : selectProviderModelInTui(otui, chrome, detected));
121238
+ const ns = await chrome.withOverlay(() => command.name === "/connect" ? selectProviderModelInTui(otui, chrome, detected, {
121239
+ onlyConnected: true,
121240
+ env: process.env,
121241
+ onDisconnected: (name, sharedWith) => {
121242
+ if (sharedWith.length > 0) {
121243
+ io.onSystem?.(`\u25C7 ${name} disconnected \u2014 this also disconnected: ${sharedWith.join(", ")}.
121244
+ `);
121245
+ }
121246
+ if (name === currentSel.provider) {
121247
+ io.onSystem?.(`\u25C7 ${name} disconnected \u2014 this session keeps its already-loaded credential until you /connect another provider or restart.
121248
+ `);
121249
+ }
121250
+ }
121251
+ }) : selectProviderModelInTui(otui, chrome, detected));
120715
121252
  if (ns !== undefined) {
120716
121253
  await switchTo(ns);
120717
121254
  } else if (command.name === "/connect") {
@@ -121055,6 +121592,7 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
121055
121592
 
121056
121593
  // src/tui/chat-shell.ts
121057
121594
  init_shell_config();
121595
+ init_md_blocks();
121058
121596
  var TURN_SEPARATOR = `
121059
121597
 
121060
121598
  `;
@@ -121452,6 +121990,8 @@ async function launchTuiChatShell(opts) {
121452
121990
  init_shell_config();
121453
121991
  init_patch_risk();
121454
121992
  init_permission_mode();
121993
+ init_ui();
121994
+ init_md_blocks();
121455
121995
  init_agent();
121456
121996
  init_slate_lifecycle();
121457
121997
  init_slate_lifecycle();
@@ -126528,9 +127068,11 @@ most one CLI-origin pause lease can be active in the clone at a time.
126528
127068
 
126529
127069
  // src/commands/modules.ts
126530
127070
  init_fs();
127071
+ init_prompt();
126531
127072
  import { readFile as readFile110 } from "fs/promises";
126532
127073
  import { stdin } from "process";
126533
127074
  import path224 from "path";
127075
+ init_ui();
126534
127076
  var MODULES = [
126535
127077
  { name: "gdgraph", flag: "--no-gdgraph", desc: "code graph, symbols, affected context", defaultEnabled: true },
126536
127078
  { name: "gdctx", flag: "--no-gdctx", desc: "token-aware command/read output", defaultEnabled: true },
@@ -127777,6 +128319,7 @@ function assembleSubmitTurn(profile, dir, seams = {}) {
127777
128319
  }
127778
128320
 
127779
128321
  // src/commands/serve.ts
128322
+ init_ui();
127780
128323
  function parseArgs3(args, valueFlags, booleanFlags) {
127781
128324
  const values = new Map;
127782
128325
  const flags = new Set;
@@ -128254,6 +128797,7 @@ init_templates2();
128254
128797
  init_templates();
128255
128798
  init_fs();
128256
128799
  init_git_hooks();
128800
+ init_ui();
128257
128801
  init_templates3();
128258
128802
  async function updateCommand(args = []) {
128259
128803
  const options = parseUpdateArgs(args);
@@ -129495,6 +130039,7 @@ function printHelp22() {
129495
130039
  import { spawn as spawn8 } from "child_process";
129496
130040
  import path229 from "path";
129497
130041
  init_args();
130042
+ init_ui();
129498
130043
  async function dashboardCommand(args = []) {
129499
130044
  const options = parseOptions(args);
129500
130045
  const subcommand = options.positionals[0];
@@ -129811,6 +130356,7 @@ ${settled.stderr}` };
129811
130356
 
129812
130357
  // src/commands/agents-external.ts
129813
130358
  init_args();
130359
+ init_ui();
129814
130360
  function describeAvailability(entry, availability) {
129815
130361
  if (availability.state === "not-probed") {
129816
130362
  return "not probed \u2014 run `keryx agents external probe " + entry.id + "`";
@@ -130174,6 +130720,7 @@ function reduceAgents(events) {
130174
130720
 
130175
130721
  // src/commands/agents.ts
130176
130722
  init_args();
130723
+ init_ui();
130177
130724
  var RUNTIME_USAGE = "<claude|opencode|zcode|codex|antigravity|all>";
130178
130725
  async function agentsCommand(args = []) {
130179
130726
  const subcommand = args[0];
@@ -133407,6 +133954,8 @@ var USAGE_BODY = `Usage:
133407
133954
  keryx sync install-hooks | uninstall-hooks
133408
133955
  keryx providers list [--json]
133409
133956
  keryx providers cross-family [--opt-in] [--json]
133957
+ keryx providers test <name> [--json]
133958
+ keryx providers remove <name> [--yes] [--json]
133410
133959
  keryx auth list [--json]
133411
133960
  keryx auth login <provider>
133412
133961
  keryx auth logout <provider>