@mrciphersmith/keryx 0.3.12 → 0.3.13

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 (2) hide show
  1. package/dist/cli.js +296 -290
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -316,6 +316,26 @@ var init_safe_exec = __esm(() => {
316
316
  FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT", "SIGWINCH"];
317
317
  });
318
318
 
319
+ // src/lib/oauth/openai-token.ts
320
+ function codexTokenMetadata(token) {
321
+ if (typeof token !== "string" || token.length > 1e5 || token.split(".").length !== 3)
322
+ return {};
323
+ try {
324
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString("utf8"));
325
+ if (typeof payload !== "object" || payload === null)
326
+ return {};
327
+ const claims = payload;
328
+ const auth = claims["https://api.openai.com/auth"];
329
+ const accountId = typeof auth === "object" && auth !== null ? auth.chatgpt_account_id : undefined;
330
+ return {
331
+ ...typeof accountId === "string" && accountId.length > 0 && accountId.trim() === accountId && !/[\r\n]/.test(accountId) ? { accountId } : {},
332
+ ...typeof claims.exp === "number" && Number.isFinite(claims.exp) && claims.exp > 0 ? { expiresAt: claims.exp * 1000 } : {}
333
+ };
334
+ } catch {
335
+ return {};
336
+ }
337
+ }
338
+
319
339
  // src/lib/config-dir.ts
