@wbern/cc-ping 0.1.0 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +241 -0
  2. package/dist/cli.js +801 -355
  3. package/package.json +15 -13
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) {
@@ -181,6 +186,26 @@ var init_bell = __esm({
181
186
  }
182
187
  });
183
188
 
189
+ // src/color.ts
190
+ function isColorEnabled() {
191
+ if (process.env.NO_COLOR) return false;
192
+ if (process.env.FORCE_COLOR === "1") return true;
193
+ if (process.env.FORCE_COLOR === "0") return false;
194
+ return process.stdout.isTTY ?? false;
195
+ }
196
+ function wrap(code, text) {
197
+ return isColorEnabled() ? `\x1B[${code}m${text}\x1B[0m` : text;
198
+ }
199
+ var green, red, yellow;
200
+ var init_color = __esm({
201
+ "src/color.ts"() {
202
+ "use strict";
203
+ green = (text) => wrap("32", text);
204
+ red = (text) => wrap("31", text);
205
+ yellow = (text) => wrap("33", text);
206
+ }
207
+ });
208
+
184
209
  // src/history.ts
185
210
  import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
186
211
  import { join as join5 } from "path";
@@ -458,13 +483,13 @@ async function runPing(accounts, options) {
458
483
  parallel: options.parallel
459
484
  });
460
485
  }
