pennyrouter 0.3.48 → 0.3.50
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/package.json +1 -1
- package/src/launch.js +22 -0
- package/standalone/web-demo/compare.html +7 -9
package/package.json
CHANGED
package/src/launch.js
CHANGED
|
@@ -1542,6 +1542,26 @@ export async function runPennyClaudeStats(args, profile) {
|
|
|
1542
1542
|
return result.code ?? 1;
|
|
1543
1543
|
}
|
|
1544
1544
|
|
|
1545
|
+
/** Stop a Penny Compare server already listening on `port`; any other listener is left alone. */
|
|
1546
|
+
async function stopRunningPennyCompare(port) {
|
|
1547
|
+
if (process.platform === "win32") return;
|
|
1548
|
+
const listeners = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { encoding: "utf8" });
|
|
1549
|
+
const pids = (listeners.stdout || "").split("\n").map(Number).filter((pid) => pid > 0 && pid !== process.pid);
|
|
1550
|
+
for (const pid of pids) {
|
|
1551
|
+
const command = spawnSync("ps", ["-o", "command=", "-p", String(pid)], { encoding: "utf8" }).stdout || "";
|
|
1552
|
+
if (!/web-demo[\\/]server\.mjs/.test(command)) {
|
|
1553
|
+
throw new Error(`Port ${port} is in use by another program (pid ${pid}). Stop it or pass --port=<other>.`);
|
|
1554
|
+
}
|
|
1555
|
+
console.log(`Stopping the existing Penny Compare session (pid ${pid}).`);
|
|
1556
|
+
process.kill(pid, "SIGTERM");
|
|
1557
|
+
for (let waited = 0; waited < 3000; waited += 100) {
|
|
1558
|
+
try { process.kill(pid, 0); } catch { break; }
|
|
1559
|
+
await new Promise((done) => setTimeout(done, 100));
|
|
1560
|
+
}
|
|
1561
|
+
try { process.kill(pid, 0); process.kill(pid, "SIGKILL"); } catch {}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1545
1565
|
/** Launch the separately installed Penny Compare app from the `penny` command. */
|
|
1546
1566
|
async function runPennyCompare(args) {
|
|
1547
1567
|
const compareDir = pennyCompareDir();
|
|
@@ -1551,6 +1571,8 @@ async function runPennyCompare(args) {
|
|
|
1551
1571
|
} catch {
|
|
1552
1572
|
throw new Error("Penny Compare is not installed. Run `npx pennyrouter compare` first.");
|
|
1553
1573
|
}
|
|
1574
|
+
const portArg = args.find((arg) => /^--port=\d+$/.test(arg));
|
|
1575
|
+
await stopRunningPennyCompare(Number(portArg?.slice("--port=".length) ?? 8789));
|
|
1554
1576
|
const child = spawn(process.execPath, [executable, "--compare", ...args], {
|
|
1555
1577
|
cwd: process.cwd(), env: process.env, stdio: "inherit",
|
|
1556
1578
|
});
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
.meter .field.read strong { color: var(--accent); }
|
|
86
86
|
.meter .field.write strong { color: var(--warn); }
|
|
87
87
|
.meter .field.in strong, .meter .field.out strong { color: #75ec45; }
|
|
88
|
-
.meter .field.
|
|
88
|
+
.meter .field.side, .meter .field.side strong { color: #77777e; font-weight: 400; }
|
|
89
89
|
.meter .wait { display: none; color: #77777e; }
|
|
90
90
|
.meter.waiting .wait { display: block; }
|
|
91
91
|
.meter.waiting .field { display: none; }
|
|
@@ -804,7 +804,7 @@
|
|
|
804
804
|
: value >= 1_000 ? `${(value / 1_000).toFixed(1)}k` : String(value);
|
|
805
805
|
// Cents alone would round a cheap side call to $0.00.
|
|
806
806
|
const dollars = (value) => `$${value.toLocaleString('en-US',
|
|
807
|
-
{ minimumFractionDigits: value < 1 ?
|
|
807
|
+
{ minimumFractionDigits: value < 1 ? 3 : 2, maximumFractionDigits: value < 1 ? 3 : 2 })}`;
|
|
808
808
|
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
|
|
809
809
|
const meters = {};
|
|
810
810
|
const buildMeter = (side) => {
|
|
@@ -817,13 +817,12 @@
|
|
|
817
817
|
root.append(model);
|
|
818
818
|
const amounts = {};
|
|
819
819
|
// Token rows count the main thread only. Background calls Claude Code makes outside the
|
|
820
|
-
// transcript (auto-mode reviewer, titles) show only as `side` cost;
|
|
821
|
-
for (const
|
|
822
|
-
|
|
823
|
-
const field = document.createElement('span'); field.className = `field ${key}`;
|
|
820
|
+
// transcript (auto-mode reviewer, titles) show only as `side` cost; `cost` is main + side.
|
|
821
|
+
for (const label of ['read', 'write', 'in', 'out', 'total', 'side', 'cost']) {
|
|
822
|
+
const field = document.createElement('span'); field.className = `field ${label}`;
|
|
824
823
|
field.append(document.createTextNode(`${label} `));
|
|
825
|
-
amounts[
|
|
826
|
-
field.append(amounts[
|
|
824
|
+
amounts[label] = document.createElement('strong');
|
|
825
|
+
field.append(amounts[label]);
|
|
827
826
|
root.append(field);
|
|
828
827
|
}
|
|
829
828
|
// Zeros would read as a measurement; the pane has simply not seen a turn yet.
|
|
@@ -865,7 +864,6 @@
|
|
|
865
864
|
fresh_input: main.fresh_input, output: main.output };
|
|
866
865
|
meter.cost = totals.cost;
|
|
867
866
|
const side = snapshot.side_calls;
|
|
868
|
-
meter.amounts.main.textContent = dollars(main.cost);
|
|
869
867
|
meter.amounts.side.textContent = dollars(side?.cost || 0);
|
|
870
868
|
meter.root.classList.add('has-data');
|
|
871
869
|
meter.root.classList.toggle('waiting', !totals.total);
|