@wbern/cc-ping 1.5.0 → 1.6.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 +104 -29
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -196,13 +196,14 @@ function isColorEnabled() {
196
196
  function wrap(code, text) {
197
197
  return isColorEnabled() ? `\x1B[${code}m${text}\x1B[0m` : text;
198
198
  }
199
- var green, red, yellow;
199
+ var green, red, yellow, blue;
200
200
  var init_color = __esm({
201
201
  "src/color.ts"() {
202
202
  "use strict";
203
203
  green = (text) => wrap("32", text);
204
204
  red = (text) => wrap("31", text);
205
205
  yellow = (text) => wrap("33", text);
206
+ blue = (text) => wrap("34", text);
206
207
  }
207
208
  });
208
209
 
@@ -701,6 +702,7 @@ __export(daemon_exports, {
701
702
  daemonStopPath: () => daemonStopPath,
702
703
  getDaemonStatus: () => getDaemonStatus,
703
704
  isProcessRunning: () => isProcessRunning,
705
+ msUntilUtcHour: () => msUntilUtcHour,
704
706
  parseInterval: () => parseInterval,
705
707
  readDaemonState: () => readDaemonState,
706
708
  removeDaemonState: () => removeDaemonState,
@@ -824,6 +826,12 @@ function formatUptime(ms) {
824
826
  if (minutes > 0) return `${minutes}m ${seconds}s`;
825
827
  return `${seconds}s`;
826
828
  }
829
+ function msUntilUtcHour(targetHour, now) {
830
+ const currentMs = now.getUTCHours() * 36e5 + now.getUTCMinutes() * 6e4 + now.getUTCSeconds() * 1e3 + now.getUTCMilliseconds();
831
+ const targetMs = targetHour * 36e5;
832
+ const diff = targetMs - currentMs;
833
+ return diff > 0 ? diff : diff + 24 * 36e5;
834
+ }
827
835
  async function daemonLoop(intervalMs, options, deps) {
828
836
  let wakeDelayMs;
829
837
  while (!deps.shouldStop()) {
@@ -837,23 +845,28 @@ async function daemonLoop(intervalMs, options, deps) {
837
845
  if (skipped > 0) {
838
846
  deps.log(`Skipping ${skipped} account(s) with active window`);
839
847
  }
848
+ let soonestDeferHour;
840
849
  if (deps.shouldDeferPing) {
841
850
  const deferResults = /* @__PURE__ */ new Map();
842
851
  for (const a of accounts) {
843
- deferResults.set(
844
- a.handle,
845
- deps.shouldDeferPing(a.handle, a.configDir).defer
846
- );
852
+ deferResults.set(a.handle, deps.shouldDeferPing(a.handle, a.configDir));
847
853
  }
848
- const deferredCount = [...deferResults.values()].filter(Boolean).length;
849
- if (deferredCount > 0) {
850
- accounts = accounts.filter((a) => !deferResults.get(a.handle));
851
- deps.log(`Deferring ${deferredCount} account(s) (smart scheduling)`);
854
+ const deferred = [...deferResults.entries()].filter(([, r]) => r.defer);
855
+ if (deferred.length > 0) {
856
+ accounts = accounts.filter((a) => !deferResults.get(a.handle)?.defer);
857
+ deps.log(`Deferring ${deferred.length} account(s) (smart scheduling)`);
858
+ for (const [, r] of deferred) {
859
+ if (r.deferUntilUtcHour !== void 0 && (soonestDeferHour === void 0 || /* c8 ignore next -- production default */
860
+ msUntilUtcHour(r.deferUntilUtcHour, deps.now?.() ?? /* @__PURE__ */ new Date()) < /* c8 ignore next -- production default */
861
+ msUntilUtcHour(soonestDeferHour, deps.now?.() ?? /* @__PURE__ */ new Date()))) {
862
+ soonestDeferHour = r.deferUntilUtcHour;
863
+ }
864
+ }
852
865
  }
853
866
  }
854
867
  if (accounts.length === 0) {
855
868
  deps.log(
856
- allAccounts.length === 0 ? "No accounts configured, waiting..." : "All accounts have active windows, waiting..."
869
+ allAccounts.length === 0 ? "No accounts configured, waiting..." : soonestDeferHour !== void 0 ? "All accounts deferred (smart scheduling), waiting..." : "All accounts have active windows, waiting..."
857
870
  );
858
871
  } else {
859
872
  deps.log(
@@ -882,9 +895,20 @@ async function daemonLoop(intervalMs, options, deps) {
882
895
  deps.updateState?.({ lastPingAt: (/* @__PURE__ */ new Date()).toISOString() });
883
896
  }
884
897
  if (deps.shouldStop()) break;
885
- deps.log(`Sleeping ${Math.round(intervalMs / 6e4)}m until next ping...`);
898
+ let sleepMs = intervalMs;
899
+ if (soonestDeferHour !== void 0) {
900
+ const msUntilDefer = msUntilUtcHour(
901
+ soonestDeferHour,
902
+ /* c8 ignore next -- production default */
903
+ deps.now?.() ?? /* @__PURE__ */ new Date()
904
+ );
905
+ if (msUntilDefer > 0 && msUntilDefer < intervalMs) {
906
+ sleepMs = msUntilDefer;
907
+ }
908
+ }
909
+ deps.log(`Sleeping ${Math.round(sleepMs / 6e4)}m until next ping...`);
886
910
  const sleepStart = Date.now();
887
- await deps.sleep(intervalMs);
911
+ await deps.sleep(sleepMs);
888
912
  const overshootMs = Date.now() - sleepStart - intervalMs;
889
913
  if (overshootMs > 6e4) {
890
914
  wakeDelayMs = overshootMs;
@@ -968,12 +992,22 @@ async function stopDaemon(deps) {
968
992
  process.kill(pid2, "SIGTERM");
969
993
  }
970
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));
971
1004
  const status = _getDaemonStatus();
972
1005
  if (!status.running || !status.pid) {
973
1006
  return { success: false, error: "Daemon is not running" };
974
1007
  }
975
1008
  const pid = status.pid;
976
1009
  _writeStopFile();
1010
+ _log(`Waiting for daemon to stop (PID: ${pid})...`);
977
1011
  for (let i = 0; i < GRACEFUL_POLL_ATTEMPTS; i++) {
978
1012
  await _sleep(GRACEFUL_POLL_MS);
979
1013
  if (!_isProcessRunning(pid)) {
@@ -982,13 +1016,28 @@ async function stopDaemon(deps) {
982
1016
  return { success: true, pid };
983
1017
  }
984
1018
  }
1019
+ _log("Force-killing daemon...");
985
1020
  try {
986
1021
  _kill(pid);
987
1022
  } catch {
988
1023
  }
989
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
+ }
990
1032
  _removeDaemonState();
991
1033
  _removeStopFile();
1034
+ if (_isProcessRunning(pid)) {
1035
+ return {
1036
+ success: false,
1037
+ pid,
1038
+ error: `Failed to stop daemon (PID: ${pid})`
1039
+ };
1040
+ }
992
1041
  return { success: true, pid };
993
1042
  }
994
1043
  async function runDaemon(intervalMs, options, deps) {
@@ -1684,6 +1733,8 @@ function colorizeStatus(windowStatus) {
1684
1733
  return green(windowStatus);
1685
1734
  case "needs ping":
1686
1735
  return red(windowStatus);
1736
+ case "deferred":
1737
+ return blue(windowStatus);
1687
1738
  default:
1688
1739
  return yellow(windowStatus);
1689
1740
  }
@@ -1694,7 +1745,7 @@ function formatStatusLine(status) {
1694
1745
  const dup = status.duplicateOf ? ` [duplicate of ${status.duplicateOf}]` : "";
1695
1746
  return ` ${status.handle}: ${colorizeStatus(status.windowStatus)} last ping: ${ping}${reset}${dup}`;
1696
1747
  }
1697
- function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicates) {
1748
+ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicates, deferredHandles) {
1698
1749
  const dupLookup = /* @__PURE__ */ new Map();
1699
1750
  if (duplicates) {
1700
1751
  for (const group of duplicates.values()) {
@@ -1723,11 +1774,12 @@ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicat
1723
1774
  };
1724
1775
  }
1725
1776
  const window = getWindowReset(account.handle, now);
1777
+ const isDeferred = !window && deferredHandles?.has(account.handle);
1726
1778
  return {
1727
1779
  handle: account.handle,
1728
1780
  configDir: account.configDir,
1729
1781
  lastPing: lastPing.toISOString(),
1730
- windowStatus: window ? "active" : "needs ping",
1782
+ windowStatus: window ? "active" : isDeferred ? "deferred" : "needs ping",
1731
1783
  timeUntilReset: window ? formatTimeRemaining(window.remainingMs) : null,
1732
1784
  lastCostUsd,
1733
1785
  lastTokens,
@@ -1735,21 +1787,21 @@ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicat
1735
1787
  };
1736
1788
  });
1737
1789
  }
1738
- function printAccountTable(log = console.log, now = /* @__PURE__ */ new Date()) {
1790
+ function printAccountTable(log = console.log, now = /* @__PURE__ */ new Date(), deferredHandles) {
1739
1791
  const accounts = listAccounts();
1740
1792
  if (accounts.length === 0) {
1741
1793
  log("No accounts configured");
1742
1794
  return;
1743
1795
  }
1744
1796
  const dupes = findDuplicates(accounts);
1745
- const statuses = getAccountStatuses(accounts, now, dupes);
1797
+ const statuses = getAccountStatuses(accounts, now, dupes, deferredHandles);
1746
1798
  for (const s of statuses) {
1747
1799
  log(formatStatusLine(s));
1748
1800
  }
1749
1801
  }
1750
1802
 
1751
1803
  // src/default-command.ts
1752
- function showDefault(log = console.log, now = /* @__PURE__ */ new Date()) {
1804
+ function showDefault(log = console.log, now = /* @__PURE__ */ new Date(), deferredHandles) {
1753
1805
  const accounts = listAccounts();
1754
1806
  if (accounts.length === 0) {
1755
1807
  log("No accounts configured.");
@@ -1759,7 +1811,7 @@ function showDefault(log = console.log, now = /* @__PURE__ */ new Date()) {
1759
1811
  return;
1760
1812
  }
1761
1813
  const dupes = findDuplicates(accounts);
1762
- const statuses = getAccountStatuses(accounts, now, dupes);
1814
+ const statuses = getAccountStatuses(accounts, now, dupes, deferredHandles);
1763
1815
  for (const s of statuses) {
1764
1816
  log(formatStatusLine(s));
1765
1817
  }
@@ -1893,7 +1945,18 @@ function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
1893
1945
  }
1894
1946
 
1895
1947
  // src/cli.ts
1896
- var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.5.0").option(
1948
+ function getDeferredHandles() {
1949
+ const deferred = /* @__PURE__ */ new Set();
1950
+ const now = /* @__PURE__ */ new Date();
1951
+ for (const account of listAccounts()) {
1952
+ const schedule = readAccountSchedule(account.configDir);
1953
+ if (schedule && shouldDefer(now, schedule.optimalPingHour).defer) {
1954
+ deferred.add(account.handle);
1955
+ }
1956
+ }
1957
+ return deferred;
1958
+ }
1959
+ var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.6.0").option(
1897
1960
  "--config <path>",
1898
1961
  "Path to config directory (default: ~/.config/cc-ping, env: CC_PING_CONFIG)"
1899
1962
  ).hook("preAction", (thisCommand) => {
@@ -1902,7 +1965,7 @@ var program = new Command().name("cc-ping").description("Ping Claude Code sessio
1902
1965
  setConfigDir(opts.config);
1903
1966
  }
1904
1967
  }).action(() => {
1905
- showDefault();
1968
+ showDefault(console.log, /* @__PURE__ */ new Date(), getDeferredHandles());
1906
1969
  });
1907
1970
  program.command("ping").description("Ping configured accounts to start quota windows").argument(
1908
1971
  "[handles...]",
@@ -2016,14 +2079,20 @@ program.command("list").description("List configured accounts").option("--json",
2016
2079
  }
2017
2080
  });
2018
2081
  program.command("status").description("Show status of all accounts with window information").option("--json", "Output as JSON", false).action((opts) => {
2082
+ const deferred = getDeferredHandles();
2019
2083
  if (opts.json) {
2020
2084
  const accounts = listAccounts();
2021
2085
  const dupes = findDuplicates(accounts);
2022
- const statuses = getAccountStatuses(accounts, /* @__PURE__ */ new Date(), dupes);
2086
+ const statuses = getAccountStatuses(
2087
+ accounts,
2088
+ /* @__PURE__ */ new Date(),
2089
+ dupes,
2090
+ deferred
2091
+ );
2023
2092
  console.log(JSON.stringify(statuses, null, 2));
2024
2093
  return;
2025
2094
  }
2026
- printAccountTable();
2095
+ printAccountTable(console.log, /* @__PURE__ */ new Date(), deferred);
2027
2096
  });
2028
2097
  program.command("next-reset").description("Show which account has its quota window resetting soonest").option("--json", "Output as JSON", false).action((opts) => {
2029
2098
  const accounts = listAccounts();
@@ -2114,7 +2183,7 @@ daemon.command("start").description("Start the daemon process").option(
2114
2183
  bell: opts.bell,
2115
2184
  notify: opts.notify,
2116
2185
  smartSchedule,
2117
- version: "1.5.0"
2186
+ version: "1.6.0"
2118
2187
  });
2119
2188
  if (!result.success) {
2120
2189
  console.error(result.error);
@@ -2128,7 +2197,7 @@ daemon.command("start").description("Start the daemon process").option(
2128
2197
  "Hint: won't survive a reboot. Use `cc-ping daemon install` for a persistent service."
2129
2198
  );
2130
2199
  }
2131
- printAccountTable();
2200
+ printAccountTable(console.log, /* @__PURE__ */ new Date(), getDeferredHandles());
2132
2201
  });
2133
2202
  daemon.command("stop").description("Stop the daemon process").action(async () => {
2134
2203
  const result = await stopDaemon();
@@ -2148,7 +2217,7 @@ daemon.command("stop").description("Stop the daemon process").action(async () =>
2148
2217
  daemon.command("status").description("Show daemon status").option("--json", "Output as JSON", false).action(async (opts) => {
2149
2218
  const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
2150
2219
  const svc = getServiceStatus2();
2151
- const status = getDaemonStatus({ currentVersion: "1.5.0" });
2220
+ const status = getDaemonStatus({ currentVersion: "1.6.0" });
2152
2221
  if (opts.json) {
2153
2222
  const serviceInfo = svc.installed ? {
2154
2223
  service: {
@@ -2163,7 +2232,13 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
2163
2232
  }
2164
2233
  const accounts = listAccounts();
2165
2234
  const dupes = findDuplicates(accounts);
2166
- const accountStatuses = getAccountStatuses(accounts, /* @__PURE__ */ new Date(), dupes);
2235
+ const deferred = getDeferredHandles();
2236
+ const accountStatuses = getAccountStatuses(
2237
+ accounts,
2238
+ /* @__PURE__ */ new Date(),
2239
+ dupes,
2240
+ deferred
2241
+ );
2167
2242
  console.log(
2168
2243
  JSON.stringify(
2169
2244
  { ...status, ...serviceInfo, accounts: accountStatuses },
@@ -2202,14 +2277,14 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
2202
2277
  }
2203
2278
  if (status.versionMismatch) {
2204
2279
  console.log(
2205
- ` Warning: daemon is running v${status.daemonVersion} but v${"1.5.0"} is installed.`
2280
+ ` Warning: daemon is running v${status.daemonVersion} but v${"1.6.0"} is installed.`
2206
2281
  );
2207
2282
  console.log(
2208
2283
  " Restart to pick up the new version: cc-ping daemon stop && cc-ping daemon start"
2209
2284
  );
2210
2285
  }
2211
2286
  console.log("");
2212
- printAccountTable();
2287
+ printAccountTable(console.log, /* @__PURE__ */ new Date(), getDeferredHandles());
2213
2288
  });
2214
2289
  daemon.command("install").description("Install daemon as a system service (launchd/systemd)").option(
2215
2290
  "--interval <minutes>",
@@ -2265,7 +2340,7 @@ daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping inte
2265
2340
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2266
2341
  intervalMs,
2267
2342
  configDir: resolveConfigDir2(),
2268
- version: "1.5.0"
2343
+ version: "1.6.0"
2269
2344
  });
2270
2345
  }
2271
2346
  await runDaemonWithDefaults(intervalMs, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/cc-ping",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Ping Claude Code sessions to trigger quota windows early across multiple accounts",
5
5
  "type": "module",
6
6
  "bin": {