@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.
- package/README.md +2 -2
- package/dist/cli.js +93 -32
- 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.
|
|
196
|
-
4. Schedules pings
|
|
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
|
-
|
|
624
|
-
let
|
|
625
|
-
let cosSum = 0;
|
|
639
|
+
let bestStart = 0;
|
|
640
|
+
let bestSum = 0;
|
|
626
641
|
for (let h = 0; h < 24; h++) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1094
|
-
|
|
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 ${
|
|
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 = (
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
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
|
|
1840
|
-
const
|
|
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
|
|
2017
|
-
|
|
2018
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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();
|