@wbern/cc-ping 1.0.0 → 1.2.0
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 +1 -1
- package/dist/cli.js +175 -105
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ Show all accounts with their quota window state — whether they have an active
|
|
|
90
90
|
|
|
91
91
|
### `cc-ping suggest`
|
|
92
92
|
|
|
93
|
-
Recommend which account to use next based on quota window state. Prefers accounts
|
|
93
|
+
Recommend which account to use next based on quota window state. Prefers accounts that need pinging or whose windows are about to reset.
|
|
94
94
|
|
|
95
95
|
### `cc-ping next-reset`
|
|
96
96
|
|
package/dist/cli.js
CHANGED
|
@@ -186,6 +186,26 @@ var init_bell = __esm({
|
|
|
186
186
|
}
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
+
// src/color.ts
|
|
190
|
+
function isColorEnabled() {
|
|
191
|
+
if (process.env.NO_COLOR) return false;
|
|
192
|
+
if (process.env.FORCE_COLOR === "1") return true;
|
|
193
|
+
if (process.env.FORCE_COLOR === "0") return false;
|
|
194
|
+
return process.stdout.isTTY ?? false;
|
|
195
|
+
}
|
|
196
|
+
function wrap(code, text) {
|
|
197
|
+
return isColorEnabled() ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
198
|
+
}
|
|
199
|
+
var green, red, yellow;
|
|
200
|
+
var init_color = __esm({
|
|
201
|
+
"src/color.ts"() {
|
|
202
|
+
"use strict";
|
|
203
|
+
green = (text) => wrap("32", text);
|
|
204
|
+
red = (text) => wrap("31", text);
|
|
205
|
+
yellow = (text) => wrap("33", text);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
189
209
|
// src/history.ts
|
|
190
210
|
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
|
|
191
211
|
import { join as join5 } from "path";
|
|
@@ -463,13 +483,13 @@ async function runPing(accounts, options) {
|
|
|
463
483
|
parallel: options.parallel
|
|
464
484
|
});
|
|
465
485
|
}
|
|
466
|
-
|
|
467
|
-
|
|
486
|
+
const total = results.length;
|
|
487
|
+
for (let idx = 0; idx < results.length; idx++) {
|
|
488
|
+
const r = results[idx];
|
|
489
|
+
const status = r.success ? green("ok") : red("FAIL");
|
|
468
490
|
const detail = r.error ? ` (${r.error})` : "";
|
|
469
|
-
const cr = r.claudeResponse;
|
|
470
|
-
const costInfo = cr ? ` $${cr.total_cost_usd.toFixed(4)} ${cr.usage.input_tokens + cr.usage.output_tokens} tok` : "";
|
|
471
491
|
logger.log(
|
|
472
|
-
` ${r.handle}: ${status} ${r.durationMs}ms${detail}
|
|
492
|
+
` [${idx + 1}/${total}] ${r.handle}: ${status} ${r.durationMs}ms${detail}`
|
|
473
493
|
);
|
|
474
494
|
appendHistoryEntry({
|
|
475
495
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -480,13 +500,13 @@ async function runPing(accounts, options) {
|
|
|
480
500
|
});
|
|
481
501
|
if (r.success) {
|
|
482
502
|
let meta;
|
|
483
|
-
if (
|
|
503
|
+
if (r.claudeResponse) {
|
|
484
504
|
meta = {
|
|
485
|
-
costUsd:
|
|
486
|
-
inputTokens:
|
|
487
|
-
outputTokens:
|
|
488
|
-
model:
|
|
489
|
-
sessionId:
|
|
505
|
+
costUsd: r.claudeResponse.total_cost_usd,
|
|
506
|
+
inputTokens: r.claudeResponse.usage.input_tokens,
|
|
507
|
+
outputTokens: r.claudeResponse.usage.output_tokens,
|
|
508
|
+
model: r.claudeResponse.model,
|
|
509
|
+
sessionId: r.claudeResponse.session_id
|
|
490
510
|
};
|
|
491
511
|
}
|
|
492
512
|
recordPing(r.handle, /* @__PURE__ */ new Date(), meta);
|
|
@@ -544,6 +564,7 @@ var init_run_ping = __esm({
|
|
|
544
564
|
"src/run-ping.ts"() {
|
|
545
565
|
"use strict";
|
|
546
566
|
init_bell();
|
|
567
|
+
init_color();
|
|
547
568
|
init_history();
|
|
548
569
|
init_logger();
|
|
549
570
|
init_notify();
|
|
@@ -1402,27 +1423,8 @@ function generateCompletion(shell) {
|
|
|
1402
1423
|
init_config();
|
|
1403
1424
|
init_daemon();
|
|
1404
1425
|
|
|
1405
|
-
// src/
|
|
1406
|
-
|
|
1407
|
-
if (handles.length === 0) return accounts;
|
|
1408
|
-
const unknown = handles.filter((h) => !accounts.some((a) => a.handle === h));
|
|
1409
|
-
if (unknown.length > 0) {
|
|
1410
|
-
throw new Error(`Unknown account(s): ${unknown.join(", ")}`);
|
|
1411
|
-
}
|
|
1412
|
-
const set = new Set(handles);
|
|
1413
|
-
return accounts.filter((a) => set.has(a.handle));
|
|
1414
|
-
}
|
|
1415
|
-
function filterByGroup(accounts, group) {
|
|
1416
|
-
if (!group) return accounts;
|
|
1417
|
-
const filtered = accounts.filter((a) => a.group === group);
|
|
1418
|
-
if (filtered.length === 0) {
|
|
1419
|
-
throw new Error(`No accounts in group: ${group}`);
|
|
1420
|
-
}
|
|
1421
|
-
return filtered;
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
// src/cli.ts
|
|
1425
|
-
init_history();
|
|
1426
|
+
// src/default-command.ts
|
|
1427
|
+
init_config();
|
|
1426
1428
|
|
|
1427
1429
|
// src/identity.ts
|
|
1428
1430
|
import { readFileSync as readFileSync6 } from "fs";
|
|
@@ -1471,76 +1473,25 @@ function findDuplicates(accounts) {
|
|
|
1471
1473
|
return result;
|
|
1472
1474
|
}
|
|
1473
1475
|
|
|
1474
|
-
// src/next-reset.ts
|
|
1475
|
-
init_state();
|
|
1476
|
-
function getNextReset(accounts, now = /* @__PURE__ */ new Date()) {
|
|
1477
|
-
let best = null;
|
|
1478
|
-
for (const account of accounts) {
|
|
1479
|
-
const window = getWindowReset(account.handle, now);
|
|
1480
|
-
if (!window) continue;
|
|
1481
|
-
if (best === null || window.remainingMs < best.remainingMs) {
|
|
1482
|
-
best = {
|
|
1483
|
-
handle: account.handle,
|
|
1484
|
-
configDir: account.configDir,
|
|
1485
|
-
remainingMs: window.remainingMs,
|
|
1486
|
-
resetAt: window.resetAt.toISOString(),
|
|
1487
|
-
timeUntilReset: formatTimeRemaining(window.remainingMs)
|
|
1488
|
-
};
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
return best;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
// src/cli.ts
|
|
1495
|
-
init_notify();
|
|
1496
|
-
init_paths();
|
|
1497
|
-
init_run_ping();
|
|
1498
|
-
|
|
1499
|
-
// src/scan.ts
|
|
1500
|
-
import { existsSync as existsSync6, readdirSync, statSync as statSync2 } from "fs";
|
|
1501
|
-
import { homedir as homedir2 } from "os";
|
|
1502
|
-
import { join as join8 } from "path";
|
|
1503
|
-
var ACCOUNTS_DIR = join8(homedir2(), ".claude-accounts");
|
|
1504
|
-
function scanAccounts() {
|
|
1505
|
-
if (!existsSync6(ACCOUNTS_DIR)) return [];
|
|
1506
|
-
return readdirSync(ACCOUNTS_DIR).filter((name) => {
|
|
1507
|
-
const full = join8(ACCOUNTS_DIR, name);
|
|
1508
|
-
return statSync2(full).isDirectory() && !name.startsWith(".");
|
|
1509
|
-
}).map((name) => ({
|
|
1510
|
-
handle: name,
|
|
1511
|
-
configDir: join8(ACCOUNTS_DIR, name)
|
|
1512
|
-
}));
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
// src/stagger.ts
|
|
1516
|
-
init_state();
|
|
1517
|
-
function calculateStagger(accountCount, windowMs = QUOTA_WINDOW_MS) {
|
|
1518
|
-
if (accountCount <= 1) return 0;
|
|
1519
|
-
return Math.floor(windowMs / accountCount);
|
|
1520
|
-
}
|
|
1521
|
-
function parseStagger(value, accountCount) {
|
|
1522
|
-
if (value === "auto") {
|
|
1523
|
-
return calculateStagger(accountCount);
|
|
1524
|
-
}
|
|
1525
|
-
const minutes = Number(value);
|
|
1526
|
-
if (Number.isNaN(minutes)) {
|
|
1527
|
-
throw new Error(`Invalid stagger value: ${value}`);
|
|
1528
|
-
}
|
|
1529
|
-
if (minutes <= 0) {
|
|
1530
|
-
throw new Error("Stagger must be a positive number");
|
|
1531
|
-
}
|
|
1532
|
-
return minutes * 60 * 1e3;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
1476
|
// src/status.ts
|
|
1477
|
+
init_color();
|
|
1536
1478
|
init_config();
|
|
1537
1479
|
init_state();
|
|
1480
|
+
function colorizeStatus(windowStatus) {
|
|
1481
|
+
switch (windowStatus) {
|
|
1482
|
+
case "active":
|
|
1483
|
+
return green(windowStatus);
|
|
1484
|
+
case "needs ping":
|
|
1485
|
+
return red(windowStatus);
|
|
1486
|
+
default:
|
|
1487
|
+
return yellow(windowStatus);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1538
1490
|
function formatStatusLine(status) {
|
|
1539
1491
|
const ping = status.lastPing === null ? "never" : status.lastPing.replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
1540
1492
|
const reset = status.timeUntilReset !== null ? ` (resets in ${status.timeUntilReset})` : "";
|
|
1541
|
-
const cost = status.lastCostUsd !== null && status.lastTokens !== null ? ` $${status.lastCostUsd.toFixed(4)} ${status.lastTokens} tok` : "";
|
|
1542
1493
|
const dup = status.duplicateOf ? ` [duplicate of ${status.duplicateOf}]` : "";
|
|
1543
|
-
return ` ${status.handle}: ${status.windowStatus} last ping: ${ping}${reset}${
|
|
1494
|
+
return ` ${status.handle}: ${colorizeStatus(status.windowStatus)} last ping: ${ping}${reset}${dup}`;
|
|
1544
1495
|
}
|
|
1545
1496
|
function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicates) {
|
|
1546
1497
|
const dupLookup = /* @__PURE__ */ new Map();
|
|
@@ -1575,7 +1526,7 @@ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicat
|
|
|
1575
1526
|
handle: account.handle,
|
|
1576
1527
|
configDir: account.configDir,
|
|
1577
1528
|
lastPing: lastPing.toISOString(),
|
|
1578
|
-
windowStatus: window ? "active" : "
|
|
1529
|
+
windowStatus: window ? "active" : "needs ping",
|
|
1579
1530
|
timeUntilReset: window ? formatTimeRemaining(window.remainingMs) : null,
|
|
1580
1531
|
lastCostUsd,
|
|
1581
1532
|
lastTokens,
|
|
@@ -1596,6 +1547,117 @@ function printAccountTable(log = console.log, now = /* @__PURE__ */ new Date())
|
|
|
1596
1547
|
}
|
|
1597
1548
|
}
|
|
1598
1549
|
|
|
1550
|
+
// src/default-command.ts
|
|
1551
|
+
function showDefault(log = console.log, now = /* @__PURE__ */ new Date()) {
|
|
1552
|
+
const accounts = listAccounts();
|
|
1553
|
+
if (accounts.length === 0) {
|
|
1554
|
+
log("No accounts configured.");
|
|
1555
|
+
log("\nGet started:");
|
|
1556
|
+
log(" cc-ping scan Auto-discover accounts");
|
|
1557
|
+
log(" cc-ping add <h> <d> Add an account manually");
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const dupes = findDuplicates(accounts);
|
|
1561
|
+
const statuses = getAccountStatuses(accounts, now, dupes);
|
|
1562
|
+
for (const s of statuses) {
|
|
1563
|
+
log(formatStatusLine(s));
|
|
1564
|
+
}
|
|
1565
|
+
const needsPing = statuses.filter((s) => s.windowStatus !== "active");
|
|
1566
|
+
if (needsPing.length > 0) {
|
|
1567
|
+
log("");
|
|
1568
|
+
log("Suggested next steps:");
|
|
1569
|
+
log(" cc-ping ping Ping accounts that need it");
|
|
1570
|
+
log(" cc-ping daemon start Auto-ping on a schedule");
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/filter-accounts.ts
|
|
1575
|
+
init_state();
|
|
1576
|
+
function filterAccounts(accounts, handles) {
|
|
1577
|
+
if (handles.length === 0) return accounts;
|
|
1578
|
+
const unknown = handles.filter((h) => !accounts.some((a) => a.handle === h));
|
|
1579
|
+
if (unknown.length > 0) {
|
|
1580
|
+
throw new Error(`Unknown account(s): ${unknown.join(", ")}`);
|
|
1581
|
+
}
|
|
1582
|
+
const set = new Set(handles);
|
|
1583
|
+
return accounts.filter((a) => set.has(a.handle));
|
|
1584
|
+
}
|
|
1585
|
+
function filterNeedsPing(accounts, now = /* @__PURE__ */ new Date()) {
|
|
1586
|
+
return accounts.filter((a) => !getWindowReset(a.handle, now));
|
|
1587
|
+
}
|
|
1588
|
+
function filterByGroup(accounts, group) {
|
|
1589
|
+
if (!group) return accounts;
|
|
1590
|
+
const filtered = accounts.filter((a) => a.group === group);
|
|
1591
|
+
if (filtered.length === 0) {
|
|
1592
|
+
throw new Error(`No accounts in group: ${group}`);
|
|
1593
|
+
}
|
|
1594
|
+
return filtered;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// src/cli.ts
|
|
1598
|
+
init_history();
|
|
1599
|
+
|
|
1600
|
+
// src/next-reset.ts
|
|
1601
|
+
init_state();
|
|
1602
|
+
function getNextReset(accounts, now = /* @__PURE__ */ new Date()) {
|
|
1603
|
+
let best = null;
|
|
1604
|
+
for (const account of accounts) {
|
|
1605
|
+
const window = getWindowReset(account.handle, now);
|
|
1606
|
+
if (!window) continue;
|
|
1607
|
+
if (best === null || window.remainingMs < best.remainingMs) {
|
|
1608
|
+
best = {
|
|
1609
|
+
handle: account.handle,
|
|
1610
|
+
configDir: account.configDir,
|
|
1611
|
+
remainingMs: window.remainingMs,
|
|
1612
|
+
resetAt: window.resetAt.toISOString(),
|
|
1613
|
+
timeUntilReset: formatTimeRemaining(window.remainingMs)
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return best;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/cli.ts
|
|
1621
|
+
init_notify();
|
|
1622
|
+
init_paths();
|
|
1623
|
+
init_run_ping();
|
|
1624
|
+
|
|
1625
|
+
// src/scan.ts
|
|
1626
|
+
import { existsSync as existsSync6, readdirSync, statSync as statSync2 } from "fs";
|
|
1627
|
+
import { homedir as homedir2 } from "os";
|
|
1628
|
+
import { join as join8 } from "path";
|
|
1629
|
+
var ACCOUNTS_DIR = join8(homedir2(), ".claude-accounts");
|
|
1630
|
+
function scanAccounts() {
|
|
1631
|
+
if (!existsSync6(ACCOUNTS_DIR)) return [];
|
|
1632
|
+
return readdirSync(ACCOUNTS_DIR).filter((name) => {
|
|
1633
|
+
const full = join8(ACCOUNTS_DIR, name);
|
|
1634
|
+
return statSync2(full).isDirectory() && !name.startsWith(".");
|
|
1635
|
+
}).map((name) => ({
|
|
1636
|
+
handle: name,
|
|
1637
|
+
configDir: join8(ACCOUNTS_DIR, name)
|
|
1638
|
+
}));
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// src/stagger.ts
|
|
1642
|
+
init_state();
|
|
1643
|
+
function calculateStagger(accountCount, windowMs = QUOTA_WINDOW_MS) {
|
|
1644
|
+
if (accountCount <= 1) return 0;
|
|
1645
|
+
return Math.floor(windowMs / accountCount);
|
|
1646
|
+
}
|
|
1647
|
+
function parseStagger(value, accountCount) {
|
|
1648
|
+
if (value === "auto") {
|
|
1649
|
+
return calculateStagger(accountCount);
|
|
1650
|
+
}
|
|
1651
|
+
const minutes = Number(value);
|
|
1652
|
+
if (Number.isNaN(minutes)) {
|
|
1653
|
+
throw new Error(`Invalid stagger value: ${value}`);
|
|
1654
|
+
}
|
|
1655
|
+
if (minutes <= 0) {
|
|
1656
|
+
throw new Error("Stagger must be a positive number");
|
|
1657
|
+
}
|
|
1658
|
+
return minutes * 60 * 1e3;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1599
1661
|
// src/suggest.ts
|
|
1600
1662
|
init_state();
|
|
1601
1663
|
function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
|
|
@@ -1627,7 +1689,7 @@ function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
|
|
|
1627
1689
|
}
|
|
1628
1690
|
|
|
1629
1691
|
// src/cli.ts
|
|
1630
|
-
var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.
|
|
1692
|
+
var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.2.0").option(
|
|
1631
1693
|
"--config <path>",
|
|
1632
1694
|
"Path to config directory (default: ~/.config/cc-ping, env: CC_PING_CONFIG)"
|
|
1633
1695
|
).hook("preAction", (thisCommand) => {
|
|
@@ -1635,8 +1697,13 @@ var program = new Command().name("cc-ping").description("Ping Claude Code sessio
|
|
|
1635
1697
|
if (opts.config) {
|
|
1636
1698
|
setConfigDir(opts.config);
|
|
1637
1699
|
}
|
|
1700
|
+
}).action(() => {
|
|
1701
|
+
showDefault();
|
|
1638
1702
|
});
|
|
1639
|
-
program.command("ping").description("Ping configured accounts to start quota windows").argument(
|
|
1703
|
+
program.command("ping").description("Ping configured accounts to start quota windows").argument(
|
|
1704
|
+
"[handles...]",
|
|
1705
|
+
"Specific handles to ping (default: accounts that need it)"
|
|
1706
|
+
).option("--parallel", "Ping all accounts in parallel", false).option("-q, --quiet", "Suppress all output except errors (for cron)", false).option("--json", "Output results as JSON", false).option("-g, --group <group>", "Ping only accounts in this group").option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).option(
|
|
1640
1707
|
"--stagger <minutes|auto>",
|
|
1641
1708
|
"Delay between account pings (minutes or 'auto')"
|
|
1642
1709
|
).action(async (handles, opts) => {
|
|
@@ -1647,10 +1714,13 @@ program.command("ping").description("Ping configured accounts to start quota win
|
|
|
1647
1714
|
);
|
|
1648
1715
|
process.exit(1);
|
|
1649
1716
|
}
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
)
|
|
1717
|
+
const grouped = filterByGroup(accounts, opts.group);
|
|
1718
|
+
const selected = handles.length > 0 ? filterAccounts(grouped, handles) : grouped;
|
|
1719
|
+
const targets = handles.length > 0 ? selected : filterNeedsPing(selected);
|
|
1720
|
+
if (targets.length === 0) {
|
|
1721
|
+
console.log("All accounts have active windows. Nothing to ping.");
|
|
1722
|
+
process.exit(0);
|
|
1723
|
+
}
|
|
1654
1724
|
const staggerMs = opts.stagger ? parseStagger(opts.stagger, targets.length) : void 0;
|
|
1655
1725
|
const exitCode = await runPing(targets, {
|
|
1656
1726
|
parallel: opts.parallel,
|
|
@@ -1837,7 +1907,7 @@ daemon.command("start").description("Start the daemon process").option(
|
|
|
1837
1907
|
const svc = getServiceStatus2();
|
|
1838
1908
|
if (!svc.installed) {
|
|
1839
1909
|
console.log(
|
|
1840
|
-
"Hint: won't survive a reboot. Use `daemon install` for a persistent service."
|
|
1910
|
+
"Hint: won't survive a reboot. Use `cc-ping daemon install` for a persistent service."
|
|
1841
1911
|
);
|
|
1842
1912
|
}
|
|
1843
1913
|
printAccountTable();
|
|
@@ -1853,7 +1923,7 @@ daemon.command("stop").description("Stop the daemon process").action(async () =>
|
|
|
1853
1923
|
const svc = getServiceStatus2();
|
|
1854
1924
|
if (svc.installed) {
|
|
1855
1925
|
console.log(
|
|
1856
|
-
"Note: system service is installed. The daemon may restart. Use `daemon uninstall` to fully remove."
|
|
1926
|
+
"Note: system service is installed. The daemon may restart. Use `cc-ping daemon uninstall` to fully remove."
|
|
1857
1927
|
);
|
|
1858
1928
|
}
|
|
1859
1929
|
});
|
|
@@ -1929,7 +1999,7 @@ daemon.command("install").description("Install daemon as a system service (launc
|
|
|
1929
1999
|
}
|
|
1930
2000
|
console.log(`Service installed: ${result.servicePath}`);
|
|
1931
2001
|
console.log(
|
|
1932
|
-
"The daemon will start automatically on login. Use `daemon uninstall` to remove."
|
|
2002
|
+
"The daemon will start automatically on login. Use `cc-ping daemon uninstall` to remove."
|
|
1933
2003
|
);
|
|
1934
2004
|
});
|
|
1935
2005
|
daemon.command("uninstall").description("Remove daemon system service").action(async () => {
|