@wbern/cc-ping 1.5.1 → 1.7.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 (2) hide show
  1. package/dist/cli.js +113 -42
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -9,6 +9,27 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/color.ts
13
+ function isColorEnabled() {
14
+ if (process.env.NO_COLOR) return false;
15
+ if (process.env.FORCE_COLOR === "1") return true;
16
+ if (process.env.FORCE_COLOR === "0") return false;
17
+ return process.stdout.isTTY ?? false;
18
+ }
19
+ function wrap(code, text) {
20
+ return isColorEnabled() ? `\x1B[${code}m${text}\x1B[0m` : text;
21
+ }
22
+ var green, red, yellow, blue;
23
+ var init_color = __esm({
24
+ "src/color.ts"() {
25
+ "use strict";
26
+ green = (text) => wrap("32", text);
27
+ red = (text) => wrap("31", text);
28
+ yellow = (text) => wrap("33", text);
29
+ blue = (text) => wrap("34", text);
30
+ }
31
+ });
32
+
12
33
  // src/paths.ts
13
34
  var paths_exports = {};
14
35
  __export(paths_exports, {
@@ -186,26 +207,6 @@ var init_bell = __esm({
186
207
  }
187
208
  });
188
209
 
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
-
209
210
  // src/history.ts
210
211
  import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
211
212
  import { join as join5 } from "path";
@@ -991,12 +992,22 @@ async function stopDaemon(deps) {
991
992
  process.kill(pid2, "SIGTERM");
992
993
  }
993
994
  });
995
+ const _forceKill = deps?.forceKill ?? /* c8 ignore next 7 -- production default */
996
+ ((pid2) => {
997
+ if (process.platform === "win32") {
998
+ execSync(`taskkill /F /PID ${pid2}`);
999
+ } else {
1000
+ process.kill(pid2, "SIGKILL");
1001
+ }
1002
+ });
1003
+ const _log = deps?.log ?? ((msg) => console.log(msg));
994
1004
  const status = _getDaemonStatus();
995
1005
  if (!status.running || !status.pid) {
996
1006
  return { success: false, error: "Daemon is not running" };
997
1007
  }
998
1008
  const pid = status.pid;
999
1009
  _writeStopFile();
1010
+ _log(`Waiting for daemon to stop (PID: ${pid})...`);
1000
1011
  for (let i = 0; i < GRACEFUL_POLL_ATTEMPTS; i++) {
1001
1012
  await _sleep(GRACEFUL_POLL_MS);
1002
1013
  if (!_isProcessRunning(pid)) {
@@ -1005,13 +1016,28 @@ async function stopDaemon(deps) {
1005
1016
  return { success: true, pid };
1006
1017
  }
1007
1018
  }
1019
+ _log("Force-killing daemon...");
1008
1020
  try {
1009
1021
  _kill(pid);
1010
1022
  } catch {
1011
1023
  }
1012
1024
  await _sleep(POST_KILL_DELAY_MS);
1025
+ if (_isProcessRunning(pid)) {
1026
+ try {
1027
+ _forceKill(pid);
1028
+ } catch {
1029
+ }
1030
+ await _sleep(POST_KILL_DELAY_MS);
1031
+ }
1013
1032
  _removeDaemonState();
1014
1033
  _removeStopFile();
1034
+ if (_isProcessRunning(pid)) {
1035
+ return {
1036
+ success: false,
1037
+ pid,
1038
+ error: `Failed to stop daemon (PID: ${pid})`
1039
+ };
1040
+ }
1015
1041
  return { success: true, pid };
1016
1042
  }
