@wbern/cc-ping 1.4.0 → 1.5.1
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 +1 -0
- package/dist/cli.js +81 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,6 +154,7 @@ The daemon is smart about what it pings:
|
|
|
154
154
|
- **Detects system sleep** — if the machine wakes from sleep and a ping cycle is overdue, the daemon notices and factors the delay into notifications
|
|
155
155
|
- **Singleton enforcement** — only one daemon runs at a time, verified by PID and process name
|
|
156
156
|
- **Graceful shutdown** — `daemon stop` writes a sentinel file and waits up to 60s for a clean exit before force-killing
|
|
157
|
+
- **Auto-restart on upgrade** — after upgrading cc-ping, the daemon detects the binary has changed and exits so the service manager can restart it with the new version. `daemon status` warns if the running daemon is outdated
|
|
157
158
|
|
|
158
159
|
Logs are written to `~/.config/cc-ping/daemon.log`.
|
|
159
160
|
|
package/dist/cli.js
CHANGED
|
@@ -701,6 +701,7 @@ __export(daemon_exports, {
|
|
|
701
701
|
daemonStopPath: () => daemonStopPath,
|
|
702
702
|
getDaemonStatus: () => getDaemonStatus,
|
|
703
703
|
isProcessRunning: () => isProcessRunning,
|
|
704
|
+
msUntilUtcHour: () => msUntilUtcHour,
|
|
704
705
|
parseInterval: () => parseInterval,
|
|
705
706
|
readDaemonState: () => readDaemonState,
|
|
706
707
|
removeDaemonState: () => removeDaemonState,
|
|
@@ -717,6 +718,8 @@ import {
|
|
|
717
718
|
openSync as fsOpenSync,
|
|
718
719
|
mkdirSync as mkdirSync4,
|
|
719
720
|
readFileSync as readFileSync6,
|
|
721
|
+
realpathSync,
|
|
722
|
+
statSync as statSync2,
|
|
720
723
|
unlinkSync,
|
|
721
724
|
writeFileSync as writeFileSync3
|
|
722
725
|
} from "fs";
|
|
@@ -800,13 +803,17 @@ function getDaemonStatus(deps) {
|
|
|
800
803
|
const nextPingMs = new Date(state.lastPingAt).getTime() + state.intervalMs - Date.now();
|
|
801
804
|
nextPingIn = formatUptime(Math.max(0, nextPingMs));
|
|
802
805
|
}
|
|
806
|
+
const currentVersion = deps?.currentVersion;
|
|
807
|
+
const versionMismatch = currentVersion != null && state.version != null ? state.version !== currentVersion : false;
|
|
803
808
|
return {
|
|
804
809
|
running: true,
|
|
805
810
|
pid: state.pid,
|
|
806
811
|
startedAt: state.startedAt,
|
|
807
812
|
intervalMs: state.intervalMs,
|
|
808
813
|
uptime,
|
|
809
|
-
nextPingIn
|
|
814
|
+
nextPingIn,
|
|
815
|
+
versionMismatch,
|
|
816
|
+
daemonVersion: state.version
|
|
810
817
|
};
|
|
811
818
|
}
|
|
812
819
|
function formatUptime(ms) {
|
|
@@ -818,32 +825,47 @@ function formatUptime(ms) {
|
|
|
818
825
|
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
819
826
|
return `${seconds}s`;
|
|
820
827
|
}
|
|
828
|
+
function msUntilUtcHour(targetHour, now) {
|
|
829
|
+
const currentMs = now.getUTCHours() * 36e5 + now.getUTCMinutes() * 6e4 + now.getUTCSeconds() * 1e3 + now.getUTCMilliseconds();
|
|
830
|
+
const targetMs = targetHour * 36e5;
|
|
831
|
+
const diff = targetMs - currentMs;
|
|
832
|
+
return diff > 0 ? diff : diff + 24 * 36e5;
|
|
833
|
+
}
|
|
821
834
|
async function daemonLoop(intervalMs, options, deps) {
|
|
822
835
|
let wakeDelayMs;
|
|
823
836
|
while (!deps.shouldStop()) {
|
|
837
|
+
if (deps.hasUpgraded?.()) {
|
|
838
|
+
deps.log("Binary upgraded, exiting for restart...");
|
|
839
|
+
return "upgrade";
|
|
840
|
+
}
|
|
824
841
|
const allAccounts = deps.listAccounts();
|
|
825
842
|
let accounts = deps.isWindowActive ? allAccounts.filter((a) => !deps.isWindowActive(a.handle)) : allAccounts;
|
|
826
843
|
const skipped = allAccounts.length - accounts.length;
|
|
827
844
|
if (skipped > 0) {
|
|
828
845
|
deps.log(`Skipping ${skipped} account(s) with active window`);
|
|
829
846
|
}
|
|
847
|
+
let soonestDeferHour;
|
|
830
848
|
if (deps.shouldDeferPing) {
|
|
831
849
|
const deferResults = /* @__PURE__ */ new Map();
|
|
832
850
|
for (const a of accounts) {
|
|
833
|
-
deferResults.set(
|
|
834
|
-
a.handle,
|
|
835
|
-
deps.shouldDeferPing(a.handle, a.configDir).defer
|
|
836
|
-
);
|
|
851
|
+
deferResults.set(a.handle, deps.shouldDeferPing(a.handle, a.configDir));
|
|
837
852
|
}
|
|
838
|
-
const
|
|
839
|
-
if (
|
|
840
|
-
accounts = accounts.filter((a) => !deferResults.get(a.handle));
|
|
841
|
-
deps.log(`Deferring ${
|
|
853
|
+
const deferred = [...deferResults.entries()].filter(([, r]) => r.defer);
|
|
854
|
+
if (deferred.length > 0) {
|
|
855
|
+
accounts = accounts.filter((a) => !deferResults.get(a.handle)?.defer);
|
|
856
|
+
deps.log(`Deferring ${deferred.length} account(s) (smart scheduling)`);
|
|
857
|
+
for (const [, r] of deferred) {
|
|
858
|
+
if (r.deferUntilUtcHour !== void 0 && (soonestDeferHour === void 0 || /* c8 ignore next -- production default */
|
|
859
|
+
msUntilUtcHour(r.deferUntilUtcHour, deps.now?.() ?? /* @__PURE__ */ new Date()) < /* c8 ignore next -- production default */
|
|
860
|
+
msUntilUtcHour(soonestDeferHour, deps.now?.() ?? /* @__PURE__ */ new Date()))) {
|
|
861
|
+
soonestDeferHour = r.deferUntilUtcHour;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
842
864
|
}
|
|
843
865
|
}
|
|
844
866
|
if (accounts.length === 0) {
|
|
845
867
|
deps.log(
|
|
846
|
-
allAccounts.length === 0 ? "No accounts configured, waiting..." : "All accounts have active windows, waiting..."
|
|
868
|
+
allAccounts.length === 0 ? "No accounts configured, waiting..." : soonestDeferHour !== void 0 ? "All accounts deferred (smart scheduling), waiting..." : "All accounts have active windows, waiting..."
|
|
847
869
|
);
|
|
848
870
|
} else {
|
|
849
871
|
deps.log(
|
|
@@ -872,9 +894,20 @@ async function daemonLoop(intervalMs, options, deps) {
|
|
|
872
894
|
deps.updateState?.({ lastPingAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
873
895
|
}
|
|
874
896
|
if (deps.shouldStop()) break;
|
|
875
|
-
|
|
897
|
+
let sleepMs = intervalMs;
|
|
898
|
+
if (soonestDeferHour !== void 0) {
|
|
899
|
+
const msUntilDefer = msUntilUtcHour(
|
|
900
|
+
soonestDeferHour,
|
|
901
|
+
/* c8 ignore next -- production default */
|
|
902
|
+
deps.now?.() ?? /* @__PURE__ */ new Date()
|
|
903
|
+
);
|
|
904
|
+
if (msUntilDefer > 0 && msUntilDefer < intervalMs) {
|
|
905
|
+
sleepMs = msUntilDefer;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
deps.log(`Sleeping ${Math.round(sleepMs / 6e4)}m until next ping...`);
|
|
876
909
|
const sleepStart = Date.now();
|
|
877
|
-
await deps.sleep(
|
|
910
|
+
await deps.sleep(sleepMs);
|
|
878
911
|
const overshootMs = Date.now() - sleepStart - intervalMs;
|
|
879
912
|
if (overshootMs > 6e4) {
|
|
880
913
|
wakeDelayMs = overshootMs;
|
|
@@ -883,6 +916,7 @@ async function daemonLoop(intervalMs, options, deps) {
|
|
|
883
916
|
wakeDelayMs = void 0;
|
|
884
917
|
}
|
|
885
918
|
}
|
|
919
|
+
return "stop";
|
|
886
920
|
}
|
|
887
921
|
function startDaemon(options, deps) {
|
|
888
922
|
const _getDaemonStatus = deps?.getDaemonStatus ?? getDaemonStatus;
|
|
@@ -928,7 +962,8 @@ function startDaemon(options, deps) {
|
|
|
928
962
|
pid: child.pid,
|
|
929
963
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
930
964
|
intervalMs,
|
|
931
|
-
configDir
|
|
965
|
+
configDir,
|
|
966
|
+
version: options.version
|
|
932
967
|
});
|
|
933
968
|
return { success: true, pid: child.pid };
|
|
934
969
|
}
|
|
@@ -999,14 +1034,18 @@ async function runDaemon(intervalMs, options, deps) {
|
|
|
999
1034
|
deps.onSignal("SIGTERM", onSigterm);
|
|
1000
1035
|
deps.onSignal("SIGINT", onSigint);
|
|
1001
1036
|
deps.log(`Daemon started. Interval: ${Math.round(intervalMs / 6e4)}m`);
|
|
1037
|
+
let exitReason = "stop";
|
|
1002
1038
|
try {
|
|
1003
|
-
await daemonLoop(intervalMs, options, deps);
|
|
1039
|
+
exitReason = await daemonLoop(intervalMs, options, deps);
|
|
1004
1040
|
} finally {
|
|
1005
1041
|
deps.removeSignal("SIGTERM", onSigterm);
|
|
1006
1042
|
deps.removeSignal("SIGINT", onSigint);
|
|
1007
1043
|
deps.log("Daemon stopping...");
|
|
1008
1044
|
cleanup();
|
|
1009
1045
|
}
|
|
1046
|
+
if (exitReason === "upgrade") {
|
|
1047
|
+
deps.exit(75);
|
|
1048
|
+
}
|
|
1010
1049
|
}
|
|
1011
1050
|
async function runDaemonWithDefaults(intervalMs, options) {
|
|
1012
1051
|
const stopPath = daemonStopPath();
|
|
@@ -1035,6 +1074,8 @@ async function runDaemonWithDefaults(intervalMs, options) {
|
|
|
1035
1074
|
return shouldDefer2(/* @__PURE__ */ new Date(), schedule.optimalPingHour);
|
|
1036
1075
|
};
|
|
1037
1076
|
}
|
|
1077
|
+
const binaryPath = realpathSync(process.argv[1]);
|
|
1078
|
+
const startMtimeMs = statSync2(binaryPath).mtimeMs;
|
|
1038
1079
|
await runDaemon(intervalMs, options, {
|
|
1039
1080
|
runPing: runPing2,
|
|
1040
1081
|
listAccounts: listAccounts2,
|
|
@@ -1043,6 +1084,13 @@ async function runDaemonWithDefaults(intervalMs, options) {
|
|
|
1043
1084
|
log: (msg) => console.log(msg),
|
|
1044
1085
|
isWindowActive: (handle) => getWindowReset2(handle) !== null,
|
|
1045
1086
|
shouldDeferPing,
|
|
1087
|
+
hasUpgraded: () => {
|
|
1088
|
+
try {
|
|
1089
|
+
return statSync2(binaryPath).mtimeMs !== startMtimeMs;
|
|
1090
|
+
} catch {
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1046
1094
|
updateState: (patch) => {
|
|
1047
1095
|
const current = readDaemonState();
|
|
1048
1096
|
if (current) writeDaemonState({ ...current, ...patch });
|
|
@@ -1799,7 +1847,7 @@ init_paths();
|
|
|
1799
1847
|
init_run_ping();
|
|
1800
1848
|
|
|
1801
1849
|
// src/scan.ts
|
|
1802
|
-
import { existsSync as existsSync7, readdirSync, statSync as
|
|
1850
|
+
import { existsSync as existsSync7, readdirSync, statSync as statSync3 } from "fs";
|
|
1803
1851
|
import { homedir as homedir2 } from "os";
|
|
1804
1852
|
import { join as join9 } from "path";
|
|
1805
1853
|
function scanAccounts(dir) {
|
|
@@ -1807,7 +1855,7 @@ function scanAccounts(dir) {
|
|
|
1807
1855
|
if (!existsSync7(accountsDir)) return [];
|
|
1808
1856
|
return readdirSync(accountsDir).filter((name) => {
|
|
1809
1857
|
const full = join9(accountsDir, name);
|
|
1810
|
-
return
|
|
1858
|
+
return statSync3(full).isDirectory() && !name.startsWith(".") && existsSync7(join9(full, ".claude.json"));
|
|
1811
1859
|
}).map((name) => ({
|
|
1812
1860
|
handle: name,
|
|
1813
1861
|
configDir: join9(accountsDir, name)
|
|
@@ -1868,7 +1916,7 @@ function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
|
|
|
1868
1916
|
}
|
|
1869
1917
|
|
|
1870
1918
|
// src/cli.ts
|
|
1871
|
-
var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.
|
|
1919
|
+
var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.5.1").option(
|
|
1872
1920
|
"--config <path>",
|
|
1873
1921
|
"Path to config directory (default: ~/.config/cc-ping, env: CC_PING_CONFIG)"
|
|
1874
1922
|
).hook("preAction", (thisCommand) => {
|
|
@@ -2088,7 +2136,8 @@ daemon.command("start").description("Start the daemon process").option(
|
|
|
2088
2136
|
quiet: opts.quiet,
|
|
2089
2137
|
bell: opts.bell,
|
|
2090
2138
|
notify: opts.notify,
|
|
2091
|
-
smartSchedule
|
|
2139
|
+
smartSchedule,
|
|
2140
|
+
version: "1.5.1"
|
|
2092
2141
|
});
|
|
2093
2142
|
if (!result.success) {
|
|
2094
2143
|
console.error(result.error);
|
|
@@ -2122,7 +2171,7 @@ daemon.command("stop").description("Stop the daemon process").action(async () =>
|
|
|
2122
2171
|
daemon.command("status").description("Show daemon status").option("--json", "Output as JSON", false).action(async (opts) => {
|
|
2123
2172
|
const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
2124
2173
|
const svc = getServiceStatus2();
|
|
2125
|
-
const status = getDaemonStatus();
|
|
2174
|
+
const status = getDaemonStatus({ currentVersion: "1.5.1" });
|
|
2126
2175
|
if (opts.json) {
|
|
2127
2176
|
const serviceInfo = svc.installed ? {
|
|
2128
2177
|
service: {
|
|
@@ -2159,6 +2208,9 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
|
|
|
2159
2208
|
return;
|
|
2160
2209
|
}
|
|
2161
2210
|
console.log(`Daemon is running (PID: ${status.pid})`);
|
|
2211
|
+
if (status.daemonVersion) {
|
|
2212
|
+
console.log(` Version: ${status.daemonVersion}`);
|
|
2213
|
+
}
|
|
2162
2214
|
console.log(` Started: ${status.startedAt}`);
|
|
2163
2215
|
console.log(
|
|
2164
2216
|
` Interval: ${Math.round((status.intervalMs ?? 0) / 6e4)}m`
|
|
@@ -2171,6 +2223,14 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
|
|
|
2171
2223
|
const kind = svc.platform === "darwin" ? "launchd" : "systemd";
|
|
2172
2224
|
console.log(` System service: installed (${kind})`);
|
|
2173
2225
|
}
|
|
2226
|
+
if (status.versionMismatch) {
|
|
2227
|
+
console.log(
|
|
2228
|
+
` Warning: daemon is running v${status.daemonVersion} but v${"1.5.1"} is installed.`
|
|
2229
|
+
);
|
|
2230
|
+
console.log(
|
|
2231
|
+
" Restart to pick up the new version: cc-ping daemon stop && cc-ping daemon start"
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2174
2234
|
console.log("");
|
|
2175
2235
|
printAccountTable();
|
|
2176
2236
|
});
|
|
@@ -2227,7 +2287,8 @@ daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping inte
|
|
|
2227
2287
|
pid: process.pid,
|
|
2228
2288
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2229
2289
|
intervalMs,
|
|
2230
|
-
configDir: resolveConfigDir2()
|
|
2290
|
+
configDir: resolveConfigDir2(),
|
|
2291
|
+
version: "1.5.1"
|
|
2231
2292
|
});
|
|
2232
2293
|
}
|
|
2233
2294
|
await runDaemonWithDefaults(intervalMs, {
|