claude-limitline 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +635 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Tyler Gray
|
|
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,281 @@
|
|
|
1
|
+
# claude-limitline
|
|
2
|
+
|
|
3
|
+
A statusline for Claude Code showing real-time usage limits and weekly tracking.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **5-Hour Block Limit** - Shows current usage percentage with time remaining until reset
|
|
12
|
+
- **7-Day Rolling Limit** - Tracks weekly usage with progress indicator
|
|
13
|
+
- **Real-time Tracking** - Uses Anthropic's OAuth usage API for accurate usage data
|
|
14
|
+
- **Progress Bar Display** - Visual progress bars for quick status checks
|
|
15
|
+
- **Cross-Platform** - Works on Windows, macOS, and Linux
|
|
16
|
+
- **Zero Runtime Dependencies** - Lightweight and fast
|
|
17
|
+
- **Multiple Themes** - Dark, light, nord, and gruvbox themes included
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
- **Node.js** 18.0.0 or higher
|
|
22
|
+
- **Claude Code** CLI installed and authenticated (for OAuth token)
|
|
23
|
+
- **Nerd Font** (optional, for powerline symbols)
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
### From npm (recommended)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g claude-limitline
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### From Source
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/tylergraydev/claude-limitline.git
|
|
37
|
+
cd claude-limitline
|
|
38
|
+
npm install
|
|
39
|
+
npm run build
|
|
40
|
+
npm link
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Using Docker
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Build the image
|
|
47
|
+
docker build -t claude-limitline .
|
|
48
|
+
|
|
49
|
+
# Run (mount your .claude directory for OAuth token access)
|
|
50
|
+
docker run --rm -v ~/.claude:/root/.claude claude-limitline
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
The easiest way to use claude-limitline is to add it directly to your Claude Code settings.
|
|
56
|
+
|
|
57
|
+
**Add to your Claude Code settings file** (`~/.claude/settings.json`):
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"statusLine": {
|
|
62
|
+
"type": "command",
|
|
63
|
+
"command": "npx claude-limitline"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
That's it! The status line will now show your usage limits in Claude Code.
|
|
69
|
+
|
|
70
|
+
### Full Settings Example
|
|
71
|
+
|
|
72
|
+
Here's a complete example with other common settings:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"permissions": {
|
|
77
|
+
"defaultMode": "default"
|
|
78
|
+
},
|
|
79
|
+
"statusLine": {
|
|
80
|
+
"type": "command",
|
|
81
|
+
"command": "npx claude-limitline"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Alternative: Global Install
|
|
87
|
+
|
|
88
|
+
If you prefer a global installation (slightly faster startup):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install -g claude-limitline
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Then update your settings:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"statusLine": {
|
|
99
|
+
"type": "command",
|
|
100
|
+
"command": "claude-limitline"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Test It
|
|
106
|
+
|
|
107
|
+
Run standalone to verify it's working:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx claude-limitline
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
You should see output like:
|
|
114
|
+
```
|
|
115
|
+
⏳ ████████░░ 45% (2h 30m left) | 📅 ██████░░░░ 62% (wk 43%)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitline.json`) or current working directory:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"display": {
|
|
125
|
+
"style": "minimal",
|
|
126
|
+
"useNerdFonts": true
|
|
127
|
+
},
|
|
128
|
+
"block": {
|
|
129
|
+
"enabled": true,
|
|
130
|
+
"displayStyle": "bar",
|
|
131
|
+
"barWidth": 10,
|
|
132
|
+
"showTimeRemaining": true
|
|
133
|
+
},
|
|
134
|
+
"weekly": {
|
|
135
|
+
"enabled": true,
|
|
136
|
+
"displayStyle": "bar",
|
|
137
|
+
"barWidth": 10,
|
|
138
|
+
"showWeekProgress": true
|
|
139
|
+
},
|
|
140
|
+
"budget": {
|
|
141
|
+
"pollInterval": 15,
|
|
142
|
+
"warningThreshold": 80
|
|
143
|
+
},
|
|
144
|
+
"theme": "dark"
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Configuration Options
|
|
149
|
+
|
|
150
|
+
| Option | Description | Default |
|
|
151
|
+
|--------|-------------|---------|
|
|
152
|
+
| `display.useNerdFonts` | Use Nerd Font symbols (⏳📅) vs ASCII | `true` |
|
|
153
|
+
| `block.enabled` | Show 5-hour block usage | `true` |
|
|
154
|
+
| `block.displayStyle` | `"bar"` or `"text"` | `"bar"` |
|
|
155
|
+
| `block.barWidth` | Width of progress bar in characters | `10` |
|
|
156
|
+
| `block.showTimeRemaining` | Show time until block resets | `true` |
|
|
157
|
+
| `weekly.enabled` | Show 7-day rolling usage | `true` |
|
|
158
|
+
| `weekly.displayStyle` | `"bar"` or `"text"` | `"bar"` |
|
|
159
|
+
| `weekly.barWidth` | Width of progress bar in characters | `10` |
|
|
160
|
+
| `weekly.showWeekProgress` | Show week progress percentage | `true` |
|
|
161
|
+
| `budget.pollInterval` | Minutes between API calls | `15` |
|
|
162
|
+
| `budget.warningThreshold` | Percentage to trigger warning color | `80` |
|
|
163
|
+
| `theme` | Color theme name | `"dark"` |
|
|
164
|
+
|
|
165
|
+
### Available Themes
|
|
166
|
+
|
|
167
|
+
- `dark` - Default dark theme
|
|
168
|
+
- `light` - Light background theme
|
|
169
|
+
- `nord` - Nord color palette
|
|
170
|
+
- `gruvbox` - Gruvbox color palette
|
|
171
|
+
|
|
172
|
+
## How It Works
|
|
173
|
+
|
|
174
|
+
claude-limitline retrieves your Claude usage data from Anthropic's OAuth usage API. It reads your OAuth token from:
|
|
175
|
+
|
|
176
|
+
| Platform | Location |
|
|
177
|
+
|----------|----------|
|
|
178
|
+
| **Windows** | Credential Manager or `~/.claude/.credentials.json` |
|
|
179
|
+
| **macOS** | Keychain or `~/.claude/.credentials.json` |
|
|
180
|
+
| **Linux** | secret-tool (GNOME Keyring) or `~/.claude/.credentials.json` |
|
|
181
|
+
|
|
182
|
+
The usage data is cached locally to respect API rate limits. The cache duration is configurable via `budget.pollInterval` (default: 15 minutes).
|
|
183
|
+
|
|
184
|
+
### API Response
|
|
185
|
+
|
|
186
|
+
The tool queries Anthropic's usage endpoint which returns:
|
|
187
|
+
|
|
188
|
+
- **5-hour block**: Usage percentage and reset time for the rolling 5-hour window
|
|
189
|
+
- **7-day rolling**: Usage percentage and reset time for the rolling 7-day window
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
### Setup
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
git clone https://github.com/tylergraydev/claude-limitline.git
|
|
197
|
+
cd claude-limitline
|
|
198
|
+
npm install
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Build
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm run build
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Development Mode (watch)
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npm run dev
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Type Checking
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm run typecheck
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Run Locally
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
node dist/index.js
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Debug Mode
|
|
226
|
+
|
|
227
|
+
Enable debug logging to troubleshoot issues:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Linux/macOS
|
|
231
|
+
CLAUDE_LIMITLINE_DEBUG=true claude-limitline
|
|
232
|
+
|
|
233
|
+
# Windows (PowerShell)
|
|
234
|
+
$env:CLAUDE_LIMITLINE_DEBUG="true"; claude-limitline
|
|
235
|
+
|
|
236
|
+
# Windows (CMD)
|
|
237
|
+
set CLAUDE_LIMITLINE_DEBUG=true && claude-limitline
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Debug output is written to stderr so it won't interfere with the status line output.
|
|
241
|
+
|
|
242
|
+
## Troubleshooting
|
|
243
|
+
|
|
244
|
+
### "No data" or empty output
|
|
245
|
+
|
|
246
|
+
1. **Check OAuth token**: Make sure you're logged into Claude Code (`claude --login`)
|
|
247
|
+
2. **Check credentials file**: Verify `~/.claude/.credentials.json` exists and contains `claudeAiOauth.accessToken`
|
|
248
|
+
3. **Enable debug mode**: Run with `CLAUDE_LIMITLINE_DEBUG=true` to see detailed logs
|
|
249
|
+
|
|
250
|
+
### Token not found
|
|
251
|
+
|
|
252
|
+
The OAuth token is stored by Claude Code when you authenticate. Try:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Re-authenticate with Claude Code
|
|
256
|
+
claude --login
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### API returns errors
|
|
260
|
+
|
|
261
|
+
- Ensure your Claude subscription is active
|
|
262
|
+
- Check if you've exceeded API rate limits (try increasing `pollInterval`)
|
|
263
|
+
|
|
264
|
+
## Contributing
|
|
265
|
+
|
|
266
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
267
|
+
|
|
268
|
+
1. Fork the repository
|
|
269
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
270
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
271
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
272
|
+
5. Open a Pull Request
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
277
|
+
|
|
278
|
+
## Acknowledgments
|
|
279
|
+
|
|
280
|
+
- Inspired by [claude-powerline](https://github.com/Owloops/claude-powerline)
|
|
281
|
+
- Built for use with [Claude Code](https://claude.com/claude-code)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config/loader.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
|
|
8
|
+
// src/utils/logger.ts
|
|
9
|
+
var DEBUG = process.env.CLAUDE_LIMITLINE_DEBUG === "true";
|
|
10
|
+
function debug(...args) {
|
|
11
|
+
if (DEBUG) {
|
|
12
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
13
|
+
console.error(`[${timestamp}] [DEBUG]`, ...args);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/config/types.ts
|
|
18
|
+
var DEFAULT_CONFIG = {
|
|
19
|
+
display: {
|
|
20
|
+
style: "minimal",
|
|
21
|
+
useNerdFonts: true
|
|
22
|
+
},
|
|
23
|
+
block: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
displayStyle: "bar",
|
|
26
|
+
barWidth: 10,
|
|
27
|
+
showTimeRemaining: true
|
|
28
|
+
},
|
|
29
|
+
weekly: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
displayStyle: "bar",
|
|
32
|
+
barWidth: 10,
|
|
33
|
+
showWeekProgress: true
|
|
34
|
+
},
|
|
35
|
+
budget: {
|
|
36
|
+
pollInterval: 15,
|
|
37
|
+
warningThreshold: 80
|
|
38
|
+
},
|
|
39
|
+
theme: "dark"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/config/loader.ts
|
|
43
|
+
function deepMerge(target, source) {
|
|
44
|
+
const result = { ...target };
|
|
45
|
+
for (const key of Object.keys(source)) {
|
|
46
|
+
const sourceValue = source[key];
|
|
47
|
+
const targetValue = target[key];
|
|
48
|
+
if (sourceValue !== void 0 && typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue) && typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) {
|
|
49
|
+
result[key] = { ...targetValue, ...sourceValue };
|
|
50
|
+
} else if (sourceValue !== void 0) {
|
|
51
|
+
result[key] = sourceValue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function loadConfig() {
|
|
57
|
+
const configPaths = [
|
|
58
|
+
path.join(process.cwd(), ".claude-limitline.json"),
|
|
59
|
+
path.join(os.homedir(), ".claude-limitline.json"),
|
|
60
|
+
path.join(os.homedir(), ".config", "claude-limitline", "config.json")
|
|
61
|
+
];
|
|
62
|
+
for (const configPath of configPaths) {
|
|
63
|
+
try {
|
|
64
|
+
if (fs.existsSync(configPath)) {
|
|
65
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
66
|
+
const userConfig = JSON.parse(content);
|
|
67
|
+
debug(`Loaded config from ${configPath}`);
|
|
68
|
+
return deepMerge(DEFAULT_CONFIG, userConfig);
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
debug(`Failed to load config from ${configPath}:`, error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
debug("Using default config");
|
|
75
|
+
return DEFAULT_CONFIG;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/utils/oauth.ts
|
|
79
|
+
import { exec } from "child_process";
|
|
80
|
+
import { promisify } from "util";
|
|
81
|
+
import fs2 from "fs";
|
|
82
|
+
import path2 from "path";
|
|
83
|
+
import os2 from "os";
|
|
84
|
+
var execAsync = promisify(exec);
|
|
85
|
+
async function getOAuthTokenWindows() {
|
|
86
|
+
try {
|
|
87
|
+
const { stdout } = await execAsync(
|
|
88
|
+
`powershell -Command "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String((Get-StoredCredential -Target 'Claude Code' -AsCredentialObject).Password))"`,
|
|
89
|
+
{ timeout: 5e3 }
|
|
90
|
+
);
|
|
91
|
+
const token = stdout.trim();
|
|
92
|
+
if (token && token.startsWith("sk-ant-oat")) {
|
|
93
|
+
return token;
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
debug("PowerShell credential retrieval failed:", error);
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const { stdout } = await execAsync(
|
|
100
|
+
`powershell -Command "$cred = cmdkey /list:Claude* | Select-String -Pattern 'User:.*'; if ($cred) { $cred.Line.Split(':')[1].Trim() }"`,
|
|
101
|
+
{ timeout: 5e3 }
|
|
102
|
+
);
|
|
103
|
+
debug("cmdkey output:", stdout);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
debug("cmdkey approach failed:", error);
|
|
106
|
+
}
|
|
107
|
+
const primaryPath = path2.join(os2.homedir(), ".claude", ".credentials.json");
|
|
108
|
+
try {
|
|
109
|
+
if (fs2.existsSync(primaryPath)) {
|
|
110
|
+
const content = fs2.readFileSync(primaryPath, "utf-8");
|
|
111
|
+
const config = JSON.parse(content);
|
|
112
|
+
if (config.claudeAiOauth && typeof config.claudeAiOauth === "object") {
|
|
113
|
+
const token = config.claudeAiOauth.accessToken;
|
|
114
|
+
if (token && typeof token === "string" && token.startsWith("sk-ant-oat")) {
|
|
115
|
+
debug(`Found OAuth token in ${primaryPath} under claudeAiOauth.accessToken`);
|
|
116
|
+
return token;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
debug(`Failed to read config from ${primaryPath}:`, error);
|
|
122
|
+
}
|
|
123
|
+
const fallbackPaths = [
|
|
124
|
+
path2.join(os2.homedir(), ".claude", "credentials.json"),
|
|
125
|
+
path2.join(os2.homedir(), ".config", "claude-code", "credentials.json"),
|
|
126
|
+
path2.join(process.env.APPDATA || "", "Claude Code", "credentials.json"),
|
|
127
|
+
path2.join(process.env.LOCALAPPDATA || "", "Claude Code", "credentials.json")
|
|
128
|
+
];
|
|
129
|
+
for (const configPath of fallbackPaths) {
|
|
130
|
+
try {
|
|
131
|
+
if (fs2.existsSync(configPath)) {
|
|
132
|
+
const content = fs2.readFileSync(configPath, "utf-8");
|
|
133
|
+
const config = JSON.parse(content);
|
|
134
|
+
for (const key of ["oauth_token", "token", "accessToken"]) {
|
|
135
|
+
const token = config[key];
|
|
136
|
+
if (token && typeof token === "string" && token.startsWith("sk-ant-oat")) {
|
|
137
|
+
debug(`Found OAuth token in ${configPath} under key ${key}`);
|
|
138
|
+
return token;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
debug(`Failed to read config from ${configPath}:`, error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
async function getOAuthTokenMacOS() {
|
|
149
|
+
try {
|
|
150
|
+
const { stdout } = await execAsync(
|
|
151
|
+
`security find-generic-password -s "Claude Code" -w`,
|
|
152
|
+
{ timeout: 5e3 }
|
|
153
|
+
);
|
|
154
|
+
const token = stdout.trim();
|
|
155
|
+
if (token && token.startsWith("sk-ant-oat")) {
|
|
156
|
+
return token;
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
debug("macOS Keychain retrieval failed:", error);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
async function getOAuthTokenLinux() {
|
|
164
|
+
try {
|
|
165
|
+
const { stdout } = await execAsync(
|
|
166
|
+
`secret-tool lookup service "Claude Code"`,
|
|
167
|
+
{ timeout: 5e3 }
|
|
168
|
+
);
|
|
169
|
+
const token = stdout.trim();
|
|
170
|
+
if (token && token.startsWith("sk-ant-oat")) {
|
|
171
|
+
return token;
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
debug("Linux secret-tool retrieval failed:", error);
|
|
175
|
+
}
|
|
176
|
+
const configPaths = [
|
|
177
|
+
path2.join(os2.homedir(), ".claude", ".credentials.json"),
|
|
178
|
+
path2.join(os2.homedir(), ".claude", "credentials.json"),
|
|
179
|
+
path2.join(os2.homedir(), ".config", "claude-code", "credentials.json")
|
|
180
|
+
];
|
|
181
|
+
for (const configPath of configPaths) {
|
|
182
|
+
try {
|
|
183
|
+
if (fs2.existsSync(configPath)) {
|
|
184
|
+
const content = fs2.readFileSync(configPath, "utf-8");
|
|
185
|
+
const config = JSON.parse(content);
|
|
186
|
+
if (config.claudeAiOauth && typeof config.claudeAiOauth === "object") {
|
|
187
|
+
const token = config.claudeAiOauth.accessToken;
|
|
188
|
+
if (token && typeof token === "string" && token.startsWith("sk-ant-oat")) {
|
|
189
|
+
debug(`Found OAuth token in ${configPath} under claudeAiOauth.accessToken`);
|
|
190
|
+
return token;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const key of ["oauth_token", "token", "accessToken"]) {
|
|
194
|
+
const token = config[key];
|
|
195
|
+
if (token && typeof token === "string" && token.startsWith("sk-ant-oat")) {
|
|
196
|
+
debug(`Found OAuth token in ${configPath} under key ${key}`);
|
|
197
|
+
return token;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
debug(`Failed to read config from ${configPath}:`, error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
async function getOAuthToken() {
|
|
208
|
+
const platform = process.platform;
|
|
209
|
+
debug(`Attempting to retrieve OAuth token on platform: ${platform}`);
|
|
210
|
+
switch (platform) {
|
|
211
|
+
case "win32":
|
|
212
|
+
return getOAuthTokenWindows();
|
|
213
|
+
case "darwin":
|
|
214
|
+
return getOAuthTokenMacOS();
|
|
215
|
+
case "linux":
|
|
216
|
+
return getOAuthTokenLinux();
|
|
217
|
+
default:
|
|
218
|
+
debug(`Unsupported platform for OAuth token retrieval: ${platform}`);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function fetchUsageFromAPI(token) {
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
225
|
+
method: "GET",
|
|
226
|
+
headers: {
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
"User-Agent": "claude-limitline/1.0.0",
|
|
229
|
+
Authorization: `Bearer ${token}`,
|
|
230
|
+
"anthropic-beta": "oauth-2025-04-20"
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
debug(`Usage API returned status ${response.status}: ${response.statusText}`);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const data = await response.json();
|
|
238
|
+
debug("Usage API response:", JSON.stringify(data));
|
|
239
|
+
const parseUsageBlock = (block) => {
|
|
240
|
+
if (!block) return null;
|
|
241
|
+
return {
|
|
242
|
+
resetAt: block.resets_at ? new Date(block.resets_at) : /* @__PURE__ */ new Date(),
|
|
243
|
+
percentUsed: block.utilization ?? 0,
|
|
244
|
+
isOverLimit: (block.utilization ?? 0) >= 100
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
fiveHour: parseUsageBlock(data.five_hour),
|
|
249
|
+
sevenDay: parseUsageBlock(data.seven_day),
|
|
250
|
+
raw: data
|
|
251
|
+
};
|
|
252
|
+
} catch (error) {
|
|
253
|
+
debug("Failed to fetch usage from API:", error);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
var cachedUsage = null;
|
|
258
|
+
var cacheTimestamp = 0;
|
|
259
|
+
var cachedToken = null;
|
|
260
|
+
async function getRealtimeUsage(pollIntervalMinutes = 15) {
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
const cacheAgeMs = now - cacheTimestamp;
|
|
263
|
+
const pollIntervalMs = pollIntervalMinutes * 60 * 1e3;
|
|
264
|
+
if (cachedUsage && cacheAgeMs < pollIntervalMs) {
|
|
265
|
+
debug(`Using cached usage data (age: ${Math.round(cacheAgeMs / 1e3)}s)`);
|
|
266
|
+
return cachedUsage;
|
|
267
|
+
}
|
|
268
|
+
if (!cachedToken) {
|
|
269
|
+
cachedToken = await getOAuthToken();
|
|
270
|
+
if (!cachedToken) {
|
|
271
|
+
debug("Could not retrieve OAuth token for realtime usage");
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const usage = await fetchUsageFromAPI(cachedToken);
|
|
276
|
+
if (usage) {
|
|
277
|
+
cachedUsage = usage;
|
|
278
|
+
cacheTimestamp = now;
|
|
279
|
+
debug("Refreshed realtime usage cache");
|
|
280
|
+
} else {
|
|
281
|
+
cachedToken = null;
|
|
282
|
+
}
|
|
283
|
+
return usage;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/segments/block.ts
|
|
287
|
+
var BlockProvider = class {
|
|
288
|
+
async getBlockInfo(pollInterval) {
|
|
289
|
+
const realtimeInfo = await this.getRealtimeBlockInfo(pollInterval);
|
|
290
|
+
if (realtimeInfo) {
|
|
291
|
+
return realtimeInfo;
|
|
292
|
+
}
|
|
293
|
+
debug("Realtime mode failed, no block data available");
|
|
294
|
+
return {
|
|
295
|
+
percentUsed: null,
|
|
296
|
+
resetAt: null,
|
|
297
|
+
timeRemaining: null,
|
|
298
|
+
isRealtime: false
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
async getRealtimeBlockInfo(pollInterval) {
|
|
302
|
+
try {
|
|
303
|
+
const usage = await getRealtimeUsage(pollInterval ?? 15);
|
|
304
|
+
if (!usage || !usage.fiveHour) {
|
|
305
|
+
debug("No realtime block usage data available");
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const fiveHour = usage.fiveHour;
|
|
309
|
+
const now = /* @__PURE__ */ new Date();
|
|
310
|
+
const resetAt = new Date(fiveHour.resetAt);
|
|
311
|
+
const timeRemaining = Math.max(
|
|
312
|
+
0,
|
|
313
|
+
Math.round((resetAt.getTime() - now.getTime()) / (1e3 * 60))
|
|
314
|
+
);
|
|
315
|
+
debug(
|
|
316
|
+
`Block segment (realtime): ${fiveHour.percentUsed}% used, resets at ${fiveHour.resetAt.toISOString()}, ${timeRemaining}m remaining`
|
|
317
|
+
);
|
|
318
|
+
return {
|
|
319
|
+
percentUsed: fiveHour.percentUsed,
|
|
320
|
+
resetAt: fiveHour.resetAt,
|
|
321
|
+
timeRemaining,
|
|
322
|
+
isRealtime: true
|
|
323
|
+
};
|
|
324
|
+
} catch (error) {
|
|
325
|
+
debug("Error getting realtime block info:", error);
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/segments/weekly.ts
|
|
332
|
+
var WeeklyProvider = class {
|
|
333
|
+
calculateWeekProgress(resetDay, resetHour, resetMinute) {
|
|
334
|
+
const now = /* @__PURE__ */ new Date();
|
|
335
|
+
const dayOfWeek = now.getDay();
|
|
336
|
+
const hours = now.getHours();
|
|
337
|
+
const minutes = now.getMinutes();
|
|
338
|
+
const targetDay = resetDay ?? 1;
|
|
339
|
+
const targetHour = resetHour ?? 0;
|
|
340
|
+
const targetMinute = resetMinute ?? 0;
|
|
341
|
+
let daysSinceReset = (dayOfWeek - targetDay + 7) % 7;
|
|
342
|
+
if (daysSinceReset === 0) {
|
|
343
|
+
const currentMinutes = hours * 60 + minutes;
|
|
344
|
+
const resetMinutes = targetHour * 60 + targetMinute;
|
|
345
|
+
if (currentMinutes < resetMinutes) {
|
|
346
|
+
daysSinceReset = 7;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const hoursIntoWeek = daysSinceReset * 24 + hours - targetHour + (minutes - targetMinute) / 60;
|
|
350
|
+
const totalHoursInWeek = 7 * 24;
|
|
351
|
+
const progress = Math.max(0, Math.min(100, hoursIntoWeek / totalHoursInWeek * 100));
|
|
352
|
+
return Math.round(progress);
|
|
353
|
+
}
|
|
354
|
+
calculateWeekProgressFromResetTime(resetAt) {
|
|
355
|
+
const now = /* @__PURE__ */ new Date();
|
|
356
|
+
const resetTime = new Date(resetAt);
|
|
357
|
+
const periodStart = new Date(resetTime);
|
|
358
|
+
periodStart.setDate(periodStart.getDate() - 7);
|
|
359
|
+
if (now > resetTime) {
|
|
360
|
+
const newPeriodStart = resetTime;
|
|
361
|
+
const newResetTime = new Date(resetTime);
|
|
362
|
+
newResetTime.setDate(newResetTime.getDate() + 7);
|
|
363
|
+
const totalMs2 = newResetTime.getTime() - newPeriodStart.getTime();
|
|
364
|
+
const elapsedMs2 = now.getTime() - newPeriodStart.getTime();
|
|
365
|
+
return Math.round(elapsedMs2 / totalMs2 * 100);
|
|
366
|
+
}
|
|
367
|
+
const totalMs = resetTime.getTime() - periodStart.getTime();
|
|
368
|
+
const elapsedMs = now.getTime() - periodStart.getTime();
|
|
369
|
+
const progress = Math.max(0, Math.min(100, elapsedMs / totalMs * 100));
|
|
370
|
+
return Math.round(progress);
|
|
371
|
+
}
|
|
372
|
+
async getWeeklyInfo(resetDay, resetHour, resetMinute, pollInterval) {
|
|
373
|
+
const realtimeInfo = await this.getRealtimeWeeklyInfo(pollInterval);
|
|
374
|
+
if (realtimeInfo) {
|
|
375
|
+
return realtimeInfo;
|
|
376
|
+
}
|
|
377
|
+
debug("Realtime mode failed, falling back to estimate mode");
|
|
378
|
+
const weekProgressPercent = this.calculateWeekProgress(resetDay, resetHour, resetMinute);
|
|
379
|
+
return {
|
|
380
|
+
percentUsed: null,
|
|
381
|
+
resetAt: null,
|
|
382
|
+
isRealtime: false,
|
|
383
|
+
weekProgressPercent
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async getRealtimeWeeklyInfo(pollInterval) {
|
|
387
|
+
try {
|
|
388
|
+
const usage = await getRealtimeUsage(pollInterval ?? 15);
|
|
389
|
+
if (!usage || !usage.sevenDay) {
|
|
390
|
+
debug("No realtime weekly usage data available");
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const sevenDay = usage.sevenDay;
|
|
394
|
+
const weekProgressPercent = this.calculateWeekProgressFromResetTime(sevenDay.resetAt);
|
|
395
|
+
debug(
|
|
396
|
+
`Weekly segment (realtime): ${sevenDay.percentUsed}% used, resets at ${sevenDay.resetAt.toISOString()}`
|
|
397
|
+
);
|
|
398
|
+
return {
|
|
399
|
+
percentUsed: sevenDay.percentUsed,
|
|
400
|
+
resetAt: sevenDay.resetAt,
|
|
401
|
+
isRealtime: true,
|
|
402
|
+
weekProgressPercent
|
|
403
|
+
};
|
|
404
|
+
} catch (error) {
|
|
405
|
+
debug("Error getting realtime weekly info:", error);
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// src/utils/constants.ts
|
|
412
|
+
var SYMBOLS = {
|
|
413
|
+
right: "\uE0B0",
|
|
414
|
+
left: "\uE0B2",
|
|
415
|
+
branch: "\uE0A0",
|
|
416
|
+
separator: "\uE0B1",
|
|
417
|
+
block_cost: "\uF252",
|
|
418
|
+
// Hourglass
|
|
419
|
+
weekly_cost: "\uF073",
|
|
420
|
+
// Calendar
|
|
421
|
+
progress_full: "\u2588",
|
|
422
|
+
// Full block
|
|
423
|
+
progress_empty: "\u2591"
|
|
424
|
+
// Light shade
|
|
425
|
+
};
|
|
426
|
+
var TEXT_SYMBOLS = {
|
|
427
|
+
right: ">",
|
|
428
|
+
left: "<",
|
|
429
|
+
branch: "",
|
|
430
|
+
separator: "|",
|
|
431
|
+
block_cost: "BLK",
|
|
432
|
+
weekly_cost: "WK",
|
|
433
|
+
progress_full: "#",
|
|
434
|
+
progress_empty: "-"
|
|
435
|
+
};
|
|
436
|
+
var RESET_CODE = "\x1B[0m";
|
|
437
|
+
|
|
438
|
+
// src/themes/index.ts
|
|
439
|
+
var color = (n) => `\x1B[38;5;${n}m`;
|
|
440
|
+
var bgColor = (n) => `\x1B[48;5;${n}m`;
|
|
441
|
+
var themes = {
|
|
442
|
+
dark: {
|
|
443
|
+
blockBg: bgColor(236),
|
|
444
|
+
blockFg: color(252),
|
|
445
|
+
weeklyBg: bgColor(236),
|
|
446
|
+
weeklyFg: color(252),
|
|
447
|
+
warningBg: bgColor(172),
|
|
448
|
+
warningFg: color(232),
|
|
449
|
+
criticalBg: bgColor(160),
|
|
450
|
+
criticalFg: color(255),
|
|
451
|
+
progressFull: color(76),
|
|
452
|
+
progressEmpty: color(240),
|
|
453
|
+
separatorFg: color(244)
|
|
454
|
+
},
|
|
455
|
+
light: {
|
|
456
|
+
blockBg: bgColor(254),
|
|
457
|
+
blockFg: color(236),
|
|
458
|
+
weeklyBg: bgColor(254),
|
|
459
|
+
weeklyFg: color(236),
|
|
460
|
+
warningBg: bgColor(214),
|
|
461
|
+
warningFg: color(232),
|
|
462
|
+
criticalBg: bgColor(196),
|
|
463
|
+
criticalFg: color(255),
|
|
464
|
+
progressFull: color(34),
|
|
465
|
+
progressEmpty: color(250),
|
|
466
|
+
separatorFg: color(244)
|
|
467
|
+
},
|
|
468
|
+
nord: {
|
|
469
|
+
blockBg: bgColor(236),
|
|
470
|
+
blockFg: color(110),
|
|
471
|
+
weeklyBg: bgColor(236),
|
|
472
|
+
weeklyFg: color(110),
|
|
473
|
+
warningBg: bgColor(179),
|
|
474
|
+
warningFg: color(232),
|
|
475
|
+
criticalBg: bgColor(131),
|
|
476
|
+
criticalFg: color(255),
|
|
477
|
+
progressFull: color(108),
|
|
478
|
+
progressEmpty: color(239),
|
|
479
|
+
separatorFg: color(60)
|
|
480
|
+
},
|
|
481
|
+
gruvbox: {
|
|
482
|
+
blockBg: bgColor(237),
|
|
483
|
+
blockFg: color(223),
|
|
484
|
+
weeklyBg: bgColor(237),
|
|
485
|
+
weeklyFg: color(223),
|
|
486
|
+
warningBg: bgColor(214),
|
|
487
|
+
warningFg: color(235),
|
|
488
|
+
criticalBg: bgColor(167),
|
|
489
|
+
criticalFg: color(235),
|
|
490
|
+
progressFull: color(142),
|
|
491
|
+
progressEmpty: color(239),
|
|
492
|
+
separatorFg: color(246)
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
function getTheme(name) {
|
|
496
|
+
return themes[name] || themes.dark;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/renderer.ts
|
|
500
|
+
var Renderer = class {
|
|
501
|
+
config;
|
|
502
|
+
theme;
|
|
503
|
+
symbols;
|
|
504
|
+
constructor(config) {
|
|
505
|
+
this.config = config;
|
|
506
|
+
this.theme = getTheme(config.theme || "dark");
|
|
507
|
+
const useNerd = config.display?.useNerdFonts ?? true;
|
|
508
|
+
const symbolSet = useNerd ? SYMBOLS : TEXT_SYMBOLS;
|
|
509
|
+
this.symbols = {
|
|
510
|
+
block: symbolSet.block_cost,
|
|
511
|
+
weekly: symbolSet.weekly_cost,
|
|
512
|
+
separator: symbolSet.separator,
|
|
513
|
+
progressFull: symbolSet.progress_full,
|
|
514
|
+
progressEmpty: symbolSet.progress_empty
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
formatProgressBar(percent, width, colors) {
|
|
518
|
+
const filled = Math.round(percent / 100 * width);
|
|
519
|
+
const empty = width - filled;
|
|
520
|
+
const filledBar = colors.progressFull + this.symbols.progressFull.repeat(filled);
|
|
521
|
+
const emptyBar = colors.progressEmpty + this.symbols.progressEmpty.repeat(empty);
|
|
522
|
+
return filledBar + emptyBar + RESET_CODE;
|
|
523
|
+
}
|
|
524
|
+
formatTimeRemaining(minutes) {
|
|
525
|
+
if (minutes >= 60) {
|
|
526
|
+
const hours = Math.floor(minutes / 60);
|
|
527
|
+
const mins = minutes % 60;
|
|
528
|
+
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
529
|
+
}
|
|
530
|
+
return `${minutes}m`;
|
|
531
|
+
}
|
|
532
|
+
getColorForPercent(percent, colors) {
|
|
533
|
+
const threshold = this.config.budget?.warningThreshold ?? 80;
|
|
534
|
+
if (percent >= 100) {
|
|
535
|
+
return { bg: colors.criticalBg, fg: colors.criticalFg };
|
|
536
|
+
} else if (percent >= threshold) {
|
|
537
|
+
return { bg: colors.warningBg, fg: colors.warningFg };
|
|
538
|
+
}
|
|
539
|
+
return { bg: colors.blockBg, fg: colors.blockFg };
|
|
540
|
+
}
|
|
541
|
+
renderBlock(blockInfo) {
|
|
542
|
+
if (!blockInfo || !this.config.block?.enabled) {
|
|
543
|
+
return "";
|
|
544
|
+
}
|
|
545
|
+
if (blockInfo.percentUsed === null) {
|
|
546
|
+
return `${this.symbols.block} --`;
|
|
547
|
+
}
|
|
548
|
+
const percent = blockInfo.percentUsed;
|
|
549
|
+
const displayStyle = this.config.block.displayStyle || "bar";
|
|
550
|
+
const barWidth = this.config.block.barWidth || 10;
|
|
551
|
+
const showTime = this.config.block.showTimeRemaining ?? true;
|
|
552
|
+
let display;
|
|
553
|
+
if (displayStyle === "bar") {
|
|
554
|
+
const bar = this.formatProgressBar(percent, barWidth, this.theme);
|
|
555
|
+
display = `${bar} ${Math.round(percent)}%`;
|
|
556
|
+
} else {
|
|
557
|
+
display = `${Math.round(percent)}%`;
|
|
558
|
+
}
|
|
559
|
+
if (showTime && blockInfo.timeRemaining !== null) {
|
|
560
|
+
const timeStr = this.formatTimeRemaining(blockInfo.timeRemaining);
|
|
561
|
+
display += ` (${timeStr} left)`;
|
|
562
|
+
}
|
|
563
|
+
return `${this.symbols.block} ${display}`;
|
|
564
|
+
}
|
|
565
|
+
renderWeekly(weeklyInfo) {
|
|
566
|
+
if (!weeklyInfo || !this.config.weekly?.enabled) {
|
|
567
|
+
return "";
|
|
568
|
+
}
|
|
569
|
+
if (weeklyInfo.percentUsed === null) {
|
|
570
|
+
return `${this.symbols.weekly} --`;
|
|
571
|
+
}
|
|
572
|
+
const percent = weeklyInfo.percentUsed;
|
|
573
|
+
const displayStyle = this.config.weekly.displayStyle || "bar";
|
|
574
|
+
const barWidth = this.config.weekly.barWidth || 10;
|
|
575
|
+
const showWeekProgress = this.config.weekly.showWeekProgress ?? true;
|
|
576
|
+
let display;
|
|
577
|
+
if (displayStyle === "bar") {
|
|
578
|
+
const bar = this.formatProgressBar(percent, barWidth, this.theme);
|
|
579
|
+
display = `${bar} ${Math.round(percent)}%`;
|
|
580
|
+
} else {
|
|
581
|
+
display = `${Math.round(percent)}%`;
|
|
582
|
+
}
|
|
583
|
+
if (showWeekProgress) {
|
|
584
|
+
display += ` (wk ${weeklyInfo.weekProgressPercent}%)`;
|
|
585
|
+
}
|
|
586
|
+
return `${this.symbols.weekly} ${display}`;
|
|
587
|
+
}
|
|
588
|
+
render(blockInfo, weeklyInfo) {
|
|
589
|
+
const parts = [];
|
|
590
|
+
const blockSegment = this.renderBlock(blockInfo);
|
|
591
|
+
if (blockSegment) {
|
|
592
|
+
parts.push(blockSegment);
|
|
593
|
+
}
|
|
594
|
+
const weeklySegment = this.renderWeekly(weeklyInfo);
|
|
595
|
+
if (weeklySegment) {
|
|
596
|
+
parts.push(weeklySegment);
|
|
597
|
+
}
|
|
598
|
+
if (parts.length === 0) {
|
|
599
|
+
return "";
|
|
600
|
+
}
|
|
601
|
+
const separator = ` ${this.theme.separatorFg}${this.symbols.separator}${RESET_CODE} `;
|
|
602
|
+
return parts.join(separator);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// src/index.ts
|
|
607
|
+
async function main() {
|
|
608
|
+
try {
|
|
609
|
+
const config = loadConfig();
|
|
610
|
+
debug("Config loaded:", JSON.stringify(config));
|
|
611
|
+
const blockProvider = new BlockProvider();
|
|
612
|
+
const weeklyProvider = new WeeklyProvider();
|
|
613
|
+
const pollInterval = config.budget?.pollInterval ?? 15;
|
|
614
|
+
const [blockInfo, weeklyInfo] = await Promise.all([
|
|
615
|
+
config.block?.enabled ? blockProvider.getBlockInfo(pollInterval) : null,
|
|
616
|
+
config.weekly?.enabled ? weeklyProvider.getWeeklyInfo(
|
|
617
|
+
config.budget?.resetDay,
|
|
618
|
+
config.budget?.resetHour,
|
|
619
|
+
config.budget?.resetMinute,
|
|
620
|
+
pollInterval
|
|
621
|
+
) : null
|
|
622
|
+
]);
|
|
623
|
+
debug("Block info:", JSON.stringify(blockInfo));
|
|
624
|
+
debug("Weekly info:", JSON.stringify(weeklyInfo));
|
|
625
|
+
const renderer = new Renderer(config);
|
|
626
|
+
const output = renderer.render(blockInfo, weeklyInfo);
|
|
627
|
+
if (output) {
|
|
628
|
+
process.stdout.write(output);
|
|
629
|
+
}
|
|
630
|
+
} catch (error) {
|
|
631
|
+
debug("Error in main:", error);
|
|
632
|
+
process.exit(0);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-limitline",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A statusline for Claude Code showing real-time usage limits and weekly tracking",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-limitline": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
12
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"lint": "eslint src/",
|
|
16
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/tylergraydev/claude-limitline.git"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"claude",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"statusline",
|
|
26
|
+
"powerline",
|
|
27
|
+
"usage",
|
|
28
|
+
"monitoring",
|
|
29
|
+
"cli",
|
|
30
|
+
"anthropic"
|
|
31
|
+
],
|
|
32
|
+
"author": "Tyler Gray",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/tylergraydev/claude-limitline/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/tylergraydev/claude-limitline#readme",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^20.10.0",
|
|
40
|
+
"tsup": "^8.0.0",
|
|
41
|
+
"typescript": "^5.3.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist"
|
|
48
|
+
]
|
|
49
|
+
}
|