@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.
- package/dist/cli.js +117 -30
- 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 =
|
|
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
|
-
|
|
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
|
|
1097
|
-
|
|
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 ${
|
|
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 = (
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
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) =>
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1430
1488
|
}
|
|
1431
1489
|
function parseIntervalForService(value) {
|
|
1432
|
-
if (!value) return
|
|
1490
|
+
if (!value) return QUOTA_WINDOW_MS3;
|
|
1433
1491
|
const minutes = Number(value);
|
|
1434
|
-
if (Number.isNaN(minutes) || minutes <= 0) return
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1843
|
-
const
|
|
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
|
|
2020
|
-
|
|
2021
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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();
|