@4pm/cli 1.17.0 → 1.19.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 (24) hide show
  1. package/dist/index.js +848 -241
  2. package/dist/project-sample/.claude/AUTONOMOUS-CRON.md +16 -23
  3. package/dist/project-sample/.claude/AUTONOMOUS.md +31 -36
  4. package/dist/project-sample/.claude/hooks/autonomous-tick.sh +8 -349
  5. package/dist/project-sample/.claude/settings.json +1 -3
  6. package/dist/project-sample/.claude/templates/AI_TODO.empty.md +2 -2
  7. package/dist/project-sample/.claude/templates/README.md +1 -1
  8. package/dist/project-sample/.claude/templates/USER_QA.empty.md +13 -6
  9. package/dist/project-sample/.claude/templates/USER_QA.sample.md +6 -3
  10. package/dist/project-sample/.claude/templates/USER_TODO.empty.md +13 -6
  11. package/dist/project-sample/.claude/templates/USER_TODO.sample.md +7 -6
  12. package/dist/project-sample/.claude/templates/project.secrets.sample.json +0 -1
  13. package/dist/project-sample/AI_PLACEHOLDER.md +5 -5
  14. package/dist/project-sample/AI_TODO.md +2 -2
  15. package/dist/project-sample/USER_QA.md +13 -6
  16. package/dist/project-sample/USER_TODO.md +13 -6
  17. package/dist/project-sample/project.secrets.json.sample +0 -1
  18. package/package.json +1 -1
  19. package/dist/project-sample/.claude/.autonomous.settings.json +0 -24
  20. package/dist/project-sample/.claude/commands/auto-cycle.md +0 -177
  21. package/dist/project-sample/.claude/hooks/__pycache__/autonomous-history.cpython-312.pyc +0 -0
  22. package/dist/project-sample/.claude/hooks/autonomous-history.py +0 -193
  23. package/dist/project-sample/.claude/skills/check-usage/SKILL.md +0 -40
  24. package/dist/project-sample/.claude/skills/check-usage/check_usage.py +0 -161