1017
1043
  async function runDaemon(intervalMs, options, deps) {
@@ -1440,6 +1466,9 @@ function checkAccounts(accounts) {
1440
1466
  return accounts.map((a) => checkAccount(a));
1441
1467
  }
1442
1468
 
1469
+ // src/cli.ts
1470
+ init_color();
1471
+
1443
1472
  // src/completions.ts
1444
1473
  var COMMANDS = "ping scan add remove list status next-reset history suggest check completions moo daemon";
1445
1474
  function bashCompletion() {
@@ -1707,17 +1736,29 @@ function colorizeStatus(windowStatus) {
1707
1736
  return green(windowStatus);
1708
1737
  case "needs ping":
1709
1738
  return red(windowStatus);
1739
+ case "deferred":
1740
+ return blue(windowStatus);
1710
1741
  default:
1711
1742
  return yellow(windowStatus);
1712
1743
  }
1713
1744
  }
1714
1745
  function formatStatusLine(status) {
1715
- const ping = status.lastPing === null ? "never" : status.lastPing.replace("T", " ").replace(/\.\d+Z$/, "Z");
1716
- const reset = status.timeUntilReset !== null ? ` (resets in ${status.timeUntilReset})` : "";
1746
+ const lines = [];
1717
1747
  const dup = status.duplicateOf ? ` [duplicate of ${status.duplicateOf}]` : "";
1718
- return ` ${status.handle}: ${colorizeStatus(status.windowStatus)} last ping: ${ping}${reset}${dup}`;
1748
+ lines.push(
1749
+ ` ${status.handle}: ${colorizeStatus(status.windowStatus)}${dup}`
1750
+ );
1751
+ const ping = status.lastPing === null ? "never" : status.lastPing.replace("T", " ").replace(/\.\d+Z$/, "Z");
1752
+ lines.push(` - last ping: ${ping}`);
1753
+ if (status.timeUntilReset !== null) {
1754
+ lines.push(` - resets in ${status.timeUntilReset}`);
1755
+ }
1756
+ if (status.deferUntilUtcHour !== void 0) {
1757
+ lines.push(` - scheduled ping at ${status.deferUntilUtcHour}:00 UTC`);
1758
+ }
1759
+ return lines.join("\n");
1719
1760
  }
1720
- function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicates) {
1761
+ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicates, deferredHandles) {
1721
1762
  const dupLookup = /* @__PURE__ */ new Map();
1722
1763
  if (duplicates) {
1723
1764
  for (const group of duplicates.values()) {
@@ -1746,33 +1787,36 @@ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicat
1746
1787
  };
1747
1788
  }
1748
1789
  const window = getWindowReset(account.handle, now);
1790
+ const isDeferred = !window && deferredHandles?.has(account.handle);
1791
+ const deferUntilUtcHour = isDeferred ? deferredHandles?.get(account.handle) : void 0;
1749
1792
  return {
1750
1793
  handle: account.handle,
1751
1794
  configDir: account.configDir,
1752
1795
  lastPing: lastPing.toISOString(),
1753
- windowStatus: window ? "active" : "needs ping",
1796
+ windowStatus: window ? "active" : isDeferred ? "deferred" : "needs ping",
1754
1797
  timeUntilReset: window ? formatTimeRemaining(window.remainingMs) : null,
1755
1798
  lastCostUsd,
1756
1799
  lastTokens,
1757
- duplicateOf
1800
+ duplicateOf,
1801
+ deferUntilUtcHour
1758
1802
  };
1759
1803
  });
1760
1804
  }
1761
- function printAccountTable(log = console.log, now = /* @__PURE__ */ new Date()) {
1805
+ function printAccountTable(log = console.log, now = /* @__PURE__ */ new Date(), deferredHandles) {
1762
1806
  const accounts = listAccounts();
1763
1807
  if (accounts.length === 0) {
1764
1808
  log("No accounts configured");
1765
1809
  return;
1766
1810
  }
1767
1811
  const dupes = findDuplicates(accounts);
1768
- const statuses = getAccountStatuses(accounts, now, dupes);
1812
+ const statuses = getAccountStatuses(accounts, now, dupes, deferredHandles);
1769
1813
  for (const s of statuses) {
1770
1814
  log(formatStatusLine(s));
1771
1815
  }
1772
1816
  }
1773
1817
 
1774
1818
  // src/default-command.ts
1775
- function showDefault(log = console.log, now = /* @__PURE__ */ new Date()) {
1819
+ function showDefault(log = console.log, now = /* @__PURE__ */ new Date(), deferredHandles) {
1776
1820
  const accounts = listAccounts();
1777
1821
  if (accounts.length === 0) {
1778
1822
  log("No accounts configured.");
@@ -1782,7 +1826,7 @@ function showDefault(log = console.log, now = /* @__PURE__ */ new Date()) {
1782
1826
  return;
1783
1827
  }
1784
1828
  const dupes = findDuplicates(accounts);
1785
- const statuses = getAccountStatuses(accounts, now, dupes);
1829
+ const statuses = getAccountStatuses(accounts, now, dupes, deferredHandles);
1786
1830
  for (const s of statuses) {
1787
1831
  log(formatStatusLine(s));
1788
1832
  }
@@ -1916,7 +1960,18 @@ function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
1916
1960
  }
1917
1961
 
1918
1962
  // src/cli.ts
1919
- var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.5.1").option(
1963
+ function getDeferredHandles() {
1964
+ const deferred = /* @__PURE__ */ new Map();
1965
+ const now = /* @__PURE__ */ new Date();
1966
+ for (const account of listAccounts()) {
1967
+ const schedule = readAccountSchedule(account.configDir);
1968
+ if (schedule && shouldDefer(now, schedule.optimalPingHour).defer) {
1969
+ deferred.set(account.handle, schedule.optimalPingHour);
1970
+ }
1971
+ }
1972
+ return deferred;
1973
+ }
1974
+ var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.7.0").option(
1920
1975
  "--config <path>",
1921
1976
  "Path to config directory (default: ~/.config/cc-ping, env: CC_PING_CONFIG)"
1922
1977
  ).hook("preAction", (thisCommand) => {
@@ -1925,7 +1980,7 @@ var program = new Command().name("cc-ping").description("Ping Claude Code sessio
1925
1980
  setConfigDir(opts.config);
1926
1981
  }
1927
1982
  }).action(() => {
1928
- showDefault();
1983
+ showDefault(console.log, /* @__PURE__ */ new Date(), getDeferredHandles());
1929
1984
  });
1930
1985
  program.command("ping").description("Ping configured accounts to start quota windows").argument(
1931
1986
  "[handles...]",
@@ -2039,14 +2094,20 @@ program.command("list").description("List configured accounts").option("--json",
2039
2094
  }
2040
2095
  });
2041
2096
  program.command("status").description("Show status of all accounts with window information").option("--json", "Output as JSON", false).action((opts) => {
2097
+ const deferred = getDeferredHandles();
2042
2098
  if (opts.json) {
2043
2099
  const accounts = listAccounts();
2044
2100
  const dupes = findDuplicates(accounts);
2045
- const statuses = getAccountStatuses(accounts, /* @__PURE__ */ new Date(), dupes);
2101
+ const statuses = getAccountStatuses(
2102
+ accounts,
2103
+ /* @__PURE__ */ new Date(),
2104
+ dupes,
2105
+ deferred
2106
+ );
2046
2107
  console.log(JSON.stringify(statuses, null, 2));
2047
2108
  return;
2048
2109
  }
2049
- printAccountTable();
2110
+ printAccountTable(console.log, /* @__PURE__ */ new Date(), deferred);
2050
2111
  });
2051
2112
  program.command("next-reset").description("Show which account has its quota window resetting soonest").option("--json", "Output as JSON", false).action((opts) => {
2052
2113
  const accounts = listAccounts();
@@ -2137,7 +2198,7 @@ daemon.command("start").description("Start the daemon process").option(
2137
2198
  bell: opts.bell,
2138
2199
  notify: opts.notify,
2139
2200
  smartSchedule,
2140
- version: "1.5.1"
2201
+ version: "1.7.0"
2141
2202
  });
2142
2203
  if (!result.success) {
2143
2204
  console.error(result.error);
@@ -2151,7 +2212,7 @@ daemon.command("start").description("Start the daemon process").option(
2151
2212
  "Hint: won't survive a reboot. Use `cc-ping daemon install` for a persistent service."
2152
2213
  );
2153
2214
  }
2154
- printAccountTable();
2215
+ printAccountTable(console.log, /* @__PURE__ */ new Date(), getDeferredHandles());
2155
2216
  });
