@wbern/cc-ping 1.11.0 → 1.13.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 +117 -30
  2. package/package.json +1 -1
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";
@@ -602,14 +618,30 @@ var init_run_ping = __esm({
602
618
  var schedule_exports = {};
603
619
  __export(schedule_exports, {
604
620
  buildHourHistogram: () => buildHourHistogram,
621
+ checkRecentActivity: () => checkRecentActivity,
605
622
  findOptimalPingHour: () => findOptimalPingHour,
606
623
  getAccountSchedule: () => getAccountSchedule,
624
+ isRecentlyActive: () => isRecentlyActive,
607
625
  parseSmartSchedule: () => parseSmartSchedule,
608
626
  readAccountSchedule: () => readAccountSchedule,
609
627
  shouldDefer: () => shouldDefer
610
628
  });
611
629
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
612
630
  import { join as join6 } from "path";
631
+ function isRecentlyActive(historyLines, now) {
632
+ let latest = 0;
633
+ for (const line of historyLines) {
634
+ try {
635
+ const entry = JSON.parse(line);
636
+ if (typeof entry.timestamp === "number" && entry.timestamp > latest) {
637
+ latest = entry.timestamp;
638
+ }
639
+ } catch {
640
+ }
641
+ }
642
+ if (latest === 0) return false;
643
+ return now.getTime() - latest < QUOTA_WINDOW_MS2;
644
+ }
613
645
  function buildHourHistogram(timestamps) {
614
646
  const bins = new Array(24).fill(0);
615
647
  for (const ts of timestamps) {
@@ -644,8 +676,11 @@ function shouldDefer(now, optimalPingHour) {
644
676
  }
645
677
  return { defer: false };
646
678
  }
647
- function getAccountSchedule(historyLines, now = /* @__PURE__ */ new Date()) {
648
- const cutoff = now.getTime() - HISTORY_WINDOW_MS;
679
+ function getAccountSchedule(historyLines, now = /* @__PURE__ */ new Date(), resetAt) {
680
+ const cutoff = Math.max(
681
+ now.getTime() - HISTORY_WINDOW_MS,
682
+ resetAt?.getTime() ?? 0
683
+ );
649
684
  const timestamps = [];
650
685
  const daysSeen = /* @__PURE__ */ new Set();
651
686
  for (const line of historyLines) {
@@ -667,14 +702,23 @@ function getAccountSchedule(historyLines, now = /* @__PURE__ */ new Date()) {
667
702
  if (max <= avg * 1.5) return null;
668
703
  const optimalPingHour = findOptimalPingHour(histogram);
669
704
  if (optimalPingHour === -1) return null;
670
- return { optimalPingHour, histogram };
705
+ const peakStart = (optimalPingHour + Math.floor(QUOTA_WINDOW_HOURS / 2) + 1) % 24;
706
+ const peakEnd = (peakStart + QUOTA_WINDOW_HOURS) % 24;
707
+ return { optimalPingHour, peakStart, peakEnd, histogram };
671
708
  }
672
- function readAccountSchedule(configDir, now = /* @__PURE__ */ new Date()) {
709
+ function readAccountSchedule(configDir, now = /* @__PURE__ */ new Date(), resetAt) {
673
710
  const historyPath = join6(configDir, "history.jsonl");
674
711
  if (!existsSync5(historyPath)) return null;
675
712
  const content = readFileSync5(historyPath, "utf-8");
676
713
  const lines = content.split("\n").filter((l) => l.trim());
677
- return getAccountSchedule(lines, now);
714
+ return getAccountSchedule(lines, now, resetAt);
715
+ }
716
+ function checkRecentActivity(configDir, now = /* @__PURE__ */ new Date()) {
717
+ const historyPath = join6(configDir, "history.jsonl");
718
+ if (!existsSync5(historyPath)) return false;
719
+ const content = readFileSync5(historyPath, "utf-8");
720
+ const lines = content.split("\n").filter((l) => l.trim());
721
+ return isRecentlyActive(lines, now);
678
722
  }
679
723
  function parseSmartSchedule(value) {
680
724
  const lower = value.toLowerCase();
@@ -684,11 +728,12 @@ function parseSmartSchedule(value) {
684
728
  `Invalid smart-schedule value: "${value}". Use true/false, on/off, or 1/0`
685
729
  );
686
730
  }
687
- var QUOTA_WINDOW_HOURS, MIN_DAYS, HISTORY_WINDOW_MS, TRUTHY, FALSY;
731
+ var QUOTA_WINDOW_HOURS, QUOTA_WINDOW_MS2, MIN_DAYS, HISTORY_WINDOW_MS, TRUTHY, FALSY;
688
732
  var init_schedule = __esm({
689
733
  "src/schedule.ts"() {
690
734
  "use strict";
691
735
  QUOTA_WINDOW_HOURS = 5;
736
+ QUOTA_WINDOW_MS2 = QUOTA_WINDOW_HOURS * 60 * 60 * 1e3;
692
737
  MIN_DAYS = 7;
693
738
  HISTORY_WINDOW_MS = 14 * 24 * 60 * 60 * 1e3;
694
739
  TRUTHY = /* @__PURE__ */ new Set(["true", "on", "1"]);
@@ -1088,15 +1133,21 @@ async function runDaemonWithDefaults(intervalMs, options) {
1088
1133
  const { runPing: runPing2 } = await Promise.resolve().then(() => (init_run_ping(), run_ping_exports));
1089
1134
  const { listAccounts: listAccounts2 } = await Promise.resolve().then(() => (init_config(), config_exports));
1090
1135
  const { getWindowReset: getWindowReset2 } = await Promise.resolve().then(() => (init_state(), state_exports));
1136
+ const { checkRecentActivity: checkRecentActivity2 } = await Promise.resolve().then(() => (init_schedule(), schedule_exports));
1091
1137
  const smartScheduleEnabled = options.smartSchedule !== false;
1092
1138
  let shouldDeferPing;
1093
1139
  if (smartScheduleEnabled) {
1094
1140
  const { readAccountSchedule: readAccountSchedule2, shouldDefer: shouldDefer2 } = await Promise.resolve().then(() => (init_schedule(), schedule_exports));
1095
1141
  for (const account of listAccounts2()) {
1096
- const schedule = readAccountSchedule2(account.configDir);
1097
- if (schedule) {
1142
+ const resetAt = account.scheduleResetAt ? new Date(account.scheduleResetAt) : void 0;
1143
+ const schedule2 = readAccountSchedule2(
1144
+ account.configDir,
1145
+ /* @__PURE__ */ new Date(),
1146
+ resetAt
1147
+ );
1148
+ if (schedule2) {
1098
1149
  console.log(
1099
- `Smart schedule: ${account.handle} \u2192 optimal ping at ${schedule.optimalPingHour}:00 UTC`
1150
+ `Smart schedule: ${account.handle} \u2192 optimal ping at ${schedule2.optimalPingHour}:00 UTC`
1100
1151
  );
1101
1152
  } else {
1102
1153
  console.log(
@@ -1104,10 +1155,13 @@ async function runDaemonWithDefaults(intervalMs, options) {
1104
1155
  );
1105
1156
  }
1106
1157
  }
1107
- shouldDeferPing = (_handle, configDir) => {
1108
- const schedule = readAccountSchedule2(configDir);
1109
- if (!schedule) return { defer: false };
1110
- return shouldDefer2(/* @__PURE__ */ new Date(), schedule.optimalPingHour);
1158
+ shouldDeferPing = (handle, configDir) => {
1159
+ const accounts = listAccounts2();
1160
+ const account = accounts.find((a) => a.handle === handle);
1161
+ const resetAt = account?.scheduleResetAt ? new Date(account.scheduleResetAt) : void 0;
1162
+ const schedule2 = readAccountSchedule2(configDir, /* @__PURE__ */ new Date(), resetAt);
1163
+ if (!schedule2) return { defer: false };
1164
+ return shouldDefer2(/* @__PURE__ */ new Date(), schedule2.optimalPingHour);
1111
1165
  };
1112
1166
  }
1113
1167
  let hasUpgraded;
@@ -1138,7 +1192,11 @@ async function runDaemonWithDefaults(intervalMs, options) {
1138
1192
  sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
1139
1193
  shouldStop: () => existsSync6(stopPath),
1140
1194
  log: (msg) => console.log(msg),
1141
- isWindowActive: (handle) => getWindowReset2(handle) !== null,
1195
+ isWindowActive: (handle) => {
1196
+ if (getWindowReset2(handle) !== null) return true;
1197
+ const account = listAccounts2().find((a) => a.handle === handle);
1198
+ return account ? checkRecentActivity2(account.configDir) : false;
1199
+ },
1142
1200
  shouldDeferPing,
1143
1201
  hasUpgraded,
1144
1202
  updateState: (patch) => {
@@ -1429,18 +1487,18 @@ function escapeXml(str) {
1429
1487
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1430
1488
  }
1431
1489
  function parseIntervalForService(value) {
1432
- if (!value) return QUOTA_WINDOW_MS2;
1490
+ if (!value) return QUOTA_WINDOW_MS3;
1433
1491
  const minutes = Number(value);
1434
- if (Number.isNaN(minutes) || minutes <= 0) return QUOTA_WINDOW_MS2;
1492
+ if (Number.isNaN(minutes) || minutes <= 0) return QUOTA_WINDOW_MS3;
1435
1493
  return minutes * 60 * 1e3;
1436
1494
  }
1437
- var PLIST_LABEL, SYSTEMD_SERVICE, QUOTA_WINDOW_MS2;
1495
+ var PLIST_LABEL, SYSTEMD_SERVICE, QUOTA_WINDOW_MS3;
1438
1496
  var init_service = __esm({
1439
1497
  "src/service.ts"() {
1440
1498
  "use strict";
1441
1499
  PLIST_LABEL = "com.cc-ping.daemon";
1442
1500
  SYSTEMD_SERVICE = "cc-ping-daemon";
1443
- QUOTA_WINDOW_MS2 = 5 * 60 * 60 * 1e3;
1501
+ QUOTA_WINDOW_MS3 = 5 * 60 * 60 * 1e3;
1444
1502
  }
1445
1503
  });
1446
1504
 
@@ -1806,7 +1864,10 @@ function formatStatusLine(status, options) {
1806
1864
  lines.push(` - resets in ${status.timeUntilReset}`);
1807
1865
  }
1808
1866
  if (status.deferUntilUtcHour !== void 0) {
1809
- lines.push(` - scheduled ping at ${status.deferUntilUtcHour}:00 UTC`);
1867
+ const peak = status.peakWindowUtc ? ` (peak: ${status.peakWindowUtc} UTC)` : "";
1868
+ lines.push(
1869
+ ` - scheduled ping at ${status.deferUntilUtcHour}:00 UTC${peak}`
1870
+ );
1810
1871
  }
1811
1872
  return lines.join("\n");
1812
1873
  }
@@ -1839,8 +1900,10 @@ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicat
1839
1900
  };
1840
1901
  }
1841
1902
  const window = getWindowReset(account.handle, now);
1842
- const isDeferred = !window && deferredHandles?.has(account.handle);
1843
- const deferUntilUtcHour = isDeferred ? deferredHandles?.get(account.handle) : void 0;
1903
+ const deferInfo = deferredHandles?.get(account.handle);
1904
+ const isDeferred = !window && deferInfo !== void 0;
1905
+ const deferUntilUtcHour = isDeferred ? deferInfo.optimalPingHour : void 0;
1906
+ const peakWindowUtc = isDeferred ? `${deferInfo.peakStart}-${deferInfo.peakEnd}` : void 0;
1844
1907
  return {
1845
1908
  handle: account.handle,
1846
1909
  configDir: account.configDir,
@@ -1850,7 +1913,8 @@ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicat
1850
1913
  lastCostUsd,
1851
1914
  lastTokens,
1852
1915
  duplicateOf,
1853
- deferUntilUtcHour
1916
+ deferUntilUtcHour,
1917
+ peakWindowUtc
1854
1918
  };
1855
1919
  });
1856
1920
  }
@@ -2016,14 +2080,19 @@ function getDeferredHandles() {
2016
2080
  const deferred = /* @__PURE__ */ new Map();
2017
2081
  const now = /* @__PURE__ */ new Date();
2018
2082
  for (const account of listAccounts()) {
2019
- const schedule = readAccountSchedule(account.configDir);
2020
- if (schedule && shouldDefer(now, schedule.optimalPingHour).defer) {
2021
- deferred.set(account.handle, schedule.optimalPingHour);
2083
+ const resetAt = account.scheduleResetAt ? new Date(account.scheduleResetAt) : void 0;
2084
+ const schedule2 = readAccountSchedule(account.configDir, now, resetAt);
2085
+ if (schedule2 && shouldDefer(now, schedule2.optimalPingHour).defer) {
2086
+ deferred.set(account.handle, {
2087
+ optimalPingHour: schedule2.optimalPingHour,
2088
+ peakStart: schedule2.peakStart,
2089
+ peakEnd: schedule2.peakEnd
2090
+ });
2022
2091
  }
2023
2092
  }
2024
2093
  return deferred;
2025
2094
  }
2026
- var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.11.0").option(
2095
+ var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.13.0").option(
2027
2096
  "--config <path>",
2028
2097
  "Path to config directory (default: ~/.config/cc-ping, env: CC_PING_CONFIG)"
2029
2098
  ).hook("preAction", (thisCommand) => {
@@ -2252,7 +2321,7 @@ daemon.command("start").description("Start the daemon process").option(
2252
2321
  bell: opts.bell,
2253
2322
  notify: opts.notify,
2254
2323
  smartSchedule,
2255
- version: "1.11.0"
2324
+ version: "1.13.0"
2256
2325
  });
2257
2326
  if (!result.success) {
2258
2327
  console.error(result.error);
@@ -2288,7 +2357,7 @@ daemon.command("stop").description("Stop the daemon process").action(async () =>
2288
2357
  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) => {
2289
2358
  const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
2290
2359
  const svc = getServiceStatus2();
2291
- const status = getDaemonStatus({ currentVersion: "1.11.0" });
2360
+ const status = getDaemonStatus({ currentVersion: "1.13.0" });
2292
2361
  if (opts.json) {
2293
2362
  const serviceInfo = svc.installed ? {
2294
2363
  service: {
@@ -2349,7 +2418,7 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
2349
2418
  if (status.versionMismatch) {
2350
2419
  console.log(
2351
2420
  yellow(
2352
- ` Warning: daemon is running v${status.daemonVersion} but v${"1.11.0"} is installed.`
2421
+ ` Warning: daemon is running v${status.daemonVersion} but v${"1.13.0"} is installed.`
2353
2422
  )
2354
2423
  );
2355
2424
  console.log(
@@ -2417,7 +2486,7 @@ daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping inte
2417
2486
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2418
2487
  intervalMs,
2419
2488
  configDir: resolveConfigDir2(),
2420
- version: "1.11.0"
2489
+ version: "1.13.0"
2421
2490
  });
2422
2491
  }
2423
2492
  await runDaemonWithDefaults(intervalMs, {
@@ -2428,4 +2497,22 @@ daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping inte
2428
2497
  autoUpdate: opts.autoUpdate
2429
2498
  });
2430
2499
  });
2500
+ var schedule = program.command("schedule").description("Manage smart scheduling");
2501
+ schedule.command("reset").description("Reset smart scheduling data to recompute optimal ping times").argument("[handle]", "Specific account handle (default: all accounts)").action((handle) => {
2502
+ if (resetSchedule(handle)) {
2503
+ if (handle) {
2504
+ console.log(`Schedule reset for: ${handle}`);
2505
+ } else {
2506
+ console.log("Schedule reset for all accounts");
2507
+ }
2508
+ } else {
2509
+ if (handle) {
2510
+ console.error(`Account not found: ${handle}`);
2511
+ process.exit(1);
2512
+ } else {
2513
+ console.error("No accounts configured");
2514
+ process.exit(1);
2515
+ }
2516
+ }
2517
+ });
2431
2518
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/cc-ping",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "Ping Claude Code sessions to trigger quota windows early across multiple accounts",
5
5
  "type": "module",
6
6
  "bin": {