320
340
  import {
321
341
  appendFileSync,
@@ -465,9 +485,258 @@ var init_config_dir = __esm(() => {
465
485
  MAX_TURN_FILE_BYTES = MAX_CONTENT_FILE_BYTES;
466
486
  });
467
487
 
468
- // src/lib/provider-config.ts
488
+ // src/lib/shell-config.ts
469
489
  import { existsSync } from "fs";
470
490
  import path3 from "path";
491
+ function noteSavedCredentialEnv(keys) {
492
+ for (const key of keys)
493
+ savedCredentialKeys.add(key);
494
+ }
495
+ function savedCredentialEnvKeys() {
496
+ return savedCredentialKeys;
497
+ }
498
+ function shellConfigPath(dir) {
499
+ return path3.join(keryxConfigDir(dir), "auth.json");
500
+ }
501
+ function loadShellConfig(dir) {
502
+ try {
503
+ const file = shellConfigPath(dir);
504
+ if (!existsSync(file)) {
505
+ return {};
506
+ }
507
+ const read = readConfigFile(file);
508
+ if (!read.ok) {
509
+ return {};
510
+ }
511
+ const raw = JSON.parse(read.text);
512
+ return raw !== null && typeof raw === "object" ? raw : {};
513
+ } catch {
514
+ return {};
515
+ }
516
+ }
517
+ function saveShellConfig(patch, dir) {
518
+ try {
519
+ ensureKeryxConfigDir(dir);
520
+ const next = { ...loadShellConfig(dir), ...patch };
521
+ writeOwnerOnlyFileAtomic(shellConfigPath(dir), `${JSON.stringify(next, null, 2)}
522
+ `);
523
+ } catch {}
524
+ }
525
+ function saveApiKey(envKey, value, dir) {
526
+ const existing = loadShellConfig(dir).apiKeys ?? {};
527
+ saveShellConfig({ apiKeys: { ...existing, [envKey]: value } }, dir);
528
+ }
529
+ function saveProviderBaseUrl(provider, baseUrl, dir) {
530
+ const existing = loadShellConfig(dir).baseUrls ?? {};
531
+ saveShellConfig({ baseUrls: { ...existing, [provider]: baseUrl } }, dir);
532
+ }
533
+ function removeApiKey(envKey, dir) {
534
+ const existing = loadShellConfig(dir).apiKeys ?? {};
535
+ if (!(envKey in existing))
536
+ return;
537
+ const { [envKey]: _removed, ...rest } = existing;
538
+ saveShellConfig({ apiKeys: rest }, dir);
539
+ }
540
+ function removeProviderBaseUrl(provider, dir) {
541
+ const existing = loadShellConfig(dir).baseUrls ?? {};
542
+ if (!(provider in existing))
543
+ return;
544
+ const { [provider]: _removed, ...rest } = existing;
545
+ saveShellConfig({ baseUrls: rest }, dir);
546
+ }
547
+ function removeProviderModelParams(provider, dir) {
548
+ const existing = loadShellConfig(dir).modelParams ?? {};
549
+ if (!(provider in existing))
550
+ return;
551
+ const { [provider]: _removed, ...rest } = existing;
552
+ saveShellConfig({ modelParams: rest }, dir);
553
+ }
554
+ function envWithSavedApiKeys(env = process.env, dir) {
555
+ const merged = { ...env };
556
+ try {
557
+ const cfg = loadShellConfig(dir);
558
+ const keys = { ...cfg.apiKeys ?? {} };
559
+ if (typeof cfg.openrouterKey === "string" && cfg.openrouterKey.length > 0 && keys.OPENROUTER_API_KEY === undefined) {
560
+ keys.OPENROUTER_API_KEY = cfg.openrouterKey;
561
+ }
562
+ for (const [envKey, value] of Object.entries(keys)) {
563
+ if (typeof value !== "string" || value.length === 0) {
564
+ continue;
565
+ }
566
+ const current = merged[envKey];
567
+ if (current === undefined || current.length === 0) {
568
+ merged[envKey] = value;
569
+ }
570
+ }
571
+ const grants = cfg.oauthGrants ?? {};
572
+ const grokAccess = grants.grok?.access;
573
+ if (typeof grokAccess === "string" && grokAccess.length > 0 && (merged.XAI_API_KEY === undefined || merged.XAI_API_KEY.length === 0)) {
574
+ merged.XAI_API_KEY = grokAccess;
575
+ }
576
+ const copilotAccess = grants["github-copilot"]?.access;
577
+ if (typeof copilotAccess === "string" && copilotAccess.length > 0 && (merged.GITHUB_COPILOT_TOKEN === undefined || merged.GITHUB_COPILOT_TOKEN.length === 0)) {
578
+ merged.GITHUB_COPILOT_TOKEN = copilotAccess;
579
+ }
580
+ } catch {}
581
+ return merged;
582
+ }
583
+ function declaredCredentialEnvKeys(dir) {
584
+ try {
585
+ return new Set(Object.keys(envWithSavedApiKeys({}, dir)));
586
+ } catch {
587
+ return new Set;
588
+ }
589
+ }
590
+ function applySavedApiKeys(dir) {
591
+ const applied = [];
592
+ try {
593
+ const before = new Set(Object.entries(process.env).filter(([, v]) => typeof v === "string" && v.length > 0).map(([k]) => k));
594
+ const merged = envWithSavedApiKeys(process.env, dir);
595
+ for (const [envKey, value] of Object.entries(merged)) {
596
+ if (typeof value !== "string" || value.length === 0) {
597
+ continue;
598
+ }
599
+ const current = process.env[envKey];
600
+ if (current === undefined || current.length === 0) {
601
+ process.env[envKey] = value;
602
+ if (!before.has(envKey)) {
603
+ applied.push(envKey);
604
+ }
605
+ }
606
+ }
607
+ } catch {}
608
+ noteSavedCredentialEnv(applied);
609
+ return applied;
610
+ }
611
+ var savedCredentialKeys;
612
+ var init_shell_config = __esm(() => {
613
+ init_config_dir();
614
+ savedCredentialKeys = new Set;
615
+ });
616
+
617
+ // src/lib/oauth/grants.ts
618
+ function loadOAuthGrant(provider, dir) {
619
+ const grants = loadShellConfig(dir).oauthGrants;
620
+ if (grants === undefined) {
621
+ return;
622
+ }
623
+ const grant = grants[provider] ?? (provider === "openai-codex" ? grants.openai : undefined);
624
+ if (grant === undefined || typeof grant.access !== "string" || grant.access.length === 0) {
625
+ return;
626
+ }
627
+ if (grant.method !== "device-code" && grant.method !== "oauth-pkce-loopback") {
628
+ return;
629
+ }
630
+ if (provider === "openai-codex" || provider === "openai") {
631
+ const metadata = codexTokenMetadata(grant.access);
632
+ return {
633
+ ...grant,
634
+ ...grant.accountId === undefined && metadata.accountId !== undefined ? { accountId: metadata.accountId } : {},
635
+ ...grant.expires === undefined && metadata.expiresAt !== undefined ? { expires: metadata.expiresAt } : {}
636
+ };
637
+ }
638
+ return grant;
639
+ }
640
+ function saveOAuthGrant(provider, grant, dir) {
641
+ const existing = loadShellConfig(dir).oauthGrants ?? {};
642
+ saveShellConfig({ oauthGrants: { ...existing, [provider]: grant } }, dir);
643
+ }
644
+ function deleteOAuthGrant(provider, dir) {
645
+ const existing = { ...loadShellConfig(dir).oauthGrants ?? {} };
646
+ if (existing[provider] === undefined && !(provider === "openai-codex" && existing.openai !== undefined)) {
647
+ return;
648
+ }
649
+ delete existing[provider];
650
+ if (provider === "openai-codex")
651
+ delete existing.openai;
652
+ saveShellConfig({ oauthGrants: existing }, dir);
653
+ }
654
+ function listOAuthGrantProviders(dir) {
655
+ const grants = loadShellConfig(dir).oauthGrants ?? {};
656
+ return [...new Set(Object.keys(grants).map((name) => name === "openai" ? "openai-codex" : name))].filter((name) => loadOAuthGrant(name, dir) !== undefined).sort();
657
+ }
658
+ function oauthGrantStatus(provider, dir, now = Date.now) {
659
+ const grant = loadOAuthGrant(provider, dir);
660
+ if (grant === undefined) {
661
+ return;
662
+ }
663
+ const expired = grant.expires !== undefined && grant.expires - now() <= 0;
664
+ return {
665
+ provider,
666
+ method: grant.method,
667
+ state: expired ? "expired" : "active",
668
+ ...grant.expires !== undefined ? { expiresAt: new Date(grant.expires).toISOString() } : {},
669
+ refreshable: typeof grant.refresh === "string" && grant.refresh.length > 0
670
+ };
671
+ }
672
+ function oauthAccessToken(provider, dir) {
673
+ const grant = loadOAuthGrant(provider, dir);
674
+ return grant?.access;
675
+ }
676
+ function grantNeedsRefresh(grant, now = Date.now) {
677
+ if (grant.refresh === undefined || grant.refresh.length === 0) {
678
+ return false;
679
+ }
680
+ if (grant.expires === undefined) {
681
+ return false;
682
+ }
683
+ return grant.expires - now() <= ACCESS_TOKEN_REFRESH_SKEW_MS;
684
+ }
685
+ function oauthEnvKeyFor(provider) {
686
+ return OAUTH_ENV_KEYS.find((entry) => entry.provider === provider)?.envKey;
687
+ }
688
+ function envWithOAuthAccess(env = process.env, dir) {
689
+ const merged = { ...env };
690
+ for (const { provider, envKey } of OAUTH_ENV_KEYS) {
691
+ const current = merged[envKey];
692
+ if (typeof current === "string" && current.length > 0) {
693
+ continue;
694
+ }
695
+ const access = oauthAccessToken(provider, dir);
696
+ if (access !== undefined) {
697
+ merged[envKey] = access;
698
+ }
699
+ }
700
+ return merged;
701
+ }
702
+ function applyOAuthAccessToEnv(dir) {
703
+ const applied = [];
704
+ const merged = envWithOAuthAccess(process.env, dir);
705
+ for (const { envKey } of OAUTH_ENV_KEYS) {
706
+ const value = merged[envKey];
707
+ const current = process.env[envKey];
708
+ if (typeof value === "string" && value.length > 0 && (current === undefined || current.length === 0)) {
709
+ process.env[envKey] = value;
710
+ applied.push(envKey);
711
+ }
712
+ }
713
+ noteSavedCredentialEnv(applied);
714
+ return applied;
715
+ }
716
+ function grantFromTokens(method, tokens, now = Date.now) {
717
+ const obtainedAt = new Date(now()).toISOString();
718
+ return {
719
+ method,
720
+ access: tokens.accessToken,
721
+ ...tokens.refreshToken !== undefined ? { refresh: tokens.refreshToken } : {},
722
+ ...tokens.expiresInSeconds !== undefined ? { expires: now() + tokens.expiresInSeconds * 1000 } : {},
723
+ ...tokens.accountId !== undefined ? { accountId: tokens.accountId } : {},
724
+ ...tokens.expiresInSeconds === undefined && tokens.expiresAt !== undefined ? { expires: tokens.expiresAt } : {},
725
+ obtainedAt
726
+ };
727
+ }
728
+ var ACCESS_TOKEN_REFRESH_SKEW_MS = 120000, OAUTH_ENV_KEYS;
729
+ var init_grants = __esm(() => {
730
+ init_shell_config();
731
+ OAUTH_ENV_KEYS = [
732
+ { provider: "grok", envKey: "XAI_API_KEY" },
733
+ { provider: "github-copilot", envKey: "GITHUB_COPILOT_TOKEN" }
734
+ ];
735
+ });
736
+
737
+ // src/lib/provider-config.ts
738
+ import { existsSync as existsSync2 } from "fs";
739
+ import path4 from "path";
471
740
  function isValidReasoningConfig(value) {
472
741
  if (value === undefined)
473
742
  return true;
@@ -486,11 +755,11 @@ function isValidReasoningConfig(value) {
486
755
  return true;
487
756
  }
488
757
  function llmProvidersConfigPath(dir) {
489
- return path3.join(keryxConfigDir(dir), "llm-providers.json");
758
+ return path4.join(keryxConfigDir(dir), "llm-providers.json");
490
759
  }
491
760
  function readJson(file) {
492
761
  try {
493
- if (!existsSync(file))
762
+ if (!existsSync2(file))
494
763
  return;
495
764
  const read = readConfigFile(file);
496
765
  return read.ok ? JSON.parse(read.text) : undefined;
@@ -779,7 +1048,7 @@ import {
779
1048
  } from "fs";
780
1049
  import { access, chmod, mkdir, readFile, rename, rm, stat, utimes, writeFile } from "fs/promises";
781
1050
  import { hostname } from "os";
782
- import path4 from "path";
1051
+ import path5 from "path";
783
1052
  async function pathExists(filePath) {
784
1053
  try {
785
1054
  await access(filePath);
@@ -795,13 +1064,13 @@ function toPosix(value) {
795
1064
  return value.replace(/\\/g, "/");
796
1065
  }
797
1066
  function isPathInside(root, candidate) {
798
- const relative = path4.relative(path4.resolve(root), path4.resolve(candidate));
799
- return relative === "" || relative.length > 0 && !relative.startsWith("..") && !path4.isAbsolute(relative);
1067
+ const relative = path5.relative(path5.resolve(root), path5.resolve(candidate));
1068
+ return relative === "" || relative.length > 0 && !relative.startsWith("..") && !path5.isAbsolute(relative);
800
1069
  }
801
1070
  async function writeFileAtomic(filePath, content, options = {}) {
802
- const dir = path4.dirname(filePath);
1071
+ const dir = path5.dirname(filePath);
803
1072
  await mkdir(dir, { recursive: true });
804
- const tmp = path4.join(dir, `.${path4.basename(filePath)}.${process.pid}.${Date.now()}.${randomUUID2()}.tmp`);
1073
+ const tmp = path5.join(dir, `.${path5.basename(filePath)}.${process.pid}.${Date.now()}.${randomUUID2()}.tmp`);
805
1074
  try {
806
1075
  if (options.mode === undefined) {
807
1076
  await writeFile(tmp, content, "utf8");
@@ -823,8 +1092,8 @@ async function withFileLock(lockPath, fn, options = {}) {
823
1092
  const heartbeatMs = options.heartbeatMs ?? Math.max(100, Math.floor(staleMs / 3));
824
1093
  const startedAt = Date.now();
825
1094
  const owner = { pid: process.pid, token: randomUUID2() };
826
- const ownerPath = path4.join(lockPath, "owner.json");
827
- await mkdir(path4.dirname(lockPath), { recursive: true });
1095
+ const ownerPath = path5.join(lockPath, "owner.json");
1096
+ await mkdir(path5.dirname(lockPath), { recursive: true });
828
1097
  while (true) {
829
1098
  try {
830
1099
  await mkdir(lockPath);
@@ -869,7 +1138,7 @@ async function isLockHeld(lockPath, staleMs = DEFAULT_LOCK_STALE_MS) {
869
1138
  const stats = await stat(lockPath);
870
1139
  if (Date.now() - stats.mtimeMs <= staleMs)
871
1140
  return true;
872
- const owner = await readLockOwner(path4.join(lockPath, "owner.json"));
1141
+ const owner = await readLockOwner(path5.join(lockPath, "owner.json"));
873
1142
  return owner !== undefined && processIsAlive(owner.pid);
874
1143
  } catch {
875
1144
  return false;
@@ -882,7 +1151,7 @@ async function isSameEmptyOrOwnerlessDir(dir, ino) {
882
1151
  try {
883
1152
  if ((await stat(dir)).ino !== ino)
884
1153
  return false;
885
- return await readLockOwner(path4.join(dir, "owner.json")) === undefined;
1154
+ return await readLockOwner(path5.join(dir, "owner.json")) === undefined;
886
1155
  } catch {
887
1156
  return false;
888
1157
  }
@@ -892,7 +1161,7 @@ async function removeStaleLock(lockPath, staleMs, raceHook) {
892
1161
  const stats = await stat(lockPath);
893
1162
  if (Date.now() - stats.mtimeMs <= staleMs)
894
1163
  return;
895
- const owner = await readLockOwner(path4.join(lockPath, "owner.json"));
1164
+ const owner = await readLockOwner(path5.join(lockPath, "owner.json"));
896
1165
  if (owner && processIsAlive(owner.pid))
897
1166
  return;
898
1167
  await raceHook?.("stale-judged");
@@ -900,7 +1169,7 @@ async function removeStaleLock(lockPath, staleMs, raceHook) {
900
1169
  await rename(lockPath, stalePath);
901
1170
  await raceHook?.("moved-aside");
902
1171
  const movedIno = (await stat(stalePath)).ino;
903
- const moved = await readLockOwner(path4.join(stalePath, "owner.json"));
1172
+ const moved = await readLockOwner(path5.join(stalePath, "owner.json"));
904
1173
  if (movedIno !== stats.ino || moved?.token !== owner?.token) {
905
1174
  await rename(stalePath, lockPath).catch(() => {});
906
1175
  await raceHook?.("restored");
@@ -933,7 +1202,7 @@ function delay(ms) {
933
1202
  }
934
1203
  function readLeaseOwnerSync(lockPath) {
935
1204
  try {
936
- const value = JSON.parse(readFileSync2(path4.join(lockPath, LEASE_OWNER_FILE), "utf8"));
1205
+ const value = JSON.parse(readFileSync2(path5.join(lockPath, LEASE_OWNER_FILE), "utf8"));
937
1206
  if (value === null || typeof value !== "object")
938
1207
  return;
939
1208
  const record = value;
@@ -975,7 +1244,7 @@ function reclaimStaleLeaseSync(lockPath, owner, opts) {
975
1244
  }
976
1245
  function acquireOrReclaim(lockPath, owner, opts, takeStale) {
977
1246
  const now = opts.now ?? Date.now;
978
- mkdirSync2(path4.dirname(lockPath), { recursive: true, mode: 448 });
1247
+ mkdirSync2(path5.dirname(lockPath), { recursive: true, mode: 448 });
979
1248
  for (let attempt = 0;attempt < MAX_LEASE_ATTEMPTS; attempt += 1) {
980
1249
  const createdIno = tryCreateLease(lockPath, owner, opts.raceHook);
981
1250
  if (createdIno !== undefined) {
@@ -1013,7 +1282,7 @@ function tryCreateLease(lockPath, owner, raceHook) {
1013
1282
  throw error;
1014
1283
  }
1015
1284
  raceHook?.("create");
1016
- const ownerPath = path4.join(lockPath, LEASE_OWNER_FILE);
1285
+ const ownerPath = path5.join(lockPath, LEASE_OWNER_FILE);
1017
1286
  try {
1018
1287
  chmodSync2(lockPath, 448);
1019
1288
  writeFileSync2(ownerPath, JSON.stringify(owner), { encoding: "utf8", flag: "wx", mode: 384 });
@@ -1063,7 +1332,7 @@ function removeLeaseDirIfUnchanged(lockPath, expectedToken) {
1063
1332
  function createLeaseHandle(lockPath, initial, createdIno, now, raceHook) {
1064
1333
  let current = { ...initial };
1065
1334
  let released = false;
1066
- const ownerPath = path4.join(lockPath, LEASE_OWNER_FILE);
1335
+ const ownerPath = path5.join(lockPath, LEASE_OWNER_FILE);
1067
1336
  const ownership = () => {
1068
1337
  let ino;
1069
1338
  try {
@@ -1089,7 +1358,7 @@ function createLeaseHandle(lockPath, initial, createdIno, now, raceHook) {
1089
1358
  return false;
1090
1359
  raceHook?.("refresh");
1091
1360
  const next = { ...current, ...patch, token: initial.token, heartbeatAt: new Date(now()).toISOString() };
1092
- const tmp = path4.join(lockPath, `.${LEASE_OWNER_FILE}.${randomUUID2()}.tmp`);
1361
+ const tmp = path5.join(lockPath, `.${LEASE_OWNER_FILE}.${randomUUID2()}.tmp`);
1093
1362
  try {
1094
1363
  writeFileSync2(tmp, JSON.stringify(next), { encoding: "utf8", flag: "wx", mode: 384 });
1095
1364
  if (!holds()) {
@@ -1124,135 +1393,6 @@ function createLeaseHandle(lockPath, initial, createdIno, now, raceHook) {
1124
1393
  var DEFAULT_LOCK_STALE_MS = 30000, LEASE_OWNER_FILE = "owner.json", MAX_LEASE_ATTEMPTS = 3;
1125
1394
  var init_fs = () => {};
1126
1395
 
1127
- // src/lib/shell-config.ts
1128
- import { existsSync as existsSync2 } from "fs";
1129
- import path5 from "path";
1130
- function noteSavedCredentialEnv(keys) {
1131
- for (const key of keys)
1132
- savedCredentialKeys.add(key);
1133
- }
1134
- function savedCredentialEnvKeys() {
1135
- return savedCredentialKeys;
1136
- }
1137
- function shellConfigPath(dir) {
1138
- return path5.join(keryxConfigDir(dir), "auth.json");
1139
- }
1140
- function loadShellConfig(dir) {
1141
- try {
1142
- const file = shellConfigPath(dir);
1143
- if (!existsSync2(file)) {
1144
- return {};
1145
- }
1146
- const read = readConfigFile(file);
1147
- if (!read.ok) {
1148
- return {};
1149
- }
1150
- const raw = JSON.parse(read.text);
1151
- return raw !== null && typeof raw === "object" ? raw : {};
1152
- } catch {
1153
- return {};
1154
- }
1155
- }
1156
- function saveShellConfig(patch, dir) {
1157
- try {
1158
- ensureKeryxConfigDir(dir);
1159
- const next = { ...loadShellConfig(dir), ...patch };
1160
- writeOwnerOnlyFileAtomic(shellConfigPath(dir), `${JSON.stringify(next, null, 2)}
1161
- `);
1162
- } catch {}
1163
- }
1164
- function saveApiKey(envKey, value, dir) {
1165
- const existing = loadShellConfig(dir).apiKeys ?? {};
1166
- saveShellConfig({ apiKeys: { ...existing, [envKey]: value } }, dir);
1167
- }
1168
- function saveProviderBaseUrl(provider, baseUrl, dir) {
1169
- const existing = loadShellConfig(dir).baseUrls ?? {};
1170
- saveShellConfig({ baseUrls: { ...existing, [provider]: baseUrl } }, dir);
1171
- }
1172
- function removeApiKey(envKey, dir) {
1173
- const existing = loadShellConfig(dir).apiKeys ?? {};
1174
- if (!(envKey in existing))
1175
- return;
1176
- const { [envKey]: _removed, ...rest } = existing;
1177
- saveShellConfig({ apiKeys: rest }, dir);
1178
- }
1179
- function removeProviderBaseUrl(provider, dir) {
1180
- const existing = loadShellConfig(dir).baseUrls ?? {};
1181
- if (!(provider in existing))
1182
- return;
1183
- const { [provider]: _removed, ...rest } = existing;
1184
- saveShellConfig({ baseUrls: rest }, dir);
1185
- }
1186
- function removeProviderModelParams(provider, dir) {
1187
- const existing = loadShellConfig(dir).modelParams ?? {};
1188
- if (!(provider in existing))
1189
- return;
1190
- const { [provider]: _removed, ...rest } = existing;
1191
- saveShellConfig({ modelParams: rest }, dir);
1192
- }
1193
- function envWithSavedApiKeys(env = process.env, dir) {
1194
- const merged = { ...env };
1195
- try {
1196
- const cfg = loadShellConfig(dir);
1197
- const keys = { ...cfg.apiKeys ?? {} };
1198
- if (typeof cfg.openrouterKey === "string" && cfg.openrouterKey.length > 0 && keys.OPENROUTER_API_KEY === undefined) {
1199
- keys.OPENROUTER_API_KEY = cfg.openrouterKey;
1200
- }
1201
- for (const [envKey, value] of Object.entries(keys)) {
1202
- if (typeof value !== "string" || value.length === 0) {
1203
- continue;
1204
- }
1205
- const current = merged[envKey];
1206
- if (current === undefined || current.length === 0) {
1207
- merged[envKey] = value;
1208
- }
1209
- }
1210
- const grants = cfg.oauthGrants ?? {};
1211
- const grokAccess = grants.grok?.access;
1212
- if (typeof grokAccess === "string" && grokAccess.length > 0 && (merged.XAI_API_KEY === undefined || merged.XAI_API_KEY.length === 0)) {
1213
- merged.XAI_API_KEY = grokAccess;
1214
- }
1215
- const copilotAccess = grants["github-copilot"]?.access;
1216
- if (typeof copilotAccess === "string" && copilotAccess.length > 0 && (merged.GITHUB_COPILOT_TOKEN === undefined || merged.GITHUB_COPILOT_TOKEN.length === 0)) {
1217
- merged.GITHUB_COPILOT_TOKEN = copilotAccess;
1218
- }
1219
- } catch {}
1220
- return merged;
1221
- }
1222
- function declaredCredentialEnvKeys(dir) {
1223
- try {
1224
- return new Set(Object.keys(envWithSavedApiKeys({}, dir)));
1225
- } catch {
1226
- return new Set;
1227
- }
1228
- }
1229
- function applySavedApiKeys(dir) {
1230
- const applied = [];
1231
- try {
1232
- const before = new Set(Object.entries(process.env).filter(([, v]) => typeof v === "string" && v.length > 0).map(([k]) => k));
1233
- const merged = envWithSavedApiKeys(process.env, dir);
1234
- for (const [envKey, value] of Object.entries(merged)) {
1235
- if (typeof value !== "string" || value.length === 0) {
1236
- continue;
1237
- }
1238
- const current = process.env[envKey];
1239
- if (current === undefined || current.length === 0) {
1240
- process.env[envKey] = value;
1241
- if (!before.has(envKey)) {
1242
- applied.push(envKey);
1243
- }
1244
- }
1245
- }
1246
- } catch {}
1247
- noteSavedCredentialEnv(applied);
1248
- return applied;
1249
- }
1250
- var savedCredentialKeys;
1251
- var init_shell_config = __esm(() => {
1252
- init_config_dir();
1253
- savedCredentialKeys = new Set;
1254
- });
1255
-
1256
1396
  // src/gdskills/model-tier.ts
1257
1397
  function isModelTier(value) {
1258
1398
  return typeof value === "string" && MODEL_TIERS.includes(value);
@@ -1996,146 +2136,6 @@ var init_model_profile = __esm(() => {
1996
2136
  STRENGTH_TIERS = new Set(["light", "standard", "deep"]);
1997
2137
  });
1998
2138
 
1999
- // src/lib/oauth/openai-token.ts
2000
- function codexTokenMetadata(token) {
2001
- if (typeof token !== "string" || token.length > 1e5 || token.split(".").length !== 3)
2002
- return {};
2003
- try {
2004
- const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString("utf8"));
2005
- if (typeof payload !== "object" || payload === null)
2006
- return {};
2007
- const claims = payload;
2008
- const auth = claims["https://api.openai.com/auth"];
2009
- const accountId = typeof auth === "object" && auth !== null ? auth.chatgpt_account_id : undefined;
2010
- return {
2011
- ...typeof accountId === "string" && accountId.length > 0 && accountId.trim() === accountId && !/[\r\n]/.test(accountId) ? { accountId } : {},
2012
- ...typeof claims.exp === "number" && Number.isFinite(claims.exp) && claims.exp > 0 ? { expiresAt: claims.exp * 1000 } : {}
2013
- };
2014
- } catch {
2015
- return {};
2016
- }
2017
- }
2018
-
2019
- // src/lib/oauth/grants.ts
2020
- function loadOAuthGrant(provider, dir) {
2021
- const grants = loadShellConfig(dir).oauthGrants;
2022
- if (grants === undefined) {
2023
- return;
2024
- }
2025
- const grant = grants[provider] ?? (provider === "openai-codex" ? grants.openai : undefined);
2026
- if (grant === undefined || typeof grant.access !== "string" || grant.access.length === 0) {
2027
- return;
2028
- }
2029
- if (grant.method !== "device-code" && grant.method !== "oauth-pkce-loopback") {
2030
- return;
2031
- }
2032
- if (provider === "openai-codex" || provider === "openai") {
2033
- const metadata = codexTokenMetadata(grant.access);
2034
- return {
2035
- ...grant,
2036
- ...grant.accountId === undefined && metadata.accountId !== undefined ? { accountId: metadata.accountId } : {},
2037
- ...grant.expires === undefined && metadata.expiresAt !== undefined ? { expires: metadata.expiresAt } : {}
2038
- };
2039
- }
2040
- return grant;
2041
- }
2042
- function saveOAuthGrant(provider, grant, dir) {
2043
- const existing = loadShellConfig(dir).oauthGrants ?? {};
2044
- saveShellConfig({ oauthGrants: { ...existing, [provider]: grant } }, dir);
2045
- }
2046
- function deleteOAuthGrant(provider, dir) {
2047
- const existing = { ...loadShellConfig(dir).oauthGrants ?? {} };
2048
- if (existing[provider] === undefined && !(provider === "openai-codex" && existing.openai !== undefined)) {
2049
- return;
2050
- }
2051
- delete existing[provider];
2052
- if (provider === "openai-codex")
2053
- delete existing.openai;
2054
- saveShellConfig({ oauthGrants: existing }, dir);
2055
- }
2056
- function listOAuthGrantProviders(dir) {
2057
- const grants = loadShellConfig(dir).oauthGrants ?? {};
2058
- return [...new Set(Object.keys(grants).map((name) => name === "openai" ? "openai-codex" : name))].filter((name) => loadOAuthGrant(name, dir) !== undefined).sort();
2059
- }
2060
- function oauthGrantStatus(provider, dir, now = Date.now) {
2061
- const grant = loadOAuthGrant(provider, dir);
2062
- if (grant === undefined) {
2063
- return;
2064
- }
2065
- const expired = grant.expires !== undefined && grant.expires - now() <= 0;
2066
- return {
2067
- provider,
2068
- method: grant.method,
2069
- state: expired ? "expired" : "active",
2070
- ...grant.expires !== undefined ? { expiresAt: new Date(grant.expires).toISOString() } : {},
2071
- refreshable: typeof grant.refresh === "string" && grant.refresh.length > 0
2072
- };
2073
- }
2074
- function oauthAccessToken(provider, dir) {
2075
- const grant = loadOAuthGrant(provider, dir);
2076
- return grant?.access;
2077
- }
2078
- function grantNeedsRefresh(grant, now = Date.now) {
2079
- if (grant.refresh === undefined || grant.refresh.length === 0) {
2080
- return false;
2081
- }
2082
- if (grant.expires === undefined) {
2083
- return false;
2084
- }
2085
- return grant.expires - now() <= ACCESS_TOKEN_REFRESH_SKEW_MS;
2086
- }
2087
- function oauthEnvKeyFor(provider) {
2088
- return OAUTH_ENV_KEYS.find((entry) => entry.provider === provider)?.envKey;
2089
- }
2090
- function envWithOAuthAccess(env = process.env, dir) {
2091
- const merged = { ...env };
2092
- for (const { provider, envKey } of OAUTH_ENV_KEYS) {
2093
- const current = merged[envKey];
2094
- if (typeof current === "string" && current.length > 0) {
2095
- continue;
2096
- }
2097
- const access = oauthAccessToken(provider, dir);
2098
- if (access !== undefined) {
2099
- merged[envKey] = access;
2100
- }
2101
- }
2102
- return merged;
2103
- }
2104
- function applyOAuthAccessToEnv(dir) {
2105
- const applied = [];
2106
- const merged = envWithOAuthAccess(process.env, dir);
2107
- for (const { envKey } of OAUTH_ENV_KEYS) {
2108
- const value = merged[envKey];
2109
- const current = process.env[envKey];
2110
- if (typeof value === "string" && value.length > 0 && (current === undefined || current.length === 0)) {
2111
- process.env[envKey] = value;
2112
- applied.push(envKey);
2113
- }
2114
- }
2115
- noteSavedCredentialEnv(applied);
2116
- return applied;
2117
- }
2118
- function grantFromTokens(method, tokens, now = Date.now) {
2119
- const obtainedAt = new Date(now()).toISOString();
2120
- return {
2121
- method,
2122
- access: tokens.accessToken,
2123
- ...tokens.refreshToken !== undefined ? { refresh: tokens.refreshToken } : {},
2124
- ...tokens.expiresInSeconds !== undefined ? { expires: now() + tokens.expiresInSeconds * 1000 } : {},
2125
- ...tokens.accountId !== undefined ? { accountId: tokens.accountId } : {},
2126
- ...tokens.expiresInSeconds === undefined && tokens.expiresAt !== undefined ? { expires: tokens.expiresAt } : {},
2127
- obtainedAt
2128
- };
2129
- }
2130
- var ACCESS_TOKEN_REFRESH_SKEW_MS = 120000, OAUTH_ENV_KEYS;
2131
- var init_grants = __esm(() => {
2132
- init_shell_config();
2133
- OAUTH_ENV_KEYS = [
2134
- { provider: "grok", envKey: "XAI_API_KEY" },
2135
- { provider: "github-copilot", envKey: "GITHUB_COPILOT_TOKEN" }
2136
- ];
2137
- });
2138
-
2139
2139
  // src/lib/oauth/device-code.ts
2140
2140
  function requestInit(base, signal) {
2141
2141
  return signal === undefined ? base : { ...base, signal };
@@ -17628,7 +17628,7 @@ var package_default;
17628
17628
  var init_package = __esm(() => {
17629
17629
  package_default = {
17630
17630
  name: "@mrciphersmith/keryx",
17631
- version: "0.3.12",
17631
+ version: "0.3.13",
17632
17632
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
17633
17633
  private: false,
17634
17634
  publishConfig: {
@@ -31215,7 +31215,10 @@ function defaultModelFor(provider) {
31215
31215
  const compat = providerByName(provider);
31216
31216
  return compat?.models[0] ?? "unknown";
31217
31217
  }
31218
- function hasCredential(provider, env) {
31218
+ function hasCredential(provider, env, configDir) {
31219
+ if (provider === "openai-codex") {
31220
+ return loadOAuthGrant(provider, configDir) !== undefined;
31221
+ }
31219
31222
  if (provider === "ollama") {
31220
31223
  return true;
31221
31224
  }
@@ -31338,6 +31341,7 @@ async function runModelTurn(input) {
31338
31341
  }
31339
31342
  var DEFAULT_PROVIDER = "anthropic", DEFAULT_MODELS;
31340
31343
  var init_single_turn = __esm(() => {
31344
+ init_grants();
31341
31345
  init_make_provider();
31342
31346
  init_providers();
31343
31347
  init_shell_config();
@@ -83593,6 +83597,8 @@ async function runSections(args) {
83593
83597
  }
83594
83598
  }
83595
83599
  async function runEnrich(args) {
83600
+ await Promise.resolve().then(() => init_grants());
83601
+ applyOAuthAccessToEnv();
83596
83602
  await Promise.resolve().then(() => init_enrich2());
83597
83603
  const prompt = optionValue(args, "--prompt");
83598
83604
  const provider = optionValue(args, "--provider");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {