pennyrouter 0.1.0 → 0.1.2

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
@@ -1,15 +1,45 @@
1
1
  # PennyRouter CLI
2
2
 
3
- Local installer for PennyRouter coding-agent integrations.
3
+ Official local installer for [PennyRouter](https://pennyrouter.com) coding-agent integrations.
4
+
5
+ PennyRouter is a model gateway for coding tools. This CLI handles the local setup work that
6
+ agents should not do by hand: browser authorization, API-key handoff, config edits, backups,
7
+ status checks, and uninstall metadata.
8
+
9
+ ## Usage
4
10
 
5
11
  ```bash
6
12
  npx pennyrouter install
7
- npx pennyrouter install --harness opencode,codex
13
+ npx pennyrouter install --harness claude-code,cline,opencode,codex
8
14
  npx pennyrouter uninstall
9
15
  npx pennyrouter status
10
16
  ```
11
17
 
12
- The CLI owns local config changes, backups, browser authorization, and uninstall metadata.
13
- The machine-readable pages at `https://pennyrouter.com/install` and
14
- `https://pennyrouter.com/uninstall` should tell coding agents to invoke this CLI instead of
15
- editing config files directly.
18
+ ## Supported Tools
19
+
20
+ - Claude Code
21
+ - Cline (manual setup instructions; no VS Code settings mutation)
22
+ - Codex
23
+ - opencode
24
+
25
+ ## Agent Flow
26
+
27
+ Users can ask a coding agent to follow:
28
+
29
+ ```text
30
+ https://pennyrouter.com/install
31
+ ```
32
+
33
+ That machine-readable page tells the agent to invoke this CLI instead of editing config files
34
+ directly.
35
+
36
+ ## What It Changes
37
+
38
+ The CLI writes only selected local tool configuration files, saves backups under the local
39
+ PennyRouter state directory, and records an uninstall manifest.
40
+
41
+ It does not delete PennyRouter accounts, credits, or remote API keys.
42
+
43
+ ## License
44
+
45
+ MIT
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
+ "homepage": "https://pennyrouter.com",
6
+ "bugs": {
7
+ "url": "https://github.com/jduke99/pennyrouter-cli/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/jduke99/pennyrouter-cli.git"
12
+ },
5
13
  "type": "module",
6
14
  "bin": {
7
15
  "pennyrouter": "bin/pennyrouter.js"
package/src/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { stdin as input, stdout as output } from "node:process";
4
+ import { claudeCodeHarness } from "./harnesses/claude-code.js";
5
+ import { clineHarness } from "./harnesses/cline.js";
4
6
  import { codexHarness } from "./harnesses/codex.js";
5
7
  import { opencodeHarness } from "./harnesses/opencode.js";
6
8
  import {
@@ -19,23 +21,12 @@ import {
19
21
  } from "./state.js";
20
22
 
21
23
  const HARNESS_BY_ID = {
24
+ "claude-code": claudeCodeHarness,
25
+ cline: clineHarness,
22
26
  opencode: opencodeHarness,
23
27
  codex: codexHarness,
24
28
  };
25
29
 
26
- const PLANNED_HARNESSES = [
27
- {
28
- id: "claude-code",
29
- name: "Claude Code",
30
- reason: "Claude Code support is planned; this preview CLI does not mutate Claude settings yet.",
31
- },
32
- {
33
- id: "cline",
34
- name: "Cline",
35
- reason: "Cline support is planned; this preview CLI does not mutate VS Code settings yet.",
36
- },
37
- ];
38
-
39
30
  export async function main(argv = process.argv.slice(2)) {
40
31
  const { command, flags } = parseArgs(argv);
41
32
 
@@ -201,10 +192,11 @@ async function status() {
201
192
  }
202
193
 
203
194
  for (const record of records) {
204
- console.log(`${record.harness}: installed`);
205
- console.log(` config: ${record.config_path}`);
195
+ console.log(`${record.harness}: ${record.manual_required ? "manual setup shown" : "installed"}`);
196
+ if (record.config_path) console.log(` config: ${record.config_path}`);
206
197
  if (record.backup_path) console.log(` backup: ${record.backup_path}`);
207
198
  if (record.installed_at) console.log(` installed: ${record.installed_at}`);
199
+ if (record.manual_required) console.log(" note: finish or remove this integration in the tool UI");
208
200
  }
209
201
  }
210
202
 
@@ -215,17 +207,13 @@ async function selectHarnesses(flags) {
215
207
  if (await harness.detect()) detected.push(harness);
216
208
  }
217
209
 
218
- for (const planned of PLANNED_HARNESSES) {
219
- if (requested.includes(planned.id)) console.log(`${planned.name}: ${planned.reason}`);
220
- }
221
-
222
210
  if (requested.length > 0) {
223
211
  return requested.map((id) => HARNESS_BY_ID[id]).filter(Boolean);
224
212
  }
225
213
 
226
214
  if (detected.length === 0) {
227
215
  console.log("No supported harness configs were detected.");
228
- console.log("Use --harness opencode,codex to create config for specific tools.");
216
+ console.log("Use --harness claude-code,cline,opencode,codex to create config for specific tools.");
229
217
  return [];
230
218
  }
231
219
 
@@ -291,14 +279,14 @@ function printHelp() {
291
279
  console.log(`PennyRouter CLI
292
280
 
293
281
  Usage:
294
- pennyrouter install [--harness opencode,codex] [--all] [--yes] [--dry-run]
295
- pennyrouter uninstall [--harness opencode,codex] [--all] [--yes]
282
+ pennyrouter install [--harness claude-code,cline,opencode,codex] [--all] [--yes] [--dry-run]
283
+ pennyrouter uninstall [--harness claude-code,cline,opencode,codex] [--all] [--yes]
296
284
  pennyrouter status
297
285
 
298
286
  Examples:
299
287
  npx pennyrouter install
300
288
  npx pennyrouter install --all
301
- npx pennyrouter install --harness opencode,codex --yes
289
+ npx pennyrouter install --harness claude-code,cline,opencode,codex --yes
302
290
  npx pennyrouter uninstall
303
291
  `);
304
292
  }
@@ -0,0 +1,56 @@
1
+ import { atomicWrite, backupFile, compactHome, exists, expandHome, readJsonFile } from "../state.js";
2
+
3
+ const CONFIG_PATH = "~/.claude/settings.json";
4
+
5
+ export const claudeCodeHarness = {
6
+ id: "claude-code",
7
+ name: "Claude Code",
8
+
9
+ async detect() {
10
+ return exists(CONFIG_PATH);
11
+ },
12
+
13
+ async planInstall() {
14
+ return {
15
+ configPath: expandHome(CONFIG_PATH),
16
+ summary: [
17
+ `write ${CONFIG_PATH}`,
18
+ "set Anthropic-compatible base URL to PennyRouter",
19
+ "store the PennyRouter API key without printing it",
20
+ ],
21
+ };
22
+ },
23
+
24
+ async install({ apiKey, gatewayBaseUrl, plan }) {
25
+ const backupPath = await backupFile(CONFIG_PATH, "claude-code");
26
+ const current = (await readJsonFile(CONFIG_PATH, null)) || {};
27
+ current.env ||= {};
28
+ current.env.ANTHROPIC_BASE_URL = gatewayBaseUrl;
29
+ current.env.ANTHROPIC_API_KEY = apiKey;
30
+
31
+ await atomicWrite(CONFIG_PATH, `${JSON.stringify(current, null, 2)}\n`);
32
+
33
+ return {
34
+ harness: "claude-code",
35
+ config_path: compactHome(plan.configPath),
36
+ backup_path: backupPath ? compactHome(backupPath) : null,
37
+ installed_at: new Date().toISOString(),
38
+ restart: "restart Claude Code",
39
+ changes: {
40
+ env_keys: ["ANTHROPIC_BASE_URL", "ANTHROPIC_API_KEY"],
41
+ },
42
+ };
43
+ },
44
+
45
+ async uninstall(record) {
46
+ const current = (await readJsonFile(record.config_path, null)) || {};
47
+ if (current.env?.ANTHROPIC_BASE_URL?.includes("pennyrouter.com")) {
48
+ delete current.env.ANTHROPIC_BASE_URL;
49
+ if (typeof current.env.ANTHROPIC_API_KEY === "string" && current.env.ANTHROPIC_API_KEY.startsWith("pr-")) {
50
+ delete current.env.ANTHROPIC_API_KEY;
51
+ }
52
+ }
53
+ if (current.env && Object.keys(current.env).length === 0) delete current.env;
54
+ await atomicWrite(record.config_path, `${JSON.stringify(current, null, 2)}\n`);
55
+ },
56
+ };
@@ -0,0 +1,76 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ export const clineHarness = {
4
+ id: "cline",
5
+ name: "Cline",
6
+
7
+ async detect() {
8
+ return false;
9
+ },
10
+
11
+ async planInstall() {
12
+ return {
13
+ configPath: null,
14
+ summary: [
15
+ "show manual OpenAI Compatible setup instructions",
16
+ "do not write VS Code settings or extension storage",
17
+ "record that manual Cline setup instructions were shown",
18
+ ],
19
+ };
20
+ },
21
+
22
+ async install({ apiKey, gatewayBaseUrl }) {
23
+ const copied = copyToClipboard(apiKey);
24
+
25
+ console.log("");
26
+ console.log("Cline manual setup");
27
+ console.log(" Open Cline settings in VS Code and set:");
28
+ console.log(" - API Provider: OpenAI Compatible");
29
+ console.log(` - Base URL: ${gatewayBaseUrl}/v1`);
30
+ console.log(` - API Key: ${copied ? "copied to clipboard" : "copy from your PennyRouter dashboard"}`);
31
+ console.log(" - Model ID: pennyrouter/auto");
32
+ console.log("");
33
+ console.log("The CLI did not write VS Code settings because Cline stores provider config differently across versions.");
34
+ if (!copied) {
35
+ console.log("Clipboard copy failed, so the key was not printed. Open PennyRouter and create or reveal a key manually for Cline.");
36
+ }
37
+
38
+ return {
39
+ harness: "cline",
40
+ config_path: null,
41
+ backup_path: null,
42
+ installed_at: new Date().toISOString(),
43
+ manual_required: true,
44
+ restart: "save Cline settings, then restart VS Code or reload the Cline window if it does not pick up the provider",
45
+ changes: {
46
+ provider: "OpenAI Compatible",
47
+ base_url: `${gatewayBaseUrl}/v1`,
48
+ model: "pennyrouter/auto",
49
+ },
50
+ };
51
+ },
52
+
53
+ async uninstall() {
54
+ console.log("");
55
+ console.log("Cline manual uninstall");
56
+ console.log(" Open Cline settings in VS Code and remove or replace the PennyRouter OpenAI Compatible provider:");
57
+ console.log(" - Base URL: https://api.pennyrouter.com/v1");
58
+ console.log(" - API Key: pr-...");
59
+ console.log(" - Model ID: pennyrouter/auto");
60
+ },
61
+ };
62
+
63
+ function copyToClipboard(text) {
64
+ const commands =
65
+ process.platform === "darwin"
66
+ ? [["pbcopy"]]
67
+ : process.platform === "win32"
68
+ ? [["clip"]]
69
+ : [["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]];
70
+
71
+ for (const [command, ...args] of commands) {
72
+ const result = spawnSync(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
73
+ if (result.status === 0) return true;
74
+ }
75
+ return false;
76
+ }