pennyrouter 0.3.24 → 0.3.26
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 +2 -1
- package/package.json +1 -1
- package/src/cli.js +11 -2
- package/src/harnesses/codex.js +19 -4
- package/src/launch.js +28 -10
- package/src/launch.test.js +6 -0
package/README.md
CHANGED
|
@@ -87,7 +87,8 @@ of `npx pennyrouter install`; to install or refresh it separately, run:
|
|
|
87
87
|
npx pennyrouter compare
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
The installer puts `penny-compare` and `penny-session` on PATH alongside the `penny` launcher.
|
|
91
|
+
Use `penny-compare` for a side-by-side run or `penny-session` for one Penny-routed session.
|
|
91
92
|
The bundle requires `tmux`, Python 3.10+, and the harnesses being compared.
|
|
92
93
|
|
|
93
94
|
On macOS and Linux, a normal install adds a small `penny` launcher under `~/.local/bin`.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -19,7 +19,13 @@ import {
|
|
|
19
19
|
} from "./harnesses/claude-code.js";
|
|
20
20
|
import { clineHarness } from "./harnesses/cline.js";
|
|
21
21
|
import { codeGptHarness } from "./harnesses/codegpt.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
codexHarness,
|
|
24
|
+
detectCodexChatgptAuth,
|
|
25
|
+
isCodexSubscriptionAuthGateway,
|
|
26
|
+
isHostedPennyRouterGateway,
|
|
27
|
+
isLocalPennyRouterGateway,
|
|
28
|
+
} from "./harnesses/codex.js";
|
|
23
29
|
import { fetchOpenCodeModels, opencodeHarness } from "./harnesses/opencode.js";
|
|
24
30
|
import { windsurfHarness } from "./harnesses/windsurf.js";
|
|
25
31
|
import { zedHarness } from "./harnesses/zed.js";
|
|
@@ -39,7 +45,10 @@ import {
|
|
|
39
45
|
} from "./session.js";
|
|
40
46
|
|
|
41
47
|
async function codexNativeAuthAvailable({ gatewayBaseUrl, apiKey, localAuth }) {
|
|
42
|
-
if (!
|
|
48
|
+
if (!isCodexSubscriptionAuthGateway(gatewayBaseUrl)) return false;
|
|
49
|
+
// A local gateway should use Codex's detected login as the opt-in. Do not infer
|
|
50
|
+
// subscription auth from some other upstream credential stored on the local gateway.
|
|
51
|
+
if (isLocalPennyRouterGateway(gatewayBaseUrl)) return Boolean(localAuth);
|
|
43
52
|
const capabilities = await fetchGatewayCapabilities({ gatewayBaseUrl, apiKey });
|
|
44
53
|
const hasOpenAiCredential = Array.isArray(capabilities?.upstream_credentials)
|
|
45
54
|
&& capabilities.upstream_credentials.includes("openai_api");
|
package/src/harnesses/codex.js
CHANGED
|
@@ -103,12 +103,13 @@ export function renderCodexConfig(
|
|
|
103
103
|
text,
|
|
104
104
|
{ apiKey, gatewayBaseUrl, subscriptionAuth, surface = "codex-terminal" },
|
|
105
105
|
) {
|
|
106
|
-
// Native Codex subscription
|
|
107
|
-
//
|
|
108
|
-
// ordinary PennyRouter bearer provider, even when Codex itself is logged into ChatGPT.
|
|
106
|
+
// Native Codex ChatGPT subscription auth is allowed through the hosted gateway and the
|
|
107
|
+
// explicitly supported local development gateway. Other custom gateways use Penny auth.
|
|
109
108
|
// Keep this guard at the final renderer so every install/update/desktop path gets the
|
|
110
109
|
// same safety rule regardless of which profile selected it.
|
|
111
|
-
subscriptionAuth = Boolean(
|
|
110
|
+
subscriptionAuth = Boolean(
|
|
111
|
+
subscriptionAuth && isCodexSubscriptionAuthGateway(gatewayBaseUrl),
|
|
112
|
+
);
|
|
112
113
|
const clean = removeManagedCodexConfig(text, { removeRootProvider: true }).trimEnd();
|
|
113
114
|
if (new RegExp(`^\\s*\\[model_providers\\.${escapeRegex(PROVIDER_ID)}\\]\\s*$`, "m").test(clean)) {
|
|
114
115
|
throw new Error(
|
|
@@ -156,6 +157,20 @@ export function isHostedPennyRouterGateway(gatewayBaseUrl) {
|
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
export function isLocalPennyRouterGateway(gatewayBaseUrl) {
|
|
161
|
+
try {
|
|
162
|
+
const url = new URL(String(gatewayBaseUrl));
|
|
163
|
+
return url.hostname.toLowerCase() === "localhost" && url.port === "8400";
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function isCodexSubscriptionAuthGateway(gatewayBaseUrl) {
|
|
170
|
+
return isHostedPennyRouterGateway(gatewayBaseUrl)
|
|
171
|
+
|| isLocalPennyRouterGateway(gatewayBaseUrl);
|
|
172
|
+
}
|
|
173
|
+
|
|
159
174
|
export function removeManagedCodexConfig(text, { removeRootProvider = false } = {}) {
|
|
160
175
|
let out = String(text || "").replace(
|
|
161
176
|
new RegExp(`${escapeRegex(MANAGED_START)}[\\s\\S]*?${escapeRegex(MANAGED_END)}\\n?`, "g"),
|
package/src/launch.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import {
|
|
3
3
|
chmod,
|
|
4
4
|
cp,
|
|
5
|
+
lstat,
|
|
5
6
|
mkdtemp,
|
|
6
7
|
mkdir,
|
|
7
8
|
readFile,
|
|
@@ -23,7 +24,10 @@ import {
|
|
|
23
24
|
upgradeClaudeCodeModelValue,
|
|
24
25
|
writeClaudeCodeModelPresentation,
|
|
25
26
|
} from "./harnesses/claude-code.js";
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
isCodexSubscriptionAuthGateway,
|
|
29
|
+
renderCodexConfig,
|
|
30
|
+
} from "./harnesses/codex.js";
|
|
27
31
|
import {
|
|
28
32
|
OPENCODE_MODEL_CHOICES,
|
|
29
33
|
fetchOpenCodeModels,
|
|
@@ -284,6 +288,24 @@ export async function installPennyCompare() {
|
|
|
284
288
|
await cp(join(source, entry), join(destination, entry), { recursive: true, force: true });
|
|
285
289
|
}
|
|
286
290
|
await mkdir(join(destination, "reports"), { recursive: true, mode: 0o700 });
|
|
291
|
+
await mkdir(pennyBinDir(), { recursive: true, mode: 0o755 });
|
|
292
|
+
for (const entry of ["penny-compare", "penny-session", "penny-compare-viewer"]) {
|
|
293
|
+
const link = join(pennyBinDir(), entry);
|
|
294
|
+
const target = join(destination, entry);
|
|
295
|
+
let replace = false;
|
|
296
|
+
try {
|
|
297
|
+
replace = (await lstat(link)).isSymbolicLink();
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error.code !== "ENOENT") throw error;
|
|
300
|
+
}
|
|
301
|
+
if (replace) await unlink(link);
|
|
302
|
+
try {
|
|
303
|
+
await symlink(target, link);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error.code !== "EEXIST") throw error;
|
|
306
|
+
// Leave a user-owned regular file in place rather than overwriting it.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
287
309
|
return { supported: true, installed: true, path: destination };
|
|
288
310
|
}
|
|
289
311
|
|
|
@@ -421,16 +443,12 @@ export async function installPennyRuntime({ updatePath = true } = {}) {
|
|
|
421
443
|
}
|
|
422
444
|
|
|
423
445
|
export function renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth }) {
|
|
424
|
-
//
|
|
425
|
-
// Apply the same
|
|
446
|
+
// Native ChatGPT subscription auth is allowed for the hosted gateway and the explicitly
|
|
447
|
+
// supported local development gateway. Apply the same rule as the config renderer at this
|
|
426
448
|
// boundary, since this path is what bare `penny codex` selects at runtime.
|
|
427
|
-
|
|
428
|
-
subscriptionAuth
|
|
429
|
-
|
|
430
|
-
);
|
|
431
|
-
} catch {
|
|
432
|
-
subscriptionAuth = false;
|
|
433
|
-
}
|
|
449
|
+
subscriptionAuth = Boolean(
|
|
450
|
+
subscriptionAuth && isCodexSubscriptionAuthGateway(gatewayBaseUrl),
|
|
451
|
+
);
|
|
434
452
|
if (subscriptionAuth) {
|
|
435
453
|
return [
|
|
436
454
|
CODEX_PROFILE_MARKER,
|
package/src/launch.test.js
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
isCodexDesktopRunning,
|
|
21
21
|
loadPennyConfig,
|
|
22
22
|
pennyConfigPath,
|
|
23
|
+
pennyCompareDir,
|
|
24
|
+
pennyBinDir,
|
|
23
25
|
pennyRuntimeDir,
|
|
24
26
|
pennyShimPath,
|
|
25
27
|
readClaudeDesktopCredential,
|
|
@@ -100,6 +102,10 @@ async function main() {
|
|
|
100
102
|
assert.equal(second.changed, false);
|
|
101
103
|
assert.equal(await fileMode(pennyShimPath()), 0o755);
|
|
102
104
|
assert.ok(fs.existsSync(path.join(pennyRuntimeDir(), "bin", "penny.js")));
|
|
105
|
+
assert.equal(
|
|
106
|
+
fs.realpathSync(path.join(pennyBinDir(), "penny-compare")),
|
|
107
|
+
fs.realpathSync(path.join(pennyCompareDir(), "penny-compare")),
|
|
108
|
+
);
|
|
103
109
|
assert.ok(!fs.existsSync(staleSkill), "runtime upgrade removes stale plugin files");
|
|
104
110
|
const rc = fs.readFileSync(path.join(home, ".zshrc"), "utf8");
|
|
105
111
|
assert.equal((rc.match(/>>> pennyrouter PATH >>>/g) || []).length, 1);
|