2156
2217
  daemon.command("stop").description("Stop the daemon process").action(async () => {
2157
2218
  const result = await stopDaemon();
@@ -2171,7 +2232,7 @@ daemon.command("stop").description("Stop the daemon process").action(async () =>
2171
2232
  daemon.command("status").description("Show daemon status").option("--json", "Output as JSON", false).action(async (opts) => {
2172
2233
  const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
2173
2234
  const svc = getServiceStatus2();
2174
- const status = getDaemonStatus({ currentVersion: "1.5.1" });
2235
+ const status = getDaemonStatus({ currentVersion: "1.7.0" });
2175
2236
  if (opts.json) {
2176
2237
  const serviceInfo = svc.installed ? {
2177
2238
  service: {
@@ -2186,7 +2247,13 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
2186
2247
  }
2187
2248
  const accounts = listAccounts();
2188
2249
  const dupes = findDuplicates(accounts);
2189
- const accountStatuses = getAccountStatuses(accounts, /* @__PURE__ */ new Date(), dupes);
2250
+ const deferred = getDeferredHandles();
2251
+ const accountStatuses = getAccountStatuses(
2252
+ accounts,
2253
+ /* @__PURE__ */ new Date(),
2254
+ dupes,
2255
+ deferred
2256
+ );
2190
2257
  console.log(
2191
2258
  JSON.stringify(
2192
2259
  { ...status, ...serviceInfo, accounts: accountStatuses },
@@ -2225,14 +2292,18 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
2225
2292
  }
2226
2293
  if (status.versionMismatch) {
2227
2294
  console.log(
2228
- ` Warning: daemon is running v${status.daemonVersion} but v${"1.5.1"} is installed.`
2295
+ yellow(
2296
+ ` Warning: daemon is running v${status.daemonVersion} but v${"1.7.0"} is installed.`
2297
+ )
2229
2298
  );
2230
2299
  console.log(
2231
- " Restart to pick up the new version: cc-ping daemon stop && cc-ping daemon start"
2300
+ yellow(
2301
+ " Restart to pick up the new version: cc-ping daemon stop && cc-ping daemon start"
2302
+ )
2232
2303
  );
2233
2304
  }
2234
2305
  console.log("");
2235
- printAccountTable();
2306
+ printAccountTable(console.log, /* @__PURE__ */ new Date(), getDeferredHandles());
2236
2307
  });
2237
2308
  daemon.command("install").description("Install daemon as a system service (launchd/systemd)").option(
2238
2309
  "--interval <minutes>",
@@ -2288,7 +2359,7 @@ daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping inte
2288
2359
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2289
2360
  intervalMs,
2290
2361
  configDir: resolveConfigDir2(),
2291
- version: "1.5.1"
2362
+ version: "1.7.0"
2292
2363
  });
2293
2364
  }
2294
2365
  await runDaemonWithDefaults(intervalMs, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/cc-ping",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "Ping Claude Code sessions to trigger quota windows early across multiple accounts",
5
5
  "type": "module",
6
6
  "bin": {