pennyrouter 0.2.1 → 0.2.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 +3 -2
- package/package.json +1 -1
- package/src/cli.js +8 -6
- package/src/launch.js +21 -7
- package/src/launch.test.js +7 -6
package/README.md
CHANGED
|
@@ -22,8 +22,9 @@ npx pennyrouter status
|
|
|
22
22
|
Installation opens a browser and requires login to the PennyRouter account that should own
|
|
23
23
|
the new machine key. CLI installs no longer create anonymous ghost accounts.
|
|
24
24
|
|
|
25
|
-
With no `--harness` flag, the interactive installer shows
|
|
26
|
-
|
|
25
|
+
With no `--harness` flag, the interactive installer shows Claude Code and Codex. Detected
|
|
26
|
+
tools start checked. The additional legacy integrations remain available through an explicit
|
|
27
|
+
`--harness <id>` selection or `--all`.
|
|
27
28
|
|
|
28
29
|
When Claude Code is selected, the installer also offers an optional Claude Pro/Max
|
|
29
30
|
subscription-auth step. Users can skip it during install and enable it later with
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -59,6 +59,7 @@ const HARNESS_BY_ID = {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const SUPPORTED_HARNESS_IDS = Object.keys(HARNESS_BY_ID).join(",");
|
|
62
|
+
const DEFAULT_INSTALL_HARNESS_IDS = ["claude-code", "codex"];
|
|
62
63
|
|
|
63
64
|
export async function main(argv = process.argv.slice(2)) {
|
|
64
65
|
const { command, flags } = parseArgs(argv);
|
|
@@ -945,15 +946,16 @@ async function status() {
|
|
|
945
946
|
|
|
946
947
|
async function selectHarnesses(flags) {
|
|
947
948
|
const requested = flags.all ? Object.keys(HARNESS_BY_ID) : parseHarnessList(flags.harness);
|
|
948
|
-
const detected = [];
|
|
949
|
-
for (const harness of Object.values(HARNESS_BY_ID)) {
|
|
950
|
-
if (await safeDetect(harness)) detected.push(harness);
|
|
951
|
-
}
|
|
952
|
-
|
|
953
949
|
if (requested.length > 0) {
|
|
954
950
|
return requested.map((id) => HARNESS_BY_ID[id]).filter(Boolean);
|
|
955
951
|
}
|
|
956
952
|
|
|
953
|
+
const detected = [];
|
|
954
|
+
for (const id of DEFAULT_INSTALL_HARNESS_IDS) {
|
|
955
|
+
const harness = HARNESS_BY_ID[id];
|
|
956
|
+
if (await safeDetect(harness)) detected.push(harness);
|
|
957
|
+
}
|
|
958
|
+
|
|
957
959
|
if (flags.yes) return detected;
|
|
958
960
|
|
|
959
961
|
return promptHarnessCheckboxes(detected);
|
|
@@ -1079,7 +1081,7 @@ async function promptHarnessCheckboxesByText(harnesses, selectedIds) {
|
|
|
1079
1081
|
}
|
|
1080
1082
|
|
|
1081
1083
|
function orderedHarnessesForPrompt(detectedIds) {
|
|
1082
|
-
return
|
|
1084
|
+
return DEFAULT_INSTALL_HARNESS_IDS.map((id) => HARNESS_BY_ID[id]).sort((left, right) => {
|
|
1083
1085
|
const leftDetected = detectedIds.has(left.id);
|
|
1084
1086
|
const rightDetected = detectedIds.has(right.id);
|
|
1085
1087
|
if (leftDetected !== rightDetected) return leftDetected ? -1 : 1;
|
package/src/launch.js
CHANGED
|
@@ -54,7 +54,11 @@ export function codexLaunchProfilePath() {
|
|
|
54
54
|
return join(codexHome, "pennyrouter.config.toml");
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function
|
|
57
|
+
export function claudeUserSkillPath() {
|
|
58
|
+
return join(homeDir(), ".claude", "skills", "penny", "SKILL.md");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function legacyClaudeUserCommandPath() {
|
|
58
62
|
return join(homeDir(), ".claude", "commands", "penny.md");
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -180,8 +184,8 @@ async function writeClaudeLaunchSettings() {
|
|
|
180
184
|
await privateWrite(claudeLaunchSettingsPath(), `${JSON.stringify(overlay, null, 2)}\n`);
|
|
181
185
|
}
|
|
182
186
|
|
|
183
|
-
async function
|
|
184
|
-
const path =
|
|
187
|
+
async function writeClaudeUserSkill() {
|
|
188
|
+
const path = claudeUserSkillPath();
|
|
185
189
|
const existing = await readText(path, "");
|
|
186
190
|
if (existing.trim() && !existing.includes(CLAUDE_COMMAND_MARKER)) {
|
|
187
191
|
throw new Error(
|
|
@@ -191,14 +195,23 @@ async function writeClaudeUserCommand() {
|
|
|
191
195
|
await mkdir(dirname(path), { recursive: true });
|
|
192
196
|
await atomicWrite(path, [
|
|
193
197
|
"---",
|
|
198
|
+
"name: penny",
|
|
194
199
|
"description: PennyRouter commands and help.",
|
|
195
200
|
"argument-hint: <command and arguments>",
|
|
201
|
+
"disable-model-invocation: true",
|
|
196
202
|
"---",
|
|
197
203
|
CLAUDE_COMMAND_MARKER,
|
|
198
204
|
"",
|
|
199
205
|
"@penny $ARGUMENTS",
|
|
200
206
|
"",
|
|
201
207
|
].join("\n"));
|
|
208
|
+
const legacyPath = legacyClaudeUserCommandPath();
|
|
209
|
+
const legacy = await readText(legacyPath, "");
|
|
210
|
+
if (legacy.includes(CLAUDE_COMMAND_MARKER)) {
|
|
211
|
+
await unlink(legacyPath).catch((error) => {
|
|
212
|
+
if (error.code !== "ENOENT") throw error;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
202
215
|
}
|
|
203
216
|
|
|
204
217
|
async function writeCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth }) {
|
|
@@ -223,7 +236,7 @@ export async function configurePennyHarness(harness, {
|
|
|
223
236
|
config.api_key = apiKey;
|
|
224
237
|
config.harnesses ||= {};
|
|
225
238
|
if (harness === "claude-code") {
|
|
226
|
-
await
|
|
239
|
+
await writeClaudeUserSkill();
|
|
227
240
|
await writeClaudeLaunchSettings();
|
|
228
241
|
config.harnesses.claude = {
|
|
229
242
|
enabled: true,
|
|
@@ -258,9 +271,10 @@ export async function removePennyHarness(harness) {
|
|
|
258
271
|
await unlink(claudeLaunchSettingsPath()).catch((error) => {
|
|
259
272
|
if (error.code !== "ENOENT") throw error;
|
|
260
273
|
});
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
274
|
+
for (const path of [claudeUserSkillPath(), legacyClaudeUserCommandPath()]) {
|
|
275
|
+
const command = await readText(path, "");
|
|
276
|
+
if (!command.includes(CLAUDE_COMMAND_MARKER)) continue;
|
|
277
|
+
await unlink(path).catch((error) => {
|
|
264
278
|
if (error.code !== "ENOENT") throw error;
|
|
265
279
|
});
|
|
266
280
|
}
|
package/src/launch.test.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import {
|
|
7
7
|
buildPennyInvocation,
|
|
8
8
|
claudeLaunchSettingsPath,
|
|
9
|
-
|
|
9
|
+
claudeUserSkillPath,
|
|
10
10
|
codexLaunchProfilePath,
|
|
11
11
|
configurePennyHarness,
|
|
12
12
|
fileMode,
|
|
@@ -108,19 +108,20 @@ async function main() {
|
|
|
108
108
|
assert.throws(() => buildPennyInvocation("claude", ["--settings=x"], config), /manages --settings/);
|
|
109
109
|
assert.throws(() => buildPennyInvocation("codex", ["-p", "other"], config), /manages --profile/);
|
|
110
110
|
|
|
111
|
-
const command = fs.readFileSync(
|
|
111
|
+
const command = fs.readFileSync(claudeUserSkillPath(), "utf8");
|
|
112
112
|
assert.ok(command.includes("@penny $ARGUMENTS"));
|
|
113
|
+
assert.ok(command.includes("name: penny"));
|
|
113
114
|
|
|
114
115
|
await removePennyHarness("claude-code");
|
|
115
|
-
assert.ok(!fs.existsSync(
|
|
116
|
-
fs.mkdirSync(path.dirname(
|
|
117
|
-
fs.writeFileSync(
|
|
116
|
+
assert.ok(!fs.existsSync(claudeUserSkillPath()));
|
|
117
|
+
fs.mkdirSync(path.dirname(claudeUserSkillPath()), { recursive: true });
|
|
118
|
+
fs.writeFileSync(claudeUserSkillPath(), "user-owned command\n");
|
|
118
119
|
await assert.rejects(configurePennyHarness("claude-code", {
|
|
119
120
|
apiKey: "pr-super-secret",
|
|
120
121
|
gatewayBaseUrl: "https://gateway.example/",
|
|
121
122
|
models: {},
|
|
122
123
|
}), /not managed by PennyRouter/);
|
|
123
|
-
assert.equal(fs.readFileSync(
|
|
124
|
+
assert.equal(fs.readFileSync(claudeUserSkillPath(), "utf8"), "user-owned command\n");
|
|
124
125
|
assert.ok(fs.existsSync(codexLaunchProfilePath()));
|
|
125
126
|
assert.ok(fs.existsSync(pennyShimPath()));
|
|
126
127
|
await removePennyHarness("codex");
|