@@ -1,161 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Fetch Claude Code plan-usage limits (same data as the built-in /usage screen).
3
-
4
- Reads the OAuth access token from ~/.claude/.credentials.json and queries the
5
- Anthropic usage endpoint, then prints session + weekly utilization and reset times
6
- in the local timezone.
7
- """
8
- import json
9
- import os
10
- import subprocess
11
- import sys
12
- import urllib.request
13
- import urllib.error
14
- from datetime import datetime, timezone
15
-
16
- # Windows consoles default to cp1252 which can't encode the emoji/bar glyphs.
17
- for _s in (sys.stdout, sys.stderr):
18
- try:
19
- _s.reconfigure(encoding="utf-8")
20
- except Exception:
21
- pass
22
-
23
- CRED_PATH = os.path.expanduser("~/.claude/.credentials.json")
24
- URL = "https://api.anthropic.com/api/oauth/usage"
25
-
26
-
27
- class UsageError(Exception):
28
- """Recoverable failure while loading the token or calling the usage API.
29
- Raised (instead of exiting) so the caller can refresh the token and retry once."""
30
-
31
-
32
- def load_token():
33
- try:
34
- with open(CRED_PATH, "r", encoding="utf-8") as f:
35
- data = json.load(f)
36
- except FileNotFoundError:
37
- raise UsageError(f"Credentials not found: {CRED_PATH} (log in to Claude Code first).")
38
- oauth = data.get("claudeAiOauth", data)
39
- token = oauth.get("accessToken")
40
- if not token:
41
- raise UsageError("No accessToken in credentials.")
42
- exp = oauth.get("expiresAt")
43
- if exp and datetime.now(timezone.utc).timestamp() * 1000 > exp:
44
- print("Warning: token may be expired — run /usage once in Claude Code to refresh.\n",
45
- file=sys.stderr)
46
- return token, oauth.get("subscriptionType", "?")
47
-
48
-
49
- def fetch(token):
50
- req = urllib.request.Request(URL, headers={
51
- "Authorization": f"Bearer {token}",
52
- "anthropic-beta": "oauth-2025-04-20",
53
- "User-Agent": "claude-cli/2.0.0 (external, cli)",
54
- })
55
- try:
56
- with urllib.request.urlopen(req, timeout=20) as r:
57
- return json.load(r)
58
- except urllib.error.HTTPError as e:
59
- body = e.read().decode("utf-8", "replace")[:300]
60
- raise UsageError(f"API error HTTP {e.code}: {body}")
61
- except urllib.error.URLError as e:
62
- raise UsageError(f"Could not reach API: {e.reason}")
63
-
64
-
65
- def get_usage():
66
- """Load the token and query the usage API. Raises UsageError on any failure."""
67
- token, sub = load_token()
68
- return fetch(token), sub
69
-
70
-
71
- def refresh_token():
72
- """Best-effort token refresh: run `claude /usage`, which refreshes the OAuth
73
- token on startup. On Windows we go through `wsl` (the credentials this script
74
- reads live in WSL); on POSIX we call `claude` directly. Output is discarded and
75
- the call is bounded by a timeout so it can never hang the usage check."""
76
- cmd = ["wsl", "claude", "/usage"] if sys.platform == "win32" else ["claude", "/usage"]
77
- print(f"Refreshing token via: {' '.join(cmd)}", file=sys.stderr)
78
- try:
79
- subprocess.run(
80
- cmd, timeout=60,
81
- stdin=subprocess.DEVNULL,
82
- stdout=subprocess.DEVNULL,
83
- stderr=subprocess.DEVNULL,
84
- )
85
- except Exception as e:
86
- print(f"Token refresh attempt failed: {e}", file=sys.stderr)
87
-
88
-
89
- def fmt_reset(iso):
90
- if not iso:
91
- return "?"
92
- dt = datetime.fromisoformat(iso).astimezone() # -> local tz
93
- return f"{dt:%a %d/%m %H:%M}"
94
-
95
-
96
- def save_last_result(record):
97
- """Delete the previous ./output/last-usage-check.json, then write a fresh one
98
- holding this run's result. Console output is still printed by the caller."""
99
- out_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")
100
- os.makedirs(out_dir, exist_ok=True)
101
- out_path = os.path.join(out_dir, "last-usage-check.json")
102
- # Remove the stale file first, then recreate it from scratch.
103
- try:
104
- os.remove(out_path)
105
- except FileNotFoundError:
106
- pass
107
- with open(out_path, "w", encoding="utf-8") as f:
108
- json.dump(record, f, ensure_ascii=False, indent=2)
109
- return out_path
110
-
111
-
112
- def main():
113
- try:
114
- data, sub = get_usage()
115
- except UsageError as e:
116
- # First attempt failed — try to refresh the token once, then retry once.
117
- print(f"Usage check failed: {e}", file=sys.stderr)
118
- refresh_token()
119
- try:
120
- data, sub = get_usage()
121
- except UsageError as e2:
122
- print(f"Still failing after token refresh — skipping: {e2}", file=sys.stderr)
123
- sys.exit(1)
124
-
125
- if "--json" in sys.argv:
126
- print(json.dumps(data, indent=2))
127
- return
128
-
129
- fh = data.get("five_hour") or {}
130
- sd = data.get("seven_day") or {}
131
-
132
- lines = [
133
- f"- Plan: {sub}",
134
- f"- Session: {fh.get('utilization', 0):.0f}% — reset {fmt_reset(fh.get('resets_at'))}",
135
- f"- Weekly: {sd.get('utilization', 0):.0f}% — reset {fmt_reset(sd.get('resets_at'))}",
136
- ]
137
- extra = data.get("extra_usage") or {}
138
- if extra.get("is_enabled") and extra.get("used_credits"):
139
- dp = extra.get("decimal_places", 2)
140
- lines.append(f"- Extra: {extra['used_credits'] / (10 ** dp):.2f} {extra.get('currency', 'USD')}")
141
-
142
- summary = "\n".join(lines)
143
- print(summary) # keep the console output
144
-
145
- # Persist this run's result so the autonomous workflow can read it back.
146
- record = {
147
- "checked_at": datetime.now().astimezone().isoformat(timespec="seconds"),
148
- "plan": sub,
149
- "session_pct": fh.get("utilization", 0),
150
- "weekly_pct": sd.get("utilization", 0),
151
- "session_resets_at": fh.get("resets_at"),
152
- "weekly_resets_at": sd.get("resets_at"),
153
- "summary": summary,
154
- "raw": data,
155
- }
156
- out_path = save_last_result(record)
157
- print(f"\nSaved result to: {out_path}")
158
-
159
-
160
- if __name__ == "__main__":
161
- main()