pennyrouter 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -5
- package/package.json +1 -1
- package/src/cli.js +84 -3
- package/src/harnesses/claude-code.js +68 -30
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ status checks, and uninstall metadata.
|
|
|
11
11
|
```bash
|
|
12
12
|
npx pennyrouter install
|
|
13
13
|
npx pennyrouter install --existing-account
|
|
14
|
+
npx pennyrouter auth anthropic
|
|
14
15
|
npx pennyrouter install --harness claude-code,cline,opencode,codex,windsurf,zed,codegpt,aider
|
|
15
16
|
npx pennyrouter disable
|
|
16
17
|
npx pennyrouter enable
|
|
@@ -34,12 +35,33 @@ detected configs start checked; press Enter to accept or type tool IDs to toggle
|
|
|
34
35
|
|
|
35
36
|
## Penny Models
|
|
36
37
|
|
|
37
|
-
Tools that expose a model picker get four curated choices
|
|
38
|
+
Tools that expose a model picker (e.g. Claude Code `/model`) get four curated choices. The three
|
|
39
|
+
named-model choices PIN the trunk to that exact model — you pick the model, PennyRouter still cuts
|
|
40
|
+
the bill via its side machinery (cache/compaction/swallowed background calls) without switching the
|
|
41
|
+
model you chose. "Penny Custom" is the dynamic option: PennyRouter cost-routes each turn.
|
|
38
42
|
|
|
39
|
-
- Penny
|
|
40
|
-
- Penny
|
|
41
|
-
- Penny
|
|
42
|
-
- Penny Custom: `pennyrouter/auto
|
|
43
|
+
- Penny Opus: pins `anthropic/claude-opus-4-8`
|
|
44
|
+
- Penny Sonnet: pins `anthropic/claude-sonnet-4-6`
|
|
45
|
+
- Penny Haiku: pins `anthropic/claude-haiku-4-5-20251001`
|
|
46
|
+
- Penny Custom: `pennyrouter/auto` — dynamic routing on the bundle/profile saved in the user's account
|
|
47
|
+
|
|
48
|
+
## Claude Pro/Max Subscription Auth
|
|
49
|
+
|
|
50
|
+
After installing PennyRouter for Claude Code, users with an Anthropic OAuth login can route
|
|
51
|
+
Anthropic-native Claude turns through their own Claude subscription token:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ant auth login
|
|
55
|
+
npx pennyrouter auth anthropic
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The command updates Claude Code so `x-api-key` carries the PennyRouter key and
|
|
59
|
+
`Authorization: Bearer ...` carries the Anthropic OAuth token. The gateway forwards that
|
|
60
|
+
bearer token to Anthropic with the required OAuth beta header and does not deduct
|
|
61
|
+
PennyRouter credit for those Anthropic-native upstream calls.
|
|
62
|
+
|
|
63
|
+
`ant auth print-credentials` refreshes the access token before the CLI stores it. If the
|
|
64
|
+
stored token expires during a long-running session, rerun `npx pennyrouter auth anthropic`.
|
|
43
65
|
|
|
44
66
|
## Agent Flow
|
|
45
67
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { emitKeypressEvents } from "node:readline";
|
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
5
5
|
import { aiderHarness } from "./harnesses/aider.js";
|
|
6
|
-
import { claudeCodeHarness } from "./harnesses/claude-code.js";
|
|
6
|
+
import { claudeCodeHarness, readClaudeCodePennyRouterKey } from "./harnesses/claude-code.js";
|
|
7
7
|
import { clineHarness } from "./harnesses/cline.js";
|
|
8
8
|
import { codeGptHarness } from "./harnesses/codegpt.js";
|
|
9
9
|
import { codexHarness } from "./harnesses/codex.js";
|
|
@@ -52,6 +52,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
if (command === "auth") {
|
|
56
|
+
await authProvider(flags);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
if (command === "uninstall") {
|
|
56
61
|
await uninstall(flags);
|
|
57
62
|
return;
|
|
@@ -79,6 +84,7 @@ function parseArgs(argv) {
|
|
|
79
84
|
const command = argv[0] && !argv[0].startsWith("-") ? argv[0] : "install";
|
|
80
85
|
const rest = command === argv[0] ? argv.slice(1) : argv;
|
|
81
86
|
const flags = {};
|
|
87
|
+
flags.args = [];
|
|
82
88
|
|
|
83
89
|
for (let i = 0; i < rest.length; i += 1) {
|
|
84
90
|
const arg = rest[i];
|
|
@@ -88,16 +94,86 @@ function parseArgs(argv) {
|
|
|
88
94
|
else if (arg === "--dry-run") flags.dryRun = true;
|
|
89
95
|
else if (arg === "--no-browser") flags.noBrowser = true;
|
|
90
96
|
else if (arg === "--existing-account") flags.existingAccount = true;
|
|
97
|
+
else if (arg === "--penny-key") flags.pennyKey = rest[++i] || "";
|
|
98
|
+
else if (arg.startsWith("--penny-key=")) flags.pennyKey = arg.slice("--penny-key=".length);
|
|
99
|
+
else if (arg === "--token") flags.token = rest[++i] || "";
|
|
100
|
+
else if (arg.startsWith("--token=")) flags.token = arg.slice("--token=".length);
|
|
91
101
|
else if (arg === "--harness") flags.harness = rest[++i] || "";
|
|
92
102
|
else if (arg.startsWith("--harness=")) flags.harness = arg.slice("--harness=".length);
|
|
93
103
|
else if (arg === "--app-url") flags.appUrl = rest[++i] || "";
|
|
94
104
|
else if (arg.startsWith("--app-url=")) flags.appUrl = arg.slice("--app-url=".length);
|
|
105
|
+
else if (arg === "--gateway-base-url") flags.gatewayBaseUrl = rest[++i] || "";
|
|
106
|
+
else if (arg.startsWith("--gateway-base-url=")) flags.gatewayBaseUrl = arg.slice("--gateway-base-url=".length);
|
|
107
|
+
// --local: point the installed harness at a locally-running gateway (default
|
|
108
|
+
// http://localhost:8400), for developing/testing against your own gateway. Auth still goes
|
|
109
|
+
// to prod (pennyrouter.com) so you get a real key; only the harness's serving URL is local.
|
|
110
|
+
// `--local=<url>` overrides the port/host.
|
|
111
|
+
else if (arg === "--local") flags.gatewayBaseUrl = "http://localhost:8400";
|
|
112
|
+
else if (arg.startsWith("--local=")) flags.gatewayBaseUrl = arg.slice("--local=".length);
|
|
113
|
+
else if (!arg.startsWith("-")) flags.args.push(arg);
|
|
95
114
|
else throw new Error(`Unknown option "${arg}". Run "pennyrouter help".`);
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
return { command, flags };
|
|
99
118
|
}
|
|
100
119
|
|
|
120
|
+
async function authProvider(flags) {
|
|
121
|
+
const provider = (flags.args[0] || "anthropic").toLowerCase();
|
|
122
|
+
if (provider !== "anthropic" && provider !== "claude") {
|
|
123
|
+
throw new Error(`Unknown auth provider "${provider}". Try "pennyrouter auth anthropic".`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (flags.dryRun) {
|
|
127
|
+
console.log("Will configure Claude Code for PennyRouter + Anthropic OAuth dual auth.");
|
|
128
|
+
console.log("Dry run only. No files changed.");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const pennyKey = flags.pennyKey || await readClaudeCodePennyRouterKey();
|
|
133
|
+
if (!pennyKey) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"No PennyRouter key found in Claude Code settings. Run `pennyrouter install --harness claude-code` first, or pass `--penny-key pr-...`.",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const token = flags.token || readAnthropicOAuthToken();
|
|
140
|
+
if (!token) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"No Anthropic OAuth token found. Run `ant auth login`, then retry `pennyrouter auth anthropic`.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const gatewayBaseUrl = flags.gatewayBaseUrl || process.env.PENNYROUTER_GATEWAY_BASE_URL || "https://api.pennyrouter.com";
|
|
147
|
+
const record = await claudeCodeHarness.installAnthropicOAuth({
|
|
148
|
+
pennyKey,
|
|
149
|
+
anthropicAuthToken: token,
|
|
150
|
+
gatewayBaseUrl,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const manifest = await loadManifest();
|
|
154
|
+
upsertManifestRecord(manifest, {
|
|
155
|
+
...record,
|
|
156
|
+
anthropic_oauth: true,
|
|
157
|
+
token_source: flags.token ? "flag" : "ant",
|
|
158
|
+
});
|
|
159
|
+
await saveManifest(manifest);
|
|
160
|
+
|
|
161
|
+
console.log("Configured Claude Code for PennyRouter + Anthropic OAuth.");
|
|
162
|
+
console.log("Anthropic-native Claude turns will use your Claude subscription token upstream.");
|
|
163
|
+
console.log("If Claude auth expires, run `pennyrouter auth anthropic` again.");
|
|
164
|
+
console.log(`Config: ${record.config_path}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readAnthropicOAuthToken() {
|
|
168
|
+
if (process.env.ANTHROPIC_AUTH_TOKEN) return process.env.ANTHROPIC_AUTH_TOKEN.trim();
|
|
169
|
+
const result = spawnSync("ant", ["auth", "print-credentials", "--access-token"], {
|
|
170
|
+
encoding: "utf8",
|
|
171
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
172
|
+
});
|
|
173
|
+
if (result.status !== 0) return "";
|
|
174
|
+
return String(result.stdout || "").trim();
|
|
175
|
+
}
|
|
176
|
+
|
|
101
177
|
async function install(flags) {
|
|
102
178
|
const selected = await selectHarnesses(flags);
|
|
103
179
|
if (selected.length === 0) {
|
|
@@ -112,6 +188,7 @@ async function install(flags) {
|
|
|
112
188
|
for (const harness of selected) {
|
|
113
189
|
console.log(`Will install on ${harness.name}${installNoteForHarness(harness.id)}.`);
|
|
114
190
|
}
|
|
191
|
+
if (flags.gatewayBaseUrl) console.log(`Serving gateway: ${flags.gatewayBaseUrl}`);
|
|
115
192
|
console.log("Dry run only. No files changed.");
|
|
116
193
|
return;
|
|
117
194
|
}
|
|
@@ -144,7 +221,9 @@ async function install(flags) {
|
|
|
144
221
|
const plan = await harness.planInstall();
|
|
145
222
|
const record = await harness.install({
|
|
146
223
|
apiKey: claim.api_key,
|
|
147
|
-
|
|
224
|
+
// An explicit --local / --gateway-base-url overrides the URL the server hands back
|
|
225
|
+
// (which points at prod) — so a --local install serves through the local gateway.
|
|
226
|
+
gatewayBaseUrl: flags.gatewayBaseUrl || claim.gateway_base_url,
|
|
148
227
|
plan,
|
|
149
228
|
});
|
|
150
229
|
records.push(record);
|
|
@@ -673,7 +752,8 @@ function printHelp() {
|
|
|
673
752
|
console.log(`PennyRouter CLI
|
|
674
753
|
|
|
675
754
|
Usage:
|
|
676
|
-
pennyrouter install [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes] [--dry-run] [--existing-account]
|
|
755
|
+
pennyrouter install [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes] [--dry-run] [--existing-account] [--local[=URL]]
|
|
756
|
+
pennyrouter auth anthropic [--penny-key pr-...] [--token oauth-token] [--gateway-base-url URL]
|
|
677
757
|
pennyrouter disable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
678
758
|
pennyrouter enable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
679
759
|
pennyrouter uninstall [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
@@ -682,6 +762,7 @@ Usage:
|
|
|
682
762
|
Examples:
|
|
683
763
|
npx pennyrouter install
|
|
684
764
|
npx pennyrouter install --existing-account
|
|
765
|
+
npx pennyrouter auth anthropic
|
|
685
766
|
npx pennyrouter install --all
|
|
686
767
|
npx pennyrouter install --harness windsurf,zed,codegpt,aider --yes
|
|
687
768
|
npx pennyrouter disable
|
|
@@ -2,20 +2,23 @@ import { atomicWrite, backupFile, compactHome, exists, expandHome, readJsonFile
|
|
|
2
2
|
|
|
3
3
|
const CONFIG_PATH = "~/.claude/settings.json";
|
|
4
4
|
const LEGACY_MODEL_LABEL = "PennyRouter Bundle";
|
|
5
|
+
// Values written into Claude Code's four model slots. CC displays a slot's VALUE verbatim in
|
|
6
|
+
// its /model menu AND sends it as the request `model`, so each value is both the menu label
|
|
7
|
+
// and the gateway's routing key. The three concrete Claude slots carry friendly PINNED labels
|
|
8
|
+
// ("Penny Opus/Sonnet/Haiku") that the gateway maps to that exact model — the user's pick
|
|
9
|
+
// runs on the model they chose, no dynamic tier switching on the trunk (background/utility
|
|
10
|
+
// calls still route cheap, which is where the savings come from). The Fable slot is the
|
|
11
|
+
// DYNAMIC "Penny Custom" option: let PennyRouter cost-route. Legacy "Penny Speed/Core/Power"
|
|
12
|
+
// installs (dynamic bundles) are still recognized for uninstall (see isPennyRouterModelValue).
|
|
5
13
|
const CLAUDE_CODE_MODELS = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
custom: "Penny Custom",
|
|
11
|
-
};
|
|
12
|
-
const MODEL_OVERRIDE_KEYS = {
|
|
13
|
-
"claude-sonnet-4-6": CLAUDE_CODE_MODELS.core,
|
|
14
|
-
"claude-sonnet-4-6-1m": CLAUDE_CODE_MODELS.coreMax,
|
|
15
|
-
"claude-opus-4-8": CLAUDE_CODE_MODELS.power,
|
|
16
|
-
"claude-haiku-4-5": CLAUDE_CODE_MODELS.speed,
|
|
17
|
-
"claude-fable-5": CLAUDE_CODE_MODELS.custom,
|
|
14
|
+
haiku: "Penny Haiku", // -> pinned anthropic/claude-haiku-4-5-20251001
|
|
15
|
+
sonnet: "Penny Sonnet", // -> pinned anthropic/claude-sonnet-4-6
|
|
16
|
+
opus: "Penny Opus", // -> pinned anthropic/claude-opus-4-8
|
|
17
|
+
custom: "Penny Custom", // -> dynamic (PennyRouter cost-routing)
|
|
18
18
|
};
|
|
19
|
+
// Retired dynamic-bundle labels (pre-pinned-trunk). Not written by a fresh install; matched
|
|
20
|
+
// only so uninstall/detect still recognize and clean up an older install.
|
|
21
|
+
const LEGACY_PENNY_LABELS = ["Penny Speed", "Penny Core", "Penny Power"];
|
|
19
22
|
|
|
20
23
|
export const claudeCodeHarness = {
|
|
21
24
|
id: "claude-code",
|
|
@@ -48,12 +51,10 @@ export const claudeCodeHarness = {
|
|
|
48
51
|
delete current.env.ANTHROPIC_API_KEY;
|
|
49
52
|
// Claude Code displays these env values directly in its model picker; the
|
|
50
53
|
// gateway decodes the friendly labels into bundle/profile overrides.
|
|
51
|
-
current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.
|
|
52
|
-
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.
|
|
53
|
-
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.
|
|
54
|
+
current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
|
|
55
|
+
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
|
|
56
|
+
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
|
|
54
57
|
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.custom;
|
|
55
|
-
current.modelOverrides ||= {};
|
|
56
|
-
Object.assign(current.modelOverrides, MODEL_OVERRIDE_KEYS);
|
|
57
58
|
|
|
58
59
|
await atomicWrite(CONFIG_PATH, `${JSON.stringify(current, null, 2)}\n`);
|
|
59
60
|
|
|
@@ -72,7 +73,41 @@ export const claudeCodeHarness = {
|
|
|
72
73
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
73
74
|
"ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
74
75
|
],
|
|
75
|
-
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async installAnthropicOAuth({ pennyKey, anthropicAuthToken, gatewayBaseUrl }) {
|
|
81
|
+
const backupPath = await backupFile(CONFIG_PATH, "claude-code-anthropic-auth");
|
|
82
|
+
const current = (await readJsonFile(CONFIG_PATH, null)) || {};
|
|
83
|
+
current.env ||= {};
|
|
84
|
+
current.env.ANTHROPIC_BASE_URL = gatewayBaseUrl;
|
|
85
|
+
// Dual-auth mode: x-api-key authenticates the local user to PennyRouter; the
|
|
86
|
+
// Authorization bearer token is forwarded by the gateway to Anthropic.
|
|
87
|
+
current.env.ANTHROPIC_API_KEY = pennyKey;
|
|
88
|
+
current.env.ANTHROPIC_AUTH_TOKEN = anthropicAuthToken;
|
|
89
|
+
current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
|
|
90
|
+
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
|
|
91
|
+
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
|
|
92
|
+
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.custom;
|
|
93
|
+
|
|
94
|
+
await atomicWrite(CONFIG_PATH, `${JSON.stringify(current, null, 2)}\n`);
|
|
95
|
+
return {
|
|
96
|
+
harness: "claude-code",
|
|
97
|
+
config_path: compactHome(expandHome(CONFIG_PATH)),
|
|
98
|
+
backup_path: backupPath ? compactHome(backupPath) : null,
|
|
99
|
+
installed_at: new Date().toISOString(),
|
|
100
|
+
restart: "restart Claude Code",
|
|
101
|
+
changes: {
|
|
102
|
+
env_keys: [
|
|
103
|
+
"ANTHROPIC_BASE_URL",
|
|
104
|
+
"ANTHROPIC_API_KEY",
|
|
105
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
106
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
107
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
108
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
109
|
+
"ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
110
|
+
],
|
|
76
111
|
},
|
|
77
112
|
};
|
|
78
113
|
},
|
|
@@ -82,7 +117,7 @@ export const claudeCodeHarness = {
|
|
|
82
117
|
if (hasPennyRouterEnv(current.env)) {
|
|
83
118
|
delete current.env.ANTHROPIC_BASE_URL;
|
|
84
119
|
const tok = current.env.ANTHROPIC_AUTH_TOKEN;
|
|
85
|
-
if (typeof tok === "string" && tok.startsWith("pr-")) {
|
|
120
|
+
if (typeof tok === "string" && (tok.startsWith("pr-") || record.anthropic_oauth)) {
|
|
86
121
|
delete current.env.ANTHROPIC_AUTH_TOKEN;
|
|
87
122
|
}
|
|
88
123
|
// Legacy installs stored the key under ANTHROPIC_API_KEY.
|
|
@@ -98,12 +133,6 @@ export const claudeCodeHarness = {
|
|
|
98
133
|
if (current.env[k] === LEGACY_MODEL_LABEL || isPennyRouterModelValue(current.env[k])) delete current.env[k];
|
|
99
134
|
}
|
|
100
135
|
}
|
|
101
|
-
if (current.modelOverrides && typeof current.modelOverrides === "object") {
|
|
102
|
-
for (const [key, value] of Object.entries(MODEL_OVERRIDE_KEYS)) {
|
|
103
|
-
if (current.modelOverrides[key] === value) delete current.modelOverrides[key];
|
|
104
|
-
}
|
|
105
|
-
if (Object.keys(current.modelOverrides).length === 0) delete current.modelOverrides;
|
|
106
|
-
}
|
|
107
136
|
if (current["anthropic.baseURL"] && isPennyRouterValue(current["anthropic.baseURL"])) {
|
|
108
137
|
delete current["anthropic.baseURL"];
|
|
109
138
|
}
|
|
@@ -143,10 +172,19 @@ function isPennyRouterBaseUrl(value) {
|
|
|
143
172
|
function isPennyRouterModelValue(value) {
|
|
144
173
|
return (
|
|
145
174
|
isPennyRouterValue(value) ||
|
|
146
|
-
value === CLAUDE_CODE_MODELS.
|
|
147
|
-
value === CLAUDE_CODE_MODELS.
|
|
148
|
-
value === CLAUDE_CODE_MODELS.
|
|
149
|
-
value === CLAUDE_CODE_MODELS.
|
|
150
|
-
value
|
|
175
|
+
value === CLAUDE_CODE_MODELS.haiku ||
|
|
176
|
+
value === CLAUDE_CODE_MODELS.sonnet ||
|
|
177
|
+
value === CLAUDE_CODE_MODELS.opus ||
|
|
178
|
+
value === CLAUDE_CODE_MODELS.custom ||
|
|
179
|
+
LEGACY_PENNY_LABELS.includes(value)
|
|
151
180
|
);
|
|
152
181
|
}
|
|
182
|
+
|
|
183
|
+
export async function readClaudeCodePennyRouterKey() {
|
|
184
|
+
const current = (await readJsonFile(CONFIG_PATH, null)) || {};
|
|
185
|
+
const env = current.env || {};
|
|
186
|
+
for (const value of [env.ANTHROPIC_API_KEY, env.ANTHROPIC_AUTH_TOKEN, current["anthropic.apiKey"]]) {
|
|
187
|
+
if (typeof value === "string" && value.startsWith("pr-")) return value;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|