461
- for (const r of results) {
462
- const status = r.success ? "ok" : "FAIL";
486
+ const total = results.length;
487
+ for (let idx = 0; idx < results.length; idx++) {
488
+ const r = results[idx];
489
+ const status = r.success ? green("ok") : red("FAIL");
463
490
  const detail = r.error ? ` (${r.error})` : "";
464
- const cr = r.claudeResponse;
465
- const costInfo = cr ? ` $${cr.total_cost_usd.toFixed(4)} ${cr.usage.input_tokens + cr.usage.output_tokens} tok` : "";
466
491
  logger.log(
467
- ` ${r.handle}: ${status} ${r.durationMs}ms${detail}${costInfo}`
492
+ ` [${idx + 1}/${total}] ${r.handle}: ${status} ${r.durationMs}ms${detail}`
468
493
  );
469
494
  appendHistoryEntry({
470
495
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -475,13 +500,13 @@ async function runPing(accounts, options) {
475
500
  });
476
501
  if (r.success) {
477
502
  let meta;
478
- if (cr) {
503
+ if (r.claudeResponse) {
479
504
  meta = {
480
- costUsd: cr.total_cost_usd,
481
- inputTokens: cr.usage.input_tokens,
482
- outputTokens: cr.usage.output_tokens,
483
- model: cr.model,
484
- sessionId: cr.session_id
505
+ costUsd: r.claudeResponse.total_cost_usd,
506
+ inputTokens: r.claudeResponse.usage.input_tokens,
507
+ outputTokens: r.claudeResponse.usage.output_tokens,
508
+ model: r.claudeResponse.model,
509
+ sessionId: r.claudeResponse.session_id
485
510
  };
486
511
  }
487
512
  recordPing(r.handle, /* @__PURE__ */ new Date(), meta);
@@ -539,6 +564,7 @@ var init_run_ping = __esm({
539
564
  "src/run-ping.ts"() {
540
565
  "use strict";
541
566
  init_bell();
567
+ init_color();
542
568
  init_history();
543
569
  init_logger();
544
570
  init_notify();
@@ -547,262 +573,24 @@ var init_run_ping = __esm({
547
573
  }
548
574
  });
549
575
 
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
576
  // src/daemon.ts
804
- init_paths();
805
- init_state();
577
+ var daemon_exports = {};
578
+ __export(daemon_exports, {
579
+ daemonLogPath: () => daemonLogPath,
580
+ daemonLoop: () => daemonLoop,
581
+ daemonPidPath: () => daemonPidPath,
582
+ daemonStopPath: () => daemonStopPath,
583
+ getDaemonStatus: () => getDaemonStatus,
584
+ isProcessRunning: () => isProcessRunning,
585
+ parseInterval: () => parseInterval,
586
+ readDaemonState: () => readDaemonState,
587
+ removeDaemonState: () => removeDaemonState,
588
+ runDaemon: () => runDaemon,
589
+ runDaemonWithDefaults: () => runDaemonWithDefaults,
590
+ startDaemon: () => startDaemon,
591
+ stopDaemon: () => stopDaemon,
592
+ writeDaemonState: () => writeDaemonState
593
+ });
806
594
  import { execSync, spawn } from "child_process";
807
595
  import {
808
596
  existsSync as existsSync5,
@@ -814,9 +602,6 @@ import {
814
602
  writeFileSync as writeFileSync3
815
603
  } from "fs";
816
604
  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
605
  function daemonPidPath() {
821
606
  return join6(resolveConfigDir(), "daemon.json");
822
607
  }
@@ -1049,6 +834,7 @@ async function stopDaemon(deps) {
1049
834
  }
1050
835
  async function runDaemon(intervalMs, options, deps) {
1051
836
  const stopPath = daemonStopPath();
837
+ if (existsSync5(stopPath)) unlinkSync(stopPath);
1052
838
  const cleanup = () => {
1053
839
  if (existsSync5(stopPath)) unlinkSync(stopPath);
1054
840
  removeDaemonState();
@@ -1096,28 +882,549 @@ async function runDaemonWithDefaults(intervalMs, options) {
1096
882
  exit: (code) => process.exit(code)
1097
883
  });
1098
884
  }
1099
-
1100
- // src/filter-accounts.ts
1101
- function filterAccounts(accounts, handles) {
1102
- if (handles.length === 0) return accounts;
1103
- const unknown = handles.filter((h) => !accounts.some((a) => a.handle === h));
1104
- if (unknown.length > 0) {
1105
- throw new Error(`Unknown account(s): ${unknown.join(", ")}`);
885
+ var GRACEFUL_POLL_MS, GRACEFUL_POLL_ATTEMPTS, POST_KILL_DELAY_MS;
886
+ var init_daemon = __esm({
887
+ "src/daemon.ts"() {
888
+ "use strict";
889
+ init_paths();
890
+ init_state();
891
+ GRACEFUL_POLL_MS = 500;
892
+ GRACEFUL_POLL_ATTEMPTS = 20;
893
+ POST_KILL_DELAY_MS = 1e3;
1106
894
  }
1107
- const set = new Set(handles);
1108
- return accounts.filter((a) => set.has(a.handle));
895
+ });
896
+
897
+ // src/service.ts
898
+ var service_exports = {};
899
+ __export(service_exports, {
900
+ generateLaunchdPlist: () => generateLaunchdPlist,
901
+ generateSystemdUnit: () => generateSystemdUnit,
902
+ getServiceStatus: () => getServiceStatus,
903
+ installService: () => installService,
904
+ resolveExecutable: () => resolveExecutable,
905
+ servicePath: () => servicePath,
906
+ uninstallService: () => uninstallService
907
+ });
908
+ import { execSync as nodeExecSync } from "child_process";
909
+ import {
910
+ existsSync as nodeExistsSync,
911
+ mkdirSync as nodeMkdirSync,
912
+ unlinkSync as nodeUnlinkSync,
913
+ writeFileSync as nodeWriteFileSync
914
+ } from "fs";
915
+ import { homedir as nodeHomedir } from "os";
916
+ import { dirname, join as join9 } from "path";
917
+ function resolveExecutable(deps) {
918
+ try {
919
+ const path = deps.execSync("which cc-ping", {
920
+ encoding: "utf-8",
921
+ stdio: ["ignore", "pipe", "ignore"]
922
+ }).trim();
923
+ if (path) {
924
+ return { executable: path, args: [] };
925
+ }
926
+ } catch {
927
+ }
928
+ return {
929
+ executable: process.execPath,
930
+ args: [process.argv[1]]
931
+ };
932
+ }
933
+ function generateLaunchdPlist(options, execInfo, configDir) {
934
+ const intervalMs = parseIntervalForService(options.interval);
935
+ const programArgs = [
936
+ ...execInfo.args,
937
+ "daemon",
938
+ "_run",
939
+ "--interval-ms",
940
+ String(intervalMs)
941
+ ];
942
+ if (options.quiet) programArgs.push("--quiet");
943
+ if (options.bell) programArgs.push("--bell");
944
+ if (options.notify) programArgs.push("--notify");
945
+ const allArgs = [execInfo.executable, ...programArgs];
946
+ const argsXml = allArgs.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
947
+ const logPath = join9(
948
+ configDir || join9(nodeHomedir(), ".config", "cc-ping"),
949
+ "daemon.log"
950
+ );
951
+ let envSection = "";
952
+ if (configDir) {
953
+ envSection = `
954
+ <key>EnvironmentVariables</key>
955
+ <dict>
956
+ <key>CC_PING_CONFIG</key>
957
+ <string>${escapeXml(configDir)}</string>
958
+ </dict>`;
959
+ }
960
+ return `<?xml version="1.0" encoding="UTF-8"?>
961
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
962
+ <plist version="1.0">
963
+ <dict>
964
+ <key>Label</key>
965
+ <string>${PLIST_LABEL}</string>
966
+ <key>ProgramArguments</key>
967
+ <array>
968
+ ${argsXml}
969
+ </array>
970
+ <key>RunAtLoad</key>
971
+ <true/>
972
+ <key>KeepAlive</key>
973
+ <dict>
974
+ <key>SuccessfulExit</key>
975
+ <false/>
976
+ </dict>
977
+ <key>StandardOutPath</key>
978
+ <string>${escapeXml(logPath)}</string>
979
+ <key>StandardErrorPath</key>
980
+ <string>${escapeXml(logPath)}</string>${envSection}
981
+ </dict>
982
+ </plist>
983
+ `;
984
+ }
985
+ function generateSystemdUnit(options, execInfo, configDir) {
986
+ const intervalMs = parseIntervalForService(options.interval);
987
+ const programArgs = [
988
+ ...execInfo.args,
989
+ "daemon",
990
+ "_run",
991
+ "--interval-ms",
992
+ String(intervalMs)
993
+ ];
994
+ if (options.quiet) programArgs.push("--quiet");
995
+ if (options.bell) programArgs.push("--bell");
996
+ if (options.notify) programArgs.push("--notify");
997
+ const execStart = [execInfo.executable, ...programArgs].map((a) => a.includes(" ") ? `"${a}"` : a).join(" ");
998
+ let envLine = "";
999
+ if (configDir) {
1000
+ envLine = `
1001
+ Environment=CC_PING_CONFIG=${configDir}`;
1002
+ }
1003
+ return `[Unit]
1004
+ Description=cc-ping daemon - auto-ping Claude Code sessions
1005
+
1006
+ [Service]
1007
+ Type=simple
1008
+ ExecStart=${execStart}${envLine}
1009
+ Restart=on-failure
1010
+ RestartSec=10
1011
+
1012
+ [Install]
1013
+ WantedBy=default.target
1014
+ `;
1015
+ }
1016
+ function servicePath(platform, home) {
1017
+ switch (platform) {
1018
+ case "darwin":
1019
+ return join9(home, "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
1020
+ case "linux":
1021
+ return join9(
1022
+ home,
1023
+ ".config",
1024
+ "systemd",
1025
+ "user",
1026
+ `${SYSTEMD_SERVICE}.service`
1027
+ );
1028
+ default:
1029
+ throw new Error(
1030
+ `Unsupported platform: ${platform}. Only macOS and Linux are supported.`
1031
+ );
1032
+ }
1033
+ }
1034
+ async function installService(options, deps) {
1035
+ const _platform = deps?.platform ?? process.platform;
1036
+ const _homedir = deps?.homedir ?? nodeHomedir;
1037
+ const _existsSync = deps?.existsSync ?? nodeExistsSync;
1038
+ const _mkdirSync = deps?.mkdirSync ?? nodeMkdirSync;
1039
+ const _writeFileSync = deps?.writeFileSync ?? nodeWriteFileSync;
1040
+ const _execSync = deps?.execSync ?? ((cmd) => nodeExecSync(cmd, { encoding: "utf-8" }));
1041
+ const _stopDaemon = deps?.stopDaemon ?? (async () => {
1042
+ const { stopDaemon: stopDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
1043
+ return stopDaemon2();
1044
+ });
1045
+ const _configDir = deps?.configDir;
1046
+ const home = _homedir();
1047
+ let path;
1048
+ try {
1049
+ path = servicePath(_platform, home);
1050
+ } catch (err) {
1051
+ return {
1052
+ success: false,
1053
+ servicePath: "",
1054
+ error: err.message
1055
+ };
1056
+ }
1057
+ if (_existsSync(path)) {
1058
+ return {
1059
+ success: false,
1060
+ servicePath: path,
1061
+ error: `Service already installed at ${path}. Run \`daemon uninstall\` first.`
1062
+ };
1063
+ }
1064
+ try {
1065
+ await _stopDaemon();
1066
+ } catch {
1067
+ }
1068
+ const execInfo = resolveExecutable({
1069
+ execSync: _execSync
1070
+ });
1071
+ const configDir = _configDir || void 0;
1072
+ let content;
1073
+ if (_platform === "darwin") {
1074
+ content = generateLaunchdPlist(options, execInfo, configDir);
1075
+ } else {
1076
+ content = generateSystemdUnit(options, execInfo, configDir);
1077
+ }
1078
+ _mkdirSync(dirname(path), { recursive: true });
1079
+ _writeFileSync(path, content);
1080
+ try {
1081
+ if (_platform === "darwin") {
1082
+ _execSync(`launchctl load ${path}`);
1083
+ } else {
1084
+ _execSync("systemctl --user daemon-reload");
1085
+ _execSync(`systemctl --user enable --now ${SYSTEMD_SERVICE}`);
1086
+ }
1087
+ } catch (err) {
1088
+ return {
1089
+ success: false,
1090
+ servicePath: path,
1091
+ error: `Service file written but failed to load: ${err.message}`
1092
+ };
1093
+ }
1094
+ return { success: true, servicePath: path };
1095
+ }
1096
+ async function uninstallService(deps) {
1097
+ const _platform = deps?.platform ?? process.platform;
1098
+ const _homedir = deps?.homedir ?? nodeHomedir;
1099
+ const _existsSync = deps?.existsSync ?? nodeExistsSync;
1100
+ const _unlinkSync = deps?.unlinkSync ?? nodeUnlinkSync;
1101
+ const _execSync = deps?.execSync ?? ((cmd) => nodeExecSync(cmd, { encoding: "utf-8" }));
1102
+ const home = _homedir();
1103
+ let path;
1104
+ try {
1105
+ path = servicePath(_platform, home);
1106
+ } catch (err) {
1107
+ return {
1108
+ success: false,
1109
+ error: err.message
1110
+ };
1111
+ }
1112
+ if (!_existsSync(path)) {
1113
+ return {
1114
+ success: false,
1115
+ servicePath: path,
1116
+ error: "No service installed."
1117
+ };
1118
+ }
1119
+ try {
1120
+ if (_platform === "darwin") {
1121
+ _execSync(`launchctl unload ${path}`);
1122
+ } else {
1123
+ _execSync(`systemctl --user disable --now ${SYSTEMD_SERVICE}`);
1124
+ }
1125
+ } catch {
1126
+ }
1127
+ _unlinkSync(path);
1128
+ return { success: true, servicePath: path };
1129
+ }
1130
+ function getServiceStatus(deps) {
1131
+ const _platform = deps?.platform ?? process.platform;
1132
+ const _homedir = deps?.homedir ?? nodeHomedir;
1133
+ const _existsSync = deps?.existsSync ?? nodeExistsSync;
1134
+ const home = _homedir();
1135
+ let path;
1136
+ try {
1137
+ path = servicePath(_platform, home);
1138
+ } catch {
1139
+ return { installed: false, platform: _platform };
1140
+ }
1141
+ return {
1142
+ installed: _existsSync(path),
1143
+ servicePath: path,
1144
+ platform: _platform
1145
+ };
1146
+ }
1147
+ function escapeXml(str) {
1148
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1149
+ }
1150
+ function parseIntervalForService(value) {
1151
+ if (!value) return QUOTA_WINDOW_MS2;
1152
+ const minutes = Number(value);
1153
+ if (Number.isNaN(minutes) || minutes <= 0) return QUOTA_WINDOW_MS2;
1154
+ return minutes * 60 * 1e3;
1155
+ }
1156
+ var PLIST_LABEL, SYSTEMD_SERVICE, QUOTA_WINDOW_MS2;
1157
+ var init_service = __esm({
1158
+ "src/service.ts"() {
1159
+ "use strict";
1160
+ PLIST_LABEL = "com.cc-ping.daemon";
1161
+ SYSTEMD_SERVICE = "cc-ping-daemon";
1162
+ QUOTA_WINDOW_MS2 = 5 * 60 * 60 * 1e3;
1163
+ }
1164
+ });
1165
+
1166
+ // src/cli.ts
1167
+ import { Command } from "commander";
1168
+
1169
+ // src/check.ts
1170
+ import { existsSync, readFileSync, statSync } from "fs";
1171
+ import { join } from "path";
1172
+ function checkAccount(account) {
1173
+ const issues = [];
1174
+ if (!existsSync(account.configDir) || !statSync(account.configDir).isDirectory()) {
1175
+ issues.push("config directory does not exist");
1176
+ return {
1177
+ handle: account.handle,
1178
+ configDir: account.configDir,
1179
+ healthy: false,
1180
+ issues
1181
+ };
1182
+ }
1183
+ const claudeJson = join(account.configDir, ".claude.json");
1184
+ if (!existsSync(claudeJson)) {
1185
+ issues.push(".claude.json not found");
1186
+ return {
1187
+ handle: account.handle,
1188
+ configDir: account.configDir,
1189
+ healthy: false,
1190
+ issues
1191
+ };
1192
+ }
1193
+ let parsed;
1194
+ try {
1195
+ const raw = readFileSync(claudeJson, "utf-8");
1196
+ parsed = JSON.parse(raw);
1197
+ } catch {
1198
+ issues.push(".claude.json is not valid JSON");
1199
+ return {
1200
+ handle: account.handle,
1201
+ configDir: account.configDir,
1202
+ healthy: false,
1203
+ issues
1204
+ };
1205
+ }
1206
+ if (!parsed.oauthAccount) {
1207
+ issues.push("no OAuth credentials found");
1208
+ }
1209
+ return {
1210
+ handle: account.handle,
1211
+ configDir: account.configDir,
1212
+ healthy: issues.length === 0,
1213
+ issues
1214
+ };
1215
+ }
1216
+ function checkAccounts(accounts) {
1217
+ return accounts.map((a) => checkAccount(a));
1218
+ }
1219
+
1220
+ // src/completions.ts
1221
+ var COMMANDS = "ping scan add remove list status next-reset history suggest check completions moo daemon";
1222
+ function bashCompletion() {
1223
+ return `_cc_ping() {
1224
+ local cur prev commands
1225
+ COMPREPLY=()
1226
+ cur="\${COMP_WORDS[COMP_CWORD]}"
1227
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
1228
+ commands="${COMMANDS}"
1229
+
1230
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
1231
+ COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
1232
+ return 0
1233
+ fi
1234
+
1235
+ case "\${COMP_WORDS[1]}" in
1236
+ ping)
1237
+ if [[ "\${cur}" == -* ]]; then
1238
+ COMPREPLY=( $(compgen -W "--parallel --quiet --json --group --bell --stagger" -- "\${cur}") )
1239
+ else
1240
+ local handles=$(cc-ping list 2>/dev/null | sed 's/ *\\(.*\\) ->.*/\\1/')
1241
+ COMPREPLY=( $(compgen -W "\${handles}" -- "\${cur}") )
1242
+ fi
1243
+ ;;
1244
+ add)
1245
+ COMPREPLY=( $(compgen -W "--group" -- "\${cur}") )
1246
+ ;;
1247
+ list|history|status|next-reset|check)
1248
+ COMPREPLY=( $(compgen -W "--json" -- "\${cur}") )
1249
+ ;;
1250
+ completions)
1251
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
1252
+ ;;
1253
+ daemon)
1254
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
1255
+ COMPREPLY=( $(compgen -W "start stop status install uninstall" -- "\${cur}") )
1256
+ elif [[ "\${COMP_WORDS[2]}" == "start" && "\${cur}" == -* ]]; then
1257
+ COMPREPLY=( $(compgen -W "--interval --quiet --bell --notify" -- "\${cur}") )
1258
+ elif [[ "\${COMP_WORDS[2]}" == "install" && "\${cur}" == -* ]]; then
1259
+ COMPREPLY=( $(compgen -W "--interval --quiet --bell --notify" -- "\${cur}") )
1260
+ elif [[ "\${COMP_WORDS[2]}" == "status" && "\${cur}" == -* ]]; then
1261
+ COMPREPLY=( $(compgen -W "--json" -- "\${cur}") )
1262
+ fi
1263
+ ;;
1264
+ esac
1265
+ return 0
1266
+ }
1267
+ complete -F _cc_ping cc-ping
1268
+ `;
1269
+ }
1270
+ function zshCompletion() {
1271
+ return `#compdef cc-ping
1272
+
1273
+ _cc_ping() {
1274
+ local -a commands
1275
+ commands=(
1276
+ 'ping:Ping configured accounts'
1277
+ 'scan:Auto-discover accounts'
1278
+ 'add:Add an account manually'
1279
+ 'remove:Remove an account'
1280
+ 'list:List configured accounts'
1281
+ 'status:Show account status'
1282
+ 'next-reset:Show soonest quota reset'
1283
+ 'history:Show ping history'
1284
+ 'suggest:Suggest next account'
1285
+ 'check:Verify account health'
1286
+ 'completions:Generate shell completions'
1287
+ 'moo:Send a test notification'
1288
+ 'daemon:Run auto-ping on a schedule'
1289
+ )
1290
+
1291
+ _arguments -C \\
1292
+ '--config[Config directory]:path:_files -/' \\
1293
+ '1:command:->command' \\
1294
+ '*::arg:->args'
1295
+
1296
+ case $state in
1297
+ command)
1298
+ _describe 'command' commands
1299
+ ;;
1300
+ args)
1301
+ case $words[1] in
1302
+ ping)
1303
+ _arguments \\
1304
+ '--parallel[Ping in parallel]' \\
1305
+ '--quiet[Suppress output]' \\
1306
+ '--json[JSON output]' \\
1307
+ '--group[Filter by group]:group:' \\
1308
+ '--bell[Ring bell on failure]' \\
1309
+ '--stagger[Delay between pings]:minutes:' \\
1310
+ '*:handle:->handles'
1311
+ if [[ $state == handles ]]; then
1312
+ local -a handles
1313
+ handles=(\${(f)"$(cc-ping list 2>/dev/null | sed 's/ *\\(.*\\) ->.*/\\1/')"})
1314
+ _describe 'handle' handles
1315
+ fi
1316
+ ;;
1317
+ completions)
1318
+ _arguments '1:shell:(bash zsh fish)'
1319
+ ;;
1320
+ list|history|status|next-reset|check)
1321
+ _arguments '--json[JSON output]'
1322
+ ;;
1323
+ add)
1324
+ _arguments '--group[Assign group]:group:'
1325
+ ;;
1326
+ daemon)
1327
+ local -a subcmds
1328
+ subcmds=(
1329
+ 'start:Start the daemon process'
1330
+ 'stop:Stop the daemon process'
1331
+ 'status:Show daemon status'
1332
+ 'install:Install as system service'
1333
+ 'uninstall:Remove system service'
1334
+ )
1335
+ _arguments '1:subcommand:->subcmd' '*::arg:->subargs'
1336
+ case $state in
1337
+ subcmd)
1338
+ _describe 'subcommand' subcmds
1339
+ ;;
1340
+ subargs)
1341
+ case $words[1] in
1342
+ start|install)
1343
+ _arguments \\
1344
+ '--interval[Ping interval in minutes]:minutes:' \\
1345
+ '--quiet[Suppress ping output]' \\
1346
+ '--bell[Ring bell on failure]' \\
1347
+ '--notify[Send notification on failure]'
1348
+ ;;
1349
+ status)
1350
+ _arguments '--json[JSON output]'
1351
+ ;;
1352
+ esac
1353
+ ;;
1354
+ esac
1355
+ ;;
1356
+ esac
1357
+ ;;
1358
+ esac
1109
1359
  }
1110
- function filterByGroup(accounts, group) {
1111
- if (!group) return accounts;
1112
- const filtered = accounts.filter((a) => a.group === group);
1113
- if (filtered.length === 0) {
1114
- throw new Error(`No accounts in group: ${group}`);
1360
+
1361
+ _cc_ping
1362
+ `;
1363
+ }
1364
+ function fishCompletion() {
1365
+ return `# Fish completions for cc-ping
1366
+ set -l commands ping scan add remove list status next-reset history suggest check completions moo
1367
+
1368
+ complete -c cc-ping -f
1369
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a ping -d "Ping configured accounts"
1370
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a scan -d "Auto-discover accounts"
1371
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a add -d "Add an account manually"
1372
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a remove -d "Remove an account"
1373
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a list -d "List configured accounts"
1374
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a status -d "Show account status"
1375
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a next-reset -d "Show soonest quota reset"
1376
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a history -d "Show ping history"
1377
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a suggest -d "Suggest next account"
1378
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a check -d "Verify account health"
1379
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a completions -d "Generate shell completions"
1380
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a moo -d "Send a test notification"
1381
+ complete -c cc-ping -n "not __fish_seen_subcommand_from $commands" -a daemon -d "Run auto-ping on a schedule"
1382
+
1383
+ complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l parallel -d "Ping in parallel"
1384
+ complete -c cc-ping -n "__fish_seen_subcommand_from ping" -s q -l quiet -d "Suppress output"
1385
+ complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l json -d "JSON output"
1386
+ complete -c cc-ping -n "__fish_seen_subcommand_from ping" -s g -l group -r -d "Filter by group"
1387
+ complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l bell -d "Ring bell on failure"
1388
+ complete -c cc-ping -n "__fish_seen_subcommand_from ping" -l stagger -r -d "Delay between pings"
1389
+ complete -c cc-ping -n "__fish_seen_subcommand_from ping" -a "(cc-ping list 2>/dev/null | string replace -r ' *(.*) ->.*' '$1')"
1390
+
1391
+ complete -c cc-ping -n "__fish_seen_subcommand_from list history status next-reset check" -l json -d "JSON output"
1392
+ complete -c cc-ping -n "__fish_seen_subcommand_from add" -s g -l group -r -d "Assign group"
1393
+ complete -c cc-ping -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"
1394
+
1395
+ 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"
1396
+ 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"
1397
+ 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"
1398
+ 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"
1399
+ 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"
1400
+ 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"
1401
+ complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -s q -l quiet -d "Suppress output"
1402
+ complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -l bell -d "Ring bell on failure"
1403
+ complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start install" -l notify -d "Send notification on failure"
1404
+ complete -c cc-ping -n "__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from status" -l json -d "JSON output"
1405
+ `;
1406
+ }
1407
+ function generateCompletion(shell) {
1408
+ switch (shell) {
1409
+ case "bash":
1410
+ return bashCompletion();
1411
+ case "zsh":
1412
+ return zshCompletion();
1413
+ case "fish":
1414
+ return fishCompletion();
1415
+ default:
1416
+ throw new Error(
1417
+ `Unsupported shell: ${shell}. Supported: bash, zsh, fish`
1418
+ );
1115
1419
  }
1116
- return filtered;
1117
1420
  }
1118
1421
 
1119
1422
  // src/cli.ts
1120
- init_history();
1423
+ init_config();
1424
+ init_daemon();
1425
+
1426
+ // src/default-command.ts
1427
+ init_config();
1121
1428
 
1122
1429
  // src/identity.ts
1123
1430
  import { readFileSync as readFileSync6 } from "fs";
@@ -1166,76 +1473,25 @@ function findDuplicates(accounts) {
1166
1473
  return result;
1167
1474
  }
1168
1475
 
1169
- // src/next-reset.ts
1170
- init_state();
1171
- function getNextReset(accounts, now = /* @__PURE__ */ new Date()) {
1172
- let best = null;
1173
- for (const account of accounts) {
1174
- const window = getWindowReset(account.handle, now);
1175
- if (!window) continue;
1176
- if (best === null || window.remainingMs < best.remainingMs) {
1177
- best = {
1178
- handle: account.handle,
1179
- configDir: account.configDir,
1180
- remainingMs: window.remainingMs,
1181
- resetAt: window.resetAt.toISOString(),
1182
- timeUntilReset: formatTimeRemaining(window.remainingMs)
1183
- };
1184
- }
1185
- }
1186
- return best;
1187
- }
1188
-
1189
- // src/cli.ts
1190
- init_notify();
1191
- init_paths();
1192
- init_run_ping();
1193
-
1194
- // src/scan.ts
1195
- import { existsSync as existsSync6, readdirSync, statSync as statSync2 } from "fs";
1196
- import { homedir as homedir2 } from "os";
1197
- import { join as join8 } from "path";
1198
- var ACCOUNTS_DIR = join8(homedir2(), ".claude-accounts");
1199
- function scanAccounts() {
1200
- if (!existsSync6(ACCOUNTS_DIR)) return [];
1201
- return readdirSync(ACCOUNTS_DIR).filter((name) => {
1202
- const full = join8(ACCOUNTS_DIR, name);
1203
- return statSync2(full).isDirectory() && !name.startsWith(".");
1204
- }).map((name) => ({
1205
- handle: name,
1206
- configDir: join8(ACCOUNTS_DIR, name)
1207
- }));
1208
- }
1209
-
1210
- // src/stagger.ts
1211
- init_state();
1212
- function calculateStagger(accountCount, windowMs = QUOTA_WINDOW_MS) {
1213
- if (accountCount <= 1) return 0;
1214
- return Math.floor(windowMs / accountCount);
1215
- }
1216
- function parseStagger(value, accountCount) {
1217
- if (value === "auto") {
1218
- return calculateStagger(accountCount);
1219
- }
1220
- const minutes = Number(value);
1221
- if (Number.isNaN(minutes)) {
1222
- throw new Error(`Invalid stagger value: ${value}`);
1223
- }
1224
- if (minutes <= 0) {
1225
- throw new Error("Stagger must be a positive number");
1226
- }
1227
- return minutes * 60 * 1e3;
1228
- }
1229
-
1230
1476
  // src/status.ts
1477
+ init_color();
1231
1478
  init_config();
1232
1479
  init_state();
1480
+ function colorizeStatus(windowStatus) {
1481
+ switch (windowStatus) {
1482
+ case "active":
1483
+ return green(windowStatus);
1484
+ case "needs ping":
1485
+ return red(windowStatus);
1486
+ default:
1487
+ return yellow(windowStatus);
1488
+ }
1489
+ }
1233
1490
  function formatStatusLine(status) {
1234
1491
  const ping = status.lastPing === null ? "never" : status.lastPing.replace("T", " ").replace(/\.\d+Z$/, "Z");
1235
1492
  const reset = status.timeUntilReset !== null ? ` (resets in ${status.timeUntilReset})` : "";
1236
- const cost = status.lastCostUsd !== null && status.lastTokens !== null ? ` $${status.lastCostUsd.toFixed(4)} ${status.lastTokens} tok` : "";
1237
1493
  const dup = status.duplicateOf ? ` [duplicate of ${status.duplicateOf}]` : "";
1238
- return ` ${status.handle}: ${status.windowStatus} last ping: ${ping}${reset}${cost}${dup}`;
1494
+ return ` ${status.handle}: ${colorizeStatus(status.windowStatus)} last ping: ${ping}${reset}${dup}`;
1239
1495
  }
1240
1496
  function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicates) {
1241
1497
  const dupLookup = /* @__PURE__ */ new Map();
@@ -1270,7 +1526,7 @@ function getAccountStatuses(accounts, now = /* @__PURE__ */ new Date(), duplicat
1270
1526
  handle: account.handle,
1271
1527
  configDir: account.configDir,
1272
1528
  lastPing: lastPing.toISOString(),
1273
- windowStatus: window ? "active" : "expired",
1529
+ windowStatus: window ? "active" : "needs ping",
1274
1530
  timeUntilReset: window ? formatTimeRemaining(window.remainingMs) : null,
1275
1531
  lastCostUsd,
1276
1532
  lastTokens,
@@ -1291,6 +1547,118 @@ function printAccountTable(log = console.log, now = /* @__PURE__ */ new Date())
1291
1547
  }
1292
1548
  }
1293
1549
 
1550
+ // src/default-command.ts
1551
+ function showDefault(log = console.log, now = /* @__PURE__ */ new Date()) {
1552
+ const accounts = listAccounts();
1553
+ if (accounts.length === 0) {
1554
+ log("No accounts configured.");
1555
+ log("\nGet started:");
1556
+ log(" cc-ping scan Auto-discover accounts");
1557
+ log(" cc-ping add <h> <d> Add an account manually");
1558
+ return;
1559
+ }
1560
+ const dupes = findDuplicates(accounts);
1561
+ const statuses = getAccountStatuses(accounts, now, dupes);
1562
+ for (const s of statuses) {
1563
+ log(formatStatusLine(s));
1564
+ }
1565
+ const needsPing = statuses.filter((s) => s.windowStatus !== "active");
1566
+ if (needsPing.length > 0) {
1567
+ log("");
1568
+ log("Suggested next steps:");
1569
+ const handles = needsPing.map((s) => s.handle).join(" ");
1570
+ if (needsPing.length < statuses.length) {
1571
+ log(` cc-ping ping ${handles} Ping accounts that need it`);
1572
+ } else {
1573
+ log(" cc-ping ping Ping all accounts");
1574
+ }
1575
+ log(" cc-ping daemon start Auto-ping on a schedule");
1576
+ }
1577
+ }
1578
+
1579
+ // src/filter-accounts.ts
1580
+ function filterAccounts(accounts, handles) {
1581
+ if (handles.length === 0) return accounts;
1582
+ const unknown = handles.filter((h) => !accounts.some((a) => a.handle === h));
1583
+ if (unknown.length > 0) {
1584
+ throw new Error(`Unknown account(s): ${unknown.join(", ")}`);
1585
+ }
1586
+ const set = new Set(handles);
1587
+ return accounts.filter((a) => set.has(a.handle));
1588
+ }
1589
+ function filterByGroup(accounts, group) {
1590
+ if (!group) return accounts;
1591
+ const filtered = accounts.filter((a) => a.group === group);
1592
+ if (filtered.length === 0) {
1593
+ throw new Error(`No accounts in group: ${group}`);
1594
+ }
1595
+ return filtered;
1596
+ }
1597
+
1598
+ // src/cli.ts
1599
+ init_history();
1600
+
1601
+ // src/next-reset.ts
1602
+ init_state();
1603
+ function getNextReset(accounts, now = /* @__PURE__ */ new Date()) {
1604
+ let best = null;
1605
+ for (const account of accounts) {
1606
+ const window = getWindowReset(account.handle, now);
1607
+ if (!window) continue;
1608
+ if (best === null || window.remainingMs < best.remainingMs) {
1609
+ best = {
1610
+ handle: account.handle,
1611
+ configDir: account.configDir,
1612
+ remainingMs: window.remainingMs,
1613
+ resetAt: window.resetAt.toISOString(),
1614
+ timeUntilReset: formatTimeRemaining(window.remainingMs)
1615
+ };
1616
+ }
1617
+ }
1618
+ return best;
1619
+ }
1620
+
1621
+ // src/cli.ts
1622
+ init_notify();
1623
+ init_paths();
1624
+ init_run_ping();
1625
+
1626
+ // src/scan.ts
1627
+ import { existsSync as existsSync6, readdirSync, statSync as statSync2 } from "fs";
1628
+ import { homedir as homedir2 } from "os";
1629
+ import { join as join8 } from "path";
1630
+ var ACCOUNTS_DIR = join8(homedir2(), ".claude-accounts");
1631
+ function scanAccounts() {
1632
+ if (!existsSync6(ACCOUNTS_DIR)) return [];
1633
+ return readdirSync(ACCOUNTS_DIR).filter((name) => {
1634
+ const full = join8(ACCOUNTS_DIR, name);
1635
+ return statSync2(full).isDirectory() && !name.startsWith(".");
1636
+ }).map((name) => ({
1637
+ handle: name,
1638
+ configDir: join8(ACCOUNTS_DIR, name)
1639
+ }));
1640
+ }
1641
+
1642
+ // src/stagger.ts
1643
+ init_state();
1644
+ function calculateStagger(accountCount, windowMs = QUOTA_WINDOW_MS) {
1645
+ if (accountCount <= 1) return 0;
1646
+ return Math.floor(windowMs / accountCount);
1647
+ }
1648
+ function parseStagger(value, accountCount) {
1649
+ if (value === "auto") {
1650
+ return calculateStagger(accountCount);
1651
+ }
1652
+ const minutes = Number(value);
1653
+ if (Number.isNaN(minutes)) {
1654
+ throw new Error(`Invalid stagger value: ${value}`);
1655
+ }
1656
+ if (minutes <= 0) {
1657
+ throw new Error("Stagger must be a positive number");
1658
+ }
1659
+ return minutes * 60 * 1e3;
1660
+ }
1661
+
1294
1662
  // src/suggest.ts
1295
1663
  init_state();
1296
1664
  function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
@@ -1322,7 +1690,7 @@ function suggestAccount(accounts, now = /* @__PURE__ */ new Date()) {
1322
1690
  }
1323
1691
 
1324
1692
  // src/cli.ts
1325
- var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("0.1.0").option(
1693
+ var program = new Command().name("cc-ping").description("Ping Claude Code sessions to trigger quota windows early").version("1.1.0").option(
1326
1694
  "--config <path>",
1327
1695
  "Path to config directory (default: ~/.config/cc-ping, env: CC_PING_CONFIG)"
1328
1696
  ).hook("preAction", (thisCommand) => {
@@ -1330,6 +1698,8 @@ var program = new Command().name("cc-ping").description("Ping Claude Code sessio
1330
1698
  if (opts.config) {
1331
1699
  setConfigDir(opts.config);
1332
1700
  }
1701
+ }).action(() => {
1702
+ showDefault();
1333
1703
  });
1334
1704
  program.command("ping").description("Ping configured accounts to start quota windows").argument("[handles...]", "Specific account handles to ping (default: all)").option("--parallel", "Ping all accounts in parallel", false).option("-q, --quiet", "Suppress all output except errors (for cron)", false).option("--json", "Output results as JSON", false).option("-g, --group <group>", "Ping only accounts in this group").option("--bell", "Ring terminal bell on ping failure", false).option("--notify", "Send desktop notification on ping failure", false).option(
1335
1705
  "--stagger <minutes|auto>",
@@ -1516,7 +1886,7 @@ var daemon = program.command("daemon").description("Run auto-ping on a schedule"
1516
1886
  daemon.command("start").description("Start the daemon process").option(
1517
1887
  "--interval <minutes>",
1518
1888
  "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) => {
1889
+ ).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
1890
  const result = startDaemon({
1521
1891
  interval: opts.interval,
1522
1892
  quiet: opts.quiet,
@@ -1528,6 +1898,13 @@ daemon.command("start").description("Start the daemon process").option(
1528
1898
  process.exit(1);
1529
1899
  }
1530
1900
  console.log(`Daemon started (PID: ${result.pid})`);
1901
+ const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
1902
+ const svc = getServiceStatus2();
1903
+ if (!svc.installed) {
1904
+ console.log(
1905
+ "Hint: won't survive a reboot. Use `daemon install` for a persistent service."
1906
+ );
1907
+ }
1531
1908
  printAccountTable();
1532
1909
  });
1533
1910
  daemon.command("stop").description("Stop the daemon process").action(async () => {
@@ -1537,24 +1914,51 @@ daemon.command("stop").description("Stop the daemon process").action(async () =>
1537
1914
  process.exit(1);
1538
1915
  }
1539
1916
  console.log(`Daemon stopped (PID: ${result.pid})`);
1917
+ const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
1918
+ const svc = getServiceStatus2();
1919
+ if (svc.installed) {
1920
+ console.log(
1921
+ "Note: system service is installed. The daemon may restart. Use `daemon uninstall` to fully remove."
1922
+ );
1923
+ }
1540
1924
  });
1541
- daemon.command("status").description("Show daemon status").option("--json", "Output as JSON", false).action((opts) => {
1925
+ daemon.command("status").description("Show daemon status").option("--json", "Output as JSON", false).action(async (opts) => {
1926
+ const { getServiceStatus: getServiceStatus2 } = await Promise.resolve().then(() => (init_service(), service_exports));
1927
+ const svc = getServiceStatus2();
1542
1928
  const status = getDaemonStatus();
1543
1929
  if (opts.json) {
1930
+ const serviceInfo = svc.installed ? {
1931
+ service: {
1932
+ installed: true,
1933
+ path: svc.servicePath,
1934
+ platform: svc.platform
1935
+ }
1936
+ } : { service: { installed: false } };
1544
1937
  if (!status.running) {
1545
- console.log(JSON.stringify(status, null, 2));
1938
+ console.log(JSON.stringify({ ...status, ...serviceInfo }, null, 2));
1546
1939
  return;
1547
1940
  }
1548
1941
  const accounts = listAccounts();
1549
1942
  const dupes = findDuplicates(accounts);
1550
1943
  const accountStatuses = getAccountStatuses(accounts, /* @__PURE__ */ new Date(), dupes);
1551
1944
  console.log(
1552
- JSON.stringify({ ...status, accounts: accountStatuses }, null, 2)
1945
+ JSON.stringify(
1946
+ { ...status, ...serviceInfo, accounts: accountStatuses },
1947
+ null,
1948
+ 2
1949
+ )
1553
1950
  );
1554
1951
  return;
1555
1952
  }
1556
1953
  if (!status.running) {
1557
- console.log("Daemon is not running");
1954
+ if (svc.installed) {
1955
+ const kind = svc.platform === "darwin" ? "launchd" : "systemd";
1956
+ console.log(
1957
+ `Daemon is not running (system service: installed via ${kind})`
1958
+ );
1959
+ } else {
1960
+ console.log("Daemon is not running");
1961
+ }
1558
1962
  return;
1559
1963
  }
1560
1964
  console.log(`Daemon is running (PID: ${status.pid})`);
@@ -1566,15 +1970,57 @@ daemon.command("status").description("Show daemon status").option("--json", "Out
1566
1970
  if (status.nextPingIn) {
1567
1971
  console.log(` Next ping in: ${status.nextPingIn}`);
1568
1972
  }
1973
+ if (svc.installed) {
1974
+ const kind = svc.platform === "darwin" ? "launchd" : "systemd";
1975
+ console.log(` System service: installed (${kind})`);
1976
+ }
1569
1977
  console.log("");
1570
1978
  printAccountTable();
1571
1979
  });
1980
+ daemon.command("install").description("Install daemon as a system service (launchd/systemd)").option(
1981
+ "--interval <minutes>",
1982
+ "Ping interval in minutes (default: 300 = 5h quota window)"
1983
+ ).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) => {
1984
+ const { installService: installService2 } = await Promise.resolve().then(() => (init_service(), service_exports));
1985
+ const result = await installService2({
1986
+ interval: opts.interval,
1987
+ quiet: opts.quiet,
1988
+ bell: opts.bell,
1989
+ notify: opts.notify
1990
+ });
1991
+ if (!result.success) {
1992
+ console.error(result.error);
1993
+ process.exit(1);
1994
+ }
1995
+ console.log(`Service installed: ${result.servicePath}`);
1996
+ console.log(
1997
+ "The daemon will start automatically on login. Use `daemon uninstall` to remove."
1998
+ );
1999
+ });
2000
+ daemon.command("uninstall").description("Remove daemon system service").action(async () => {
2001
+ const { uninstallService: uninstallService2 } = await Promise.resolve().then(() => (init_service(), service_exports));
2002
+ const result = await uninstallService2();
2003
+ if (!result.success) {
2004
+ console.error(result.error);
2005
+ process.exit(1);
2006
+ }
2007
+ console.log(`Service removed: ${result.servicePath}`);
2008
+ });
1572
2009
  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
2010
  const intervalMs = Number(opts.intervalMs);
1574
2011
  if (!intervalMs || intervalMs <= 0) {
1575
2012
  console.error("Invalid --interval-ms");
1576
2013
  process.exit(1);
1577
2014
  }
2015
+ if (!readDaemonState()) {
2016
+ const { resolveConfigDir: resolveConfigDir2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
2017
+ writeDaemonState({
2018
+ pid: process.pid,
2019
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2020
+ intervalMs,
2021
+ configDir: resolveConfigDir2()
2022
+ });
2023
+ }
1578
2024
  await runDaemonWithDefaults(intervalMs, {
1579
2025
  quiet: opts.quiet,
1580
2026
  bell: opts.bell,