cc-peak-hours 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: Bug report
3
+ about: Report something that isn't working
4
+ title: ''
5
+ labels: bug
6
+ assignees: ''
7
+ ---
8
+
9
+ ## Describe the bug
10
+
11
+ <!-- A clear description of what's wrong -->
12
+
13
+ ## Expected behavior
14
+
15
+ <!-- What should happen -->
16
+
17
+ ## Environment
18
+
19
+ - **OS**: <!-- macOS / Linux distro -->
20
+ - **Shell**: <!-- bash version (`bash --version`) -->
21
+ - **Mode**: <!-- minimal / full -->
22
+ - **Flags**: <!-- e.g. --24h --lang fr -->
23
+ - **jq version**: <!-- `jq --version` -->
24
+
25
+ ## Steps to reproduce
26
+
27
+ ```bash
28
+ # Paste the command you ran
29
+ ```
30
+
31
+ ## Output
32
+
33
+ ```
34
+ # Paste the output or screenshot
35
+ ```
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an improvement
4
+ title: ''
5
+ labels: enhancement
6
+ assignees: ''
7
+ ---
8
+
9
+ ## Problem
10
+
11
+ <!-- What problem does this solve? -->
12
+
13
+ ## Proposed solution
14
+
15
+ <!-- How would you like it to work? -->
16
+
17
+ ## Alternatives considered
18
+
19
+ <!-- Any other approaches you thought of? -->
@@ -0,0 +1,19 @@
1
+ ## Summary
2
+
3
+ <!-- What does this PR do? -->
4
+
5
+ ## Type of change
6
+
7
+ - [ ] Bug fix
8
+ - [ ] New feature
9
+ - [ ] Peak hours config update (`peak-hours.json`)
10
+ - [ ] Documentation
11
+ - [ ] Other
12
+
13
+ ## Test plan
14
+
15
+ - [ ] Tested minimal mode
16
+ - [ ] Tested full mode
17
+ - [ ] Tested both languages (en/fr)
18
+ - [ ] Tested both time formats (12h/24h)
19
+ - [ ] Tested with empty stdin (`echo '{}' | bash bin/statusline.sh`)
@@ -0,0 +1,58 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ name: Lint
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: ShellCheck statusline.sh
17
+ uses: ludeeus/action-shellcheck@2.0.0
18
+ with:
19
+ scandir: bin
20
+ severity: warning
21
+
22
+ - name: Validate peak-hours.json
23
+ run: jq empty peak-hours.json
24
+
25
+ - name: Validate package.json
26
+ run: node -e "JSON.parse(require('fs').readFileSync('package.json','utf8'))"
27
+
28
+ - name: Check install.js syntax
29
+ run: node --check bin/install.js
30
+
31
+ test:
32
+ name: Integration test
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+
37
+ - name: Install jq
38
+ run: sudo apt-get install -y jq
39
+
40
+ - name: Test minimal mode (EN, 12h)
41
+ run: |
42
+ output=$(echo '{"model":{"display_name":"Opus 4.6"},"context_window":{"context_window_size":200000,"current_usage":{"input_tokens":78000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}' | bash bin/statusline.sh --lang en --12h)
43
+ [ -n "$output" ] && echo "PASS: minimal EN" || (echo "FAIL: no output" && exit 1)
44
+
45
+ - name: Test minimal mode (FR, 24h)
46
+ run: |
47
+ output=$(echo '{}' | bash bin/statusline.sh --lang fr --24h)
48
+ [ -n "$output" ] && echo "PASS: minimal FR" || (echo "FAIL: no output" && exit 1)
49
+
50
+ - name: Test full mode
51
+ run: |
52
+ output=$(echo '{}' | bash bin/statusline.sh --full --lang en --24h)
53
+ [ -n "$output" ] && echo "PASS: full mode" || (echo "FAIL: no output" && exit 1)
54
+
55
+ - name: Test empty stdin
56
+ run: |
57
+ output=$(echo '{}' | bash bin/statusline.sh)
58
+ [ -n "$output" ] && echo "PASS: empty stdin" || (echo "FAIL: no output" && exit 1)
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-03-31
9
+
10
+ ### Added
11
+
12
+ - Minimal mode: single-line statusline with model, context %, peak status, and countdown
13
+ - Full mode (`--full`): dashboard with visual timeline bar and real-time rate limits via OAuth API
14
+ - Remote config (`peak-hours.json`): peak hours loaded from GitHub, cached 1h, hardcoded fallback
15
+ - Multi-window support: multiple peak windows per day, midnight-crossing windows
16
+ - Localization: English (default) and French (`--lang fr`), auto-detected from locale
17
+ - Time format: 12h/24h (`--12h`/`--24h`), auto-detected from locale
18
+ - Node installer (`npx claude-peak-hours`): backup, install, uninstall, flag forwarding
19
+ - macOS and Linux support (GNU coreutils + BSD date)
@@ -0,0 +1,41 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to a positive environment:
15
+
16
+ - Using welcoming and inclusive language
17
+ - Being respectful of differing viewpoints and experiences
18
+ - Gracefully accepting constructive criticism
19
+ - Focusing on what is best for the community
20
+
21
+ Examples of unacceptable behavior:
22
+
23
+ - The use of sexualized language or imagery and unwelcome sexual attention
24
+ - Trolling, insulting or derogatory comments, and personal or political attacks
25
+ - Public or private harassment
26
+ - Publishing others' private information without explicit permission
27
+ - Other conduct which could reasonably be considered inappropriate
28
+
29
+ ## Enforcement
30
+
31
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
32
+ reported by opening a [private security advisory](https://github.com/nickywan/claude-peak-hours/security/advisories/new)
33
+ or contacting the project maintainer directly.
34
+
35
+ All complaints will be reviewed and investigated promptly and fairly.
36
+
37
+ ## Attribution
38
+
39
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
40
+ version 2.1, available at
41
+ https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
@@ -0,0 +1,9 @@
1
+ # Contributing
2
+
3
+ This project is **not open to external contributions**. Issues and pull requests from outside contributors will be closed.
4
+
5
+ If you've found a bug, you're welcome to [open an issue](https://github.com/nickywan/claude-peak-hours/issues) to report it — but fixes are handled internally.
6
+
7
+ If you've noticed that Anthropic changed their peak hours, please [open an issue](https://github.com/nickywan/claude-peak-hours/issues) with the source and we'll update the config.
8
+
9
+ For security issues, see [SECURITY.md](SECURITY.md).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nickywan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # claude-peak-hours
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
+ [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-lightgrey.svg)]()
5
+ [![CI](https://github.com/nickywan/claude-peak-hours/actions/workflows/ci.yml/badge.svg)](https://github.com/nickywan/claude-peak-hours/actions)
6
+
7
+ A [Claude Code](https://claude.ai/code) statusline plugin that shows whether you're in **peak** or **off-peak** hours, with a countdown to the next transition.
8
+
9
+ During peak hours (12:00-18:00 UTC / 8AM-2PM ET on weekdays), Anthropic applies stricter session limits and tokens are consumed faster. This statusline shows you exactly when you're in peak, when it changes, and helps you plan your usage.
10
+
11
+ Inspired by [isclaude-2x](https://github.com/Adiazgallici/isclaude-2x).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npx cc-peak-hours
17
+ ```
18
+
19
+ Restart Claude Code after installing.
20
+
21
+ ### Options
22
+
23
+ ```bash
24
+ npx cc-peak-hours # minimal (default)
25
+ npx cc-peak-hours --full # dashboard with timeline + rate limits
26
+ npx cc-peak-hours --24h # force 24h time format
27
+ npx cc-peak-hours --12h # force 12h time format
28
+ npx cc-peak-hours --lang fr # French labels
29
+ npx cc-peak-hours --full --24h --lang fr # combined
30
+ npx cc-peak-hours --uninstall # restore previous statusline
31
+ ```
32
+
33
+ ## Modes
34
+
35
+ ### Minimal (default)
36
+
37
+ One line. Everything you need at a glance.
38
+
39
+ **Off-peak (French, 24h):**
40
+
41
+ ![Minimal off-peak FR](assets/minimal-offpeak-fr.svg)
42
+
43
+ **Off-peak (English, 12h):**
44
+
45
+ ![Minimal off-peak EN](assets/minimal-offpeak-en.svg)
46
+
47
+ Shows: model name, current directory, context remaining %, peak status with countdown.
48
+
49
+ ### Full (`--full`)
50
+
51
+ Dashboard with visual timeline and real-time rate limits.
52
+
53
+ **Full mode (French, 24h):**
54
+
55
+ ![Full mode FR](assets/full-fr.svg)
56
+
57
+ **Full mode (English, 12h):**
58
+
59
+ ![Full mode EN](assets/full-en.svg)
60
+
61
+ The timeline bar shows your full day in local time -- green for off-peak hours, yellow for peak hours, and a white dot for where you are now.
62
+
63
+ Rate limits (session 5h + weekly) are read directly from Claude Code -- no extra API calls, zero latency.
64
+
65
+ ## Localization
66
+
67
+ Supports English (default) and French. Auto-detected from your locale, or forced with `--lang`.
68
+
69
+ ## Time format
70
+
71
+ Auto-detected from locale (French -> 24h, English -> 12h), or forced with `--24h` / `--12h`.
72
+
73
+ ## Remote config
74
+
75
+ Peak hours are loaded from a [remote config file](peak-hours.json) on this repo, cached locally for 1 hour. If Anthropic changes peak hours, updating this file updates all users automatically -- no plugin update needed.
76
+
77
+ Falls back to hardcoded defaults (Mon-Fri 12:00-18:00 UTC) if the fetch fails.
78
+
79
+ ### Config format
80
+
81
+ ```json
82
+ {
83
+ "version": 2,
84
+ "peak_windows": [
85
+ { "days": [1, 2, 3, 4, 5], "start_utc": 12, "end_utc": 18 }
86
+ ]
87
+ }
88
+ ```
89
+
90
+ All times are in UTC. Multiple windows supported. `end_utc` < `start_utc` means the window crosses midnight.
91
+
92
+ ## How it works
93
+
94
+ - Loads peak hours config from GitHub (cached 1h, hardcoded fallback)
95
+ - All peak calculations done in **UTC** -- no DST ambiguity
96
+ - Converts to your local timezone for display
97
+ - Rate limits read directly from Claude Code stdin (no network calls)
98
+ - Supports multiple peak windows and midnight-crossing windows
99
+
100
+ ## Requirements
101
+
102
+ - **macOS or Linux**
103
+ - **Node.js** -- for the installer only
104
+ - **jq** -- `brew install jq` or `sudo apt install jq`
105
+ - **curl** -- pre-installed on most systems
106
+
107
+ ## Contributing
108
+
109
+ This project is not open to external contributions. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
110
+
111
+ If you notice Anthropic changed their peak hours, please [open an issue](https://github.com/nickywan/claude-peak-hours/issues) and we'll update the config.
112
+
113
+ ## Security
114
+
115
+ This is a read-only statusline plugin. It does **not**:
116
+ - Send your data anywhere (except fetching the public config from this repo)
117
+ - Store credentials (it reads existing Claude Code data from stdin)
118
+ - Modify your code or files (except `~/.claude/settings.json` and `~/.claude/statusline.sh` during install)
119
+
120
+ If you find a security issue, please open a [private security advisory](https://github.com/nickywan/claude-peak-hours/security/advisories/new) instead of a public issue.
121
+
122
+ ## License
123
+
124
+ [MIT](LICENSE)
package/SECURITY.md ADDED
@@ -0,0 +1,33 @@
1
+ # Security Policy
2
+
3
+ ## Scope
4
+
5
+ This is a read-only Claude Code statusline plugin. It:
6
+
7
+ - Reads Claude Code's stdin JSON (model info, context window)
8
+ - Fetches a public JSON config from this GitHub repo
9
+ - Fetches usage data from Anthropic's official OAuth API using your existing Claude Code tokens
10
+ - Writes cached data to `/tmp/claude/`
11
+
12
+ It does **not** store credentials, transmit your data to third parties, or modify your code.
13
+
14
+ ## Supported Versions
15
+
16
+ | Version | Supported |
17
+ |---------|-----------|
18
+ | 1.x | Yes |
19
+
20
+ ## Reporting a Vulnerability
21
+
22
+ If you discover a security issue, **do not open a public issue**.
23
+
24
+ Instead, please use [GitHub's private security advisory feature](https://github.com/nickywan/claude-peak-hours/security/advisories/new) to report it confidentially.
25
+
26
+ You can expect an initial response within 48 hours.
27
+
28
+ ## What Counts as a Security Issue
29
+
30
+ - The plugin sending data to unexpected endpoints
31
+ - Credential leakage (OAuth tokens exposed in logs, cache files with wrong permissions, etc.)
32
+ - Code injection via the remote `peak-hours.json` config
33
+ - Any behavior that could compromise the user's system or Claude Code session
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="950" height="108">
2
+ <rect width="100%" height="100%" rx="8" fill="#1e1e1e"/>
3
+ <text x="14" y="30" font-family="Menlo,Monaco,Consolas,'Courier New',monospace" font-size="13" fill="#d4d4d4"><tspan fill="rgb(0,153,255)" font-weight="normal" opacity="1">Opus 4.6 (1M context)</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(86,182,194)" font-weight="normal" opacity="1">~/.../claude/plugin/claude-peak-hours</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">87% free</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="bold" opacity="1">⚡ Off-peak</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">~</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(255,85,85)" font-weight="normal" opacity="1">Peak 2:00pm</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">(12h30m)</tspan></text>
4
+ <text x="14" y="50" font-family="Menlo,Monaco,Consolas,'Courier New',monospace" font-size="13" fill="#d4d4d4"></text>
5
+ <text x="14" y="70" font-family="Menlo,Monaco,Consolas,'Courier New',monospace" font-size="13" fill="#d4d4d4"><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">today</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(220,220,220)" font-weight="bold" opacity="1">●</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55"> Off-peak</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55"> Peak 2pm-8pm</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(220,220,220)" font-weight="bold" opacity="1">●</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55"> now</tspan></text>
6
+ <text x="14" y="90" font-family="Menlo,Monaco,Consolas,'Courier New',monospace" font-size="13" fill="#d4d4d4"><tspan fill="rgb(220,220,220)" font-weight="normal" opacity="1">current</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">●●●●</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="0.55">○○○○○○</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1"> 42%</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">⟳</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(220,220,220)" font-weight="normal" opacity="1">10:00am</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(220,220,220)" font-weight="normal" opacity="1">weekly</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">●●●</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="0.55">○○○○○○○</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1"> 31%</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">⟳</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(220,220,220)" font-weight="normal" opacity="1">apr 3, 5:00pm</tspan></text>
7
+ </svg>
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="950" height="108">
2
+ <rect width="100%" height="100%" rx="8" fill="#1e1e1e"/>
3
+ <text x="14" y="30" font-family="Menlo,Monaco,Consolas,'Courier New',monospace" font-size="13" fill="#d4d4d4"><tspan fill="rgb(0,153,255)" font-weight="normal" opacity="1">Opus 4.6 (1M context)</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(86,182,194)" font-weight="normal" opacity="1">~/.../claude/plugin/claude-peak-hours</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">87% free</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="bold" opacity="1">⚡ Hors pointe</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">~</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(255,85,85)" font-weight="normal" opacity="1">Pointe 14:00</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">(12h30m)</tspan></text>
4
+ <text x="14" y="50" font-family="Menlo,Monaco,Consolas,'Courier New',monospace" font-size="13" fill="#d4d4d4"></text>
5
+ <text x="14" y="70" font-family="Menlo,Monaco,Consolas,'Courier New',monospace" font-size="13" fill="#d4d4d4"><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">auj.</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(220,220,220)" font-weight="bold" opacity="1">●</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">━</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55"> Hors pointe</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(230,200,0)" font-weight="normal" opacity="1">━</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55"> Pointe 14:00-20:00</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(220,220,220)" font-weight="bold" opacity="1">●</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55"> maint.</tspan></text>
6
+ <text x="14" y="90" font-family="Menlo,Monaco,Consolas,'Courier New',monospace" font-size="13" fill="#d4d4d4"><tspan fill="rgb(220,220,220)" font-weight="normal" opacity="1">session</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">●●●●</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="0.55">○○○○○○</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1"> 42%</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">⟳</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(220,220,220)" font-weight="normal" opacity="1">10:00</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(220,220,220)" font-weight="normal" opacity="1">hebdo</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">●●●</tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="0.55">○○○○○○○</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1"> 31%</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">⟳</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(220,220,220)" font-weight="normal" opacity="1">avr 3, 17:00</tspan></text>
7
+ </svg>
@@ -0,0 +1,94 @@
1
+ #!/bin/bash
2
+ # Generate SVG screenshots from statusline ANSI output
3
+ # Usage: bash assets/gen-svg.sh
4
+
5
+ cd "$(dirname "$0")/.."
6
+
7
+ MOCK_INPUT='{"model":{"display_name":"Opus 4.6 (1M context)"},"cwd":"/home/nickywan/dev/projects/claude/plugin/claude-peak-hours","context_window":{"remaining_percentage":87},"rate_limits":{"five_hour":{"used_percentage":42,"resets_at":1774944000},"seven_day":{"used_percentage":31,"resets_at":1775228400}}}'
8
+
9
+ ansi_to_svg() {
10
+ local output_file="$1"
11
+ local width="${2:-950}"
12
+ shift 2
13
+ # Remaining args are lines of ANSI text
14
+ local lines=("$@")
15
+
16
+ node -e '
17
+ const lines = JSON.parse(process.argv[1]);
18
+ const width = parseInt(process.argv[2]);
19
+ const outFile = process.argv[3];
20
+
21
+ const lineHeight = 20;
22
+ const padY = 14;
23
+ const padX = 14;
24
+ const totalHeight = padY * 2 + lines.length * lineHeight;
25
+
26
+ function parseAnsi(text) {
27
+ let spans = "";
28
+ let color = "#d4d4d4";
29
+ let isBold = false;
30
+ let isDim = false;
31
+
32
+ const parts = text.split(/(\x1b\[[0-9;]*m)/);
33
+ for (const part of parts) {
34
+ if (part.startsWith("\x1b[")) {
35
+ const code = part.slice(2, -1);
36
+ if (code === "0") { color = "#d4d4d4"; isBold = false; isDim = false; continue; }
37
+ if (code === "1") { isBold = true; continue; }
38
+ if (code === "2") { isDim = true; continue; }
39
+ const m = code.match(/^38;2;(\d+);(\d+);(\d+)$/);
40
+ if (m) { color = `rgb(${m[1]},${m[2]},${m[3]})`; continue; }
41
+ } else if (part.length > 0) {
42
+ const esc = part.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
43
+ const op = isDim ? "0.55" : "1";
44
+ const fw = isBold ? "bold" : "normal";
45
+ spans += `<tspan fill="${color}" font-weight="${fw}" opacity="${op}">${esc}</tspan>`;
46
+ }
47
+ }
48
+ return spans;
49
+ }
50
+
51
+ let textEls = "";
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const y = padY + (i + 1) * lineHeight - 4;
54
+ textEls += `<text x="${padX}" y="${y}" font-family="Menlo,Monaco,Consolas,\x27Courier New\x27,monospace" font-size="13" fill="#d4d4d4">${parseAnsi(lines[i])}</text>\n`;
55
+ }
56
+
57
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${totalHeight}">
58
+ <rect width="100%" height="100%" rx="8" fill="#1e1e1e"/>
59
+ ${textEls}</svg>`;
60
+
61
+ require("fs").writeFileSync(outFile, svg);
62
+ console.log("Generated:", outFile, `(${lines.length} lines, ${width}x${totalHeight})`);
63
+ ' "$(printf '%s\n' "${lines[@]}" | jq -R . | jq -s .)" "$width" "$output_file"
64
+ }
65
+
66
+ echo "=== Generating SVG screenshots ==="
67
+
68
+ # Minimal off-peak FR 24h
69
+ echo "1/4: minimal off-peak (FR, 24h)"
70
+ output=$(echo "$MOCK_INPUT" | bash bin/statusline.sh --24h --lang fr 2>/dev/null)
71
+ ansi_to_svg "assets/minimal-offpeak-fr.svg" 950 "$output"
72
+
73
+ # Minimal off-peak EN 12h
74
+ echo "2/4: minimal off-peak (EN, 12h)"
75
+ output=$(echo "$MOCK_INPUT" | bash bin/statusline.sh --12h --lang en 2>/dev/null)
76
+ ansi_to_svg "assets/minimal-offpeak-en.svg" 950 "$output"
77
+
78
+ # Full mode FR 24h
79
+ echo "3/4: full mode (FR, 24h)"
80
+ full_output=$(echo "$MOCK_INPUT" | bash bin/statusline.sh --full --24h --lang fr 2>/dev/null)
81
+ line1=$(echo "$full_output" | sed -n '1p')
82
+ line2=$(echo "$full_output" | sed -n '3p')
83
+ line3=$(echo "$full_output" | sed -n '4p')
84
+ ansi_to_svg "assets/full-fr.svg" 950 "$line1" "" "$line2" "$line3"
85
+
86
+ # Full mode EN 12h
87
+ echo "4/4: full mode (EN, 12h)"
88
+ full_output=$(echo "$MOCK_INPUT" | bash bin/statusline.sh --full --12h --lang en 2>/dev/null)
89
+ line1=$(echo "$full_output" | sed -n '1p')
90
+ line2=$(echo "$full_output" | sed -n '3p')
91
+ line3=$(echo "$full_output" | sed -n '4p')
92
+ ansi_to_svg "assets/full-en.svg" 950 "$line1" "" "$line2" "$line3"
93
+
94
+ echo "=== Done ==="
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="950" height="48">
2
+ <rect width="100%" height="100%" rx="8" fill="#1e1e1e"/>
3
+ <text x="14" y="30" font-family="Menlo,Monaco,Consolas,'Courier New',monospace" font-size="13" fill="#d4d4d4"><tspan fill="rgb(0,153,255)" font-weight="normal" opacity="1">Opus 4.6 (1M context)</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(86,182,194)" font-weight="normal" opacity="1">~/.../claude/plugin/claude-peak-hours</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">87% free</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="bold" opacity="1">⚡ Off-peak</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">~</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(255,85,85)" font-weight="normal" opacity="1">Peak 2:00pm</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">(12h30m)</tspan></text>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="950" height="48">
2
+ <rect width="100%" height="100%" rx="8" fill="#1e1e1e"/>
3
+ <text x="14" y="30" font-family="Menlo,Monaco,Consolas,'Courier New',monospace" font-size="13" fill="#d4d4d4"><tspan fill="rgb(0,153,255)" font-weight="normal" opacity="1">Opus 4.6 (1M context)</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(86,182,194)" font-weight="normal" opacity="1">~/.../claude/plugin/claude-peak-hours</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="normal" opacity="1">87% free</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">│</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(0,175,80)" font-weight="bold" opacity="1">⚡ Hors pointe</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">~</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="rgb(255,85,85)" font-weight="normal" opacity="1">Pointe 14:00</tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="1"> </tspan><tspan fill="#d4d4d4" font-weight="normal" opacity="0.55">(12h30m)</tspan></text>
4
+ </svg>