pennyrouter 0.1.12 → 0.1.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.
- package/package.json +1 -1
- package/src/cli.js +47 -2
- package/src/statusline.js +21 -12
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
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";
|
|
@@ -234,6 +234,41 @@ function readAnthropicOAuthToken(flags = {}) {
|
|
|
234
234
|
return { token: normalizeAnthropicToken(result.stdout), source: "ant" };
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// ANSI/CSI escape sequences that `claude setup-token`'s TUI emits (colors, cursor
|
|
238
|
+
// moves, bracketed-paste). We strip these before scanning for the token so a match
|
|
239
|
+
// isn't split by control bytes. Kept deliberately broad.
|
|
240
|
+
// eslint-disable-next-line no-control-regex
|
|
241
|
+
const ANSI_ESCAPE_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
242
|
+
// The token line printed by `claude setup-token`. Match any sk-ant-* token so we don't
|
|
243
|
+
// depend on the exact variant/version (e.g. sk-ant-oat01-...).
|
|
244
|
+
const ANTHROPIC_SETUP_TOKEN_RE = /sk-ant-[A-Za-z0-9_-]+/;
|
|
245
|
+
|
|
246
|
+
// Run `claude setup-token` inline and extract the token it prints. The command is a
|
|
247
|
+
// full-screen TUI (ANSI art + banner) that also prints the real token on a line like
|
|
248
|
+
// "sk-ant-oat01-...". We pipe stdout so we can read it, but tee every chunk straight
|
|
249
|
+
// back to the terminal so the user still sees the whole flow (including "you won't be
|
|
250
|
+
// able to see it again"). stdin/stderr inherit the TTY for the interactive parts. After
|
|
251
|
+
// exit we ANSI-strip the captured stdout and regex out the token. Returns "" on
|
|
252
|
+
// non-zero exit or if no token line was found, so the caller falls back to paste.
|
|
253
|
+
function runClaudeSetupToken() {
|
|
254
|
+
return new Promise((resolve) => {
|
|
255
|
+
let captured = "";
|
|
256
|
+
const child = spawn("claude", ["setup-token"], {
|
|
257
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
258
|
+
});
|
|
259
|
+
child.stdout.on("data", (chunk) => {
|
|
260
|
+
captured += chunk.toString("utf8");
|
|
261
|
+
output.write(chunk); // tee: keep the TUI visible to the user
|
|
262
|
+
});
|
|
263
|
+
child.on("error", () => resolve(""));
|
|
264
|
+
child.on("close", (code) => {
|
|
265
|
+
if (code !== 0) return resolve("");
|
|
266
|
+
const match = captured.replace(ANSI_ESCAPE_RE, "").match(ANTHROPIC_SETUP_TOKEN_RE);
|
|
267
|
+
resolve(match ? match[0] : "");
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
237
272
|
function normalizeAnthropicToken(value) {
|
|
238
273
|
let text = String(value || "").trim();
|
|
239
274
|
if (!text) return "";
|
|
@@ -373,7 +408,17 @@ async function maybeConfigureClaudeAnthropicAuth(flags, selected) {
|
|
|
373
408
|
|
|
374
409
|
console.log("");
|
|
375
410
|
if (commandExists("claude")) {
|
|
376
|
-
|
|
411
|
+
// The user already opted into subscription auth above, so run `claude setup-token`
|
|
412
|
+
// directly and capture the token — no copy-paste, no second confirm. The command
|
|
413
|
+
// opens the browser for approval inline. On failure we fall through to manual paste.
|
|
414
|
+
console.log("Launching `claude setup-token` — approve in your browser when prompted.");
|
|
415
|
+
const captured = await runClaudeSetupToken();
|
|
416
|
+
if (captured) {
|
|
417
|
+
console.log("");
|
|
418
|
+
return { token: captured, source: "setup-token" };
|
|
419
|
+
}
|
|
420
|
+
console.log("");
|
|
421
|
+
console.log("Couldn't capture a token automatically. Paste it below, or leave blank to skip.");
|
|
377
422
|
} else {
|
|
378
423
|
console.log("Claude Code was not found on PATH. Paste an Anthropic bearer token, or leave blank to skip.");
|
|
379
424
|
}
|
package/src/statusline.js
CHANGED
|
@@ -72,7 +72,9 @@ export async function restoreClaudeCodeStatusline(current) {
|
|
|
72
72
|
// free and fast: read stdin, print from cache, refresh out-of-band.
|
|
73
73
|
const STATUSLINE_SCRIPT = `#!/usr/bin/env node
|
|
74
74
|
// PennyRouter statusline for Claude Code (installed by \`pennyrouter install\`).
|
|
75
|
-
// Prints: [wrapped original statusline |] penny segment (
|
|
75
|
+
// Prints: [wrapped original statusline (all lines) |] penny segment (penny / penny (sub)
|
|
76
|
+
// / penny (byok) / penny off). The wrapped statusline's first line gets the penny suffix;
|
|
77
|
+
// any further lines (e.g. a context bar) pass through verbatim.
|
|
76
78
|
// Never blocks on the network: reads a per-session cache and refreshes it detached.
|
|
77
79
|
import { readFileSync, mkdirSync, writeFileSync, statSync } from "node:fs";
|
|
78
80
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -137,33 +139,40 @@ if (sessionId && pennyInstalled && cacheAge > refreshMs) {
|
|
|
137
139
|
}).unref();
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return
|
|
142
|
+
// Auth tier suffix: only "sub"/"byok" are shown (penny-tier and unknown render bare).
|
|
143
|
+
function authSuffix(mode) {
|
|
144
|
+
return mode === "sub" || mode === "byok" ? " (" + mode + ")" : "";
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
let penny;
|
|
146
148
|
if (!pennyInstalled) {
|
|
147
149
|
penny = "🪙 penny off";
|
|
148
150
|
} else if (cache && cache.known && cache.bypass) {
|
|
149
|
-
penny = "🪙 penny
|
|
150
|
-
} else if (cache && cache.known && cache.model) {
|
|
151
|
-
penny = "🪙 penny · " + shortModel(cache.model);
|
|
151
|
+
penny = "🪙 penny off";
|
|
152
152
|
} else {
|
|
153
|
-
penny = "🪙 penny
|
|
153
|
+
penny = "🪙 penny" + authSuffix(cache && cache.auth_mode);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
// Wrap the underlying statusline (e.g. ccstatusline) and append the penny segment to
|
|
157
|
+
// its FIRST line only, preserving every subsequent line verbatim (the Context bar and
|
|
158
|
+
// any other rows). Taking only line [0] here is what dropped multi-line statuslines.
|
|
159
|
+
let lines;
|
|
157
160
|
if (config.wrap_command) {
|
|
158
161
|
const run = spawnSync(config.wrap_command, {
|
|
159
162
|
shell: true, input: raw, encoding: "utf8", timeout: 1500,
|
|
160
163
|
});
|
|
161
|
-
|
|
164
|
+
lines = (run.stdout || "").replace(/\\n$/, "").split("\\n");
|
|
165
|
+
if (lines.length === 1 && lines[0] === "") lines = [];
|
|
162
166
|
} else {
|
|
163
167
|
const model = input.model?.display_name || "";
|
|
164
168
|
const dir = input.workspace?.current_dir ? basename(input.workspace.current_dir) : "";
|
|
165
|
-
|
|
169
|
+
lines = [[model, dir].filter(Boolean).join(" · ")].filter(Boolean);
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
|
|
172
|
+
if (lines.length === 0) {
|
|
173
|
+
console.log(penny);
|
|
174
|
+
} else {
|
|
175
|
+
lines[0] = lines[0].trimEnd() + " | " + penny;
|
|
176
|
+
console.log(lines.join("\\n"));
|
|
177
|
+
}
|
|
169
178
|
`;
|