pennyrouter 0.3.21 → 0.3.22

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.3.21",
3
+ "version": "0.3.22",
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
@@ -209,7 +209,7 @@ async function profiles(flags) {
209
209
  console.log("Switch with: pennyrouter profiles <name> (or penny claude --penny-profile <name>)");
210
210
  }
211
211
 
212
- function parseArgs(argv) {
212
+ export function parseArgs(argv) {
213
213
  const command = argv[0] && !argv[0].startsWith("-") ? argv[0] : "install";
214
214
  const rest = command === argv[0] ? argv.slice(1) : argv;
215
215
  const flags = {};
@@ -260,6 +260,8 @@ function parseArgs(argv) {
260
260
  // clobbering the cloud credentials that `penny claude` uses by default.
261
261
  else if (arg === "--profile") flags.profile = rest[++i] || "";
262
262
  else if (arg.startsWith("--profile=")) flags.profile = arg.slice("--profile=".length);
263
+ else if (arg === "--private") flags.profile = "private";
264
+ else if (arg === "--cloud") flags.profile = "cloud";
263
265
  else if (arg === "--harness") flags.harness = rest[++i] || "";
264
266
  else if (arg.startsWith("--harness=")) flags.harness = arg.slice("--harness=".length);
265
267
  // --private-gateway [url]: install against a self-hosted gateway. Unlike
@@ -320,9 +322,12 @@ function parseArgs(argv) {
320
322
 
321
323
  if (flags.configPath) applyConfigFile(flags);
322
324
 
323
- // A self-hosted gateway is a distinct identity with its own key store, so give it its own
324
- // profile unless one was named. Hosted installs keep the default profile.
325
- if (!flags.profile) flags.profile = flags.privateGateway ? "private" : DEFAULT_PROFILE;
325
+ // Install chooses a destination profile when none was named: hosted installs go to cloud,
326
+ // while self-hosted installs go to private. Operational commands such as update and auth
327
+ // should instead leave the profile unset so loadPennyConfig() resolves active_profile.
328
+ if (!flags.profile && command === "install") {
329
+ flags.profile = flags.privateGateway ? "private" : DEFAULT_PROFILE;
330
+ }
326
331
 
327
332
  return { command, flags };
328
333
  }
@@ -925,10 +930,11 @@ export async function install(flags = {}) {
925
930
  record.launch_available = true;
926
931
  const launchConfigPath = harness.id === "claude-code"
927
932
  ? claudeLaunchSettingsPath()
928
- : harness.id === "codex" ? codexLaunchProfilePath() : null;
933
+ : harness.id === "codex" ? codexLaunchProfilePath(flags.profile) : null;
929
934
  record.launch_config_path = launchConfigPath ? compactHome(launchConfigPath) : null;
930
935
  record.default_routing = Boolean(makeDefault);
931
936
  }
937
+ record.profile = flags.profile;
932
938
  if (harness.id === "claude-code" && claudeSubscriptionAuth) {
933
939
  record.anthropic_oauth = true;
934
940
  record.anthropic_token_stored = true;
@@ -1256,6 +1262,12 @@ async function update(flags) {
1256
1262
  if (record.disabled) return false;
1257
1263
  if (flags.all || requested.length === 0) return true;
1258
1264
  return requested.includes(record.harness);
1265
+ }).filter((record) => {
1266
+ if (!flags.profile) return true;
1267
+ const recordProfile = record.profile || profileFromManifestRecord(record);
1268
+ // Records written before profile ownership was added have no reliable Claude profile
1269
+ // marker, so leave them eligible for an explicit profile as a compatibility fallback.
1270
+ return !recordProfile || recordProfile === flags.profile;
1259
1271
  });
1260
1272
 
1261
1273
  if (records.length === 0) {
@@ -1266,25 +1278,32 @@ async function update(flags) {
1266
1278
  return;
1267
1279
  }
1268
1280
 
1269
- const launchConfig = await loadPennyConfig({ profile: flags.profile });
1270
- const globalAuth = await readClaudeCodeGatewayAuth();
1271
- const apiKey = flags.pennyKey || launchConfig.api_key || globalAuth.apiKey;
1272
- const gatewayBaseUrl = flags.gatewayBaseUrl
1273
- || launchConfig.gateway_base_url
1274
- || globalAuth.gatewayBaseUrl
1275
- || process.env.PENNYROUTER_GATEWAY_BASE_URL
1276
- || DEFAULT_GATEWAY_BASE_URL;
1277
-
1278
- if (!apiKey) {
1279
- throw new Error(
1280
- "Could not find your PennyRouter key in the local config. Pass `--penny-key pr-...`, "
1281
- + "or run `npx pennyrouter install` to re-authorize.",
1282
- );
1281
+ const updateContexts = new Map();
1282
+ for (const record of targets) {
1283
+ const profile = record.profile || profileFromManifestRecord(record) || flags.profile;
1284
+ const launchConfig = await loadPennyConfig({ profile });
1285
+ const globalAuth = await readClaudeCodeGatewayAuth();
1286
+ const apiKey = flags.pennyKey || launchConfig.api_key || globalAuth.apiKey;
1287
+ const gatewayBaseUrl = flags.gatewayBaseUrl
1288
+ || launchConfig.gateway_base_url
1289
+ || globalAuth.gatewayBaseUrl
1290
+ || process.env.PENNYROUTER_GATEWAY_BASE_URL
1291
+ || DEFAULT_GATEWAY_BASE_URL;
1292
+ if (!apiKey) {
1293
+ const suffix = profile ? ` for profile "${profile}"` : "";
1294
+ throw new Error(
1295
+ `Could not find your PennyRouter key in the local config${suffix}. `
1296
+ + "Pass `--penny-key pr-...`, or run `npx pennyrouter install` to re-authorize.",
1297
+ );
1298
+ }
1299
+ updateContexts.set(record, { profile, apiKey, gatewayBaseUrl });
1283
1300
  }
1284
1301
 
1285
1302
  console.log("PennyRouter update");
1286
1303
  console.log("");
1287
- console.log(`Key ${maskKey(apiKey)} · gateway ${gatewayBaseUrl}`);
1304
+ for (const { profile, apiKey, gatewayBaseUrl } of updateContexts.values()) {
1305
+ console.log(`${profile ? `Profile ${profile} · ` : ""}Key ${maskKey(apiKey)} · gateway ${gatewayBaseUrl}`);
1306
+ }
1288
1307
  console.log("Subscription and API keys are left untouched.");
1289
1308
  console.log("");
1290
1309
 
@@ -1307,6 +1326,7 @@ async function update(flags) {
1307
1326
  for (const record of targets) {
1308
1327
  const harness = HARNESS_BY_ID[record.harness];
1309
1328
  if (!harness) continue;
1329
+ const { profile, apiKey, gatewayBaseUrl } = updateContexts.get(record);
1310
1330
 
1311
1331
  await runHarnessJob(failures, record.harness, "update", async () => {
1312
1332
  const supportsPennyCommand = LAUNCHABLE_HARNESS_IDS.includes(record.harness);
@@ -1333,7 +1353,7 @@ async function update(flags) {
1333
1353
  ? claudeSubscriptionAuth
1334
1354
  : codexSubscriptionAuth,
1335
1355
  models: record.harness === "claude-code" ? CLAUDE_CODE_MODELS : null,
1336
- profile: flags.profile,
1356
+ profile,
1337
1357
  });
1338
1358
  }
1339
1359
 
@@ -1368,9 +1388,13 @@ async function update(flags) {
1368
1388
  // and `pennyrouter mcp uninstall` removes it for good.
1369
1389
  if (!flags.dryRun && !flags.noMcp) {
1370
1390
  const wanted = targets.filter((record) => MCP_HARNESS_IDS.includes(record.harness));
1371
- const results = await registerMcp(wanted.map((record) => record.harness), {
1372
- gatewayBaseUrl: customGatewayBaseUrl(gatewayBaseUrl),
1373
- });
1391
+ const results = [];
1392
+ for (const record of wanted) {
1393
+ const context = updateContexts.get(record);
1394
+ results.push(...await registerMcp([record.harness], {
1395
+ gatewayBaseUrl: customGatewayBaseUrl(context.gatewayBaseUrl),
1396
+ }));
1397
+ }
1374
1398
  const added = results.filter((r) => r.changed).map((r) => HARNESS_BY_ID[r.harness]?.name);
1375
1399
  if (added.length) console.log(`Registered the Threads MCP server on ${added.join(" and ")}.`);
1376
1400
  for (const result of results.filter((r) => r.error)) {
@@ -2277,6 +2301,14 @@ function upsertManifestRecord(manifest, record) {
2277
2301
  manifest.installations.push(record);
2278
2302
  }
2279
2303
 
2304
+ // Older manifests predate profile ownership. Codex's per-profile launch filename gives us a
2305
+ // reliable migration signal; Claude's launch settings are shared, so it follows active_profile.
2306
+ function profileFromManifestRecord(record) {
2307
+ const launchPath = String(record.launch_config_path || "");
2308
+ const match = launchPath.match(/pennyrouter-([^/\\.]+)\.config\.toml$/);
2309
+ return match ? match[1] : null;
2310
+ }
2311
+
2280
2312
  function pruneUnsupportedManifestRecords(manifest) {
2281
2313
  manifest.installations ||= [];
2282
2314
  const kept = manifest.installations.filter((record) => HARNESS_BY_ID[record.harness]);
@@ -2442,7 +2474,7 @@ function printHelp() {
2442
2474
 
2443
2475
  Usage:
2444
2476
  pennyrouter install [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes] [--dry-run] [--make-default|--penny-only] [--no-path-update] [--no-mcp] [--anthropic-auth|--no-anthropic-auth] [--anthropic-api-key KEY] [--openai-api-key KEY] [--openai-proxy-base-url URL --openai-proxy-api-key KEY] [--openrouter-api-key KEY] [--anthropic-proxy-base-url URL --anthropic-proxy-api-key KEY] [--proxy-base-url URL --proxy-api-key KEY] [--private-gateway|--private-server[=URL] [--email you@example.com]] [--gateway-base-url URL] [--private-key pr-...] [--local[=URL]|--prod] [--config PATH]
2445
- pennyrouter update [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--dry-run] [--penny-key pr-...] [--gateway-base-url URL|--local[=URL]|--prod] [--no-mcp]
2477
+ pennyrouter update [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--dry-run] [--penny-key pr-...] [--profile NAME|--private|--cloud] [--gateway-base-url URL|--local[=URL]|--prod] [--no-mcp]
2446
2478
  pennyrouter auth anthropic [--penny-key pr-...] [--token oauth-token] [--token-command CMD] [--gateway-base-url URL] [--forget]
2447
2479
  pennyrouter disable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
2448
2480
  pennyrouter enable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
package/src/cli.test.js CHANGED
@@ -4,7 +4,8 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import test from "node:test";
6
6
  import {
7
- applyConfigFile, harnessFundingChanged, INSTALL_MANIFEST, main, SUPPORTED_HARNESS_IDS,
7
+ applyConfigFile, harnessFundingChanged, INSTALL_MANIFEST, main, parseArgs,
8
+ SUPPORTED_HARNESS_IDS,
8
9
  } from "./cli.js";
9
10
 
10
11
  // --config lets a GUI installer (the macOS DMG) answer install questions up front instead of
@@ -29,6 +30,14 @@ function captureLogs(run) {
29
30
  return run().finally(() => { console.log = original; }).then(() => lines);
30
31
  }
31
32
 
33
+ test("update uses the active profile unless one is explicitly selected", () => {
34
+ assert.equal(parseArgs(["update"]).flags.profile, undefined);
35
+ assert.equal(parseArgs(["update", "--profile", "private"]).flags.profile, "private");
36
+ assert.equal(parseArgs(["update", "--cloud"]).flags.profile, "cloud");
37
+ assert.equal(parseArgs(["install"]).flags.profile, "cloud");
38
+ assert.equal(parseArgs(["install", "--private-gateway"]).flags.profile, "private");
39
+ });
40
+
32
41
  test("--config selects harnesses without prompting", async () => {
33
42
  await withConfigFile({ harnesses: ["claude-code"] }, async (path) => {
34
43
  const lines = await captureLogs(() => main(["install", "--config", path, "--dry-run"]));