@wbern/cc-ping 0.1.0 → 1.0.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 +241 -0
- package/dist/cli.js +648 -267
- package/package.json +15 -13
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# @wbern/cc-ping
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@wbern/cc-ping)
|
|
4
|
+
[](https://www.npmjs.com/package/@wbern/cc-ping)
|
|
5
|
+
[](https://github.com/wbern/cc-ping/actions/workflows/ci.yml)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**Ping Claude Code sessions to trigger quota windows early across multiple accounts.**
|
|
9
|
+
|
|
10
|
+
Claude Code has a 5-hour quota window that starts on your first message. If you rotate between accounts, your idle accounts sit there with full quota doing nothing. cc-ping pings them so their windows start ticking — when you need them, they've already reset.
|
|
11
|
+
|
|
12
|
+
Zero telemetry. No data is collected, sent, or phoned home. Everything stays in `~/.config/cc-ping/`. The only network activity is the `claude` CLI call itself, which is subject to Anthropic's normal Claude Code telemetry.
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
[Claude Code](https://docs.anthropic.com/en/docs/claude-code) must be installed and on your PATH.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
claude --version # verify it's available
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick run (no install)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm dlx @wbern/cc-ping ping
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add -g @wbern/cc-ping
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g @wbern/cc-ping # also works
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
Discover accounts from `~/.claude-accounts/`, then verify they have valid credentials:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cc-ping scan # auto-discover accounts
|
|
44
|
+
cc-ping check # verify credentials are valid
|
|
45
|
+
cc-ping list # show configured accounts
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or add accounts manually:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cc-ping add my-account ~/.claude-accounts/my-account
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cc-ping ping # ping all accounts
|
|
58
|
+
cc-ping ping alice bob # ping specific accounts
|
|
59
|
+
cc-ping ping --parallel # ping all at once
|
|
60
|
+
cc-ping ping --notify # desktop notification on new windows
|
|
61
|
+
cc-ping ping --bell # terminal bell on failure
|
|
62
|
+
cc-ping ping --stagger 5s # wait 5s between each account
|
|
63
|
+
cc-ping status # show account status table
|
|
64
|
+
cc-ping suggest # which account should I use next?
|
|
65
|
+
cc-ping next-reset # when does the next window expire?
|
|
66
|
+
cc-ping daemon start --interval 300m # auto-ping every 5 hours
|
|
67
|
+
cc-ping history # recent ping results
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
### `cc-ping ping [handles...]`
|
|
73
|
+
|
|
74
|
+
Ping configured accounts to start their quota windows. Pings accounts sequentially by default.
|
|
75
|
+
|
|
76
|
+
| Flag | Default | Description |
|
|
77
|
+
|------|---------|-------------|
|
|
78
|
+
| `--parallel` | `false` | Ping all accounts simultaneously |
|
|
79
|
+
| `--stagger <duration>` | — | Wait between each account (e.g. `5s`, `1m`) |
|
|
80
|
+
| `--notify` | `false` | Desktop notification when new windows open |
|
|
81
|
+
| `--bell` | `false` | Terminal bell on failure |
|
|
82
|
+
| `--json` | `false` | Output results as JSON |
|
|
83
|
+
| `--quiet` | `false` | Suppress per-account output |
|
|
84
|
+
| `--group <name>` | — | Only ping accounts in this group |
|
|
85
|
+
| `--exclude <handles>` | — | Skip specific accounts |
|
|
86
|
+
|
|
87
|
+
### `cc-ping status`
|
|
88
|
+
|
|
89
|
+
Show all accounts with their quota window state — whether they have an active window, when it resets, and duplicate detection.
|
|
90
|
+
|
|
91
|
+
### `cc-ping suggest`
|
|
92
|
+
|
|
93
|
+
Recommend which account to use next based on quota window state. Prefers accounts whose windows have already expired or are about to.
|
|
94
|
+
|
|
95
|
+
### `cc-ping next-reset`
|
|
96
|
+
|
|
97
|
+
Show which account has its quota window resetting soonest — useful for knowing when capacity frees up.
|
|
98
|
+
|
|
99
|
+
### `cc-ping scan`
|
|
100
|
+
|
|
101
|
+
Auto-discover accounts from `~/.claude-accounts/`. Each subdirectory with a `claude_user.json` is detected as an account. Duplicate identities (same `accountUuid` across directories) are flagged.
|
|
102
|
+
|
|
103
|
+
### `cc-ping check`
|
|
104
|
+
|
|
105
|
+
Verify that each configured account's config directory exists and has credentials.
|
|
106
|
+
|
|
107
|
+
### `cc-ping add <handle> <config-dir>`
|
|
108
|
+
|
|
109
|
+
Manually add an account by name and Claude Code config directory path.
|
|
110
|
+
|
|
111
|
+
### `cc-ping remove <handle>`
|
|
112
|
+
|
|
113
|
+
Remove an account from the configuration.
|
|
114
|
+
|
|
115
|
+
### `cc-ping list`
|
|
116
|
+
|
|
117
|
+
List all configured accounts with their config directory paths.
|
|
118
|
+
|
|
119
|
+
### `cc-ping history`
|
|
120
|
+
|
|
121
|
+
Show recent ping results — handle, success/failure, duration, cost.
|
|
122
|
+
|
|
123
|
+
### `cc-ping completions <shell>`
|
|
124
|
+
|
|
125
|
+
Generate shell completion scripts for `bash`, `zsh`, or `fish`.
|
|
126
|
+
|
|
127
|
+
### `cc-ping moo`
|
|
128
|
+
|
|
129
|
+
Send a test desktop notification to verify notifications work on your platform.
|
|
130
|
+
|
|
131
|
+
## Daemon
|
|
132
|
+
|
|
133
|
+
The daemon auto-pings on a schedule so you don't have to remember.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
cc-ping daemon start --interval 300m # every 5 hours
|
|
137
|
+
cc-ping daemon status # check if running, next ping time
|
|
138
|
+
cc-ping daemon stop # graceful shutdown
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| Flag | Default | Description |
|
|
142
|
+
|------|---------|-------------|
|
|
143
|
+
| `--interval <duration>` | `300m` | Time between ping cycles |
|
|
144
|
+
| `--notify` | `false` | Desktop notification when new windows open |
|
|
145
|
+
| `--bell` | `false` | Terminal bell on failure |
|
|
146
|
+
| `--quiet` | `true` | Suppress per-account output in logs |
|
|
147
|
+
|
|
148
|
+
The daemon is smart about what it pings:
|
|
149
|
+
|
|
150
|
+
- **Skips active windows** — accounts with a quota window still running are skipped to avoid wasting pings
|
|
151
|
+
- **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
|
+
- **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
|
|
154
|
+
|
|
155
|
+
Logs are written to `~/.config/cc-ping/daemon.log`.
|
|
156
|
+
|
|
157
|
+
### System service (survive reboots)
|
|
158
|
+
|
|
159
|
+
`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:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
cc-ping daemon install --interval 300m --notify # install and start
|
|
163
|
+
cc-ping daemon status # shows "System service: installed"
|
|
164
|
+
cc-ping daemon uninstall # remove service and stop
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Platform | Service manager | Service file |
|
|
168
|
+
|----------|----------------|--------------|
|
|
169
|
+
| macOS | launchd | `~/Library/LaunchAgents/com.cc-ping.daemon.plist` |
|
|
170
|
+
| Linux | systemd (user) | `~/.config/systemd/user/cc-ping-daemon.service` |
|
|
171
|
+
|
|
172
|
+
The service restarts the daemon on crash (but not on clean exit via `daemon stop`). No `sudo` required — both use user-level service managers.
|
|
173
|
+
|
|
174
|
+
**`daemon stop` vs `daemon uninstall`:** When a service is installed, `daemon stop` kills the process but the service manager may restart it on crash. Use `daemon uninstall` to fully remove the service, or `daemon stop` if you just need a temporary pause.
|
|
175
|
+
|
|
176
|
+
## Notifications
|
|
177
|
+
|
|
178
|
+
Desktop notifications work on macOS, Linux, and Windows:
|
|
179
|
+
|
|
180
|
+
| Platform | Mechanism |
|
|
181
|
+
|----------|-----------|
|
|
182
|
+
| macOS | `osascript` (AppleScript `display notification`) |
|
|
183
|
+
| Linux | `notify-send` |
|
|
184
|
+
| Windows | PowerShell `New-BurntToastNotification` |
|
|
185
|
+
|
|
186
|
+
Use `cc-ping moo` to verify notifications work on your system.
|
|
187
|
+
|
|
188
|
+
## Shell completions
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# bash
|
|
192
|
+
cc-ping completions bash >> ~/.bashrc
|
|
193
|
+
|
|
194
|
+
# zsh
|
|
195
|
+
cc-ping completions zsh >> ~/.zshrc
|
|
196
|
+
|
|
197
|
+
# fish
|
|
198
|
+
cc-ping completions fish > ~/.config/fish/completions/cc-ping.fish
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## How it works
|
|
202
|
+
|
|
203
|
+
Each ping spawns the `claude` CLI with a trivial arithmetic prompt:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
claude -p "Quick, take a guess: what is 2847 + 6192?" \
|
|
207
|
+
--output-format json \
|
|
208
|
+
--tools "" \
|
|
209
|
+
--max-turns 1
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The account is selected by setting `CLAUDE_CONFIG_DIR` to the account's config directory, so the `claude` CLI authenticates as that account.
|
|
213
|
+
|
|
214
|
+
Key design choices:
|
|
215
|
+
|
|
216
|
+
- **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
|
+
- **Tools disabled** — `--tools ""` prevents the model from doing anything beyond answering the question.
|
|
218
|
+
- **Single turn** — `--max-turns 1` ensures one request-response cycle, no follow-ups.
|
|
219
|
+
- **30s timeout** — pings that take longer are killed.
|
|
220
|
+
- **Cost tracking** — each ping records its USD cost and token usage so you can audit spend.
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
## Privacy
|
|
225
|
+
|
|
226
|
+
cc-ping sends **zero telemetry**. No analytics, no tracking, no phoning home.
|
|
227
|
+
|
|
228
|
+
All data stays local in `~/.config/cc-ping/`:
|
|
229
|
+
|
|
230
|
+
| File | Contents |
|
|
231
|
+
|------|----------|
|
|
232
|
+
| `config.json` | Account names and config directory paths |
|
|
233
|
+
| `state.json` | Last ping timestamp and cost metadata per account |
|
|
234
|
+
| `daemon.json` | Daemon PID, interval, start time |
|
|
235
|
+
| `daemon.log` | Daemon output log |
|
|
236
|
+
|
|
237
|
+
The only network activity is the `claude` CLI call itself, which communicates with Anthropic's API under their standard [terms](https://www.anthropic.com/terms) and [privacy policy](https://www.anthropic.com/privacy). cc-ping does not intercept, modify, or inspect this traffic beyond reading the JSON response for cost metadata.
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,11 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/paths.ts
|
|
13
|
+
var paths_exports = {};
|
|
14
|
+
__export(paths_exports, {
|
|
15
|
+
resolveConfigDir: () => resolveConfigDir,
|
|
16
|
+
setConfigDir: () => setConfigDir
|
|
17
|
+
});
|
|
13
18
|
import { homedir } from "os";
|
|
14
19
|
import { join as join2 } from "path";
|
|
15
20
|
function setConfigDir(dir) {
|
|
@@ -547,262 +552,24 @@ var init_run_ping = __esm({
|
|
|
547
552
|
}
|
|
548
553
|
});
|
|
549
554
|
|
|
550
|
-
// src/cli.ts
|
|
551
|
-
import { Command } from "commander";
|
|
552
|
-
|
|
553
|
-
// src/check.ts
|
|
554
|
-
import { existsSync, readFileSync, statSync } from "fs";
|
|
555
|
-
import { join } from "path";
|
|
556
|
-
function checkAccount(account) {
|
|
557
|
-
const issues = [];
|
|
558
|
-
if (!existsSync(account.configDir) || !statSync(account.configDir).isDirectory()) {
|
|
559
|
-
issues.push("config directory does not exist");
|
|
560
|
-
return {
|
|
561
|
-
handle: account.handle,
|
|
562
|
-
configDir: account.configDir,
|
|
563
|
-
healthy: false,
|
|
564
|
-
issues
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
const claudeJson = join(account.configDir, ".claude.json");
|
|
568
|
-
if (!existsSync(claudeJson)) {
|
|
569
|
-
issues.push(".claude.json not found");
|
|
570
|
-
return {
|
|
571
|
-
handle: account.handle,
|
|
572
|
-
configDir: account.configDir,
|
|
573
|
-
healthy: false,
|
|
574
|
-
issues
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
let parsed;
|
|
578
|
-
try {
|
|
579
|
-
const raw = readFileSync(claudeJson, "utf-8");
|
|
580
|
-
parsed = JSON.parse(raw);
|
|
581
|
-
} catch {
|
|
582
|
-
issues.push(".claude.json is not valid JSON");
|
|
583
|
-
return {
|
|
584
|
-
handle: account.handle,
|
|
585
|
-
configDir: account.configDir,
|
|
586
|
-
healthy: false,
|
|
587
|
-
issues
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
if (!parsed.oauthAccount) {
|
|
591
|
-
issues.push("no OAuth credentials found");
|
|
592
|
-
}
|
|
593
|
-
return {
|
|
594
|
-
handle: account.handle,
|
|
595
|
-
configDir: account.configDir,
|
|
596
|
-
healthy: issues.length === 0,
|
|
597
|
-
issues
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
function checkAccounts(accounts) {
|
|
601
|
-
return accounts.map((a) => checkAccount(a));
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// src/completions.ts
|
|
605
|
-
var COMMANDS = "ping scan add remove list status next-reset history suggest check completions moo daemon";
|
|
606
|
-
function bashCompletion() {
|
|
607
|
-
return `_cc_ping() {
|
|
608
|
-
local cur prev commands
|
|
609
|
-
COMPREPLY=()
|
|
610
|
-
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
611
|
-
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
612
|
-
commands="${COMMANDS}"
|
|
613
|
-
|
|
614
|
-
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
615
|
-
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
616
|
-
return 0
|
|
617
|
-
fi
|
|
618
|
-
|
|
619
|
-
case "\${COMP_WORDS[1]}" in
|
|
620
|
-
ping)
|
|
621
|
-
if [[ "\${cur}" == -* ]]; then
|
|
622
|
-
COMPREPLY=( $(compgen -W "--parallel --quiet --json --group --bell --stagger" -- "\${cur}") )
|
|
623
|
-
else
|
|
624
|
-
local handles=$(cc-ping list 2>/dev/null | sed 's/ *\\(.*\\) ->.*/\\1/')
|
|
625
|
-
COMPREPLY=( $(compgen -W "\${handles}" -- "\${cur}") )
|
|
626
|
-
fi
|
|
627
|
-
;;
|
|
628
|
-
add)
|
|
629
|
-
COMPREPLY=( $(compgen -W "--group" -- "\${cur}") )
|
|
630
|
-
;;
|
|
631
|
-
list|history|status|next-reset|check)
|
|
632
|
-
COMPREPLY=( $(compgen -W "--json" -- "\${cur}") )
|
|
633
|
-
;;
|
|
634
|
-
completions)
|
|
635
|
-
COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
|
|
636
|
-
;;
|
|
637
|
-
daemon)
|
|
638
|
-
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
639
|
-
COMPREPLY=( $(compgen -W "start stop status" -- "\${cur}") )
|
|
640
|
-
elif [[ "\${COMP_WORDS[2]}" == "start" && "\${cur}" == -* ]]; then
|
|
641
|
-
COMPREPLY=( $(compgen -W "--interval --quiet --bell --notify" -- "\${cur}") )
|
|
642
|
-
elif [[ "\${COMP_WORDS[2]}" == "status" && "\${cur}" == -* ]]; then
|
|
643
|
-
COMPREPLY=( $(compgen -W "--json" -- "\${cur}") )
|
|
644
|
-
fi
|
|
645
|
-
;;
|
|
646
|
-
esac
|
|
647
|
-
return 0
|
|
648
|
-
}
|
|
649
|
-
complete -F _cc_ping cc-ping
|
|
650
|
-
`;
|
|
651
|
-
}
|
|
652
|
-
function zshCompletion() {
|
|
653
|
-
return `#compdef cc-ping
|
|
654
|
-
|
|
655
|
-
_cc_ping() {
|
|
656
|
-
local -a commands
|
|
657
|
-
commands=(
|
|
658
|
-
'ping:Ping configured accounts'
|
|
659
|
-
'scan:Auto-discover accounts'
|
|
660
|
-
'add:Add an account manually'
|
|
661
|
-
'remove:Remove an account'
|
|
662
|
-
'list:List configured accounts'
|
|
663
|
-
'status:Show account status'
|
|
664
|
-
'next-reset:Show soonest quota reset'
|
|
665
|
-
'history:Show ping history'
|
|
666
|
-
'suggest:Suggest next account'
|
|
667
|
-
'check:Verify account health'
|
|
668
|
-
'completions:Generate shell completions'
|
|
669
|
-
'moo:Send a test notification'
|
|
670
|
-
'daemon:Run auto-ping on a schedule'
|
|
671
|
-
)
|
|
672
|
-
|
|
673
|
-
_arguments -C \\
|
|
674
|
-
'--config[Config directory]:path:_files -/' \\
|
|
675
|
-
'1:command:->command' \\
|
|
676
|
-
'*::arg:->args'
|
|
677
|
-
|
|
678
|
-
case $state in
|
|
679
|
-
command)
|
|
680
|
-
_describe 'command' commands
|
|
681
|
-
;;
|
|
682
|
-
args)
|
|
683
|
-
case $words[1] in
|
|
684
|
-
ping)
|
|
685
|
-
_arguments \\
|
|
686
|
-
'--parallel[Ping in parallel]' \\
|
|
687
|
-
'--quiet[Suppress output]' \\
|
|
688
|
-
'--json[JSON output]' \\
|
|
689
|
-
'--group[Filter by group]:group:' \\
|
|
690
|
-
'--bell[Ring bell on failure]' \\
|
|
691
|
-
'--stagger[Delay between pings]:minutes:' \\
|
|
692
|
-
'*:handle:->handles'
|
|
693
|
-
if [[ $state == handles ]]; then
|
|
694
|
-
local -a handles
|
|
695
|
-
handles=(\${(f)"$(cc-ping list 2>/dev/null | sed 's/ *\\(.*\\) ->.*/\\1/')"})
|
|
696
|
-
_describe 'handle' handles
|
|
697
|
-
fi
|
|
698
|
-
;;
|
|
699
|
-
completions)
|
|
700
|
-
_arguments '1:shell:(bash zsh fish)'
|
|
701
|
-
;;
|
|
702
|
-
list|history|status|next-reset|check)
|
|
703
|
-
_arguments '--json[JSON output]'
|
|
704
|
-
;;
|
|
705
|
-
add)
|
|
706
|
-
_arguments '--group[Assign group]:group:'
|
|
707
|
-
;;
|
|
708
|
-
daemon)
|
|
709
|
-
local -a subcmds
|
|
710
|
-
subcmds=(
|
|
711
|
-
'start:Start the daemon process'
|
|
712
|
-
'stop:Stop the daemon process'
|
|
713
|
-
'status:Show daemon status'
|
|
714
|
-
)
|
|
715
|
-
_arguments '1:subcommand:->subcmd' '*::arg:->subargs'
|
|
716
|
-
case $state in
|
|
717
|
-
subcmd)
|
|
718
|
-
_describe 'subcommand' subcmds
|
|
719
|
-
;;
|
|
720
|
-
subargs)
|
|
721
|
-
case $words[1] in
|
|
722
|
-
start)
|
|
723
|
-
_arguments \\
|
|
724
|
-
'--interval[Ping interval in minutes]:minutes:' \\
|
|
725
|
-
'--quiet[Suppress ping output]' \\
|
|
726
|
-
'--bell[Ring bell on failure]' \\
|
|
727
|
-
'--notify[Send notification on failure]'
|
|
728
|
-
;;
|
|
729
|
-
status)
|
|
730
|
-
_arguments '--json[JSON output]'
|
|
731
|
-
;;
|
|
732
|
-
esac
|
|
733
|
-
;;
|
|
734
|
-
esac
|
|
735
|
-
;;
|
|
736
|
-
esac
|
|
737
|
-
;;
|
|
738
|
-
esac
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
_cc_ping
|
|
742
|
-
`;
|
|
743
|
-
}
|
|
744
|
-
function fishCompletion() {
|
|
745
|
-
return `# Fish completions for cc-ping
|
|
746
|
-
set -l commands ping scan add remove list status next-reset history suggest check completions moo
|
|
747
|
-
|
|
748
|
-
complete -c cc-ping -f
|
|
749
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a ping -d "Ping configured accounts"
|
|
750
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a scan -d "Auto-discover accounts"
|
|
751
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a add -d "Add an account manually"
|
|
752
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a remove -d "Remove an account"
|
|
753
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a list -d "List configured accounts"
|
|
754
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a status -d "Show account status"
|
|
755
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a next-reset -d "Show soonest quota reset"
|
|
756
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a history -d "Show ping history"
|
|
757
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a suggest -d "Suggest next account"
|
|
758
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a check -d "Verify account health"
|
|
759
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a completions -d "Generate shell completions"
|
|
760
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a moo -d "Send a test notification"
|
|
761
|
-
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a daemon -d "Run auto-ping on a schedule"
|
|
762
|
-
|
|
763
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l parallel -d "Ping in parallel"
|
|
764
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -s q -l quiet -d "Suppress output"
|
|
765
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l json -d "JSON output"
|
|
766
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -s g -l group -r -d "Filter by group"
|
|
767
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l bell -d "Ring bell on failure"
|
|
768
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l stagger -r -d "Delay between pings"
|
|
769
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -a "(cc-ping list 2>/dev/null | string replace -r ' *(.*) ->.*' '$1')"
|
|
770
|
-
|
|
771
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from list history status next-reset check" -l json -d "JSON output"
|
|
772
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from add" -s g -l group -r -d "Assign group"
|
|
773
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"
|
|
774
|
-
|
|
775
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status" -a start -d "Start the daemon"
|
|
776
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status" -a stop -d "Stop the daemon"
|
|
777
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status" -a status -d "Show daemon status"
|
|
778
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start" -l interval -r -d "Ping interval in minutes"
|
|
779
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start" -s q -l quiet -d "Suppress output"
|
|
780
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start" -l bell -d "Ring bell on failure"
|
|
781
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start" -l notify -d "Send notification on failure"
|
|
782
|
-
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from status" -l json -d "JSON output"
|
|
783
|
-
`;
|
|
784
|
-
}
|
|
785
|
-
function generateCompletion(shell) {
|
|
786
|
-
switch (shell) {
|
|
787
|
-
case "bash":
|
|
788
|
-
return bashCompletion();
|
|
789
|
-
case "zsh":
|
|
790
|
-
return zshCompletion();
|
|
791
|
-
case "fish":
|
|
792
|
-
return fishCompletion();
|
|
793
|
-
default:
|
|
794
|
-
throw new Error(
|
|
795
|
-
`Unsupported shell: ${shell}. Supported: bash, zsh, fish`
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// src/cli.ts
|
|
801
|
-
init_config();
|
|
802
|
-
|
|
803
555
|
// src/daemon.ts
|
|
804
|
-
|
|
805
|
-
|
|
556
|
+
var daemon_exports = {};
|
|
557
|
+
__export(daemon_exports, {
|
|
558
|
+
daemonLogPath: () => daemonLogPath,
|
|
559
|
+
daemonLoop: () => daemonLoop,
|
|
560
|
+
daemonPidPath: () => daemonPidPath,
|
|
561
|
+
daemonStopPath: () => daemonStopPath,
|
|
562
|
+
getDaemonStatus: () => getDaemonStatus,
|
|
563
|
+
isProcessRunning: () => isProcessRunning,
|
|
564
|
+
parseInterval: () => parseInterval,
|
|
565
|
+
readDaemonState: () => readDaemonState,
|
|
566
|
+
removeDaemonState: () => removeDaemonState,
|
|
567
|
+
runDaemon: () => runDaemon,
|
|
568
|
+
runDaemonWithDefaults: () => runDaemonWithDefaults,
|
|
569
|
+
startDaemon: () => startDaemon,
|
|
570
|
+
stopDaemon: () => stopDaemon,
|
|
571
|
+
writeDaemonState: () => writeDaemonState
|
|
572
|
+
});
|
|
806
573
|
import { execSync, spawn } from "child_process";
|
|
807
574
|
import {
|
|
808
575
|
existsSync as existsSync5,
|
|
@@ -814,9 +581,6 @@ import {
|
|
|
814
581
|
writeFileSync as writeFileSync3
|
|
815
582
|
} from "fs";
|
|
816
583
|
import { join as join6 } from "path";
|
|
817
|
-
var GRACEFUL_POLL_MS = 500;
|
|
818
|
-
var GRACEFUL_POLL_ATTEMPTS = 20;
|
|
819
|
-
var POST_KILL_DELAY_MS = 1e3;
|
|
820
584
|
function daemonPidPath() {
|
|
821
585
|
return join6(resolveConfigDir(), "daemon.json");
|
|
822
586
|
}
|
|
@@ -1049,6 +813,7 @@ async function stopDaemon(deps) {
|
|
|
1049
813
|
}
|
|
1050
814
|
async function runDaemon(intervalMs, options, deps) {
|
|
1051
815
|
const stopPath = daemonStopPath();
|
|
816
|
+
if (existsSync5(stopPath)) unlinkSync(stopPath);
|
|
1052
817
|
const cleanup = () => {
|
|
1053
818
|
if (existsSync5(stopPath)) unlinkSync(stopPath);
|
|
1054
819
|
removeDaemonState();
|
|
@@ -1096,10 +861,550 @@ async function runDaemonWithDefaults(intervalMs, options) {
|
|
|
1096
861
|
exit: (code) => process.exit(code)
|
|
1097
862
|
});
|
|
1098
863
|
}
|
|
864
|
+
var GRACEFUL_POLL_MS, GRACEFUL_POLL_ATTEMPTS, POST_KILL_DELAY_MS;
|
|
865
|
+
var init_daemon = __esm({
|
|
866
|
+
"src/daemon.ts"() {
|
|
867
|
+
"use strict";
|
|
868
|
+
init_paths();
|
|
869
|
+
init_state();
|
|
870
|
+
GRACEFUL_POLL_MS = 500;
|
|
871
|
+
GRACEFUL_POLL_ATTEMPTS = 20;
|
|
872
|
+
POST_KILL_DELAY_MS = 1e3;
|
|
873
|
+
}
|
|
874
|
+
});
|
|
1099
875
|
|
|
1100
|
-
// src/
|
|
1101
|
-
|
|
1102
|
-
|
|
876
|
+
// src/service.ts
|
|
877
|
+
var service_exports = {};
|
|
878
|
+
__export(service_exports, {
|
|
879
|
+
generateLaunchdPlist: () => generateLaunchdPlist,
|
|
880
|
+
generateSystemdUnit: () => generateSystemdUnit,
|
|
881
|
+
getServiceStatus: () => getServiceStatus,
|
|
882
|
+
installService: () => installService,
|
|
883
|
+
resolveExecutable: () => resolveExecutable,
|
|
884
|
+
servicePath: () => servicePath,
|
|
885
|
+
uninstallService: () => uninstallService
|
|
886
|
+
});
|
|
887
|
+
import { execSync as nodeExecSync } from "child_process";
|
|
888
|
+
import {
|
|
889
|
+
existsSync as nodeExistsSync,
|
|
890
|
+
mkdirSync as nodeMkdirSync,
|
|
891
|
+
unlinkSync as nodeUnlinkSync,
|
|
892
|
+
writeFileSync as nodeWriteFileSync
|
|
893
|
+
} from "fs";
|
|
894
|
+
import { homedir as nodeHomedir } from "os";
|
|
895
|
+
import { dirname, join as join9 } from "path";
|
|
896
|
+
function resolveExecutable(deps) {
|
|
897
|
+
try {
|
|
898
|
+
const path = deps.execSync("which cc-ping", {
|
|
899
|
+
encoding: "utf-8",
|
|
900
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
901
|
+
}).trim();
|
|
902
|
+
if (path) {
|
|
903
|
+
return { executable: path, args: [] };
|
|
904
|
+
}
|
|
905
|
+
} catch {
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
executable: process.execPath,
|
|
909
|
+
args: [process.argv[1]]
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function generateLaunchdPlist(options, execInfo, configDir) {
|
|
913
|
+
const intervalMs = parseIntervalForService(options.interval);
|
|
914
|
+
const programArgs = [
|
|
915
|
+
...execInfo.args,
|
|
916
|
+
"daemon",
|
|
917
|
+
"_run",
|
|
918
|
+
"--interval-ms",
|
|
919
|
+
String(intervalMs)
|
|
920
|
+
];
|
|
921
|
+
if (options.quiet) programArgs.push("--quiet");
|
|
922
|
+
if (options.bell) programArgs.push("--bell");
|
|
923
|
+
if (options.notify) programArgs.push("--notify");
|
|
924
|
+
const allArgs = [execInfo.executable, ...programArgs];
|
|
925
|
+
const argsXml = allArgs.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
926
|
+
const logPath = join9(
|
|
927
|
+
configDir || join9(nodeHomedir(), ".config", "cc-ping"),
|
|
928
|
+
"daemon.log"
|
|
929
|
+
);
|
|
930
|
+
let envSection = "";
|
|
931
|
+
if (configDir) {
|
|
932
|
+
envSection = `
|
|
933
|
+
<key>EnvironmentVariables</key>
|
|
934
|
+
<dict>
|
|
935
|
+
<key>CC_PING_CONFIG</key>
|
|
936
|
+
<string>${escapeXml(configDir)}</string>
|
|
937
|
+
</dict>`;
|
|
938
|
+
}
|
|
939
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
940
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
941
|
+
<plist version="1.0">
|
|
942
|
+
<dict>
|
|
943
|
+
<key>Label</key>
|
|
944
|
+
<string>${PLIST_LABEL}</string>
|
|
945
|
+
<key>ProgramArguments</key>
|
|
946
|
+
<array>
|
|
947
|
+
${argsXml}
|
|
948
|
+
</array>
|
|
949
|
+
<key>RunAtLoad</key>
|
|
950
|
+
<true/>
|
|
951
|
+
<key>KeepAlive</key>
|
|
952
|
+
<dict>
|
|
953
|
+
<key>SuccessfulExit</key>
|
|
954
|
+
<false/>
|
|
955
|
+
</dict>
|
|
956
|
+
<key>StandardOutPath</key>
|
|
957
|
+
<string>${escapeXml(logPath)}</string>
|
|
958
|
+
<key>StandardErrorPath</key>
|
|
959
|
+
<string>${escapeXml(logPath)}</string>${envSection}
|
|
960
|
+
</dict>
|
|
961
|
+
</plist>
|
|
962
|
+
`;
|
|
963
|
+
}
|
|
964
|
+
function generateSystemdUnit(options, execInfo, configDir) {
|
|
965
|
+
const intervalMs = parseIntervalForService(options.interval);
|
|
966
|
+
const programArgs = [
|
|
967
|
+
...execInfo.args,
|
|
968
|
+
"daemon",
|
|
969
|
+
"_run",
|
|
970
|
+
"--interval-ms",
|
|
971
|
+
String(intervalMs)
|
|
972
|
+
];
|
|
973
|
+
if (options.quiet) programArgs.push("--quiet");
|
|
974
|
+
if (options.bell) programArgs.push("--bell");
|
|
975
|
+
if (options.notify) programArgs.push("--notify");
|
|
976
|
+
const execStart = [execInfo.executable, ...programArgs].map((a) => a.includes(" ") ? `"${a}"` : a).join(" ");
|
|
977
|
+
let envLine = "";
|
|
978
|
+
if (configDir) {
|
|
979
|
+
envLine = `
|
|
980
|
+
Environment=CC_PING_CONFIG=${configDir}`;
|
|
981
|
+
}
|
|
982
|
+
return `[Unit]
|
|
983
|
+
Description=cc-ping daemon - auto-ping Claude Code sessions
|
|
984
|
+
|
|
985
|
+
[Service]
|
|
986
|
+
Type=simple
|
|
987
|
+
ExecStart=${execStart}${envLine}
|
|
988
|
+
Restart=on-failure
|
|
989
|
+
RestartSec=10
|
|
990
|
+
|
|
991
|
+
[Install]
|
|
992
|
+
WantedBy=default.target
|
|
993
|
+
`;
|
|
994
|
+
}
|
|
995
|
+
function servicePath(platform, home) {
|
|
996
|
+
switch (platform) {
|
|
997
|
+
case "darwin":
|
|
998
|
+
return join9(home, "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
999
|
+
case "linux":
|
|
1000
|
+
return join9(
|
|
1001
|
+
home,
|
|
1002
|
+
".config",
|
|
1003
|
+
"systemd",
|
|
1004
|
+
"user",
|
|
1005
|
+
`${SYSTEMD_SERVICE}.service`
|
|
1006
|
+
);
|
|
1007
|
+
default:
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`Unsupported platform: ${platform}. Only macOS and Linux are supported.`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
async function installService(options, deps) {
|
|
1014
|
+
const _platform = deps?.platform ?? process.platform;
|
|
1015
|
+
const _homedir = deps?.homedir ?? nodeHomedir;
|
|
1016
|
+
const _existsSync = deps?.existsSync ?? nodeExistsSync;
|
|
1017
|
+
const _mkdirSync = deps?.mkdirSync ?? nodeMkdirSync;
|
|
1018
|
+
const _writeFileSync = deps?.writeFileSync ?? nodeWriteFileSync;
|
|
1019
|
+
const _execSync = deps?.execSync ?? ((cmd) => nodeExecSync(cmd, { encoding: "utf-8" }));
|
|
1020
|
+
const _stopDaemon = deps?.stopDaemon ?? (async () => {
|
|
1021
|
+
const { stopDaemon: stopDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
1022
|
+
return stopDaemon2();
|
|
1023
|
+
});
|
|
1024
|
+
const _configDir = deps?.configDir;
|
|
1025
|
+
const home = _homedir();
|
|
1026
|
+
let path;
|
|
1027
|
+
try {
|
|
1028
|
+
path = servicePath(_platform, home);
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
return {
|
|
1031
|
+
success: false,
|
|
1032
|
+
servicePath: "",
|
|
1033
|
+
error: err.message
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
if (_existsSync(path)) {
|
|
1037
|
+
return {
|
|
1038
|
+
success: false,
|
|
1039
|
+
servicePath: path,
|
|
1040
|
+
error: `Service already installed at ${path}. Run \`daemon uninstall\` first.`
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
await _stopDaemon();
|
|
1045
|
+
} catch {
|
|
1046
|
+
}
|
|
1047
|
+
const execInfo = resolveExecutable({
|
|
1048
|
+
execSync: _execSync
|
|
1049
|
+
});
|
|
1050
|
+
const configDir = _configDir || void 0;
|
|
1051
|
+
let content;
|
|
1052
|
+
if (_platform === "darwin") {
|
|
1053
|
+
content = generateLaunchdPlist(options, execInfo, configDir);
|
|
1054
|
+
} else {
|
|
1055
|
+
content = generateSystemdUnit(options, execInfo, configDir);
|
|
1056
|
+
}
|
|
1057
|
+
_mkdirSync(dirname(path), { recursive: true });
|
|
1058
|
+
_writeFileSync(path, content);
|
|
1059
|
+
try {
|
|
1060
|
+
if (_platform === "darwin") {
|
|
1061
|
+
_execSync(`launchctl load ${path}`);
|
|
1062
|
+
} else {
|
|
1063
|
+
_execSync("systemctl --user daemon-reload");
|
|
1064
|
+
_execSync(`systemctl --user enable --now ${SYSTEMD_SERVICE}`);
|
|
1065
|
+
}
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
return {
|
|
1068
|
+
success: false,
|
|
1069
|
+
servicePath: path,
|
|
1070
|
+
error: `Service file written but failed to load: ${err.message}`
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
return { success: true, servicePath: path };
|
|
1074
|
+
}
|
|
1075
|
+
async function uninstallService(deps) {
|
|
1076
|
+
const _platform = deps?.platform ?? process.platform;
|
|
1077
|
+
const _homedir = deps?.homedir ?? nodeHomedir;
|
|
1078
|
+
const _existsSync = deps?.existsSync ?? nodeExistsSync;
|
|
1079
|
+
const _unlinkSync = deps?.unlinkSync ?? nodeUnlinkSync;
|
|
1080
|
+
const _execSync = deps?.execSync ?? ((cmd) => nodeExecSync(cmd, { encoding: "utf-8" }));
|
|
1081
|
+
const home = _homedir();
|
|
1082
|
+
let path;
|
|
1083
|
+
try {
|
|
1084
|
+
path = servicePath(_platform, home);
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
return {
|
|
1087
|
+
success: false,
|
|
1088
|
+
error: err.message
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
if (!_existsSync(path)) {
|
|
1092
|
+
return {
|
|
1093
|
+
success: false,
|
|
1094
|
+
servicePath: path,
|
|
1095
|
+
error: "No service installed."
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
try {
|
|
1099
|
+
if (_platform === "darwin") {
|
|
1100
|
+
_execSync(`launchctl unload ${path}`);
|
|
1101
|
+
} else {
|
|
1102
|
+
_execSync(`systemctl --user disable --now ${SYSTEMD_SERVICE}`);
|
|
1103
|
+
}
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
_unlinkSync(path);
|
|
1107
|
+
return { success: true, servicePath: path };
|
|
1108
|
+
}
|
|
1109
|
+
function getServiceStatus(deps) {
|
|
1110
|
+
const _platform = deps?.platform ?? process.platform;
|
|
1111
|
+
const _homedir = deps?.homedir ?? nodeHomedir;
|
|
1112
|
+
const _existsSync = deps?.existsSync ?? nodeExistsSync;
|
|
1113
|
+
const home = _homedir();
|
|
1114
|
+
let path;
|
|
1115
|
+
try {
|
|
1116
|
+
path = servicePath(_platform, home);
|
|
1117
|
+
} catch {
|
|
1118
|
+
return { installed: false, platform: _platform };
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
installed: _existsSync(path),
|
|
1122
|
+
servicePath: path,
|
|
1123
|
+
platform: _platform
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function escapeXml(str) {
|
|
1127
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1128
|
+
}
|
|
1129
|
+
function parseIntervalForService(value) {
|
|
1130
|
+
if (!value) return QUOTA_WINDOW_MS2;
|
|
1131
|
+
const minutes = Number(value);
|
|
1132
|
+
if (Number.isNaN(minutes) || minutes <= 0) return QUOTA_WINDOW_MS2;
|
|
1133
|
+
return minutes * 60 * 1e3;
|
|
1134
|
+
}
|
|
1135
|
+
var PLIST_LABEL, SYSTEMD_SERVICE, QUOTA_WINDOW_MS2;
|
|
1136
|
+
var init_service = __esm({
|
|
1137
|
+
"src/service.ts"() {
|
|
1138
|
+
"use strict";
|
|
1139
|
+
PLIST_LABEL = "com.cc-ping.daemon";
|
|
1140
|
+
SYSTEMD_SERVICE = "cc-ping-daemon";
|
|
1141
|
+
QUOTA_WINDOW_MS2 = 5 * 60 * 60 * 1e3;
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// src/cli.ts
|
|
1146
|
+
import { Command } from "commander";
|
|
1147
|
+
|
|
1148
|
+
// src/check.ts
|
|
1149
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
1150
|
+
import { join } from "path";
|
|
1151
|
+
function checkAccount(account) {
|
|
1152
|
+
const issues = [];
|
|
1153
|
+
if (!existsSync(account.configDir) || !statSync(account.configDir).isDirectory()) {
|
|
1154
|
+
issues.push("config directory does not exist");
|
|
1155
|
+
return {
|
|
1156
|
+
handle: account.handle,
|
|
1157
|
+
configDir: account.configDir,
|
|
1158
|
+
healthy: false,
|
|
1159
|
+
issues
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
const claudeJson = join(account.configDir, ".claude.json");
|
|
1163
|
+
if (!existsSync(claudeJson)) {
|
|
1164
|
+
issues.push(".claude.json not found");
|
|
1165
|
+
return {
|
|
1166
|
+
handle: account.handle,
|
|
1167
|
+
configDir: account.configDir,
|
|
1168
|
+
healthy: false,
|
|
1169
|
+
issues
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
let parsed;
|
|
1173
|
+
try {
|
|
1174
|
+
const raw = readFileSync(claudeJson, "utf-8");
|
|
1175
|
+
parsed = JSON.parse(raw);
|
|
1176
|
+
} catch {
|
|
1177
|
+
issues.push(".claude.json is not valid JSON");
|
|
1178
|
+
return {
|
|
1179
|
+
handle: account.handle,
|
|
1180
|
+
configDir: account.configDir,
|
|
1181
|
+
healthy: false,
|
|
1182
|
+
issues
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
if (!parsed.oauthAccount) {
|
|
1186
|
+
issues.push("no OAuth credentials found");
|
|
1187
|
+
}
|
|
1188
|
+
return {
|
|
1189
|
+
handle: account.handle,
|
|
1190
|
+
configDir: account.configDir,
|
|
1191
|
+
healthy: issues.length === 0,
|
|
1192
|
+
issues
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function checkAccounts(accounts) {
|
|
1196
|
+
return accounts.map((a) => checkAccount(a));
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/completions.ts
|
|
1200
|
+
var COMMANDS = "ping scan add remove list status next-reset history suggest check completions moo daemon";
|
|
1201
|
+
function bashCompletion() {
|
|
1202
|
+
return `_cc_ping() {
|
|
1203
|
+
local cur prev commands
|
|
1204
|
+
COMPREPLY=()
|
|
1205
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1206
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1207
|
+
commands="${COMMANDS}"
|
|
1208
|
+
|
|
1209
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
1210
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
1211
|
+
return 0
|
|
1212
|
+
fi
|
|
1213
|
+
|
|
1214
|
+
case "\${COMP_WORDS[1]}" in
|
|
1215
|
+
ping)
|
|
1216
|
+
if [[ "\${cur}" == -* ]]; then
|
|
1217
|
+
COMPREPLY=( $(compgen -W "--parallel --quiet --json --group --bell --stagger" -- "\${cur}") )
|
|
1218
|
+
else
|
|
1219
|
+
local handles=$(cc-ping list 2>/dev/null | sed 's/ *\\(.*\\) ->.*/\\1/')
|
|
1220
|
+
COMPREPLY=( $(compgen -W "\${handles}" -- "\${cur}") )
|
|
1221
|
+
fi
|
|
1222
|
+
;;
|
|
1223
|
+
add)
|
|
1224
|
+
COMPREPLY=( $(compgen -W "--group" -- "\${cur}") )
|
|
1225
|
+
;;
|
|
1226
|
+
list|history|status|next-reset|check)
|
|
1227
|
+
COMPREPLY=( $(compgen -W "--json" -- "\${cur}") )
|
|
1228
|
+
;;
|
|
1229
|
+
completions)
|
|
1230
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
|
|
1231
|
+
;;
|
|
1232
|
+
daemon)
|
|
1233
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
1234
|
+
COMPREPLY=( $(compgen -W "start stop status install uninstall" -- "\${cur}") )
|
|
1235
|
+
elif [[ "\${COMP_WORDS[2]}" == "start" && "\${cur}" == -* ]]; then
|
|
1236
|
+
COMPREPLY=( $(compgen -W "--interval --quiet --bell --notify" -- "\${cur}") )
|
|
1237
|
+
elif [[ "\${COMP_WORDS[2]}" == "install" && "\${cur}" == -* ]]; then
|
|
1238
|
+
COMPREPLY=( $(compgen -W "--interval --quiet --bell --notify" -- "\${cur}") )
|
|
1239
|
+
elif [[ "\${COMP_WORDS[2]}" == "status" && "\${cur}" == -* ]]; then
|
|
1240
|
+
COMPREPLY=( $(compgen -W "--json" -- "\${cur}") )
|
|
1241
|
+
fi
|
|
1242
|
+
;;
|
|
1243
|
+
esac
|
|
1244
|
+
return 0
|
|
1245
|
+
}
|
|
1246
|
+
complete -F _cc_ping cc-ping
|
|
1247
|
+
`;
|
|
1248
|
+
}
|
|
1249
|
+
function zshCompletion() {
|
|
1250
|
+
return `#compdef cc-ping
|
|
1251
|
+
|
|
1252
|
+
_cc_ping() {
|
|
1253
|
+
local -a commands
|
|
1254
|
+
commands=(
|
|
1255
|
+
'ping:Ping configured accounts'
|
|
1256
|
+
'scan:Auto-discover accounts'
|
|
1257
|
+
'add:Add an account manually'
|
|
1258
|
+
'remove:Remove an account'
|
|
1259
|
+
'list:List configured accounts'
|
|
1260
|
+
'status:Show account status'
|
|
1261
|
+
'next-reset:Show soonest quota reset'
|
|
1262
|
+
'history:Show ping history'
|
|
1263
|
+
'suggest:Suggest next account'
|
|
1264
|
+
'check:Verify account health'
|
|
1265
|
+
'completions:Generate shell completions'
|
|
1266
|
+
'moo:Send a test notification'
|
|
1267
|
+
'daemon:Run auto-ping on a schedule'
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
_arguments -C \\
|
|
1271
|
+
'--config[Config directory]:path:_files -/' \\
|
|
1272
|
+
'1:command:->command' \\
|
|
1273
|
+
'*::arg:->args'
|
|
1274
|
+
|
|
1275
|
+
case $state in
|
|
1276
|
+
command)
|
|
1277
|
+
_describe 'command' commands
|
|
1278
|
+
;;
|
|
1279
|
+
args)
|
|
1280
|
+
case $words[1] in
|
|
1281
|
+
ping)
|
|
1282
|
+
_arguments \\
|
|
1283
|
+
'--parallel[Ping in parallel]' \\
|
|
1284
|
+
'--quiet[Suppress output]' \\
|
|
1285
|
+
'--json[JSON output]' \\
|
|
1286
|
+
'--group[Filter by group]:group:' \\
|
|
1287
|
+
'--bell[Ring bell on failure]' \\
|
|
1288
|
+
'--stagger[Delay between pings]:minutes:' \\
|
|
1289
|
+
'*:handle:->handles'
|
|
1290
|
+
if [[ $state == handles ]]; then
|
|
1291
|
+
local -a handles
|
|
1292
|
+
handles=(\${(f)"$(cc-ping list 2>/dev/null | sed 's/ *\\(.*\\) ->.*/\\1/')"})
|
|
1293
|
+
_describe 'handle' handles
|
|
1294
|
+
fi
|
|
1295
|
+
;;
|
|
1296
|
+
completions)
|
|
1297
|
+
_arguments '1:shell:(bash zsh fish)'
|
|
1298
|
+
;;
|
|
1299
|
+
list|history|status|next-reset|check)
|
|
1300
|
+
_arguments '--json[JSON output]'
|
|
1301
|
+
;;
|
|
1302
|
+
add)
|
|
1303
|
+
_arguments '--group[Assign group]:group:'
|
|
1304
|
+
;;
|
|
1305
|
+
daemon)
|
|
1306
|
+
local -a subcmds
|
|
1307
|
+
subcmds=(
|
|
1308
|
+
'start:Start the daemon process'
|
|
1309
|
+
'stop:Stop the daemon process'
|
|
1310
|
+
'status:Show daemon status'
|
|
1311
|
+
'install:Install as system service'
|
|
1312
|
+
'uninstall:Remove system service'
|
|
1313
|
+
)
|
|
1314
|
+
_arguments '1:subcommand:->subcmd' '*::arg:->subargs'
|
|
1315
|
+
case $state in
|
|
1316
|
+
subcmd)
|
|
1317
|
+
_describe 'subcommand' subcmds
|
|
1318
|
+
;;
|
|
1319
|
+
subargs)
|
|
1320
|
+
case $words[1] in
|
|
1321
|
+
start|install)
|
|
1322
|
+
_arguments \\
|
|
1323
|
+
'--interval[Ping interval in minutes]:minutes:' \\
|
|
1324
|
+
'--quiet[Suppress ping output]' \\
|
|
1325
|
+
'--bell[Ring bell on failure]' \\
|
|
1326
|
+
'--notify[Send notification on failure]'
|
|
1327
|
+
;;
|
|
1328
|
+
status)
|
|
1329
|
+
_arguments '--json[JSON output]'
|
|
1330
|
+
;;
|
|
1331
|
+
esac
|
|
1332
|
+
;;
|
|
1333
|
+
esac
|
|
1334
|
+
;;
|
|
1335
|
+
esac
|
|
1336
|
+
;;
|
|
1337
|
+
esac
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
_cc_ping
|
|
1341
|
+
`;
|
|
1342
|
+
}
|
|
1343
|
+
function fishCompletion() {
|
|
1344
|
+
return `# Fish completions for cc-ping
|
|
1345
|
+
set -l commands ping scan add remove list status next-reset history suggest check completions moo
|
|
1346
|
+
|
|
1347
|
+
complete -c cc-ping -f
|
|
1348
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a ping -d "Ping configured accounts"
|
|
1349
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a scan -d "Auto-discover accounts"
|
|
1350
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a add -d "Add an account manually"
|
|
1351
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a remove -d "Remove an account"
|
|
1352
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a list -d "List configured accounts"
|
|
1353
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a status -d "Show account status"
|
|
1354
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a next-reset -d "Show soonest quota reset"
|
|
1355
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a history -d "Show ping history"
|
|
1356
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a suggest -d "Suggest next account"
|
|
1357
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a check -d "Verify account health"
|
|
1358
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a completions -d "Generate shell completions"
|
|
1359
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a moo -d "Send a test notification"
|
|
1360
|
+
complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a daemon -d "Run auto-ping on a schedule"
|
|
1361
|
+
|
|
1362
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l parallel -d "Ping in parallel"
|
|
1363
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -s q -l quiet -d "Suppress output"
|
|
1364
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l json -d "JSON output"
|
|
1365
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -s g -l group -r -d "Filter by group"
|
|
1366
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l bell -d "Ring bell on failure"
|
|
1367
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l stagger -r -d "Delay between pings"
|
|
1368
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from ping" -a "(cc-ping list 2>/dev/null | string replace -r ' *(.*) ->.*' '$1')"
|
|
1369
|
+
|
|
1370
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from list history status next-reset check" -l json -d "JSON output"
|
|
1371
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from add" -s g -l group -r -d "Assign group"
|
|
1372
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"
|
|
1373
|
+
|
|
1374
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status install uninstall" -a start -d "Start the daemon"
|
|
1375
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status install uninstall" -a stop -d "Stop the daemon"
|
|
1376
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status install uninstall" -a status -d "Show daemon status"
|
|
1377
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status install uninstall" -a install -d "Install as system service"
|
|
1378
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status install uninstall" -a uninstall -d "Remove system service"
|
|
1379
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -l interval -r -d "Ping interval in minutes"
|
|
1380
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -s q -l quiet -d "Suppress output"
|
|
1381
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -l bell -d "Ring bell on failure"
|
|
1382
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -l notify -d "Send notification on failure"
|
|
1383
|
+
complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from status" -l json -d "JSON output"
|
|
1384
|
+
`;
|
|
1385
|
+
}
|
|
1386
|
+
function generateCompletion(shell) {
|
|
1387
|
+
switch (shell) {
|
|
1388
|
+
case "bash":
|
|
1389
|
+
return bashCompletion();
|
|
1390
|
+
case "zsh":
|
|
1391
|
+
return zshCompletion();
|
|
1392
|
+
case "fish":
|
|
1393
|
+
return fishCompletion();
|
|
1394
|
+
default:
|
|
1395
|
+
throw new Error(
|
|
1396
|
+
`Unsupported shell: ${shell}. Supported: bash, zsh, fish`
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/cli.ts
|
|
1402
|
+
init_config();
|
|
1403
|
+
init_daemon();
|
|
1404
|
+
|
|
1405
|
+
// src/filter-accounts.ts
|
|
1406
|
+
function filterAccounts(accounts, handles) {
|
|
1407
|
+
if (handles.length === 0) return accounts;
|
|
1103
1408
|
const unknown = handles.filter((h) => !accounts.some((a) => a.handle === h));
|
|
1104
1409
|
if (unknown.length > 0) {
|
|
1105
1410
|
throw new Error(`Unknown account(s): ${unknown.join(", ")}`);
|
|
@@ -1322,7 +1627,7 @@ function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
|
|
|
1322
1627
|
}
|
|
1323
1628
|
|
|
1324
1629
|
// src/cli.ts
|
|
1325
|
-
var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("
|
|
1630
|
+
var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.0.0").option(
|
|
1326
1631
|
"--config <path>",
|
|
1327
1632
|
"Path to config directory (default: ~/.config/cc-ping, env: CC_PING_CONFIG)"
|
|
1328
1633
|
).hook("preAction", (thisCommand) => {
|
|
@@ -1516,7 +1821,7 @@ var daemon = program.command("daemon").description("Run auto-ping on a schedule"
|
|
|
1516
1821
|
daemon.command("start").description("Start the daemon process").option(
|
|
1517
1822
|
"--interval <minutes>",
|
|
1518
1823
|
"Ping interval in minutes (default: 300 = 5h quota window)"
|
|
1519
|
-
).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((opts) => {
|
|
1824
|
+
).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) => {
|
|
1520
1825
|
const result = startDaemon({
|
|
1521
1826
|
interval: opts.interval,
|
|
1522
1827
|
quiet: opts.quiet,
|
|
@@ -1528,6 +1833,13 @@ daemon.command("start").description("Start the daemon process").option(
|
|
|
1528
1833
|
process.exit(1);
|
|
1529
1834
|
}
|
|
1530
1835
|
console.log(`Daemon started (PID: ${result.pid})`);
|
|
1836
|
+
const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
1837
|
+
const svc = getServiceStatus2();
|
|
1838
|
+
if (!svc.installed) {
|
|
1839
|
+
console.log(
|
|
1840
|
+
"Hint: won't survive a reboot. Use `daemon install` for a persistent service."
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1531
1843
|
printAccountTable();
|
|
1532
1844
|
});
|
|
1533
1845
|
daemon.command("stop").description("Stop the daemon process").action(async () => {
|
|
@@ -1537,24 +1849,51 @@ daemon.command("stop").description("Stop the daemon process").action(async () =>
|
|
|
1537
1849
|
process.exit(1);
|
|
1538
1850
|
}
|
|
1539
1851
|
console.log(`Daemon stopped (PID: ${result.pid})`);
|
|
1852
|
+
const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
1853
|
+
const svc = getServiceStatus2();
|
|
1854
|
+
if (svc.installed) {
|
|
1855
|
+
console.log(
|
|
1856
|
+
"Note: system service is installed. The daemon may restart. Use `daemon uninstall` to fully remove."
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1540
1859
|
});
|
|
1541
|
-
daemon.command("status").description("Show daemon status").option("--json", "Output as JSON", false).action((opts) => {
|
|
1860
|
+
daemon.command("status").description("Show daemon status").option("--json", "Output as JSON", false).action(async (opts) => {
|
|
1861
|
+
const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
1862
|
+
const svc = getServiceStatus2();
|
|
1542
1863
|
const status = getDaemonStatus();
|
|
1543
1864
|
if (opts.json) {
|
|
1865
|
+
const serviceInfo = svc.installed ? {
|
|
1866
|
+
service: {
|
|
1867
|
+
installed: true,
|
|
1868
|
+
path: svc.servicePath,
|
|
1869
|
+
platform: svc.platform
|
|
1870
|
+
}
|
|
1871
|
+
} : { service: { installed: false } };
|
|
1544
1872
|
if (!status.running) {
|
|
1545
|
-
console.log(JSON.stringify(status, null, 2));
|
|
1873
|
+
console.log(JSON.stringify({ ...status, ...serviceInfo }, null, 2));
|
|
1546
1874
|
return;
|
|
1547
1875
|
}
|
|
1548
1876
|
const accounts = listAccounts();
|
|
1549
1877
|
const dupes = findDuplicates(accounts);
|
|
1550
1878
|
const accountStatuses = getAccountStatuses(accounts, /* @__PURE__ */ new Date(), dupes);
|
|
1551
1879
|
console.log(
|
|
1552
|
-
JSON.stringify(
|
|
1880
|
+
JSON.stringify(
|
|
1881
|
+
{ ...status, ...serviceInfo, accounts: accountStatuses },
|
|
1882
|
+
null,
|
|
1883
|
+
2
|
|
1884
|
+
)
|
|
1553
1885
|
);
|
|
1554
1886
|
return;
|
|
1555
1887
|
}
|
|
1556
1888
|
if (!status.running) {
|
|
1557
|
-
|
|
1889
|
+
if (svc.installed) {
|
|
1890
|
+
const kind = svc.platform === "darwin" ? "launchd" : "systemd";
|
|
1891
|
+
console.log(
|
|
1892
|
+
`Daemon is not running (system service: installed via ${kind})`
|
|
1893
|
+
);
|
|
1894
|
+
} else {
|
|
1895
|
+
console.log("Daemon is not running");
|
|
1896
|
+
}
|
|
1558
1897
|
return;
|
|
1559
1898
|
}
|
|
1560
1899
|
console.log(`Daemon is running (PID: ${status.pid})`);
|
|
@@ -1566,15 +1905,57 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
|
|
|
1566
1905
|
if (status.nextPingIn) {
|
|
1567
1906
|
console.log(` Next ping in: ${status.nextPingIn}`);
|
|
1568
1907
|
}
|
|
1908
|
+
if (svc.installed) {
|
|
1909
|
+
const kind = svc.platform === "darwin" ? "launchd" : "systemd";
|
|
1910
|
+
console.log(` System service: installed (${kind})`);
|
|
1911
|
+
}
|
|
1569
1912
|
console.log("");
|
|
1570
1913
|
printAccountTable();
|
|
1571
1914
|
});
|
|
1915
|
+
daemon.command("install").description("Install daemon as a system service (launchd/systemd)").option(
|
|
1916
|
+
"--interval <minutes>",
|
|
1917
|
+
"Ping interval in minutes (default: 300 = 5h quota window)"
|
|
1918
|
+
).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) => {
|
|
1919
|
+
const { installService: installService2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
1920
|
+
const result = await installService2({
|
|
1921
|
+
interval: opts.interval,
|
|
1922
|
+
quiet: opts.quiet,
|
|
1923
|
+
bell: opts.bell,
|
|
1924
|
+
notify: opts.notify
|
|
1925
|
+
});
|
|
1926
|
+
if (!result.success) {
|
|
1927
|
+
console.error(result.error);
|
|
1928
|
+
process.exit(1);
|
|
1929
|
+
}
|
|
1930
|
+
console.log(`Service installed: ${result.servicePath}`);
|
|
1931
|
+
console.log(
|
|
1932
|
+
"The daemon will start automatically on login. Use `daemon uninstall` to remove."
|
|
1933
|
+
);
|
|
1934
|
+
});
|
|
1935
|
+
daemon.command("uninstall").description("Remove daemon system service").action(async () => {
|
|
1936
|
+
const { uninstallService: uninstallService2 } = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
1937
|
+
const result = await uninstallService2();
|
|
1938
|
+
if (!result.success) {
|
|
1939
|
+
console.error(result.error);
|
|
1940
|
+
process.exit(1);
|
|
1941
|
+
}
|
|
1942
|
+
console.log(`Service removed: ${result.servicePath}`);
|
|
1943
|
+
});
|
|
1572
1944
|
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) => {
|
|
1573
1945
|
const intervalMs = Number(opts.intervalMs);
|
|
1574
1946
|
if (!intervalMs || intervalMs <= 0) {
|
|
1575
1947
|
console.error("Invalid --interval-ms");
|
|
1576
1948
|
process.exit(1);
|
|
1577
1949
|
}
|
|
1950
|
+
if (!readDaemonState()) {
|
|
1951
|
+
const { resolveConfigDir: resolveConfigDir2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
1952
|
+
writeDaemonState({
|
|
1953
|
+
pid: process.pid,
|
|
1954
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1955
|
+
intervalMs,
|
|
1956
|
+
configDir: resolveConfigDir2()
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1578
1959
|
await runDaemonWithDefaults(intervalMs, {
|
|
1579
1960
|
quiet: opts.quiet,
|
|
1580
1961
|
bell: opts.bell,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wbern/cc-ping",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Ping Claude Code sessions to trigger quota windows early across multiple accounts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,19 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"prepublishOnly": "node scripts/check-publish.js && pnpm run build",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:coverage": "vitest run --coverage",
|
|
17
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
18
|
+
"test:e2e:ping": "E2E_PING=1 vitest run --config vitest.e2e.config.ts",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "biome check .",
|
|
21
|
+
"lint:fix": "biome check --write .",
|
|
22
|
+
"knip": "knip --no-config-hints",
|
|
23
|
+
"prepare": "husky"
|
|
24
|
+
},
|
|
12
25
|
"keywords": [
|
|
13
26
|
"claude",
|
|
14
27
|
"claude-code",
|
|
@@ -75,16 +88,5 @@
|
|
|
75
88
|
}
|
|
76
89
|
]
|
|
77
90
|
]
|
|
78
|
-
},
|
|
79
|
-
"scripts": {
|
|
80
|
-
"build": "tsup",
|
|
81
|
-
"test": "vitest run",
|
|
82
|
-
"test:coverage": "vitest run --coverage",
|
|
83
|
-
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
84
|
-
"test:e2e:ping": "E2E_PING=1 vitest run --config vitest.e2e.config.ts",
|
|
85
|
-
"typecheck": "tsc --noEmit",
|
|
86
|
-
"lint": "biome check .",
|
|
87
|
-
"lint:fix": "biome check --write .",
|
|
88
|
-
"knip": "knip --no-config-hints"
|
|
89
91
|
}
|
|
90
|
-
}
|
|
92
|
+
}
|