pennyrouter 0.1.18 → 0.1.20

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 CHANGED
@@ -61,6 +61,7 @@ model you chose. "Penny Custom" is the dynamic option: PennyRouter cost-routes e
61
61
  - Penny Opus: pins `anthropic/claude-opus-4-8`
62
62
  - Penny Sonnet: pins `anthropic/claude-sonnet-4-6`
63
63
  - Penny Haiku: pins `anthropic/claude-haiku-4-5-20251001`
64
+ - Penny Fable: pins `anthropic/claude-fable-5`
64
65
  - Penny Custom: `pennyrouter/auto` — dynamic routing on the bundle/profile saved in the user's account
65
66
 
66
67
  ## Claude Pro/Max Subscription Auth
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
5
  "homepage": "https://pennyrouter.com",
6
6
  "bugs": {
@@ -7,15 +7,17 @@ const CONNECTOR_SETTING = "disableClaudeAiConnectors";
7
7
  // Values written into Claude Code's four model slots. CC displays a slot's VALUE verbatim in
8
8
  // its /model menu AND sends it as the request `model`, so each value is both the menu label
9
9
  // and the gateway's routing key. The three concrete Claude slots carry friendly PINNED labels
10
- // ("Penny Opus/Sonnet/Haiku") that the gateway maps to that exact model — the user's pick
10
+ // ("Penny Opus/Sonnet/Haiku/Fable") that the gateway maps to that exact model — the user's pick
11
11
  // runs on the model they chose, no dynamic tier switching on the trunk (background/utility
12
- // calls still route cheap, which is where the savings come from). The Fable slot is the
13
- // DYNAMIC "Penny Custom" option: let PennyRouter cost-route. Legacy "Penny Speed/Core/Power"
12
+ // calls still route cheap, which is where the savings come from). The separate Fable slot is
13
+ // pinned to Fable; the DYNAMIC "Penny Custom" option lets PennyRouter cost-route. Legacy
14
+ // "Penny Speed/Core/Power"
14
15
  // installs (dynamic bundles) are still recognized for uninstall (see isPennyRouterModelValue).
15
16
  const CLAUDE_CODE_MODELS = {
16
17
  haiku: "Penny Haiku", // -> pinned anthropic/claude-haiku-4-5-20251001
17
18
  sonnet: "Penny Sonnet", // -> pinned anthropic/claude-sonnet-4-6
18
19
  opus: "Penny Opus", // -> pinned anthropic/claude-opus-4-8
20
+ fable: "Penny Fable", // -> pinned anthropic/claude-fable-5
19
21
  custom: "Penny Custom", // -> dynamic (PennyRouter cost-routing)
20
22
  };
21
23
  // Retired dynamic-bundle labels (pre-pinned-trunk). Not written by a fresh install; matched
@@ -57,7 +59,7 @@ export const claudeCodeHarness = {
57
59
  current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
58
60
  current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
59
61
  current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
60
- current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.custom;
62
+ current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
61
63
 
62
64
  const statusline = await configureClaudeCodeStatusline(current);
63
65
 
@@ -102,7 +104,7 @@ export const claudeCodeHarness = {
102
104
  current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
103
105
  current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
104
106
  current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
105
- current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.custom;
107
+ current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
106
108
 
107
109
  const statusline = await configureClaudeCodeStatusline(current);
108
110
 
@@ -143,7 +145,7 @@ export const claudeCodeHarness = {
143
145
  current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
144
146
  current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
145
147
  current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
146
- current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.custom;
148
+ current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
147
149
 
148
150
  const statusline = await configureClaudeCodeStatusline(current);
149
151
 
@@ -266,6 +268,7 @@ function isPennyRouterModelValue(value) {
266
268
  value === CLAUDE_CODE_MODELS.haiku ||
267
269
  value === CLAUDE_CODE_MODELS.sonnet ||
268
270
  value === CLAUDE_CODE_MODELS.opus ||
271
+ value === CLAUDE_CODE_MODELS.fable ||
269
272
  value === CLAUDE_CODE_MODELS.custom ||
270
273
  LEGACY_PENNY_LABELS.includes(value)
271
274
  );
package/src/statusline.js CHANGED
@@ -140,10 +140,7 @@ let cacheAge = Infinity;
140
140
  try { cacheAge = Date.now() - statSync(cachePath).mtimeMs; } catch { /* no cache yet */ }
141
141
 
142
142
  const { base, key } = claudeEnv();
143
- // Installed = a base URL plus a pr- key (claudeEnv only returns pr- keys, and the installer
144
- // writes both together). Do NOT sniff the base URL for "pennyrouter": that substring check
145
- // false-negatived every dev/localhost gateway (e.g. localhost:8400), rendering a hard
146
- // "penny off" while the gateway was serving the session fine (observed 2026-07-18).
143
+
147
144
  const pennyInstalled = Boolean(base) && Boolean(key);
148
145
  if (sessionId && pennyInstalled && cacheAge > refreshMs) {
149
146
  spawn(process.execPath, [process.argv[1], "--refresh", sessionId], {
@@ -156,22 +153,31 @@ function authSuffix(mode) {
156
153
  return mode === "sub" || mode === "byok" ? " (" + mode + ")" : "";
157
154
  }
158
155
 
156
+ // Teammate badges (@penny ask). Rendered from the STRUCTURED fields, not the gateway's
157
+ // prebuilt \`label\` — this line is composed here (and may be appended to a wrap_command's
158
+ // output), so the client owns the layout. 📨 = questions waiting on you; 📬 = an answer came
159
+ // back to you. Both absent on an older gateway, which renders nothing.
160
+ function askBadges(c) {
161
+ if (!c) return "";
162
+ let out = "";
163
+ if (c.asks_open > 0) out += " · 📨 " + c.asks_open;
164
+ if (c.answers_fresh > 0) out += " · 📬 " + c.answers_fresh;
165
+ return out;
166
+ }
167
+
159
168
  let penny;
160
169
  if (!pennyInstalled) {
161
170
  penny = "🪙 penny off";
162
171
  } else if (cache && cache.known && cache.bypass) {
163
172
  penny = "🪙 penny off";
164
173
  } else {
165
- penny = "🪙 penny" + authSuffix(cache && cache.auth_mode);
174
+ penny = "🪙 penny" + authSuffix(cache && cache.auth_mode) + askBadges(cache);
166
175
  }
167
176
 
168
- // Wrap the underlying statusline (e.g. ccstatusline) and append the penny segment to
169
- // its FIRST line only, preserving every subsequent line verbatim (the Context bar and
170
- // any other rows). Taking only line [0] here is what dropped multi-line statuslines.
171
177
  let lines;
172
178
  if (config.wrap_command) {
173
179
  const run = spawnSync(config.wrap_command, {
174
- shell: true, input: raw, encoding: "utf8", timeout: 1500,
180
+ shell: true, input: raw, encoding: "utf8", timeout: 5000,
175
181
  });
176
182
  lines = (run.stdout || "").replace(/\\n$/, "").split("\\n");
177
183
  if (lines.length === 1 && lines[0] === "") lines = [];