@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +175 -105
  3. 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 whose windows have already expired or are about to.
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
- for (const r of results) {
467
- const status = r.success ? "ok" : "FAIL";
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}${costInfo}`
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 (cr) {
503
+ if (r.claudeResponse) {
484
504
  meta = {
485
- costUsd: cr.total_cost_usd,
486
- inputTokens: cr.usage.input_tokens,
487
- outputTokens: cr.usage.output_tokens,
488
- model: cr.model,
489
- sessionId: cr.session_id
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/filter-accounts.ts
1406
- function filterAccounts(accounts, handles) {
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}${cost}${dup}`;
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" : "expired",
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.0.0").option(
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("[handles...]", "Specific account handles to ping (default: all)").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(
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 targets = filterAccounts(
1651
- filterByGroup(accounts, opts.group),
1652
- handles
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 () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/cc-ping",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Ping Claude Code sessions to trigger quota windows early across multiple accounts",
5
5
  "type": "module",
6
6
  "bin": {