@wbern/cc-ping 1.3.2 → 1.4.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 +47 -5
- package/dist/cli.js +212 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,10 +37,11 @@ npm install -g @wbern/cc-ping # also works
|
|
|
37
37
|
|
|
38
38
|
## Setup
|
|
39
39
|
|
|
40
|
-
Discover accounts from
|
|
40
|
+
Discover accounts from `~` (or a custom directory), then verify they have valid credentials:
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
cc-ping scan # auto-discover accounts
|
|
43
|
+
cc-ping scan # auto-discover accounts from ~
|
|
44
|
+
cc-ping scan /path/to/dir # scan a specific directory
|
|
44
45
|
cc-ping check # verify credentials are valid
|
|
45
46
|
cc-ping list # show configured accounts
|
|
46
47
|
```
|
|
@@ -98,7 +99,7 @@ Show which account has its quota window resetting soonest — useful for knowing
|
|
|
98
99
|
|
|
99
100
|
### `cc-ping scan`
|
|
100
101
|
|
|
101
|
-
Auto-discover
|
|
102
|
+
Auto-discover Claude Code accounts. Scans `~` by default, or pass a directory to scan. Each subdirectory containing a `.claude.json` is detected as an account. Duplicate identities (same `accountUuid` across directories) are flagged.
|
|
102
103
|
|
|
103
104
|
### `cc-ping check`
|
|
104
105
|
|
|
@@ -141,6 +142,7 @@ cc-ping daemon stop # graceful shutdown
|
|
|
141
142
|
| Flag | Default | Description |
|
|
142
143
|
|------|---------|-------------|
|
|
143
144
|
| `--interval <duration>` | `300m` | Time between ping cycles |
|
|
145
|
+
| `--smart-schedule <on\|off>` | `on` | Time pings based on your usage patterns |
|
|
144
146
|
| `--notify` | `false` | Desktop notification when new windows open |
|
|
145
147
|
| `--bell` | `false` | Terminal bell on failure |
|
|
146
148
|
| `--quiet` | `true` | Suppress per-account output in logs |
|
|
@@ -148,12 +150,52 @@ cc-ping daemon stop # graceful shutdown
|
|
|
148
150
|
The daemon is smart about what it pings:
|
|
149
151
|
|
|
150
152
|
- **Skips active windows** — accounts with a quota window still running are skipped to avoid wasting pings
|
|
153
|
+
- **Retries failures** — if any accounts fail to ping, the daemon retries only the failed ones before sleeping
|
|
151
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
|
|
152
155
|
- **Singleton enforcement** — only one daemon runs at a time, verified by PID and process name
|
|
153
|
-
- **Graceful shutdown** — `daemon stop` writes a sentinel file and waits for a clean exit before force-killing
|
|
156
|
+
- **Graceful shutdown** — `daemon stop` writes a sentinel file and waits up to 60s for a clean exit before force-killing
|
|
154
157
|
|
|
155
158
|
Logs are written to `~/.config/cc-ping/daemon.log`.
|
|
156
159
|
|
|
160
|
+
### Smart scheduling
|
|
161
|
+
|
|
162
|
+
By default, the daemon analyzes your Claude Code usage history to time pings optimally. The goal: your 5-hour quota window expires right when you're most active, not while you're asleep.
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
Your typical day:
|
|
166
|
+
|
|
167
|
+
12am 6am 12pm 6pm 12am
|
|
168
|
+
| | ________|________ | |
|
|
169
|
+
. . . . . | coding time | . . . .
|
|
170
|
+
^
|
|
171
|
+
peak activity
|
|
172
|
+
|
|
173
|
+
Fixed interval -- ping fires whenever the timer says:
|
|
174
|
+
|
|
175
|
+
[======= 5h window =======]
|
|
176
|
+
12am 5am
|
|
177
|
+
^ expires while you sleep
|
|
178
|
+
|
|
179
|
+
Smart scheduling -- ping timed so window expires at peak:
|
|
180
|
+
|
|
181
|
+
[======= 5h window =======]
|
|
182
|
+
8am 1pm
|
|
183
|
+
^ expires while you code!
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**How it works:**
|
|
187
|
+
|
|
188
|
+
1. Reads `<configDir>/history.jsonl` from each account's config directory (Claude Code's prompt timestamps)
|
|
189
|
+
2. Builds an hour-of-day histogram from the last 14 days
|
|
190
|
+
3. Finds the weighted midpoint of your activity
|
|
191
|
+
4. Schedules pings at `midpoint - 5h` so the window expires at your peak
|
|
192
|
+
|
|
193
|
+
**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.
|
|
194
|
+
|
|
195
|
+
**Fallback:** If an account has fewer than 7 days of history or a flat usage pattern (no clear peak), smart scheduling is skipped and the fixed interval is used instead.
|
|
196
|
+
|
|
197
|
+
To disable: `cc-ping daemon start --smart-schedule off`
|
|
198
|
+
|
|
157
199
|
### System service (survive reboots)
|
|
158
200
|
|
|
159
201
|
`daemon start` runs as a detached process that won't survive a reboot. Use `daemon install` to register as a system service that starts automatically on login:
|
|
@@ -216,7 +258,7 @@ Key design choices:
|
|
|
216
258
|
- **Arithmetic prompts** — random math questions minimize token usage (~150 input tokens, ~10 output). Templates and operands are randomized to avoid cache hits across pings.
|
|
217
259
|
- **Tools disabled** — `--tools ""` prevents the model from doing anything beyond answering the question.
|
|
218
260
|
- **Single turn** — `--max-turns 1` ensures one request-response cycle, no follow-ups.
|
|
219
|
-
- **30s timeout** — pings that take longer are
|
|
261
|
+
- **30s timeout with hard kill** — pings that take longer are sent SIGKILL. A backstop timer force-resolves the promise even if the child process doesn't exit cleanly.
|
|
220
262
|
- **Cost tracking** — each ping records its USD cost and token usage so you can audit spend.
|
|
221
263
|
|
|
222
264
|
After a successful ping, the account's last-ping timestamp is saved to `~/.config/cc-ping/state.json`. The 5-hour quota window is calculated from this timestamp — commands like `status`, `suggest`, and the daemon all use it to determine window state.
|
package/dist/cli.js
CHANGED
|
@@ -290,9 +290,9 @@ function sendNotification(title, body, opts) {
|
|
|
290
290
|
const exec = opts?.exec ?? defaultExecFile;
|
|
291
291
|
const cmd = buildNotifyCommand(title, body, platform, { sound: opts?.sound });
|
|
292
292
|
if (!cmd) return Promise.resolve(false);
|
|
293
|
-
return new Promise((
|
|
293
|
+
return new Promise((resolve2) => {
|
|
294
294
|
exec(cmd[0], cmd[1], (error) => {
|
|
295
|
-
|
|
295
|
+
resolve2(!error);
|
|
296
296
|
});
|
|
297
297
|
});
|
|
298
298
|
}
|
|
@@ -386,7 +386,7 @@ function formatExecError(error) {
|
|
|
386
386
|
}
|
|
387
387
|
function pingOne(account) {
|
|
388
388
|
const start = Date.now();
|
|
389
|
-
return new Promise((
|
|
389
|
+
return new Promise((resolve2) => {
|
|
390
390
|
let resolved = false;
|
|
391
391
|
const child = execFile(
|
|
392
392
|
"claude",
|
|
@@ -421,7 +421,7 @@ function pingOne(account) {
|
|
|
421
421
|
} else if (isError) {
|
|
422
422
|
errorMsg = claudeResponse?.subtype;
|
|
423
423
|
}
|
|
424
|
-
|
|
424
|
+
resolve2({
|
|
425
425
|
handle: account.handle,
|
|
426
426
|
success: !error && !isError,
|
|
427
427
|
durationMs: Date.now() - start,
|
|
@@ -437,7 +437,7 @@ function pingOne(account) {
|
|
|
437
437
|
child.kill("SIGKILL");
|
|
438
438
|
} catch {
|
|
439
439
|
}
|
|
440
|
-
|
|
440
|
+
resolve2({
|
|
441
441
|
handle: account.handle,
|
|
442
442
|
success: false,
|
|
443
443
|
durationMs: Date.now() - start,
|
|
@@ -488,7 +488,7 @@ async function runPing(accounts, options) {
|
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
490
|
logger.log(`Pinging ${accounts.length} account(s)...`);
|
|
491
|
-
const sleep = options._sleep ?? ((ms) => new Promise((
|
|
491
|
+
const sleep = options._sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
492
492
|
let results;
|
|
493
493
|
if (options.staggerMs && options.staggerMs > 0 && accounts.length > 1) {
|
|
494
494
|
results = [];
|
|
@@ -597,6 +597,101 @@ var init_run_ping = __esm({
|
|
|
597
597
|
}
|
|
598
598
|
});
|
|
599
599
|
|
|
600
|
+
// src/schedule.ts
|
|
601
|
+
var schedule_exports = {};
|
|
602
|
+
__export(schedule_exports, {
|
|
603
|
+
buildHourHistogram: () => buildHourHistogram,
|
|
604
|
+
findOptimalPingHour: () => findOptimalPingHour,
|
|
605
|
+
getAccountSchedule: () => getAccountSchedule,
|
|
606
|
+
parseSmartSchedule: () => parseSmartSchedule,
|
|
607
|
+
readAccountSchedule: () => readAccountSchedule,
|
|
608
|
+
shouldDefer: () => shouldDefer
|
|
609
|
+
});
|
|
610
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
611
|
+
import { join as join6 } from "path";
|
|
612
|
+
function buildHourHistogram(timestamps) {
|
|
613
|
+
const bins = new Array(24).fill(0);
|
|
614
|
+
for (const ts of timestamps) {
|
|
615
|
+
bins[ts.getUTCHours()]++;
|
|
616
|
+
}
|
|
617
|
+
return bins;
|
|
618
|
+
}
|
|
619
|
+
function findOptimalPingHour(histogram) {
|
|
620
|
+
const total = histogram.reduce((sum, v) => sum + v, 0);
|
|
621
|
+
if (total === 0) return -1;
|
|
622
|
+
const HOUR_TO_RAD = 2 * Math.PI / 24;
|
|
623
|
+
let sinSum = 0;
|
|
624
|
+
let cosSum = 0;
|
|
625
|
+
for (let h = 0; h < 24; h++) {
|
|
626
|
+
const angle = h * HOUR_TO_RAD;
|
|
627
|
+
sinSum += histogram[h] * Math.sin(angle);
|
|
628
|
+
cosSum += histogram[h] * Math.cos(angle);
|
|
629
|
+
}
|
|
630
|
+
const meanAngle = Math.atan2(sinSum, cosSum);
|
|
631
|
+
const midpoint = Math.round((meanAngle / HOUR_TO_RAD + 24) % 24);
|
|
632
|
+
return (midpoint - QUOTA_WINDOW_HOURS + 24) % 24;
|
|
633
|
+
}
|
|
634
|
+
function shouldDefer(now, optimalPingHour) {
|
|
635
|
+
const currentHour = now.getUTCHours();
|
|
636
|
+
const zoneStart = (optimalPingHour - QUOTA_WINDOW_HOURS + 24) % 24;
|
|
637
|
+
const inZone = zoneStart < optimalPingHour ? currentHour >= zoneStart && currentHour < optimalPingHour : currentHour >= zoneStart || currentHour < optimalPingHour;
|
|
638
|
+
if (inZone) {
|
|
639
|
+
return { defer: true, deferUntilUtcHour: optimalPingHour };
|
|
640
|
+
}
|
|
641
|
+
return { defer: false };
|
|
642
|
+
}
|
|
643
|
+
function getAccountSchedule(historyLines, now = /* @__PURE__ */ new Date()) {
|
|
644
|
+
const cutoff = now.getTime() - HISTORY_WINDOW_MS;
|
|
645
|
+
const timestamps = [];
|
|
646
|
+
const daysSeen = /* @__PURE__ */ new Set();
|
|
647
|
+
for (const line of historyLines) {
|
|
648
|
+
try {
|
|
649
|
+
const entry = JSON.parse(line);
|
|
650
|
+
if (typeof entry.timestamp !== "number") continue;
|
|
651
|
+
if (entry.timestamp < cutoff) continue;
|
|
652
|
+
const date = new Date(entry.timestamp);
|
|
653
|
+
timestamps.push(date);
|
|
654
|
+
daysSeen.add(date.toISOString().slice(0, 10));
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (daysSeen.size < MIN_DAYS) return null;
|
|
659
|
+
const histogram = buildHourHistogram(timestamps);
|
|
660
|
+
const total = histogram.reduce((sum, v) => sum + v, 0);
|
|
661
|
+
const avg = total / 24;
|
|
662
|
+
const max = Math.max(...histogram);
|
|
663
|
+
if (max <= avg * 1.5) return null;
|
|
664
|
+
const optimalPingHour = findOptimalPingHour(histogram);
|
|
665
|
+
if (optimalPingHour === -1) return null;
|
|
666
|
+
return { optimalPingHour, histogram };
|
|
667
|
+
}
|
|
668
|
+
function readAccountSchedule(configDir, now = /* @__PURE__ */ new Date()) {
|
|
669
|
+
const historyPath = join6(configDir, "history.jsonl");
|
|
670
|
+
if (!existsSync5(historyPath)) return null;
|
|
671
|
+
const content = readFileSync5(historyPath, "utf-8");
|
|
672
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
673
|
+
return getAccountSchedule(lines, now);
|
|
674
|
+
}
|
|
675
|
+
function parseSmartSchedule(value) {
|
|
676
|
+
const lower = value.toLowerCase();
|
|
677
|
+
if (TRUTHY.has(lower)) return true;
|
|
678
|
+
if (FALSY.has(lower)) return false;
|
|
679
|
+
throw new Error(
|
|
680
|
+
`Invalid smart-schedule value: "${value}". Use true/false, on/off, or 1/0`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
var QUOTA_WINDOW_HOURS, MIN_DAYS, HISTORY_WINDOW_MS, TRUTHY, FALSY;
|
|
684
|
+
var init_schedule = __esm({
|
|
685
|
+
"src/schedule.ts"() {
|
|
686
|
+
"use strict";
|
|
687
|
+
QUOTA_WINDOW_HOURS = 5;
|
|
688
|
+
MIN_DAYS = 7;
|
|
689
|
+
HISTORY_WINDOW_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
690
|
+
TRUTHY = /* @__PURE__ */ new Set(["true", "on", "1"]);
|
|
691
|
+
FALSY = /* @__PURE__ */ new Set(["false", "off", "0"]);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
600
695
|
// src/daemon.ts
|
|
601
696
|
var daemon_exports = {};
|
|
602
697
|
__export(daemon_exports, {
|
|
@@ -617,23 +712,23 @@ __export(daemon_exports, {
|
|
|
617
712
|
});
|
|
618
713
|
import { execSync, spawn } from "child_process";
|
|
619
714
|
import {
|
|
620
|
-
existsSync as
|
|
715
|
+
existsSync as existsSync6,
|
|
621
716
|
closeSync as fsCloseSync,
|
|
622
717
|
openSync as fsOpenSync,
|
|
623
718
|
mkdirSync as mkdirSync4,
|
|
624
|
-
readFileSync as
|
|
719
|
+
readFileSync as readFileSync6,
|
|
625
720
|
unlinkSync,
|
|
626
721
|
writeFileSync as writeFileSync3
|
|
627
722
|
} from "fs";
|
|
628
|
-
import { join as
|
|
723
|
+
import { join as join7 } from "path";
|
|
629
724
|
function daemonPidPath() {
|
|
630
|
-
return
|
|
725
|
+
return join7(resolveConfigDir(), "daemon.json");
|
|
631
726
|
}
|
|
632
727
|
function daemonLogPath() {
|
|
633
|
-
return
|
|
728
|
+
return join7(resolveConfigDir(), "daemon.log");
|
|
634
729
|
}
|
|
635
730
|
function daemonStopPath() {
|
|
636
|
-
return
|
|
731
|
+
return join7(resolveConfigDir(), "daemon.stop");
|
|
637
732
|
}
|
|
638
733
|
function writeDaemonState(state) {
|
|
639
734
|
const configDir = resolveConfigDir();
|
|
@@ -643,9 +738,9 @@ function writeDaemonState(state) {
|
|
|
643
738
|
}
|
|
644
739
|
function readDaemonState() {
|
|
645
740
|
const pidPath = daemonPidPath();
|
|
646
|
-
if (!
|
|
741
|
+
if (!existsSync6(pidPath)) return null;
|
|
647
742
|
try {
|
|
648
|
-
const raw =
|
|
743
|
+
const raw = readFileSync6(pidPath, "utf-8");
|
|
649
744
|
return JSON.parse(raw);
|
|
650
745
|
} catch {
|
|
651
746
|
return null;
|
|
@@ -653,7 +748,7 @@ function readDaemonState() {
|
|
|
653
748
|
}
|
|
654
749
|
function removeDaemonState() {
|
|
655
750
|
const pidPath = daemonPidPath();
|
|
656
|
-
if (!
|
|
751
|
+
if (!existsSync6(pidPath)) return false;
|
|
657
752
|
unlinkSync(pidPath);
|
|
658
753
|
return true;
|
|
659
754
|
}
|
|
@@ -727,11 +822,25 @@ async function daemonLoop(intervalMs, options, deps) {
|
|
|
727
822
|
let wakeDelayMs;
|
|
728
823
|
while (!deps.shouldStop()) {
|
|
729
824
|
const allAccounts = deps.listAccounts();
|
|
730
|
-
|
|
825
|
+
let accounts = deps.isWindowActive ? allAccounts.filter((a) => !deps.isWindowActive(a.handle)) : allAccounts;
|
|
731
826
|
const skipped = allAccounts.length - accounts.length;
|
|
732
827
|
if (skipped > 0) {
|
|
733
828
|
deps.log(`Skipping ${skipped} account(s) with active window`);
|
|
734
829
|
}
|
|
830
|
+
if (deps.shouldDeferPing) {
|
|
831
|
+
const deferResults = /* @__PURE__ */ new Map();
|
|
832
|
+
for (const a of accounts) {
|
|
833
|
+
deferResults.set(
|
|
834
|
+
a.handle,
|
|
835
|
+
deps.shouldDeferPing(a.handle, a.configDir).defer
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
const deferredCount = [...deferResults.values()].filter(Boolean).length;
|
|
839
|
+
if (deferredCount > 0) {
|
|
840
|
+
accounts = accounts.filter((a) => !deferResults.get(a.handle));
|
|
841
|
+
deps.log(`Deferring ${deferredCount} account(s) (smart scheduling)`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
735
844
|
if (accounts.length === 0) {
|
|
736
845
|
deps.log(
|
|
737
846
|
allAccounts.length === 0 ? "No accounts configured, waiting..." : "All accounts have active windows, waiting..."
|
|
@@ -803,6 +912,7 @@ function startDaemon(options, deps) {
|
|
|
803
912
|
if (options.quiet) args.push("--quiet");
|
|
804
913
|
if (options.bell) args.push("--bell");
|
|
805
914
|
if (options.notify) args.push("--notify");
|
|
915
|
+
if (options.smartSchedule === false) args.push("--smart-schedule", "off");
|
|
806
916
|
const child = _spawn(process.execPath, [process.argv[1], ...args], {
|
|
807
917
|
detached: true,
|
|
808
918
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -835,9 +945,9 @@ async function stopDaemon(deps) {
|
|
|
835
945
|
const _removeStopFile = deps?.removeStopFile ?? /* c8 ignore next 4 -- production default */
|
|
836
946
|
(() => {
|
|
837
947
|
const stopPath = daemonStopPath();
|
|
838
|
-
if (
|
|
948
|
+
if (existsSync6(stopPath)) unlinkSync(stopPath);
|
|
839
949
|
});
|
|
840
|
-
const _sleep = deps?.sleep ?? ((ms) => new Promise((
|
|
950
|
+
const _sleep = deps?.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
841
951
|
const _kill = deps?.kill ?? /* c8 ignore next 7 -- production default */
|
|
842
952
|
((pid2) => {
|
|
843
953
|
if (process.platform === "win32") {
|
|
@@ -871,9 +981,9 @@ async function stopDaemon(deps) {
|
|
|
871
981
|
}
|
|
872
982
|
async function runDaemon(intervalMs, options, deps) {
|
|
873
983
|
const stopPath = daemonStopPath();
|
|
874
|
-
if (
|
|
984
|
+
if (existsSync6(stopPath)) unlinkSync(stopPath);
|
|
875
985
|
const cleanup = () => {
|
|
876
|
-
if (
|
|
986
|
+
if (existsSync6(stopPath)) unlinkSync(stopPath);
|
|
877
987
|
removeDaemonState();
|
|
878
988
|
};
|
|
879
989
|
const onSigterm = () => {
|
|
@@ -903,13 +1013,36 @@ async function runDaemonWithDefaults(intervalMs, options) {
|
|
|
903
1013
|
const { runPing: runPing2 } = await Promise.resolve().then(() => (init_run_ping(), run_ping_exports));
|
|
904
1014
|
const { listAccounts: listAccounts2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
905
1015
|
const { getWindowReset: getWindowReset2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
1016
|
+
const smartScheduleEnabled = options.smartSchedule !== false;
|
|
1017
|
+
let shouldDeferPing;
|
|
1018
|
+
if (smartScheduleEnabled) {
|
|
1019
|
+
const { readAccountSchedule: readAccountSchedule2, shouldDefer: shouldDefer2 } = await Promise.resolve().then(() => (init_schedule(), schedule_exports));
|
|
1020
|
+
for (const account of listAccounts2()) {
|
|
1021
|
+
const schedule = readAccountSchedule2(account.configDir);
|
|
1022
|
+
if (schedule) {
|
|
1023
|
+
console.log(
|
|
1024
|
+
`Smart schedule: ${account.handle} \u2192 optimal ping at ${schedule.optimalPingHour}:00 UTC`
|
|
1025
|
+
);
|
|
1026
|
+
} else {
|
|
1027
|
+
console.log(
|
|
1028
|
+
`Smart schedule: ${account.handle} \u2192 insufficient history, using fixed interval`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
shouldDeferPing = (_handle, configDir) => {
|
|
1033
|
+
const schedule = readAccountSchedule2(configDir);
|
|
1034
|
+
if (!schedule) return { defer: false };
|
|
1035
|
+
return shouldDefer2(/* @__PURE__ */ new Date(), schedule.optimalPingHour);
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
906
1038
|
await runDaemon(intervalMs, options, {
|
|
907
1039
|
runPing: runPing2,
|
|
908
1040
|
listAccounts: listAccounts2,
|
|
909
|
-
sleep: (ms) => new Promise((
|
|
910
|
-
shouldStop: () =>
|
|
1041
|
+
sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
|
|
1042
|
+
shouldStop: () => existsSync6(stopPath),
|
|
911
1043
|
log: (msg) => console.log(msg),
|
|
912
1044
|
isWindowActive: (handle) => getWindowReset2(handle) !== null,
|
|
1045
|
+
shouldDeferPing,
|
|
913
1046
|
updateState: (patch) => {
|
|
914
1047
|
const current = readDaemonState();
|
|
915
1048
|
if (current) writeDaemonState({ ...current, ...patch });
|
|
@@ -950,7 +1083,7 @@ import {
|
|
|
950
1083
|
writeFileSync as nodeWriteFileSync
|
|
951
1084
|
} from "fs";
|
|
952
1085
|
import { homedir as nodeHomedir } from "os";
|
|
953
|
-
import { dirname, join as
|
|
1086
|
+
import { dirname, join as join10 } from "path";
|
|
954
1087
|
function resolveExecutable(deps) {
|
|
955
1088
|
try {
|
|
956
1089
|
const path = deps.execSync("which cc-ping", {
|
|
@@ -979,10 +1112,12 @@ function generateLaunchdPlist(options, execInfo, configDir) {
|
|
|
979
1112
|
if (options.quiet) programArgs.push("--quiet");
|
|
980
1113
|
if (options.bell) programArgs.push("--bell");
|
|
981
1114
|
if (options.notify) programArgs.push("--notify");
|
|
1115
|
+
if (options.smartSchedule === false)
|
|
1116
|
+
programArgs.push("--smart-schedule", "off");
|
|
982
1117
|
const allArgs = [execInfo.executable, ...programArgs];
|
|
983
1118
|
const argsXml = allArgs.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
984
|
-
const logPath =
|
|
985
|
-
configDir ||
|
|
1119
|
+
const logPath = join10(
|
|
1120
|
+
configDir || join10(nodeHomedir(), ".config", "cc-ping"),
|
|
986
1121
|
"daemon.log"
|
|
987
1122
|
);
|
|
988
1123
|
let envSection = "";
|
|
@@ -1031,6 +1166,8 @@ function generateSystemdUnit(options, execInfo, configDir) {
|
|
|
1031
1166
|
if (options.quiet) programArgs.push("--quiet");
|
|
1032
1167
|
if (options.bell) programArgs.push("--bell");
|
|
1033
1168
|
if (options.notify) programArgs.push("--notify");
|
|
1169
|
+
if (options.smartSchedule === false)
|
|
1170
|
+
programArgs.push("--smart-schedule", "off");
|
|
1034
1171
|
const execStart = [execInfo.executable, ...programArgs].map((a) => a.includes(" ") ? `"${a}"` : a).join(" ");
|
|
1035
1172
|
let envLine = "";
|
|
1036
1173
|
if (configDir) {
|
|
@@ -1053,9 +1190,9 @@ WantedBy=default.target
|
|
|
1053
1190
|
function servicePath(platform, home) {
|
|
1054
1191
|
switch (platform) {
|
|
1055
1192
|
case "darwin":
|
|
1056
|
-
return
|
|
1193
|
+
return join10(home, "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
1057
1194
|
case "linux":
|
|
1058
|
-
return
|
|
1195
|
+
return join10(
|
|
1059
1196
|
home,
|
|
1060
1197
|
".config",
|
|
1061
1198
|
"systemd",
|
|
@@ -1201,7 +1338,7 @@ var init_service = __esm({
|
|
|
1201
1338
|
});
|
|
1202
1339
|
|
|
1203
1340
|
// src/cli.ts
|
|
1204
|
-
import { basename } from "path";
|
|
1341
|
+
import { basename, resolve } from "path";
|
|
1205
1342
|
import { Command } from "commander";
|
|
1206
1343
|
|
|
1207
1344
|
// src/check.ts
|
|
@@ -1466,12 +1603,12 @@ init_daemon();
|
|
|
1466
1603
|
init_config();
|
|
1467
1604
|
|
|
1468
1605
|
// src/identity.ts
|
|
1469
|
-
import { readFileSync as
|
|
1470
|
-
import { join as
|
|
1606
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1607
|
+
import { join as join8 } from "path";
|
|
1471
1608
|
function readAccountIdentity(configDir) {
|
|
1472
1609
|
let raw;
|
|
1473
1610
|
try {
|
|
1474
|
-
raw =
|
|
1611
|
+
raw = readFileSync7(join8(configDir, ".claude.json"), "utf-8");
|
|
1475
1612
|
} catch {
|
|
1476
1613
|
return null;
|
|
1477
1614
|
}
|
|
@@ -1662,21 +1799,24 @@ init_paths();
|
|
|
1662
1799
|
init_run_ping();
|
|
1663
1800
|
|
|
1664
1801
|
// src/scan.ts
|
|
1665
|
-
import { existsSync as
|
|
1802
|
+
import { existsSync as existsSync7, readdirSync, statSync as statSync2 } from "fs";
|
|
1666
1803
|
import { homedir as homedir2 } from "os";
|
|
1667
|
-
import { join as
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
if (!
|
|
1671
|
-
return readdirSync(
|
|
1672
|
-
const full =
|
|
1673
|
-
return statSync2(full).isDirectory() && !name.startsWith(".");
|
|
1804
|
+
import { join as join9 } from "path";
|
|
1805
|
+
function scanAccounts(dir) {
|
|
1806
|
+
const accountsDir = dir ?? homedir2();
|
|
1807
|
+
if (!existsSync7(accountsDir)) return [];
|
|
1808
|
+
return readdirSync(accountsDir).filter((name) => {
|
|
1809
|
+
const full = join9(accountsDir, name);
|
|
1810
|
+
return statSync2(full).isDirectory() && !name.startsWith(".") && existsSync7(join9(full, ".claude.json"));
|
|
1674
1811
|
}).map((name) => ({
|
|
1675
1812
|
handle: name,
|
|
1676
|
-
configDir:
|
|
1813
|
+
configDir: join9(accountsDir, name)
|
|
1677
1814
|
}));
|
|
1678
1815
|
}
|
|
1679
1816
|
|
|
1817
|
+
// src/cli.ts
|
|
1818
|
+
init_schedule();
|
|
1819
|
+
|
|
1680
1820
|
// src/stagger.ts
|
|
1681
1821
|
init_state();
|
|
1682
1822
|
function calculateStagger(accountCount, windowMs = QUOTA_WINDOW_MS) {
|
|
@@ -1728,7 +1868,7 @@ function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
|
|
|
1728
1868
|
}
|
|
1729
1869
|
|
|
1730
1870
|
// src/cli.ts
|
|
1731
|
-
var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.
|
|
1871
|
+
var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.4.0").option(
|
|
1732
1872
|
"--config <path>",
|
|
1733
1873
|
"Path to config directory (default: ~/.config/cc-ping, env: CC_PING_CONFIG)"
|
|
1734
1874
|
).hook("preAction", (thisCommand) => {
|
|
@@ -1794,10 +1934,11 @@ program.command("check").description(
|
|
|
1794
1934
|
process.exit(1);
|
|
1795
1935
|
}
|
|
1796
1936
|
});
|
|
1797
|
-
program.command("scan").description("Auto-discover accounts
|
|
1798
|
-
const
|
|
1937
|
+
program.command("scan").description("Auto-discover Claude Code accounts (scans ~ by default)").argument("[dir]", "Directory to scan (default: ~)").option("--dry-run", "Show what would be added without saving", false).action((dir, opts) => {
|
|
1938
|
+
const scanDir = dir ? resolve(dir) : void 0;
|
|
1939
|
+
const found = scanAccounts(scanDir);
|
|
1799
1940
|
if (found.length === 0) {
|
|
1800
|
-
console.log(
|
|
1941
|
+
console.log(`No accounts found in ${scanDir ?? "~"}`);
|
|
1801
1942
|
return;
|
|
1802
1943
|
}
|
|
1803
1944
|
console.log(`Found ${found.length} account(s):`);
|
|
@@ -1934,12 +2075,20 @@ var daemon = program.command("daemon").description("Run auto-ping on a schedule"
|
|
|
1934
2075
|
daemon.command("start").description("Start the daemon process").option(
|
|
1935
2076
|
"--interval <minutes>",
|
|
1936
2077
|
"Ping interval in minutes (default: 300 = 5h quota window)"
|
|
1937
|
-
).option("-q, --quiet", "Suppress ping output", false).option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).
|
|
2078
|
+
).option("-q, --quiet", "Suppress ping output", false).option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).option(
|
|
2079
|
+
"--smart-schedule <on|off>",
|
|
2080
|
+
"Time pings based on usage patterns (default: on)"
|
|
2081
|
+
).action(async (opts) => {
|
|
2082
|
+
let smartSchedule;
|
|
2083
|
+
if (opts.smartSchedule !== void 0) {
|
|
2084
|
+
smartSchedule = parseSmartSchedule(opts.smartSchedule);
|
|
2085
|
+
}
|
|
1938
2086
|
const result = startDaemon({
|
|
1939
2087
|
interval: opts.interval,
|
|
1940
2088
|
quiet: opts.quiet,
|
|
1941
2089
|
bell: opts.bell,
|
|
1942
|
-
notify: opts.notify
|
|
2090
|
+
notify: opts.notify,
|
|
2091
|
+
smartSchedule
|
|
1943
2092
|
});
|
|
1944
2093
|
if (!result.success) {
|
|
1945
2094
|
console.error(result.error);
|
|
@@ -2028,13 +2177,21 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
|
|
|
2028
2177
|
daemon.command("install").description("Install daemon as a system service (launchd/systemd)").option(
|
|
2029
2178
|
"--interval <minutes>",
|
|
2030
2179
|
"Ping interval in minutes (default: 300 = 5h quota window)"
|
|
2031
|
-
).option("-q, --quiet", "Suppress ping output", false).option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).
|
|
2180
|
+
).option("-q, --quiet", "Suppress ping output", false).option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).option(
|
|
2181
|
+
"--smart-schedule <on|off>",
|
|
2182
|
+
"Time pings based on usage patterns (default: on)"
|
|
2183
|
+
).action(async (opts) => {
|
|
2184
|
+
let smartSchedule;
|
|
2185
|
+
if (opts.smartSchedule !== void 0) {
|
|
2186
|
+
smartSchedule = parseSmartSchedule(opts.smartSchedule);
|
|
2187
|
+
}
|
|
2032
2188
|
const { installService: installService2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
2033
2189
|
const result = await installService2({
|
|
2034
2190
|
interval: opts.interval,
|
|
2035
2191
|
quiet: opts.quiet,
|
|
2036
2192
|
bell: opts.bell,
|
|
2037
|
-
notify: opts.notify
|
|
2193
|
+
notify: opts.notify,
|
|
2194
|
+
smartSchedule
|
|
2038
2195
|
});
|
|
2039
2196
|
if (!result.success) {
|
|
2040
2197
|
console.error(result.error);
|
|
@@ -2054,12 +2211,16 @@ daemon.command("uninstall").description("Remove daemon system service").action(a
|
|
|
2054
2211
|
}
|
|
2055
2212
|
console.log(`Service removed: ${result.servicePath}`);
|
|
2056
2213
|
});
|
|
2057
|
-
daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping interval in milliseconds").option("-q, --quiet", "Suppress ping output", false).option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).action(async (opts) => {
|
|
2214
|
+
daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping interval in milliseconds").option("-q, --quiet", "Suppress ping output", false).option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).option("--smart-schedule <on|off>", "Smart scheduling (default: on)").action(async (opts) => {
|
|
2058
2215
|
const intervalMs = Number(opts.intervalMs);
|
|
2059
2216
|
if (!intervalMs || intervalMs <= 0) {
|
|
2060
2217
|
console.error("Invalid --interval-ms");
|
|
2061
2218
|
process.exit(1);
|
|
2062
2219
|
}
|
|
2220
|
+
let smartSchedule;
|
|
2221
|
+
if (opts.smartSchedule !== void 0) {
|
|
2222
|
+
smartSchedule = parseSmartSchedule(opts.smartSchedule);
|
|
2223
|
+
}
|
|
2063
2224
|
if (!readDaemonState()) {
|
|
2064
2225
|
const { resolveConfigDir: resolveConfigDir2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
2065
2226
|
writeDaemonState({
|
|
@@ -2072,7 +2233,8 @@ daemon.command("_run", { hidden: true }).option("--interval-ms <ms>", "Ping inte
|
|
|
2072
2233
|
await runDaemonWithDefaults(intervalMs, {
|
|
2073
2234
|
quiet: opts.quiet,
|
|
2074
2235
|
bell: opts.bell,
|
|
2075
|
-
notify: opts.notify
|
|
2236
|
+
notify: opts.notify,
|
|
2237
|
+
smartSchedule
|
|
2076
2238
|
});
|
|
2077
2239
|
});
|
|
2078
2240
|
program.parse();
|