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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
5
  "homepage": "https://pennyrouter.com",
6
6
  "bugs": {
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
- console.log("Run `claude setup-token` in another terminal, then paste the token here.");
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 (managed model / BYPASS / off).
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
- function shortModel(m) {
141
- if (typeof m !== "string" || !m) return "";
142
- return m.split("/").pop().replace(/^claude-/, "");
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 BYPASS";
150
- } else if (cache && cache.known && cache.model) {
151
- penny = "🪙 penny · " + shortModel(cache.model);
151
+ penny = "🪙 penny off";
152
152
  } else {
153
- penny = "🪙 penny on";
153
+ penny = "🪙 penny" + authSuffix(cache && cache.auth_mode);
154
154
  }
155
155
 
156
- let prefix = "";
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
- prefix = (run.stdout || "").split("\\n")[0].trim();
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
- prefix = [model, dir].filter(Boolean).join(" · ");
169
+ lines = [[model, dir].filter(Boolean).join(" · ")].filter(Boolean);
166
170
  }
167
171
 
168
- console.log(prefix ? prefix + " | " + penny : penny);
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
  `;