@wbern/cc-ping 1.10.2 → 1.12.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 +2 -2
  2. package/dist/cli.js +93 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -192,8 +192,8 @@ Smart scheduling -- ping timed so window expires at peak:
192
192
 
193
193
  1. Reads `<configDir>/history.jsonl` from each account's config directory (Claude Code's prompt timestamps)
194
194
  2. Builds an hour-of-day histogram from the last 14 days
195
- 3. Finds the weighted midpoint of your activity
196
- 4. Schedules pings at `midpoint - 5h` so the window expires at your peak
195
+ 3. Slides a 5-hour window across the histogram to find the densest period
196
+ 4. Schedules pings so the window expires at the midpoint of peak activity
197
197
 
198
198
  **Defer zone:** When smart scheduling is active, pings that would fire in the 5 hours before the optimal time are deferred. Pings outside this zone proceed normally for continuous coverage.
199
199
 
package/dist/cli.js CHANGED
@@ -60,6 +60,7 @@ __export(config_exports, {
60
60
  listAccounts: () => listAccounts,
61
61
  loadConfig: () => loadConfig,
62
62
  removeAccount: () => removeAccount,
63
+ resetSchedule: () => resetSchedule,
63
64
  saveConfig: () => saveConfig
64
65
  });
65
66
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
@@ -109,6 +110,21 @@ function removeAccount(handle) {
109
110
  function listAccounts() {
110
111
  return loadConfig().accounts;
111
112
  }
113
+ function resetSchedule(handle, now = /* @__PURE__ */ new Date()) {
114
+ const config = loadConfig();
115
+ if (handle) {
116
+ const account = config.accounts.find((a) => a.handle === handle);
117
+ if (!account) return false;
118
+ account.scheduleResetAt = now.toISOString();
119
+ } else {
120
+ if (config.accounts.length === 0) return false;
121
+ for (const account of config.accounts) {
122
+ account.scheduleResetAt = now.toISOString();
123
+ }
124
+ }
125
+ saveConfig(config);
126
+ return true;
127
+ }
112
128
  var init_config = __esm({
113
129
  "src/config.ts"() {
114
130
  "use strict";
@@ -620,16 +636,19 @@ function buildHourHistogram(timestamps) {
620
636
  function findOptimalPingHour(histogram) {
621
637
  const total = histogram.reduce((sum, v) => sum + v, 0);
622
638
  if (total === 0) return -1;
623
- const HOUR_TO_RAD = 2 * Math.PI / 24;
624
- let sinSum = 0;
625
- let cosSum = 0;
639
+ let bestStart = 0;
640
+ let bestSum = 0;
626
641
  for (let h = 0; h < 24; h++) {
627
- const angle = h * HOUR_TO_RAD;
628
- sinSum += histogram[h] * Math.sin(angle);
629
- cosSum += histogram[h] * Math.cos(angle);
642
+ let windowSum = 0;
643
+ for (let offset = 0; offset < QUOTA_WINDOW_HOURS; offset++) {
644
+ windowSum += histogram[(h + offset) % 24];
645
+ }
646
+ if (windowSum > bestSum) {
647
+ bestSum = windowSum;
648
+ bestStart = h;
649
+ }
630
650
  }
631
- const meanAngle = Math.atan2(sinSum, cosSum);
632
- const midpoint = Math.round((meanAngle / HOUR_TO_RAD + 24) % 24);
651
+ const midpoint = (bestStart + Math.floor(QUOTA_WINDOW_HOURS / 2)) % 24;
633
652
  return (midpoint - QUOTA_WINDOW_HOURS + 24) % 24;
634
653
  }
635
654
  function shouldDefer(now, optimalPingHour) {
@@ -641,8 +660,11 @@ function shouldDefer(now, optimalPingHour) {
641
660
  }
642
661
  return { defer: false };
643
662
  }
644
- function getAccountSchedule(historyLines, now = /* @__PURE__ */ new Date()) {
645
- const cutoff = now.getTime() - HISTORY_WINDOW_MS;
663
+ function getAccountSchedule(historyLines, now = /* @__PURE__ */ new Date(), resetAt) {
664
+ const cutoff = Math.max(
665
+ now.getTime() - HISTORY_WINDOW_MS,
666
+ resetAt?.getTime() ?? 0
667
+ );
646
668
  const timestamps = [];
647
669
  const daysSeen = /* @__PURE__ */ new Set();
648
670
  for (const line of historyLines) {
@@ -664,14 +686,16 @@ function getAccountSchedule(historyLines, now = /* @__PURE__ */ new Date()) {
664
686
  if (max <= avg * 1.5) return null;
665
687
  const optimalPingHour = findOptimalPingHour(histogram);
666
688
  if (optimalPingHour === -1) return null;
667
- return { optimalPingHour, histogram };
689
+ const peakStart = (optimalPingHour + Math.floor(QUOTA_WINDOW_HOURS / 2) + 1) % 24;
690
+ const peakEnd = (peakStart + QUOTA_WINDOW_HOURS) % 24;
691
+ return { optimalPingHour, peakStart, peakEnd, histogram };
668
692
  }
669
- function readAccountSchedule(configDir, now = /* @__PURE__ */ new Date()) {
693
+ function readAccountSchedule(configDir, now = /* @__PURE__ */ new Date(), resetAt) {
670
694
  const historyPath = join6(configDir, "history.jsonl");
671
695
  if (!existsSync5(historyPath)) return null;
672
696
  const content = readFileSync5(historyPath, "utf-8");
673
697
  const lines = content.split("\n").filter((l) => l.trim());
674
- return getAccountSchedule(lines, now);
698
+ return getAccountSchedule(lines, now, resetAt);
675
699
  }
676
700
  function parseSmartSchedule(value) {
677
701
  const lower = value.toLowerCase();
@@ -1090,10 +1114,15 @@ async function runDaemonWithDefaults(intervalMs, options) {
1090
1114
  if (smartScheduleEnabled) {
1091
1115
  const { readAccountSchedule: readAccountSchedule2, shouldDefer: shouldDefer2 } = await Promise.resolve().then(() => (init_schedule(), schedule_exports));
1092
1116
  for (const account of listAccounts2()) {
1093
- const schedule = readAccountSchedule2(account.configDir);
1094
- if (schedule) {
1117
+ const resetAt = account.scheduleResetAt ? new Date(account.scheduleResetAt) : void 0;
1118
+ const schedule2 = readAccountSchedule2(
1119
+ account.configDir,
1120
+ /* @__PURE__ */ new Date(),
1121
+ resetAt
1122
+ );
1123
+ if (schedule2) {
1095
1124
  console.log(
1096
- `Smart schedule: ${account.handle} \u2192 optimal ping at ${schedule.optimalPingHour}:00 UTC`
1125
+ `Smart schedule: ${account.handle} \u2192 optimal ping at ${schedule2.optimalPingHour}:00 UTC`
1097
1126
  );
1098
1127
  } else {
1099
1128
  console.log(
@@ -1101,10 +1130,13 @@ async function runDaemonWithDefaults(intervalMs, options) {
1101
1130
  );
1102
1131
  }
1103
1132
  }
1104
- shouldDeferPing = (_handle, configDir) => {
1105
- const schedule = readAccountSchedule2(configDir);
1106
- if (!schedule) return { defer: false };
1107
- return shouldDefer2(/* @__PURE__ */ new Date(), schedule.optimalPingHour);
1133
+ shouldDeferPing = (handle, configDir) => {
1134
+ const accounts = listAccounts2();
1135
+ const account = accounts.find((a) => a.handle === handle);
1136
+ const resetAt = account?.scheduleResetAt ? new Date(account.scheduleResetAt) : void 0;
1137
+ const schedule2 = readAccountSchedule2(configDir, /* @__PURE__ */ new Date(), resetAt);
1138
+ if (!schedule2) return { defer: false };
1139
+ return shouldDefer2(/* @__PURE__ */ new Date(), schedule2.optimalPingHour);
1108
1140
  };
1109
1141
  }
1110
1142
  let hasUpgraded;
@@ -1803,7 +1835,10 @@ function formatStatusLine(status, options) {
1803
1835
  lines.push(` - resets in ${status.timeUntilReset}`);
1804
1836
  }
1805
1837
  if (status.deferUntilUtcHour !== void 0) {
1806
- lines.push(` - scheduled ping at ${status.deferUntilUtcHour}:00 UTC`);
1838
+ const peak = status.peakWindowUtc ? ` (peak: ${status.peakWindowUtc} UTC)` : "";
1839
+ lines.push(
1840
+ ` - scheduled ping at ${status.deferUntilUtcHour}:00 UTC${peak}`
1841
+ );
1807
1842
  }
1808
1843
  return lines.join("\n");
1809
1844
  }
@@ -1836,8 +1871,10 @@ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicat
1836
1871
  };
1837
1872
  }
1838
1873
  const window = getWindowReset(account.handle, now);
1839
- const isDeferred = !window && deferredHandles?.has(account.handle);
1840
- const deferUntilUtcHour = isDeferred ? deferredHandles?.get(account.handle) : void 0;
1874
+ const deferInfo = deferredHandles?.get(account.handle);
1875
+ const isDeferred = !window && deferInfo !== void 0;
1876
+ const deferUntilUtcHour = isDeferred ? deferInfo.optimalPingHour : void 0;
1877
+ const peakWindowUtc = isDeferred ? `${deferInfo.peakStart}-${deferInfo.peakEnd}` : void 0;
1841
1878
  return {
1842
1879
  handle: account.handle,
1843
1880
  configDir: account.configDir,
@@ -1847,7 +1884,8 @@ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicat
1847
1884
  lastCostUsd,
1848
1885
  lastTokens,
1849
1886
  duplicateOf,
1850
- deferUntilUtcHour
1887
+ deferUntilUtcHour,
1888
+ peakWindowUtc
1851
1889
  };
1852
1890
  });
1853
1891
  }
@@ -2013,14 +2051,19 @@ function getDeferredHandles() {
2013
2051
  const deferred = /* @__PURE__ */ new Map();
2014
2052
  const now = /* @__PURE__ */ new Date();
2015
2053
  for (const account of listAccounts()) {
2016
- const schedule = readAccountSchedule(account.configDir);
2017
- if (schedule && shouldDefer(now, schedule.optimalPingHour).defer) {
2018
- deferred.set(account.handle, schedule.optimalPingHour);
2054
+ const resetAt = account.scheduleResetAt ? new Date(account.scheduleResetAt) : void 0;
2055
+ const schedule2 = readAccountSchedule(account.configDir, now, resetAt);
2056
+ if (schedule2 && shouldDefer(now, schedule2.optimalPingHour).defer) {
2057
+ deferred.set(account.handle, {
2058
+ optimalPingHour: schedule2.optimalPingHour,
2059
+ peakStart: schedule2.peakStart,
2060
+ peakEnd: schedule2.peakEnd
2061
+ });
2019
2062
  }
2020
2063
  }
2021
2064
  return deferred;
2022
2065
  }
2023
- var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.10.2").option(
2066
+ var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.12.0").option(
2024
2067
  "--config <path>",
2025
2068
  "Path to config directory (default: ~/.config/cc-ping, env: CC_PING_CONFIG)"
2026
2069
  ).hook("preAction", (thisCommand) => {
@@ -2249,7 +2292,7 @@ daemon.command("start").description("Start the daemon process").option(
2249
2292
  bell: opts.bell,
2250
2293
  notify: opts.notify,
2251
2294
  smartSchedule,
2252
- version: "1.10.2"
2295
+ version: "1.12.0"
2253
2296
  });
2254
2297
  if (!result.success) {
2255
2298
  console.error(result.error);
@@ -2285,7 +2328,7 @@ daemon.command("stop").description("Stop the daemon process").action(async () =>
2285
2328
  daemon.command("status").description("Show daemon status").option("--json", "Output as JSON", false).option("--censor", "Mask account handles in output (for screenshots)").action(async (opts) => {
2286
2329
  const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
2287
2330
  const svc = getServiceStatus2();
2288
- const status = getDaemonStatus({ currentVersion: "1.10.2" });
2331
+ const status = getDaemonStatus({ currentVersion: "1.12.0" });
2289
2332
  if (opts.json) {
2290
2333
  const serviceInfo = svc.installed ? {
2291
2334
  service: {
@@ -2346,7 +2389,7 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
2346
2389
  if (status.versionMismatch) {
2347
2390
  console.log(
2348
2391
  yellow(
2349
- ` Warning: daemon is running v${status.daemonVersion} but v${"1.10.2"} is installed.`
2392
+ ` Warning: daemon is running v${status.daemonVersion} but v${"1.12.0"} is installed.`
2350
2393
  )
2351
2394
  );
2352
2395
  console.log(
@@ -2414,7 +2457,7 @@ daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping inte
2414
2457
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2415
2458
  intervalMs,
2416
2459
  configDir: resolveConfigDir2(),
2417
- version: "1.10.2"
2460
+ version: "1.12.0"
2418
2461
  });
2419
2462
  }
2420
2463
  await runDaemonWithDefaults(intervalMs, {
@@ -2425,4 +2468,22 @@ daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping inte
2425
2468
  autoUpdate: opts.autoUpdate
2426
2469
  });
2427
2470
  });
2471
+ var schedule = program.command("schedule").description("Manage smart scheduling");
2472
+ schedule.command("reset").description("Reset smart scheduling data to recompute optimal ping times").argument("[handle]", "Specific account handle (default: all accounts)").action((handle) => {
2473
+ if (resetSchedule(handle)) {
2474
+ if (handle) {
2475
+ console.log(`Schedule reset for: ${handle}`);
2476
+ } else {
2477
+ console.log("Schedule reset for all accounts");
2478
+ }
2479
+ } else {
2480
+ if (handle) {
2481
+ console.error(`Account not found: ${handle}`);
2482
+ process.exit(1);
2483
+ } else {
2484
+ console.error("No accounts configured");
2485
+ process.exit(1);
2486
+ }
2487
+ }
2488
+ });
2428
2489
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/cc-ping",
3
- "version": "1.10.2",
3
+ "version": "1.12.0",
4
4
  "description": "Ping Claude Code sessions to trigger quota windows early across multiple accounts",
5
5
  "type": "module",
6
6
  "bin": {