pennyrouter 0.1.7 → 0.1.9
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 +31 -4
- package/package.json +1 -1
- package/src/cli.js +166 -17
package/README.md
CHANGED
|
@@ -22,6 +22,10 @@ npx pennyrouter status
|
|
|
22
22
|
With no `--harness` flag, the interactive installer shows a checkbox list. Tools with
|
|
23
23
|
detected configs start checked; press Enter to accept or type tool IDs to toggle them.
|
|
24
24
|
|
|
25
|
+
When Claude Code is selected, the installer also offers an optional Claude Pro/Max
|
|
26
|
+
subscription-auth step. Users can skip it during install and enable it later with
|
|
27
|
+
`@penny auth` or `npx pennyrouter auth anthropic`.
|
|
28
|
+
|
|
25
29
|
## Supported Tools
|
|
26
30
|
|
|
27
31
|
- Claude Code
|
|
@@ -47,8 +51,31 @@ model you chose. "Penny Custom" is the dynamic option: PennyRouter cost-routes e
|
|
|
47
51
|
|
|
48
52
|
## Claude Pro/Max Subscription Auth
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
Anthropic-native Claude turns through
|
|
54
|
+
When installing PennyRouter for Claude Code, the interactive installer offers to route
|
|
55
|
+
Anthropic-native Claude turns through the user's own Claude subscription token. To force this
|
|
56
|
+
in non-interactive installs:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx pennyrouter install --harness claude-code --anthropic-auth --token <token>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
If Claude Code is already logged in to a Pro/Max subscription, use Claude Code's token setup
|
|
63
|
+
flow:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
claude setup-token
|
|
67
|
+
ANTHROPIC_AUTH_TOKEN=<token> npx pennyrouter auth anthropic
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Other token sources are supported:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx pennyrouter auth anthropic --token <token>
|
|
74
|
+
npx pennyrouter auth anthropic --token-command '<command that prints the token>'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If the separate Anthropic `ant` CLI is installed and logged in, PennyRouter can use it as a
|
|
78
|
+
convenience fallback:
|
|
52
79
|
|
|
53
80
|
```bash
|
|
54
81
|
ant auth login
|
|
@@ -60,8 +87,8 @@ The command updates Claude Code so `x-api-key` carries the PennyRouter key and
|
|
|
60
87
|
bearer token to Anthropic with the required OAuth beta header and does not deduct
|
|
61
88
|
PennyRouter credit for those Anthropic-native upstream calls.
|
|
62
89
|
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
If the stored token expires during a long-running session, refresh the token source and rerun
|
|
91
|
+
`npx pennyrouter auth anthropic`.
|
|
65
92
|
|
|
66
93
|
## Agent Flow
|
|
67
94
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -94,10 +94,14 @@ function parseArgs(argv) {
|
|
|
94
94
|
else if (arg === "--dry-run") flags.dryRun = true;
|
|
95
95
|
else if (arg === "--no-browser") flags.noBrowser = true;
|
|
96
96
|
else if (arg === "--existing-account") flags.existingAccount = true;
|
|
97
|
+
else if (arg === "--anthropic-auth") flags.anthropicAuth = true;
|
|
98
|
+
else if (arg === "--no-anthropic-auth") flags.noAnthropicAuth = true;
|
|
97
99
|
else if (arg === "--penny-key") flags.pennyKey = rest[++i] || "";
|
|
98
100
|
else if (arg.startsWith("--penny-key=")) flags.pennyKey = arg.slice("--penny-key=".length);
|
|
99
101
|
else if (arg === "--token") flags.token = rest[++i] || "";
|
|
100
102
|
else if (arg.startsWith("--token=")) flags.token = arg.slice("--token=".length);
|
|
103
|
+
else if (arg === "--token-command") flags.tokenCommand = rest[++i] || "";
|
|
104
|
+
else if (arg.startsWith("--token-command=")) flags.tokenCommand = arg.slice("--token-command=".length);
|
|
101
105
|
else if (arg === "--harness") flags.harness = rest[++i] || "";
|
|
102
106
|
else if (arg.startsWith("--harness=")) flags.harness = arg.slice("--harness=".length);
|
|
103
107
|
else if (arg === "--app-url") flags.appUrl = rest[++i] || "";
|
|
@@ -136,10 +140,11 @@ async function authProvider(flags) {
|
|
|
136
140
|
);
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
const
|
|
143
|
+
const tokenResult = readAnthropicOAuthToken(flags);
|
|
144
|
+
const token = tokenResult.token;
|
|
140
145
|
if (!token) {
|
|
141
146
|
throw new Error(
|
|
142
|
-
"No Anthropic OAuth token found. Run `
|
|
147
|
+
"No Anthropic OAuth token found. Run `claude setup-token` and pass it with `--token`, set ANTHROPIC_AUTH_TOKEN, or pass `--token-command <cmd>`.",
|
|
143
148
|
);
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -154,24 +159,51 @@ async function authProvider(flags) {
|
|
|
154
159
|
upsertManifestRecord(manifest, {
|
|
155
160
|
...record,
|
|
156
161
|
anthropic_oauth: true,
|
|
157
|
-
token_source:
|
|
162
|
+
token_source: tokenResult.source,
|
|
158
163
|
});
|
|
159
164
|
await saveManifest(manifest);
|
|
160
165
|
|
|
161
166
|
console.log("Configured Claude Code for PennyRouter + Anthropic OAuth.");
|
|
162
167
|
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.");
|
|
168
|
+
console.log("If Claude auth expires, refresh the token source and run `pennyrouter auth anthropic` again.");
|
|
164
169
|
console.log(`Config: ${record.config_path}`);
|
|
165
170
|
}
|
|
166
171
|
|
|
167
|
-
function readAnthropicOAuthToken() {
|
|
168
|
-
if (
|
|
172
|
+
function readAnthropicOAuthToken(flags = {}) {
|
|
173
|
+
if (flags.token) return { token: normalizeAnthropicToken(flags.token), source: "flag" };
|
|
174
|
+
if (flags.tokenCommand) {
|
|
175
|
+
const result = spawnSync(flags.tokenCommand, {
|
|
176
|
+
encoding: "utf8",
|
|
177
|
+
shell: true,
|
|
178
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
179
|
+
});
|
|
180
|
+
if (result.status === 0) {
|
|
181
|
+
return { token: normalizeAnthropicToken(result.stdout), source: "token-command" };
|
|
182
|
+
}
|
|
183
|
+
return { token: "", source: "token-command" };
|
|
184
|
+
}
|
|
185
|
+
if (process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
186
|
+
return { token: normalizeAnthropicToken(process.env.ANTHROPIC_AUTH_TOKEN), source: "env" };
|
|
187
|
+
}
|
|
169
188
|
const result = spawnSync("ant", ["auth", "print-credentials", "--access-token"], {
|
|
170
189
|
encoding: "utf8",
|
|
171
190
|
stdio: ["ignore", "pipe", "pipe"],
|
|
172
191
|
});
|
|
173
|
-
if (result.status !== 0) return "";
|
|
174
|
-
return
|
|
192
|
+
if (result.status !== 0) return { token: "", source: "none" };
|
|
193
|
+
return { token: normalizeAnthropicToken(result.stdout), source: "ant" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function normalizeAnthropicToken(value) {
|
|
197
|
+
let text = String(value || "").trim();
|
|
198
|
+
if (!text) return "";
|
|
199
|
+
const line = text.split(/\r?\n/).map((part) => part.trim()).find(Boolean) || "";
|
|
200
|
+
text = line.replace(/^export\s+/, "");
|
|
201
|
+
const match = text.match(/^ANTHROPIC_AUTH_TOKEN=(.*)$/);
|
|
202
|
+
if (match) text = match[1].trim();
|
|
203
|
+
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
204
|
+
text = text.slice(1, -1);
|
|
205
|
+
}
|
|
206
|
+
return text.trim();
|
|
175
207
|
}
|
|
176
208
|
|
|
177
209
|
async function install(flags) {
|
|
@@ -189,6 +221,9 @@ async function install(flags) {
|
|
|
189
221
|
console.log(`Will install on ${harness.name}${installNoteForHarness(harness.id)}.`);
|
|
190
222
|
}
|
|
191
223
|
if (flags.gatewayBaseUrl) console.log(`Serving gateway: ${flags.gatewayBaseUrl}`);
|
|
224
|
+
if (selected.some((harness) => harness.id === "claude-code")) {
|
|
225
|
+
console.log("Claude Code subscription auth prompt would be shown unless skipped.");
|
|
226
|
+
}
|
|
192
227
|
console.log("Dry run only. No files changed.");
|
|
193
228
|
return;
|
|
194
229
|
}
|
|
@@ -213,19 +248,32 @@ async function install(flags) {
|
|
|
213
248
|
console.log(`Authorized. Received key ${maskKey(claim.api_key)}.`);
|
|
214
249
|
console.log("");
|
|
215
250
|
|
|
251
|
+
const claudeAnthropicAuth = await maybeConfigureClaudeAnthropicAuth(flags, selected);
|
|
252
|
+
|
|
216
253
|
const manifest = await loadManifest();
|
|
217
254
|
const records = [];
|
|
218
255
|
const failures = [];
|
|
219
256
|
for (const harness of selected) {
|
|
220
257
|
await runHarnessJob(failures, harness.id, "install", async () => {
|
|
221
258
|
const plan = await harness.planInstall();
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
259
|
+
const gatewayBaseUrl = flags.gatewayBaseUrl || claim.gateway_base_url;
|
|
260
|
+
const record = harness.id === "claude-code" && claudeAnthropicAuth
|
|
261
|
+
? await harness.installAnthropicOAuth({
|
|
262
|
+
pennyKey: claim.api_key,
|
|
263
|
+
anthropicAuthToken: claudeAnthropicAuth.token,
|
|
264
|
+
gatewayBaseUrl,
|
|
265
|
+
})
|
|
266
|
+
: await harness.install({
|
|
267
|
+
apiKey: claim.api_key,
|
|
268
|
+
// An explicit --local / --gateway-base-url overrides the URL the server hands back
|
|
269
|
+
// (which points at prod) — so a --local install serves through the local gateway.
|
|
270
|
+
gatewayBaseUrl,
|
|
271
|
+
plan,
|
|
272
|
+
});
|
|
273
|
+
if (harness.id === "claude-code" && claudeAnthropicAuth) {
|
|
274
|
+
record.anthropic_oauth = true;
|
|
275
|
+
record.token_source = claudeAnthropicAuth.source;
|
|
276
|
+
}
|
|
229
277
|
records.push(record);
|
|
230
278
|
upsertManifestRecord(manifest, record);
|
|
231
279
|
await saveManifest(manifest);
|
|
@@ -243,6 +291,56 @@ async function install(flags) {
|
|
|
243
291
|
reportFailures(failures);
|
|
244
292
|
}
|
|
245
293
|
|
|
294
|
+
async function maybeConfigureClaudeAnthropicAuth(flags, selected) {
|
|
295
|
+
if (!selected.some((harness) => harness.id === "claude-code")) return null;
|
|
296
|
+
if (flags.noAnthropicAuth) return null;
|
|
297
|
+
|
|
298
|
+
if (flags.token || flags.tokenCommand || process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
299
|
+
const tokenResult = readAnthropicOAuthToken(flags);
|
|
300
|
+
if (tokenResult.token) return tokenResult;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (flags.yes || !input.isTTY || !output.isTTY) {
|
|
304
|
+
if (flags.anthropicAuth) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
"Claude subscription auth was requested, but no Anthropic bearer token was found. Pass --token, --token-command, or ANTHROPIC_AUTH_TOKEN.",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!flags.anthropicAuth) {
|
|
313
|
+
console.log("Claude Code subscription auth");
|
|
314
|
+
console.log("PennyRouter can use your Claude Pro/Max subscription for Anthropic Claude calls.");
|
|
315
|
+
console.log("PennyRouter still handles routing, optimization, and @penny commands.");
|
|
316
|
+
console.log("");
|
|
317
|
+
const ok = await confirm("Enable Claude Pro/Max subscription auth for Claude Code?");
|
|
318
|
+
if (!ok) {
|
|
319
|
+
console.log("Skipped Claude subscription auth. Enable later with `@penny auth` or `pennyrouter auth anthropic`.");
|
|
320
|
+
console.log("");
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const tokenResult = readAnthropicOAuthToken({ tokenCommand: flags.tokenCommand });
|
|
326
|
+
if (tokenResult.token) return tokenResult;
|
|
327
|
+
|
|
328
|
+
console.log("");
|
|
329
|
+
if (commandExists("claude")) {
|
|
330
|
+
console.log("Run `claude setup-token` in another terminal, then paste the token here.");
|
|
331
|
+
} else {
|
|
332
|
+
console.log("Claude Code was not found on PATH. Paste an Anthropic bearer token, or leave blank to skip.");
|
|
333
|
+
}
|
|
334
|
+
const token = normalizeAnthropicToken(await promptSecretish("Anthropic token (blank to skip): "));
|
|
335
|
+
if (!token) {
|
|
336
|
+
console.log("Skipped Claude subscription auth. Enable later with `@penny auth` or `pennyrouter auth anthropic`.");
|
|
337
|
+
console.log("");
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
console.log("");
|
|
341
|
+
return { token, source: "prompt" };
|
|
342
|
+
}
|
|
343
|
+
|
|
246
344
|
async function disable(flags) {
|
|
247
345
|
const manifest = await loadManifest();
|
|
248
346
|
if (pruneUnsupportedManifestRecords(manifest).length > 0) await saveManifest(manifest);
|
|
@@ -489,6 +587,7 @@ async function status() {
|
|
|
489
587
|
if (record.disabled_snapshot_path) console.log(` disabled snapshot: ${record.disabled_snapshot_path}`);
|
|
490
588
|
if (record.installed_at) console.log(` installed: ${record.installed_at}`);
|
|
491
589
|
if (record.disabled_at) console.log(` disabled: ${record.disabled_at}`);
|
|
590
|
+
if (record.anthropic_oauth) console.log(` Claude subscription auth: enabled (${record.token_source || "unknown"} token)`);
|
|
492
591
|
if (record.manual_required) console.log(" note: finish or remove this integration in the tool UI");
|
|
493
592
|
}
|
|
494
593
|
}
|
|
@@ -729,8 +828,57 @@ async function confirm(question) {
|
|
|
729
828
|
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
730
829
|
}
|
|
731
830
|
|
|
831
|
+
async function promptSecretish(question) {
|
|
832
|
+
if (!input.isTTY || !output.isTTY) {
|
|
833
|
+
const rl = createInterface({ input, output });
|
|
834
|
+
const answer = await rl.question(question);
|
|
835
|
+
rl.close();
|
|
836
|
+
return answer.trim();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const previousRawMode = input.isRaw;
|
|
840
|
+
emitKeypressEvents(input);
|
|
841
|
+
input.setRawMode(true);
|
|
842
|
+
input.resume();
|
|
843
|
+
output.write(question);
|
|
844
|
+
|
|
845
|
+
return new Promise((resolve, reject) => {
|
|
846
|
+
let value = "";
|
|
847
|
+
const cleanup = () => {
|
|
848
|
+
input.off("keypress", onKeypress);
|
|
849
|
+
input.setRawMode(Boolean(previousRawMode));
|
|
850
|
+
input.pause();
|
|
851
|
+
output.write("\n");
|
|
852
|
+
};
|
|
853
|
+
const onKeypress = (str, key = {}) => {
|
|
854
|
+
if (key.ctrl && key.name === "c") {
|
|
855
|
+
cleanup();
|
|
856
|
+
reject(new Error("Cancelled."));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (key.name === "return" || key.name === "enter") {
|
|
860
|
+
cleanup();
|
|
861
|
+
resolve(value.trim());
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (key.name === "backspace") {
|
|
865
|
+
value = value.slice(0, -1);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (str) value += str;
|
|
869
|
+
};
|
|
870
|
+
input.on("keypress", onKeypress);
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function commandExists(command) {
|
|
875
|
+
const result = spawnSync(command, ["--version"], { stdio: "ignore" });
|
|
876
|
+
return result.status === 0;
|
|
877
|
+
}
|
|
878
|
+
|
|
732
879
|
function installNoteForRecord(record) {
|
|
733
880
|
if (record.manual_required) return " (manual setup shown)";
|
|
881
|
+
if (record.anthropic_oauth) return " (Claude subscription auth)";
|
|
734
882
|
return installNoteForHarness(record.harness);
|
|
735
883
|
}
|
|
736
884
|
|
|
@@ -752,8 +900,8 @@ function printHelp() {
|
|
|
752
900
|
console.log(`PennyRouter CLI
|
|
753
901
|
|
|
754
902
|
Usage:
|
|
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]
|
|
903
|
+
pennyrouter install [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes] [--dry-run] [--existing-account] [--anthropic-auth|--no-anthropic-auth] [--local[=URL]]
|
|
904
|
+
pennyrouter auth anthropic [--penny-key pr-...] [--token oauth-token] [--token-command CMD] [--gateway-base-url URL]
|
|
757
905
|
pennyrouter disable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
758
906
|
pennyrouter enable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
759
907
|
pennyrouter uninstall [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
@@ -762,6 +910,7 @@ Usage:
|
|
|
762
910
|
Examples:
|
|
763
911
|
npx pennyrouter install
|
|
764
912
|
npx pennyrouter install --existing-account
|
|
913
|
+
npx pennyrouter install --harness claude-code --anthropic-auth --token <token>
|
|
765
914
|
npx pennyrouter auth anthropic
|
|
766
915
|
npx pennyrouter install --all
|
|
767
916
|
npx pennyrouter install --harness windsurf,zed,codegpt,aider --yes
|