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 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 a checkbox list. Tools with
26
- detected configs start checked; press Enter to accept or type tool IDs to toggle them.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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
@@ -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 Object.values(HARNESS_BY_ID).sort((left, right) => {
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 claudeUserCommandPath() {
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 writeClaudeUserCommand() {
184
- const path = claudeUserCommandPath();
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 writeClaudeUserCommand();
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 command = await readText(claudeUserCommandPath(), "");
262
- if (command.includes(CLAUDE_COMMAND_MARKER)) {
263
- await unlink(claudeUserCommandPath()).catch((error) => {
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
  }
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import {
7
7
  buildPennyInvocation,
8
8
  claudeLaunchSettingsPath,
9
- claudeUserCommandPath,
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(claudeUserCommandPath(), "utf8");
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(claudeUserCommandPath()));
116
- fs.mkdirSync(path.dirname(claudeUserCommandPath()), { recursive: true });
117
- fs.writeFileSync(claudeUserCommandPath(), "user-owned command\n");
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(claudeUserCommandPath(), "utf8"), "user-owned command\n");
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");