@kurokeita/add-skill 1.17.2 → 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.
- package/dist/skills/gh-fix-ci/SKILL.md +13 -5
- package/dist/skills/grill-me/SKILL.md +9 -0
- package/dist/skills/prd-to-tasks/SKILL.md +164 -0
- package/dist/skills/statusline-setup/SKILL.md +479 -0
- package/dist/skills/test-driven-development/SKILL.md +70 -345
- package/dist/skills/test-driven-development/deep-modules.md +33 -0
- package/dist/skills/test-driven-development/interface-design.md +35 -0
- package/dist/skills/test-driven-development/mocking.md +68 -0
- package/dist/skills/test-driven-development/refactoring.md +10 -0
- package/dist/skills/test-driven-development/tests.md +61 -0
- package/dist/skills/write-prd/SKILL.md +162 -0
- package/package.json +1 -1
- package/dist/skills/test-driven-development/testing-anti-patterns.md +0 -316
|
@@ -21,7 +21,11 @@ Prereq: authenticate with the standard GitHub CLI once (for example, run `gh aut
|
|
|
21
21
|
|
|
22
22
|
## Quick start
|
|
23
23
|
|
|
24
|
-
-
|
|
24
|
+
- Detect the interpreter first:
|
|
25
|
+
`PYTHON_BIN=$(command -v python || command -v python3 || true)`
|
|
26
|
+
- If `PYTHON_BIN` is empty, skip the bundled script and use the manual `gh` fallback workflow.
|
|
27
|
+
- Otherwise run:
|
|
28
|
+
`"$PYTHON_BIN" "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "<number-or-url>"`
|
|
25
29
|
- Add `--json` if you want machine-friendly output for summarization.
|
|
26
30
|
|
|
27
31
|
## Workflow
|
|
@@ -34,7 +38,11 @@ Prereq: authenticate with the standard GitHub CLI once (for example, run `gh aut
|
|
|
34
38
|
- If the user provides a PR number or URL, use that directly.
|
|
35
39
|
3. Inspect failing checks (GitHub Actions only).
|
|
36
40
|
- Preferred: run the bundled script (handles gh field drift and job-log fallbacks):
|
|
37
|
-
-
|
|
41
|
+
- Resolve the interpreter first:
|
|
42
|
+
`PYTHON_BIN=$(command -v python || command -v python3 || true)`
|
|
43
|
+
- If `PYTHON_BIN` is set, run:
|
|
44
|
+
`"$PYTHON_BIN" "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "<number-or-url>"`
|
|
45
|
+
- If neither `python` nor `python3` is available, skip the bundled script and use the manual fallback below.
|
|
38
46
|
- Add `--json` for machine-friendly output.
|
|
39
47
|
- Manual fallback:
|
|
40
48
|
- `gh pr checks <pr> --json name,state,bucket,link,startedAt,completedAt,workflow`
|
|
@@ -65,6 +73,6 @@ Fetch failing PR checks, pull GitHub Actions logs, and extract a failure snippet
|
|
|
65
73
|
|
|
66
74
|
Usage examples:
|
|
67
75
|
|
|
68
|
-
- `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "123"`
|
|
69
|
-
- `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "https://github.com/org/repo/pull/123" --json`
|
|
70
|
-
- `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --max-lines 200 --context 40`
|
|
76
|
+
- `PYTHON_BIN=$(command -v python || command -v python3 || true); test -n "$PYTHON_BIN" && "$PYTHON_BIN" "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "123"`
|
|
77
|
+
- `PYTHON_BIN=$(command -v python || command -v python3 || true); test -n "$PYTHON_BIN" && "$PYTHON_BIN" "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "https://github.com/org/repo/pull/123" --json`
|
|
78
|
+
- `PYTHON_BIN=$(command -v python || command -v python3 || true); test -n "$PYTHON_BIN" && "$PYTHON_BIN" "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --max-lines 200 --context 40`
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grill-me
|
|
3
|
+
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Grill me
|
|
7
|
+
|
|
8
|
+
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
|
9
|
+
If a question can be answered by exploring the codebase, explore the codebase instead.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: prd-to-tasks
|
|
3
|
+
description: >
|
|
4
|
+
Break a local PRD into independently grabbable implementation tasks using vertical slices, then save them under
|
|
5
|
+
.prd/{prd-name}/tasks/ in the current workspace. Use when the user wants to convert a PRD to tasks, break a spec
|
|
6
|
+
into actionable work, slice a PRD, or create implementation tasks from a local planning document. Do NOT trigger
|
|
7
|
+
for: writing PRDs, reviewing PRDs without task creation, or remote work item management systems.
|
|
8
|
+
argument-hint: "[prd-name]"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# PRD to Tasks
|
|
12
|
+
|
|
13
|
+
Break a local PRD into independently grabbable implementation tasks using vertical slices.
|
|
14
|
+
|
|
15
|
+
Input location:
|
|
16
|
+
|
|
17
|
+
- PRD directory: `.prd/{prd-name}/`
|
|
18
|
+
- Source PRD: `.prd/{prd-name}/prd.md`
|
|
19
|
+
|
|
20
|
+
Output location:
|
|
21
|
+
|
|
22
|
+
- Tasks directory: `.prd/{prd-name}/tasks/`
|
|
23
|
+
- Optional index: `.prd/{prd-name}/tasks/README.md`
|
|
24
|
+
|
|
25
|
+
`{prd-name}` should be a filesystem-safe kebab-case slug. If the user provides a title instead, normalize it to a slug
|
|
26
|
+
and state the slug you used.
|
|
27
|
+
|
|
28
|
+
## Step 1: Locate the PRD
|
|
29
|
+
|
|
30
|
+
If the user provided `$ARGUMENTS`, treat that as the PRD name and normalize it.
|
|
31
|
+
|
|
32
|
+
If not, ask which PRD directory to use.
|
|
33
|
+
|
|
34
|
+
Then:
|
|
35
|
+
|
|
36
|
+
1. Confirm `.prd/{prd-name}/prd.md` exists
|
|
37
|
+
2. Read it fully
|
|
38
|
+
3. Check whether `.prd/{prd-name}/tasks/` already exists
|
|
39
|
+
4. If task files already exist, summarize them and ask whether to overwrite, append, or regenerate selectively
|
|
40
|
+
|
|
41
|
+
If the PRD does not exist, stop and tell the user exactly which path is missing.
|
|
42
|
+
|
|
43
|
+
## Step 2: Explore the codebase
|
|
44
|
+
|
|
45
|
+
If you have not already explored the relevant code in this conversation, do so now:
|
|
46
|
+
|
|
47
|
+
1. Read top-level architecture guidance if present
|
|
48
|
+
2. Search for code related to the PRD concepts
|
|
49
|
+
3. Read nearby guidance files for affected subsystems
|
|
50
|
+
4. Look for similar features and test patterns
|
|
51
|
+
|
|
52
|
+
The PRD tells you *what* to build. The codebase tells you enough about *where* and *how* to create useful tasks.
|
|
53
|
+
|
|
54
|
+
## Step 3: Draft vertical slices
|
|
55
|
+
|
|
56
|
+
Break the PRD into **tracer bullet** tasks.
|
|
57
|
+
|
|
58
|
+
### What is a tracer bullet?
|
|
59
|
+
|
|
60
|
+
A tracer bullet is a thin vertical slice through the whole feature path. It proves that the relevant layers connect
|
|
61
|
+
correctly end to end. It is not a horizontal "build all backend first, then all UI" slice.
|
|
62
|
+
|
|
63
|
+
Prefer slices that are:
|
|
64
|
+
|
|
65
|
+
- Independently verifiable
|
|
66
|
+
- Demoable on their own
|
|
67
|
+
- Thin and end to end
|
|
68
|
+
- Ordered by dependency
|
|
69
|
+
|
|
70
|
+
### HITL vs AFK
|
|
71
|
+
|
|
72
|
+
Each slice should be marked:
|
|
73
|
+
|
|
74
|
+
- `HITL`: needs human input, review, testing on real devices, design signoff, or external coordination
|
|
75
|
+
- `AFK`: can be implemented and validated without waiting on a person
|
|
76
|
+
|
|
77
|
+
### Vertical slice rules
|
|
78
|
+
|
|
79
|
+
- Each slice should deliver a complete behavior, not one technical layer
|
|
80
|
+
- Prefer many thin slices over a few broad ones
|
|
81
|
+
- Use the PRD's module design to inform slicing, but do not create one task per module
|
|
82
|
+
- Order slices so blockers come first
|
|
83
|
+
|
|
84
|
+
## Step 4: Quiz the user
|
|
85
|
+
|
|
86
|
+
Present the proposed slices as a numbered list. For each slice, include:
|
|
87
|
+
|
|
88
|
+
- Title
|
|
89
|
+
- Type: `HITL` or `AFK`
|
|
90
|
+
- Blocked by
|
|
91
|
+
- User stories covered
|
|
92
|
+
|
|
93
|
+
Ask the user whether:
|
|
94
|
+
|
|
95
|
+
- The granularity is right
|
|
96
|
+
- The dependencies are correct
|
|
97
|
+
- Any slices should be merged or split
|
|
98
|
+
- The `HITL` versus `AFK` labels are accurate
|
|
99
|
+
|
|
100
|
+
Iterate until approved.
|
|
101
|
+
|
|
102
|
+
## Step 5: Write task files
|
|
103
|
+
|
|
104
|
+
Only write files after the user approves the breakdown.
|
|
105
|
+
|
|
106
|
+
Create `.prd/{prd-name}/tasks/` if needed.
|
|
107
|
+
|
|
108
|
+
Create one Markdown file per task using zero-padded numeric prefixes:
|
|
109
|
+
|
|
110
|
+
- `.prd/{prd-name}/tasks/01-{task-slug}.md`
|
|
111
|
+
- `.prd/{prd-name}/tasks/02-{task-slug}.md`
|
|
112
|
+
|
|
113
|
+
`{task-slug}` should be kebab-case and derived from the task title.
|
|
114
|
+
|
|
115
|
+
Each task file should use this structure:
|
|
116
|
+
|
|
117
|
+
```md
|
|
118
|
+
# {Task title}
|
|
119
|
+
|
|
120
|
+
- Type: AFK | HITL
|
|
121
|
+
- Status: Proposed
|
|
122
|
+
- Blocked by: None | 01-other-task
|
|
123
|
+
- Source PRD: ../prd.md
|
|
124
|
+
|
|
125
|
+
## What to Build
|
|
126
|
+
|
|
127
|
+
Concise description of the vertical slice. Describe end-to-end behavior, not a layer-by-layer checklist.
|
|
128
|
+
|
|
129
|
+
## Acceptance Criteria
|
|
130
|
+
|
|
131
|
+
- [ ] Criterion 1
|
|
132
|
+
- [ ] Criterion 2
|
|
133
|
+
- [ ] Criterion 3
|
|
134
|
+
|
|
135
|
+
## User Stories Addressed
|
|
136
|
+
|
|
137
|
+
- User story 3
|
|
138
|
+
- User story 7
|
|
139
|
+
|
|
140
|
+
## Notes
|
|
141
|
+
|
|
142
|
+
- Optional implementation hints grounded in the current codebase
|
|
143
|
+
- Reference relevant modules or concepts, but avoid binding the task to fragile file-level details unless necessary
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Also create or update `.prd/{prd-name}/tasks/README.md` with:
|
|
147
|
+
|
|
148
|
+
- The PRD title
|
|
149
|
+
- The approved slice list in dependency order
|
|
150
|
+
- A short summary of each task
|
|
151
|
+
|
|
152
|
+
## Important constraints
|
|
153
|
+
|
|
154
|
+
- Do not modify `prd.md` unless the user asks
|
|
155
|
+
- Do not delete existing task files unless the user explicitly approves replacement
|
|
156
|
+
- If existing tasks overlap the proposed slices, call that out before writing duplicates
|
|
157
|
+
|
|
158
|
+
## After writing
|
|
159
|
+
|
|
160
|
+
Summarize:
|
|
161
|
+
|
|
162
|
+
- The PRD slug used
|
|
163
|
+
- The task files created or updated
|
|
164
|
+
- Any existing task files that were preserved
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: statusline-setup
|
|
3
|
+
description: 'Set up the Claude Code statusline with cwd, git branch, model, context %, session token totals, and 5h/7d quota usage with ETA. Use when asked to "set up my statusline", "install statusline", "configure Claude Code statusline", or to add a colored status line showing rate limits and token usage. Detects OS and installs the appropriate variant (bash + jq on Linux/macOS, PowerShell on Windows).'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Statusline Setup
|
|
7
|
+
|
|
8
|
+
Install a Claude Code statusline that renders, left to right, pipe-separated:
|
|
9
|
+
|
|
10
|
+
1. **cwd** — 24-bit ANSI `#5EFFFF` (`38;2;94;255;255`), with `$HOME` / `%USERPROFILE%` shortened to `~`.
|
|
11
|
+
2. **git branch** in parentheses — 24-bit ANSI `#C24870` (`38;2;194;72;112`), only when inside a repo.
|
|
12
|
+
3. **model display name + effort** in parens — 24-bit ANSI `#E89440` (`38;2;232;148;64`), effort only when present.
|
|
13
|
+
4. **`ctx N%`** — 24-bit ANSI `#009AFB` (`38;2;0;154;251`), the `context_window.used_percentage`.
|
|
14
|
+
5. **tokens** as `↑<in> ↓<out>` — 24-bit ANSI `#937bda` (`38;2;147;123;218`), its own pipe-separated segment immediately after `ctx`.
|
|
15
|
+
6. **`5h:N%(eta)`** and **`7d:N%(eta)`** — colored independently by remaining quota (see below).
|
|
16
|
+
|
|
17
|
+
Segments are joined with ANSI `0;37` pipes (` | `).
|
|
18
|
+
|
|
19
|
+
## When to Use This Skill
|
|
20
|
+
|
|
21
|
+
- "Set up my Claude Code statusline"
|
|
22
|
+
- "Install the statusline"
|
|
23
|
+
- "Configure my statusline with token counts and quota"
|
|
24
|
+
- Any request to add a colored status line showing context %, rate limits, or session tokens.
|
|
25
|
+
|
|
26
|
+
## OS Detection
|
|
27
|
+
|
|
28
|
+
Detect the platform first and install only the matching variant:
|
|
29
|
+
|
|
30
|
+
- **Linux / macOS** → write `~/.claude/statusline-command.sh` (pure bash + `jq`), wire into `~/.claude/settings.json`.
|
|
31
|
+
- **Windows** → write `%USERPROFILE%\.claude\statusline-command.ps1`, wire into `%USERPROFILE%\.claude\settings.json`.
|
|
32
|
+
|
|
33
|
+
## Token Formatting
|
|
34
|
+
|
|
35
|
+
- `< 1000` → raw integer (e.g. `↑850 ↓120`)
|
|
36
|
+
- `>= 1_000` → one decimal + `k` (e.g. `↑1.2k ↓34.5k`)
|
|
37
|
+
- `>= 1_000_000` → one decimal + `M` (e.g. `↑1.2M ↓3.4M`)
|
|
38
|
+
- Missing/unparseable transcript → omit the segment entirely (no stray separator, never render `↑0 ↓0`).
|
|
39
|
+
|
|
40
|
+
## Token Source
|
|
41
|
+
|
|
42
|
+
Claude Code does **not** include token totals in the statusline JSON. Read `transcript_path` (JSONL, one message per line) and sum across **assistant** messages:
|
|
43
|
+
|
|
44
|
+
- `input` = `message.usage.input_tokens` + `message.usage.cache_creation_input_tokens` + `message.usage.cache_read_input_tokens`
|
|
45
|
+
- `output` = `message.usage.output_tokens`
|
|
46
|
+
|
|
47
|
+
Skip lines that fail JSON parsing or lack `message.usage`. If `transcript_path` is missing or the file does not exist, omit the tokens segment.
|
|
48
|
+
|
|
49
|
+
## Quota Colors (by REMAINING quota = 100 - used%)
|
|
50
|
+
|
|
51
|
+
| Remaining | Color | 24-bit ANSI |
|
|
52
|
+
|-----------|-------|-------------|
|
|
53
|
+
| `> 60%` | bright green `#4eba65` | `38;2;78;186;101` |
|
|
54
|
+
| `31–60%` | amber `#de8e3e` | `38;2;222;142;62` |
|
|
55
|
+
| `11–30%` | orange `#ea580c` | `38;2;234;88;12` |
|
|
56
|
+
| `<= 10%` | deep red-orange `#b74426` | `38;2;183;68;38` |
|
|
57
|
+
|
|
58
|
+
The 5h and 7d windows must be colored **independently**.
|
|
59
|
+
|
|
60
|
+
## ETA Format (from epoch seconds in `resets_at`)
|
|
61
|
+
|
|
62
|
+
- `> 1 day` → `Xd Yh`
|
|
63
|
+
- `> 1 hour` → `Xh Ym`
|
|
64
|
+
- else → `Xm`
|
|
65
|
+
- past → `now`
|
|
66
|
+
|
|
67
|
+
## Input Schema (stdin JSON)
|
|
68
|
+
|
|
69
|
+
Any field may be missing:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
cwd
|
|
73
|
+
transcript_path
|
|
74
|
+
model.display_name
|
|
75
|
+
effort.level
|
|
76
|
+
context_window.used_percentage
|
|
77
|
+
rate_limits.five_hour.used_percentage
|
|
78
|
+
rate_limits.five_hour.resets_at
|
|
79
|
+
rate_limits.seven_day.used_percentage
|
|
80
|
+
rate_limits.seven_day.resets_at
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Linux / macOS Implementation
|
|
84
|
+
|
|
85
|
+
Write `~/.claude/statusline-command.sh` (chmod +x). Pure bash + `jq` — no Python. `jq` must be installed (`apt install jq`, `brew install jq`, etc.); if missing, the script prints a one-line hint and exits 0 so the statusline area stays clean.
|
|
86
|
+
|
|
87
|
+
JSON parsing rules:
|
|
88
|
+
|
|
89
|
+
- Read the full stdin payload **once** with `cat`, parse all fields with a single `jq` call.
|
|
90
|
+
- For the JSONL transcript, invoke `jq` **once over the whole file** (do not call `jq` per line) — use a streaming filter that reduces over `inputs`.
|
|
91
|
+
|
|
92
|
+
`~/.claude/statusline-command.sh`:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
#!/usr/bin/env bash
|
|
96
|
+
# Claude Code statusline — pure bash + jq.
|
|
97
|
+
|
|
98
|
+
set -u
|
|
99
|
+
|
|
100
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
101
|
+
printf '%s' "statusline: jq not installed"
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
C_CWD=$'\033[38;2;94;255;255m'
|
|
106
|
+
C_BRANCH=$'\033[38;2;194;72;112m'
|
|
107
|
+
C_PIPE=$'\033[0;37m'
|
|
108
|
+
C_MODEL=$'\033[38;2;232;148;64m'
|
|
109
|
+
C_CTX=$'\033[38;2;0;154;251m'
|
|
110
|
+
C_TOK=$'\033[38;2;147;123;218m'
|
|
111
|
+
RESET=$'\033[0m'
|
|
112
|
+
|
|
113
|
+
GREEN=$'\033[38;2;78;186;101m'
|
|
114
|
+
AMBER=$'\033[38;2;222;142;62m'
|
|
115
|
+
ORANGE=$'\033[38;2;234;88;12m'
|
|
116
|
+
REDORG=$'\033[38;2;183;68;38m'
|
|
117
|
+
|
|
118
|
+
quota_color() {
|
|
119
|
+
local used="$1"
|
|
120
|
+
local rem=$(( 100 - used ))
|
|
121
|
+
if (( rem > 60 )); then printf '%s' "$GREEN"
|
|
122
|
+
elif (( rem > 30 )); then printf '%s' "$AMBER"
|
|
123
|
+
elif (( rem > 10 )); then printf '%s' "$ORANGE"
|
|
124
|
+
else printf '%s' "$REDORG"
|
|
125
|
+
fi
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fmt_eta() {
|
|
129
|
+
local resets="$1"
|
|
130
|
+
[[ -z "$resets" || "$resets" == "null" ]] && { printf ''; return; }
|
|
131
|
+
local now delta days hours mins
|
|
132
|
+
now=$(date +%s)
|
|
133
|
+
delta=$(( resets - now ))
|
|
134
|
+
if (( delta <= 0 )); then printf 'now'
|
|
135
|
+
elif (( delta >= 86400 )); then
|
|
136
|
+
days=$(( delta / 86400 )); hours=$(( (delta % 86400) / 3600 ))
|
|
137
|
+
printf '%dd %dh' "$days" "$hours"
|
|
138
|
+
elif (( delta >= 3600 )); then
|
|
139
|
+
hours=$(( delta / 3600 )); mins=$(( (delta % 3600) / 60 ))
|
|
140
|
+
printf '%dh %dm' "$hours" "$mins"
|
|
141
|
+
else
|
|
142
|
+
mins=$(( delta / 60 ))
|
|
143
|
+
printf '%dm' "$mins"
|
|
144
|
+
fi
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fmt_tokens() {
|
|
148
|
+
local n="$1"
|
|
149
|
+
if (( n < 1000 )); then printf '%d' "$n"
|
|
150
|
+
elif (( n < 1000000 )); then awk -v n="$n" 'BEGIN{printf "%.1fk", n/1000}'
|
|
151
|
+
else awk -v n="$n" 'BEGIN{printf "%.1fM", n/1000000}'
|
|
152
|
+
fi
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
shorten_home() {
|
|
156
|
+
local p="$1"
|
|
157
|
+
[[ -z "$p" ]] && return
|
|
158
|
+
if [[ "$p" == "$HOME" ]]; then printf '~'
|
|
159
|
+
elif [[ "$p" == "$HOME"/* ]]; then printf '~%s' "${p#$HOME}"
|
|
160
|
+
else printf '%s' "$p"
|
|
161
|
+
fi
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# --- read & parse stdin payload in a single jq call ---
|
|
165
|
+
payload=$(cat)
|
|
166
|
+
if [[ -z "${payload// }" ]]; then payload='{}'; fi
|
|
167
|
+
|
|
168
|
+
read_fields() {
|
|
169
|
+
jq -r '
|
|
170
|
+
[
|
|
171
|
+
.cwd // "",
|
|
172
|
+
.transcript_path // "",
|
|
173
|
+
.model.display_name // "",
|
|
174
|
+
.effort.level // "",
|
|
175
|
+
(.context_window.used_percentage // "" | tostring),
|
|
176
|
+
(.rate_limits.five_hour.used_percentage // "" | tostring),
|
|
177
|
+
(.rate_limits.five_hour.resets_at // "" | tostring),
|
|
178
|
+
(.rate_limits.seven_day.used_percentage // "" | tostring),
|
|
179
|
+
(.rate_limits.seven_day.resets_at // "" | tostring)
|
|
180
|
+
] | join("\u001f")
|
|
181
|
+
' 2>/dev/null <<<"$payload"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
IFS=$'\x1f' read -r cwd transcript model effort ctx five_used five_reset seven_used seven_reset < <(read_fields)
|
|
185
|
+
|
|
186
|
+
segments=()
|
|
187
|
+
|
|
188
|
+
if [[ -n "$cwd" ]]; then
|
|
189
|
+
segments+=("${C_CWD}$(shorten_home "$cwd")${RESET}")
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# Branch: must use git -C "$cwd", never PWD.
|
|
193
|
+
branch=""
|
|
194
|
+
if [[ -n "$cwd" ]]; then
|
|
195
|
+
branch=$(git -C "$cwd" rev-parse --abbrev-ref HEAD 2>/dev/null) || branch=""
|
|
196
|
+
fi
|
|
197
|
+
if [[ -n "$branch" ]]; then
|
|
198
|
+
segments+=("${C_BRANCH}(${branch})${RESET}")
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
if [[ -n "$model" ]]; then
|
|
202
|
+
if [[ -n "$effort" ]]; then
|
|
203
|
+
segments+=("${C_MODEL}${model} (${effort})${RESET}")
|
|
204
|
+
else
|
|
205
|
+
segments+=("${C_MODEL}${model}${RESET}")
|
|
206
|
+
fi
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
if [[ -n "$ctx" ]]; then
|
|
210
|
+
ctx_int=$(awk -v n="$ctx" 'BEGIN{printf "%d", (n+0.5)}')
|
|
211
|
+
segments+=("${C_CTX}ctx ${ctx_int}%${RESET}")
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# --- tokens: one jq pass over the whole JSONL transcript ---
|
|
215
|
+
if [[ -n "$transcript" && -f "$transcript" ]]; then
|
|
216
|
+
tok=$(jq -rs '
|
|
217
|
+
reduce .[] as $m ({i:0,o:0};
|
|
218
|
+
($m.message.usage // null) as $u
|
|
219
|
+
| if $u == null then .
|
|
220
|
+
else
|
|
221
|
+
.i += (($u.input_tokens // 0) + ($u.cache_creation_input_tokens // 0) + ($u.cache_read_input_tokens // 0))
|
|
222
|
+
| .o += ($u.output_tokens // 0)
|
|
223
|
+
end
|
|
224
|
+
)
|
|
225
|
+
| "\(.i)\t\(.o)"
|
|
226
|
+
' "$transcript" 2>/dev/null) || tok=""
|
|
227
|
+
if [[ -n "$tok" ]]; then
|
|
228
|
+
IFS=$'\t' read -r ti to <<<"$tok"
|
|
229
|
+
if (( ti > 0 || to > 0 )); then
|
|
230
|
+
segments+=("${C_TOK}↑$(fmt_tokens "$ti") ↓$(fmt_tokens "$to")${RESET}")
|
|
231
|
+
fi
|
|
232
|
+
fi
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
if [[ -n "$five_used" ]]; then
|
|
236
|
+
fu=$(awk -v n="$five_used" 'BEGIN{printf "%d", (n+0.5)}')
|
|
237
|
+
eta=$(fmt_eta "$five_reset")
|
|
238
|
+
segments+=("$(quota_color "$fu")5h:${fu}%(${eta})${RESET}")
|
|
239
|
+
fi
|
|
240
|
+
if [[ -n "$seven_used" ]]; then
|
|
241
|
+
su=$(awk -v n="$seven_used" 'BEGIN{printf "%d", (n+0.5)}')
|
|
242
|
+
eta=$(fmt_eta "$seven_reset")
|
|
243
|
+
segments+=("$(quota_color "$su")7d:${su}%(${eta})${RESET}")
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
sep=" ${C_PIPE}|${RESET} "
|
|
247
|
+
out=""
|
|
248
|
+
for i in "${!segments[@]}"; do
|
|
249
|
+
if (( i == 0 )); then out="${segments[$i]}"
|
|
250
|
+
else out="${out}${sep}${segments[$i]}"
|
|
251
|
+
fi
|
|
252
|
+
done
|
|
253
|
+
printf '%s' "$out"
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Wire into `~/.claude/settings.json` (merge into existing JSON, don't clobber other keys):
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"statusLine": {
|
|
261
|
+
"type": "command",
|
|
262
|
+
"command": "bash ~/.claude/statusline-command.sh"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Windows Implementation
|
|
268
|
+
|
|
269
|
+
Write `%USERPROFILE%\.claude\statusline-command.ps1`. Use `ConvertFrom-Json` (no Python dep). Read the JSONL transcript with `Get-Content -ReadCount 0` and `ConvertFrom-Json` per line inside try/catch. Use `[char]27` for ANSI escapes (works on PS 5.1 and 7+). Read stdin with `[Console]::In.ReadToEnd()`.
|
|
270
|
+
|
|
271
|
+
`%USERPROFILE%\.claude\statusline-command.ps1`:
|
|
272
|
+
|
|
273
|
+
```powershell
|
|
274
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
275
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
276
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
277
|
+
$ESC = [char]27
|
|
278
|
+
|
|
279
|
+
$C_CWD = "$ESC[38;2;94;255;255m"
|
|
280
|
+
$C_BRANCH = "$ESC[38;2;194;72;112m"
|
|
281
|
+
$C_PIPE = "$ESC[0;37m"
|
|
282
|
+
$C_MODEL = "$ESC[38;2;232;148;64m"
|
|
283
|
+
$C_CTX = "$ESC[38;2;0;154;251m"
|
|
284
|
+
$C_TOK = "$ESC[38;2;147;123;218m"
|
|
285
|
+
$RESET = "$ESC[0m"
|
|
286
|
+
|
|
287
|
+
$GREEN = "$ESC[38;2;78;186;101m"
|
|
288
|
+
$AMBER = "$ESC[38;2;222;142;62m"
|
|
289
|
+
$ORANGE = "$ESC[38;2;234;88;12m"
|
|
290
|
+
$REDORG = "$ESC[38;2;183;68;38m"
|
|
291
|
+
|
|
292
|
+
function QuotaColor([double]$used) {
|
|
293
|
+
$remaining = 100 - $used
|
|
294
|
+
if ($remaining -gt 60) { return $GREEN }
|
|
295
|
+
if ($remaining -gt 30) { return $AMBER }
|
|
296
|
+
if ($remaining -gt 10) { return $ORANGE }
|
|
297
|
+
return $REDORG
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function FmtEta($resetsAt) {
|
|
301
|
+
if ($null -eq $resetsAt) { return "" }
|
|
302
|
+
$delta = [int64]$resetsAt - [int64](Get-Date -UFormat %s)
|
|
303
|
+
if ($delta -le 0) { return "now" }
|
|
304
|
+
$days = [math]::Floor($delta / 86400); $delta = $delta % 86400
|
|
305
|
+
$hours = [math]::Floor($delta / 3600); $delta = $delta % 3600
|
|
306
|
+
$mins = [math]::Floor($delta / 60)
|
|
307
|
+
if ($days -gt 0) { return "${days}d ${hours}h" }
|
|
308
|
+
if ($hours -gt 0) { return "${hours}h ${mins}m" }
|
|
309
|
+
return "${mins}m"
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function FmtTokens([int64]$n) {
|
|
313
|
+
if ($n -lt 1000) { return "$n" }
|
|
314
|
+
if ($n -lt 1000000) { return ("{0:N1}k" -f ($n / 1000.0)) }
|
|
315
|
+
return ("{0:N1}M" -f ($n / 1000000.0))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function ShortenHome($p) {
|
|
319
|
+
if (-not $p) { return "" }
|
|
320
|
+
$home = $env:USERPROFILE
|
|
321
|
+
if ($p -eq $home) { return "~" }
|
|
322
|
+
if ($p.StartsWith("$home\")) { return "~" + $p.Substring($home.Length) }
|
|
323
|
+
return $p
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function GetBranch($cwd) {
|
|
327
|
+
if (-not $cwd) { return $null }
|
|
328
|
+
try {
|
|
329
|
+
$b = & git -C "$cwd" rev-parse --abbrev-ref HEAD 2>$null
|
|
330
|
+
if ($LASTEXITCODE -eq 0 -and $b) { return $b.Trim() }
|
|
331
|
+
} catch {}
|
|
332
|
+
return $null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function SumTokens($path) {
|
|
336
|
+
if (-not $path) { return $null }
|
|
337
|
+
if (-not (Test-Path -LiteralPath $path)) { return $null }
|
|
338
|
+
$sumIn = 0; $sumOut = 0; $found = $false
|
|
339
|
+
$lines = Get-Content -LiteralPath $path -ReadCount 0 -ErrorAction SilentlyContinue
|
|
340
|
+
if ($null -eq $lines) { return $null }
|
|
341
|
+
foreach ($line in $lines) {
|
|
342
|
+
if (-not $line) { continue }
|
|
343
|
+
try { $obj = $line | ConvertFrom-Json -ErrorAction Stop } catch { continue }
|
|
344
|
+
$msg = $obj.message
|
|
345
|
+
if (-not $msg) { continue }
|
|
346
|
+
$usage = $msg.usage
|
|
347
|
+
if (-not $usage) { continue }
|
|
348
|
+
$found = $true
|
|
349
|
+
if ($usage.input_tokens) { $sumIn += [int64]$usage.input_tokens }
|
|
350
|
+
if ($usage.cache_creation_input_tokens) { $sumIn += [int64]$usage.cache_creation_input_tokens }
|
|
351
|
+
if ($usage.cache_read_input_tokens) { $sumIn += [int64]$usage.cache_read_input_tokens }
|
|
352
|
+
if ($usage.output_tokens) { $sumOut += [int64]$usage.output_tokens }
|
|
353
|
+
}
|
|
354
|
+
if (-not $found) { return $null }
|
|
355
|
+
return ,@($sumIn, $sumOut)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
$raw = [Console]::In.ReadToEnd()
|
|
359
|
+
try { $data = if ($raw.Trim()) { $raw | ConvertFrom-Json } else { @{} } } catch { $data = @{} }
|
|
360
|
+
|
|
361
|
+
$segments = @()
|
|
362
|
+
|
|
363
|
+
$cwd = $data.cwd
|
|
364
|
+
if ($cwd) { $segments += "$C_CWD$(ShortenHome $cwd)$RESET" }
|
|
365
|
+
|
|
366
|
+
$branch = GetBranch $cwd
|
|
367
|
+
if ($branch) { $segments += "$C_BRANCH($branch)$RESET" }
|
|
368
|
+
|
|
369
|
+
$model = $data.model.display_name
|
|
370
|
+
$effort = $data.effort.level
|
|
371
|
+
if ($model) {
|
|
372
|
+
if ($effort) { $segments += "$C_MODEL$model ($effort)$RESET" }
|
|
373
|
+
else { $segments += "$C_MODEL$model$RESET" }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
$ctx = $data.context_window.used_percentage
|
|
377
|
+
if ($null -ne $ctx) { $segments += "$C_CTX" + "ctx " + [int][math]::Round([double]$ctx) + "%$RESET" }
|
|
378
|
+
|
|
379
|
+
$tokens = SumTokens $data.transcript_path
|
|
380
|
+
if ($null -ne $tokens) {
|
|
381
|
+
$segments += "$C_TOK↑$(FmtTokens $tokens[0]) ↓$(FmtTokens $tokens[1])$RESET"
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
$five = $data.rate_limits.five_hour
|
|
385
|
+
$seven = $data.rate_limits.seven_day
|
|
386
|
+
if ($null -ne $five.used_percentage) {
|
|
387
|
+
$u = [double]$five.used_percentage
|
|
388
|
+
$segments += "$(QuotaColor $u)5h:$([int][math]::Round($u))%($(FmtEta $five.resets_at))$RESET"
|
|
389
|
+
}
|
|
390
|
+
if ($null -ne $seven.used_percentage) {
|
|
391
|
+
$u = [double]$seven.used_percentage
|
|
392
|
+
$segments += "$(QuotaColor $u)7d:$([int][math]::Round($u))%($(FmtEta $seven.resets_at))$RESET"
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
$sep = " $C_PIPE|$RESET "
|
|
396
|
+
[Console]::Out.Write([string]::Join($sep, $segments))
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Wire into `%USERPROFILE%\.claude\settings.json`:
|
|
400
|
+
|
|
401
|
+
```json
|
|
402
|
+
{
|
|
403
|
+
"statusLine": {
|
|
404
|
+
"type": "command",
|
|
405
|
+
"command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"%USERPROFILE%\\.claude\\statusline-command.ps1\""
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Smoke Test
|
|
411
|
+
|
|
412
|
+
After installing, generate a fake JSON payload with 5h at 80% used, 7d at 50% used, both with future `resets_at`, plus a `transcript_path` pointing at a small fixture JSONL containing two assistant messages with `usage` blocks. Pipe it into the script and confirm:
|
|
413
|
+
|
|
414
|
+
- 5h renders **orange**, 7d renders **amber**.
|
|
415
|
+
- cwd, model, and context % are present.
|
|
416
|
+
- Tokens segment renders `↑<sum> ↓<sum>` matching the fixture, in `#937bda`, as its own pipe-separated segment immediately after `ctx`.
|
|
417
|
+
- Omitting `transcript_path` hides the tokens segment cleanly (no stray separator).
|
|
418
|
+
|
|
419
|
+
### Linux/macOS smoke test
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
mkdir -p /tmp/statusline-smoke
|
|
423
|
+
cat >/tmp/statusline-smoke/transcript.jsonl <<'EOF'
|
|
424
|
+
{"type":"assistant","message":{"role":"assistant","usage":{"input_tokens":100,"cache_creation_input_tokens":200,"cache_read_input_tokens":300,"output_tokens":50}}}
|
|
425
|
+
{"type":"assistant","message":{"role":"assistant","usage":{"input_tokens":10,"cache_creation_input_tokens":20,"cache_read_input_tokens":30,"output_tokens":5}}}
|
|
426
|
+
EOF
|
|
427
|
+
FUTURE=$(($(date +%s) + 7200))
|
|
428
|
+
FUTURE7=$(($(date +%s) + 3*86400))
|
|
429
|
+
printf '{"cwd":"%s","transcript_path":"/tmp/statusline-smoke/transcript.jsonl","model":{"display_name":"Opus 4.7"},"effort":{"level":"medium"},"context_window":{"used_percentage":42},"rate_limits":{"five_hour":{"used_percentage":80,"resets_at":%d},"seven_day":{"used_percentage":50,"resets_at":%d}}}' "$HOME" "$FUTURE" "$FUTURE7" | bash ~/.claude/statusline-command.sh; echo
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Windows smoke test (PowerShell)
|
|
433
|
+
|
|
434
|
+
```powershell
|
|
435
|
+
$tmp = "$env:TEMP\statusline-smoke"
|
|
436
|
+
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
|
437
|
+
@'
|
|
438
|
+
{"type":"assistant","message":{"role":"assistant","usage":{"input_tokens":100,"cache_creation_input_tokens":200,"cache_read_input_tokens":300,"output_tokens":50}}}
|
|
439
|
+
{"type":"assistant","message":{"role":"assistant","usage":{"input_tokens":10,"cache_creation_input_tokens":20,"cache_read_input_tokens":30,"output_tokens":5}}}
|
|
440
|
+
'@ | Set-Content -LiteralPath "$tmp\transcript.jsonl" -Encoding UTF8
|
|
441
|
+
$future = [int64](Get-Date -UFormat %s) + 7200
|
|
442
|
+
$future7 = [int64](Get-Date -UFormat %s) + 3*86400
|
|
443
|
+
$payload = @{
|
|
444
|
+
cwd = $env:USERPROFILE
|
|
445
|
+
transcript_path = "$tmp\transcript.jsonl"
|
|
446
|
+
model = @{ display_name = "Opus 4.7" }
|
|
447
|
+
effort = @{ level = "medium" }
|
|
448
|
+
context_window = @{ used_percentage = 42 }
|
|
449
|
+
rate_limits = @{
|
|
450
|
+
five_hour = @{ used_percentage = 80; resets_at = $future }
|
|
451
|
+
seven_day = @{ used_percentage = 50; resets_at = $future7 }
|
|
452
|
+
}
|
|
453
|
+
} | ConvertTo-Json -Depth 5 -Compress
|
|
454
|
+
$payload | powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$env:USERPROFILE\.claude\statusline-command.ps1"
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Color Preview One-Liner
|
|
458
|
+
|
|
459
|
+
After install, output a one-liner the user can paste to preview all four quota tiers, with both 5h and 7d on each line, separated by ` | `, one tier per line.
|
|
460
|
+
|
|
461
|
+
### Bash form
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
printf '\033[38;2;78;186;101m5h:20%%(2h30m)\033[0m \033[0;37m|\033[0m \033[38;2;78;186;101m7d:25%%(3d12h)\033[0m\n\033[38;2;222;142;62m5h:55%%(2h30m)\033[0m \033[0;37m|\033[0m \033[38;2;222;142;62m7d:45%%(3d12h)\033[0m\n\033[38;2;234;88;12m5h:80%%(1h15m)\033[0m \033[0;37m|\033[0m \033[38;2;234;88;12m7d:75%%(2d3h)\033[0m\n\033[38;2;183;68;38m5h:95%%(0h30m)\033[0m \033[0;37m|\033[0m \033[38;2;183;68;38m7d:92%%(1d2h)\033[0m\n'
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### PowerShell form
|
|
468
|
+
|
|
469
|
+
```powershell
|
|
470
|
+
$e=[char]27; Write-Host "$e[38;2;78;186;101m5h:20%(2h30m)$e[0m $e[0;37m|$e[0m $e[38;2;78;186;101m7d:25%(3d12h)$e[0m`n$e[38;2;222;142;62m5h:55%(2h30m)$e[0m $e[0;37m|$e[0m $e[38;2;222;142;62m7d:45%(3d12h)$e[0m`n$e[38;2;234;88;12m5h:80%(1h15m)$e[0m $e[0;37m|$e[0m $e[38;2;234;88;12m7d:75%(2d3h)$e[0m`n$e[38;2;183;68;38m5h:95%(0h30m)$e[0m $e[0;37m|$e[0m $e[38;2;183;68;38m7d:92%(1d2h)$e[0m"
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Other Rules
|
|
474
|
+
|
|
475
|
+
- Do not add features beyond this spec.
|
|
476
|
+
- Do not auto-commit any changes.
|
|
477
|
+
- Do not add Co-Authored-By trailers.
|
|
478
|
+
- Read git branch with `git -C "$cwd" rev-parse --abbrev-ref HEAD`, never from `PWD`.
|
|
479
|
+
- When merging into existing `settings.json`, preserve all other keys; only set/replace `statusLine`.
|