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 +36 -6
- package/package.json +9 -1
- package/src/cli.js +11 -23
- package/src/harnesses/claude-code.js +56 -0
- package/src/harnesses/cline.js +76 -0
package/README.md
CHANGED
|
@@ -1,15 +1,45 @@
|
|
|
1
1
|
# PennyRouter CLI
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
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
|
+
}
|