claude-usage-limits 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/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +22 -0
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/bin/cli.js +62 -0
- package/commands/check.md +14 -0
- package/package.json +43 -0
- package/skills/usage-limits/SKILL.md +163 -0
- package/skills/usage-limits/references/how-it-works.md +136 -0
- package/skills/usage-limits/references/tactics.md +170 -0
- package/skills/usage-limits/scripts/lowpower.js +230 -0
- package/skills/usage-limits/scripts/usage.js +918 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Where the numbers come from
|
|
2
|
+
|
|
3
|
+
Everything is read from local files. Nothing is sent anywhere, and no API key
|
|
4
|
+
or token is read.
|
|
5
|
+
|
|
6
|
+
## Sources
|
|
7
|
+
|
|
8
|
+
### The limits
|
|
9
|
+
|
|
10
|
+
`~/.claude.json`, key `cachedUsageUtilization`. Claude Code refreshes this
|
|
11
|
+
during normal use, so it is usually a few minutes old at most. The shape:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"fetchedAtMs": 1787522765060,
|
|
16
|
+
"utilization": {
|
|
17
|
+
"five_hour": { "utilization": 2, "resets_at": "2026-08-24T02:59:59Z" },
|
|
18
|
+
"seven_day": { "utilization": 75, "resets_at": "2026-08-23T22:59:59Z" },
|
|
19
|
+
"extra_usage": { "is_enabled": false }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`utilization` is a whole-number percentage. `resets_at` is when that window
|
|
25
|
+
rolls over. Some plans also carry `seven_day_opus` and `seven_day_sonnet`;
|
|
26
|
+
the report includes them when they are present.
|
|
27
|
+
|
|
28
|
+
If the key is missing, run `/usage` once inside Claude Code. That is what
|
|
29
|
+
populates it.
|
|
30
|
+
|
|
31
|
+
The plan name comes from `oauthAccount.organizationType` in the same file.
|
|
32
|
+
Current effort and model come from `settings.json` in the config directory.
|
|
33
|
+
|
|
34
|
+
`CLAUDE_CONFIG_DIR` is honoured if set.
|
|
35
|
+
|
|
36
|
+
Every surface writes here: the terminal CLI, the VS Code and JetBrains
|
|
37
|
+
extensions, and the desktop app all share one config directory, and their
|
|
38
|
+
sessions are counted together. Entries carry an `entrypoint` field (`cli`,
|
|
39
|
+
`claude-vscode`) if you want to tell them apart, but the report does not
|
|
40
|
+
filter on it.
|
|
41
|
+
|
|
42
|
+
### The pace
|
|
43
|
+
|
|
44
|
+
`~/.claude/projects/<project>/<session>.jsonl`. One JSON object per line.
|
|
45
|
+
Assistant turns carry a usage record:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"type": "assistant",
|
|
50
|
+
"timestamp": "2026-08-23T22:04:56.858Z",
|
|
51
|
+
"requestId": "req_011...",
|
|
52
|
+
"effort": "xhigh",
|
|
53
|
+
"message": {
|
|
54
|
+
"id": "msg_01...",
|
|
55
|
+
"model": "claude-opus-5",
|
|
56
|
+
"usage": {
|
|
57
|
+
"input_tokens": 2,
|
|
58
|
+
"cache_creation_input_tokens": 8049,
|
|
59
|
+
"cache_read_input_tokens": 24780,
|
|
60
|
+
"output_tokens": 2144,
|
|
61
|
+
"cache_creation": { "ephemeral_1h_input_tokens": 8049 }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Files whose modification time predates the window are skipped. Turns are keyed
|
|
68
|
+
by `message.id` plus `requestId` and counted once, because resuming or forking
|
|
69
|
+
a session copies earlier turns into a new file.
|
|
70
|
+
|
|
71
|
+
## The arithmetic
|
|
72
|
+
|
|
73
|
+
Each turn is priced at published API rates. Cache traffic is a multiple of the
|
|
74
|
+
input rate: 1.25x for a five-minute write, 2x for a one-hour write, 0.1x for a
|
|
75
|
+
read.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
turn = (input + 0.1*cache_read + 1.25*write5m + 2*write1h) * input_rate
|
|
79
|
+
+ output * output_rate
|
|
80
|
+
|
|
81
|
+
spent = sum of turns inside the window
|
|
82
|
+
per_percent = spent / utilization
|
|
83
|
+
left = per_percent * (100 - utilization)
|
|
84
|
+
turns_left = (100 - utilization) / (recent_cost_per_turn / per_percent)
|
|
85
|
+
headroom = (100 - utilization) / (recent_dollars_per_hour / per_percent)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The window opens at `resets_at` minus its span: five hours, or seven days.
|
|
89
|
+
Recent pace is measured over the last hour, or since the window opened if that
|
|
90
|
+
is more recent.
|
|
91
|
+
|
|
92
|
+
The self-calibration is the point. Nobody outside Anthropic knows what a
|
|
93
|
+
subscription limit is worth in tokens, and it differs by plan. But if 75
|
|
94
|
+
percent of the week corresponds to a measurable amount of local traffic, the
|
|
95
|
+
remaining 25 percent is worth a quarter of that. The absolute dollar figures
|
|
96
|
+
are an internal unit for that ratio. On a subscription plan you are not billed
|
|
97
|
+
them, and they should not be read as a bill.
|
|
98
|
+
|
|
99
|
+
## Where it is soft
|
|
100
|
+
|
|
101
|
+
**Whole percent granularity.** The meter reports integers, so 2 percent is
|
|
102
|
+
really somewhere in 1.5 to 2.5. At low readings the projection can be off by
|
|
103
|
+
a quarter or more in either direction. The report flags this below 5 percent.
|
|
104
|
+
Above about 20 percent it tightens up considerably.
|
|
105
|
+
|
|
106
|
+
**One machine only.** Transcripts are local. Usage from another machine, from
|
|
107
|
+
claude.ai, or from a cloud session counts against the same limit but leaves no
|
|
108
|
+
local record. The percentages stay correct; the calibration reads low, which
|
|
109
|
+
makes the remaining headroom look smaller than it is.
|
|
110
|
+
|
|
111
|
+
**Deleted transcripts.** Same effect. Old session files get cleaned up, and
|
|
112
|
+
anything cleaned up inside the seven-day window is invisible to the pace
|
|
113
|
+
calculation.
|
|
114
|
+
|
|
115
|
+
**List prices are a proxy.** The rate table is first-party API pricing. How a
|
|
116
|
+
subscription plan actually meters usage is not published, and the weighting
|
|
117
|
+
almost certainly is not exactly this. It is close enough for ratios, which is
|
|
118
|
+
all it is used for.
|
|
119
|
+
|
|
120
|
+
**A reset time can be in the past.** The cache refreshes when Claude Code
|
|
121
|
+
talks to the API, so an idle spell leaves it behind. A window whose `resets_at`
|
|
122
|
+
has passed has already turned over, and its cached percentage describes a
|
|
123
|
+
window that no longer exists. Those are marked stale, excluded from the
|
|
124
|
+
binding choice, and never used for projections, because treating one as
|
|
125
|
+
current would report an empty budget at the exact moment the budget came back.
|
|
126
|
+
|
|
127
|
+
**Pace is not a promise.** Turns left assumes the next turns look like the last
|
|
128
|
+
hour's. A debugging spiral or a large file read breaks that assumption
|
|
129
|
+
immediately. Re-run the report if the shape of the work changes.
|
|
130
|
+
|
|
131
|
+
## Keeping it accurate
|
|
132
|
+
|
|
133
|
+
The rate table in `scripts/usage.js` is a plain object at the top of the file.
|
|
134
|
+
When new models ship, add a row. An unknown id falls back to the family it
|
|
135
|
+
names (`opus`, `sonnet`, `haiku`, `fable`) and then to Opus rates, so a missing
|
|
136
|
+
row degrades to an estimate rather than a crash.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Spending less per turn
|
|
2
|
+
|
|
3
|
+
Everything here works by shrinking one of three quantities: the tokens you
|
|
4
|
+
resend, the tokens you generate, or the number of times you do either.
|
|
5
|
+
|
|
6
|
+
## The part people get wrong
|
|
7
|
+
|
|
8
|
+
A conversation is stateless. Every turn resends the entire conversation so
|
|
9
|
+
far. Nothing is stored server-side between requests, so a file you read on
|
|
10
|
+
turn 5 is re-sent on turns 6 through 60.
|
|
11
|
+
|
|
12
|
+
That resend is cheap per token but not free. Cached prefix tokens bill at a
|
|
13
|
+
tenth of the input rate, fresh input at the full rate, cache writes at 1.25x
|
|
14
|
+
(five-minute) or 2x (one-hour), and output at five times the input rate. On
|
|
15
|
+
Opus rates, a 100k-token context costs roughly five cents a turn just to be
|
|
16
|
+
re-read, before the model generates anything.
|
|
17
|
+
|
|
18
|
+
Two consequences follow, and they drive most of the list below:
|
|
19
|
+
|
|
20
|
+
- **Context length is a recurring tax, not a one-off cost.** Dumping a large
|
|
21
|
+
file into context on turn 5 is not one expensive turn, it is a surcharge on
|
|
22
|
+
every turn after it.
|
|
23
|
+
- **Output is the expensive direction.** Reasoning tokens are output tokens.
|
|
24
|
+
A turn that thinks for 4,000 tokens and edits one line costs more than a
|
|
25
|
+
turn that reads 20,000 tokens of cached context.
|
|
26
|
+
|
|
27
|
+
## Levers, largest first
|
|
28
|
+
|
|
29
|
+
### 1. Drop the effort level
|
|
30
|
+
|
|
31
|
+
Effort controls how much the model reasons, and reasoning is output. Going
|
|
32
|
+
from `xhigh` to `low` cuts the expensive half of the turn several times over.
|
|
33
|
+
This is the biggest saving available that does not change what gets built.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
node scripts/lowpower.js on # writes effortLevel, remembers the old one
|
|
37
|
+
/effort low # same change, applied to the running session
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Keep high effort for the decisions that are hard to undo: schema changes,
|
|
41
|
+
migrations, anything touching auth. Mechanical work does not need it.
|
|
42
|
+
|
|
43
|
+
### 2. Move mechanical work to a cheaper model
|
|
44
|
+
|
|
45
|
+
Per million tokens, input and output:
|
|
46
|
+
|
|
47
|
+
| Model | Input | Output |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| Opus 5 | $5 | $25 |
|
|
50
|
+
| Sonnet 5 | $3 | $15 |
|
|
51
|
+
| Haiku 4.5 | $1 | $5 |
|
|
52
|
+
|
|
53
|
+
Renaming symbols, writing boilerplate tests, formatting, mechanical
|
|
54
|
+
translation between two known formats: none of that needs the top model.
|
|
55
|
+
Switch at a task boundary rather than mid-task, because a model switch
|
|
56
|
+
invalidates the prompt cache and the rebuild can cost more than the saving on
|
|
57
|
+
a short remaining task.
|
|
58
|
+
|
|
59
|
+
### 3. Read less, and read it once
|
|
60
|
+
|
|
61
|
+
- Line ranges instead of whole files. Grep with a result limit instead of
|
|
62
|
+
opening candidates one by one.
|
|
63
|
+
- Pipe noisy commands through `head`, `tail`, or `grep`. A full `npm test`
|
|
64
|
+
dump or an untrimmed `git log` lands in context and stays there.
|
|
65
|
+
- Never re-read a file to confirm an edit applied. The edit already failed
|
|
66
|
+
loudly if it did not.
|
|
67
|
+
- Prefer `git diff --stat` before `git diff`. Usually the stat is the answer.
|
|
68
|
+
|
|
69
|
+
### 4. Cut the tool surface
|
|
70
|
+
|
|
71
|
+
Every connected MCP server's tool definitions sit in the system prompt of
|
|
72
|
+
every single request. A handful of large servers can add tens of thousands of
|
|
73
|
+
tokens to the prefix, on every turn, whether or not you use them. Disconnect
|
|
74
|
+
the ones this project does not need with `/mcp` or `claude mcp`.
|
|
75
|
+
|
|
76
|
+
`DISABLE_BUNDLED_SKILLS=1` trims the bundled skill catalogue for the same
|
|
77
|
+
reason, if none of them are in use.
|
|
78
|
+
|
|
79
|
+
### 5. Protect the cache prefix
|
|
80
|
+
|
|
81
|
+
Cache reads are ten times cheaper than fresh input, and the cache matches on
|
|
82
|
+
an exact prefix. Any byte that changes early invalidates everything after it.
|
|
83
|
+
Things that invalidate it mid-session:
|
|
84
|
+
|
|
85
|
+
- switching models
|
|
86
|
+
- editing `CLAUDE.md` or project settings
|
|
87
|
+
- connecting or disconnecting an MCP server
|
|
88
|
+
- changing the tool set
|
|
89
|
+
|
|
90
|
+
None of these are forbidden. Just do them at a session boundary instead of in
|
|
91
|
+
the middle of a long run.
|
|
92
|
+
|
|
93
|
+
### 6. Batch tool calls
|
|
94
|
+
|
|
95
|
+
Independent calls belong in one message. Three greps in one turn cost one
|
|
96
|
+
context resend. Three greps in three turns cost three, plus three rounds of
|
|
97
|
+
reasoning and narration.
|
|
98
|
+
|
|
99
|
+
### 7. Skip subagents when the context already exists
|
|
100
|
+
|
|
101
|
+
A subagent starts with an empty context and re-derives what the main session
|
|
102
|
+
already knows. That is worth paying for genuine fan-out across more material
|
|
103
|
+
than one context can hold. It is not worth paying to answer a question the
|
|
104
|
+
main session could answer directly.
|
|
105
|
+
|
|
106
|
+
### 8. Stop failing loops early
|
|
107
|
+
|
|
108
|
+
The single most expensive pattern is retrying a fix that does not work.
|
|
109
|
+
Three blind attempts cost more than one turn spent reading the actual error.
|
|
110
|
+
If the second attempt fails for a new reason, stop and re-read.
|
|
111
|
+
|
|
112
|
+
### 9. End sessions at task boundaries
|
|
113
|
+
|
|
114
|
+
Compaction reads the whole conversation and writes a summary, which is a
|
|
115
|
+
full-price pass over everything. It is worth it compared to dragging a bloated
|
|
116
|
+
context through another twenty turns, but a fresh session started from a short
|
|
117
|
+
handoff note is cheaper than either.
|
|
118
|
+
|
|
119
|
+
Write the handoff, exit, start clean.
|
|
120
|
+
|
|
121
|
+
### 10. Specify the work properly the first time
|
|
122
|
+
|
|
123
|
+
Underspecifying is the most expensive habit in the list, and it looks like
|
|
124
|
+
saving. A vague instruction that produces the wrong thing costs the build, the
|
|
125
|
+
review, the revert, and the rebuild. Two hundred tokens of precise
|
|
126
|
+
instructions routinely save several thousand.
|
|
127
|
+
|
|
128
|
+
## Environment knobs
|
|
129
|
+
|
|
130
|
+
| Variable | Effect |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `MAX_THINKING_TOKENS` | Hard ceiling on reasoning per turn. |
|
|
133
|
+
| `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | Hard ceiling on response length. Too low truncates mid-answer. |
|
|
134
|
+
| `DISABLE_BUNDLED_SKILLS` | Drops the bundled skill catalogue from the prompt. |
|
|
135
|
+
| `DISABLE_NONESSENTIAL_TRAFFIC` | Suppresses background requests. Small effect. |
|
|
136
|
+
|
|
137
|
+
Ceilings are blunt. Effort level gets you most of the same saving while
|
|
138
|
+
letting the model still finish its sentence.
|
|
139
|
+
|
|
140
|
+
## What does not save money
|
|
141
|
+
|
|
142
|
+
**Clearing between related tasks.** A fresh session pays a fresh cache write
|
|
143
|
+
and re-reads the same files. Clear at real boundaries, not every few turns.
|
|
144
|
+
|
|
145
|
+
**Terse prompts.** See lever 10. The tokens saved on the instruction come back
|
|
146
|
+
multiplied in rework.
|
|
147
|
+
|
|
148
|
+
**Turning thinking off entirely.** On Opus 5 this has known failure modes: the
|
|
149
|
+
model can write a tool call into its visible text instead of actually calling
|
|
150
|
+
the tool, which fails silently and pollutes later turns. Lower the effort level
|
|
151
|
+
instead. It is cheaper *and* it still works.
|
|
152
|
+
|
|
153
|
+
**Asking for shorter answers when the cost is elsewhere.** If the spend is in
|
|
154
|
+
tool results, prose length is rounding error. Check where it actually went
|
|
155
|
+
before optimising the visible part.
|
|
156
|
+
|
|
157
|
+
## Rough arithmetic
|
|
158
|
+
|
|
159
|
+
Illustrative, not measured. A 60-turn session with about 80k tokens of cached
|
|
160
|
+
context, on Opus rates:
|
|
161
|
+
|
|
162
|
+
| | Per turn | Over 60 turns |
|
|
163
|
+
| --- | --- | --- |
|
|
164
|
+
| Re-reading cached context | $0.04 | $2.40 |
|
|
165
|
+
| Output at high effort, ~2,500 tokens | $0.063 | $3.75 |
|
|
166
|
+
| Output at low effort, ~700 tokens | $0.018 | $1.05 |
|
|
167
|
+
|
|
168
|
+
Same work, same context, roughly 44 percent cheaper. Halving the context on
|
|
169
|
+
top of that takes another $1.20 off. Neither change removes a single feature
|
|
170
|
+
from what gets built.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Turns the expensive knobs down and remembers what they were.
|
|
5
|
+
//
|
|
6
|
+
// effortLevel is the setting behind the /effort picker. It drives how much
|
|
7
|
+
// reasoning the model does per turn, and reasoning is billed as output
|
|
8
|
+
// tokens, which are the priciest tokens in the request. Dropping xhigh to
|
|
9
|
+
// low is the single largest per-turn saving available without changing
|
|
10
|
+
// model or scope.
|
|
11
|
+
//
|
|
12
|
+
// node scripts/lowpower.js status
|
|
13
|
+
// node scripts/lowpower.js on
|
|
14
|
+
// node scripts/lowpower.js on --effort medium --model sonnet
|
|
15
|
+
// node scripts/lowpower.js off
|
|
16
|
+
//
|
|
17
|
+
// The saved values live in usage-limits-lowpower.json inside the config
|
|
18
|
+
// directory, so "off" puts back exactly what was there, including keys
|
|
19
|
+
// that were not set in the first place.
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
26
|
+
const MANAGED_KEYS = ['effortLevel', 'model'];
|
|
27
|
+
const DEFAULTS = { effortLevel: 'low' };
|
|
28
|
+
|
|
29
|
+
function configDir() {
|
|
30
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function settingsFile() {
|
|
34
|
+
return path.join(configDir(), 'settings.json');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stateFile() {
|
|
38
|
+
return path.join(configDir(), 'usage-limits-lowpower.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readJson(file) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err.code === 'ENOENT') return null;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Replace through a temporary file so an interrupted run cannot leave
|
|
51
|
+
// settings.json half written.
|
|
52
|
+
function writeJson(file, value) {
|
|
53
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
54
|
+
const temp = file + '.usage-limits-tmp';
|
|
55
|
+
fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
56
|
+
fs.renameSync(temp, file);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseArgs(argv) {
|
|
60
|
+
const args = { command: null, effort: null, model: null, dryRun: false };
|
|
61
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
62
|
+
const arg = argv[i];
|
|
63
|
+
if (arg === '--dry-run') args.dryRun = true;
|
|
64
|
+
else if (arg === '--effort') args.effort = argv[++i];
|
|
65
|
+
else if (arg === '--model') args.model = argv[++i];
|
|
66
|
+
else if (arg.startsWith('--effort=')) args.effort = arg.slice('--effort='.length);
|
|
67
|
+
else if (arg.startsWith('--model=')) args.model = arg.slice('--model='.length);
|
|
68
|
+
else if (!args.command) args.command = arg;
|
|
69
|
+
}
|
|
70
|
+
if (!args.command) args.command = 'status';
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Work out the new settings without touching disk. Returns the object to
|
|
75
|
+
// write, the state to remember, and a readable list of what moved.
|
|
76
|
+
function planApply(settings, options, existingState) {
|
|
77
|
+
const current = settings || {};
|
|
78
|
+
const wanted = {};
|
|
79
|
+
const effort = options.effort || DEFAULTS.effortLevel;
|
|
80
|
+
|
|
81
|
+
if (EFFORT_LEVELS.indexOf(effort) === -1) {
|
|
82
|
+
throw new Error('unknown effort "' + effort + '", expected one of ' + EFFORT_LEVELS.join(', '));
|
|
83
|
+
}
|
|
84
|
+
wanted.effortLevel = effort;
|
|
85
|
+
if (options.model) wanted.model = options.model;
|
|
86
|
+
|
|
87
|
+
// A second "on" must not record the already-lowered values as the
|
|
88
|
+
// originals, or "off" would restore low power forever.
|
|
89
|
+
const saved =
|
|
90
|
+
existingState && existingState.previous ? existingState.previous : capture(current);
|
|
91
|
+
|
|
92
|
+
const next = Object.assign({}, current, wanted);
|
|
93
|
+
const changes = [];
|
|
94
|
+
for (const key of Object.keys(wanted)) {
|
|
95
|
+
const before = current[key] === undefined ? '(unset)' : current[key];
|
|
96
|
+
if (String(before) !== String(wanted[key])) {
|
|
97
|
+
changes.push(key + ': ' + before + ' -> ' + wanted[key]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
settings: next,
|
|
103
|
+
state: { savedAt: new Date().toISOString(), previous: saved, applied: wanted },
|
|
104
|
+
changes,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Snapshot only the keys this tool is allowed to touch. A key that was not
|
|
109
|
+
// present is recorded as null so it can be removed again on restore.
|
|
110
|
+
function capture(settings) {
|
|
111
|
+
const previous = {};
|
|
112
|
+
for (const key of MANAGED_KEYS) {
|
|
113
|
+
previous[key] = Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : null;
|
|
114
|
+
}
|
|
115
|
+
return previous;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function planRestore(settings, state) {
|
|
119
|
+
if (!state || !state.previous) {
|
|
120
|
+
return { settings: settings || {}, changes: [], restored: false };
|
|
121
|
+
}
|
|
122
|
+
const next = Object.assign({}, settings || {});
|
|
123
|
+
const changes = [];
|
|
124
|
+
|
|
125
|
+
for (const key of MANAGED_KEYS) {
|
|
126
|
+
const before = next[key] === undefined ? '(unset)' : next[key];
|
|
127
|
+
const target = state.previous[key];
|
|
128
|
+
if (target === null || target === undefined) {
|
|
129
|
+
if (Object.prototype.hasOwnProperty.call(next, key)) {
|
|
130
|
+
delete next[key];
|
|
131
|
+
changes.push(key + ': ' + before + ' -> (unset)');
|
|
132
|
+
}
|
|
133
|
+
} else if (String(before) !== String(target)) {
|
|
134
|
+
next[key] = target;
|
|
135
|
+
changes.push(key + ': ' + before + ' -> ' + target);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { settings: next, changes, restored: true };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function describe(settings, state) {
|
|
143
|
+
const lines = [];
|
|
144
|
+
const current = settings || {};
|
|
145
|
+
lines.push('Low power ' + (state ? 'on since ' + state.savedAt : 'off'));
|
|
146
|
+
lines.push('effortLevel ' + (current.effortLevel || '(unset, defaults to xhigh)'));
|
|
147
|
+
lines.push('model ' + (current.model || '(unset)'));
|
|
148
|
+
if (state && state.previous) {
|
|
149
|
+
const previous = state.previous;
|
|
150
|
+
const parts = MANAGED_KEYS.map(
|
|
151
|
+
(key) => key + '=' + (previous[key] === null ? '(unset)' : previous[key])
|
|
152
|
+
);
|
|
153
|
+
lines.push('will restore ' + parts.join(' '));
|
|
154
|
+
}
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function main(argv) {
|
|
159
|
+
const args = parseArgs(argv);
|
|
160
|
+
const file = settingsFile();
|
|
161
|
+
const settings = readJson(file) || {};
|
|
162
|
+
const state = readJson(stateFile());
|
|
163
|
+
|
|
164
|
+
if (args.command === 'status') {
|
|
165
|
+
process.stdout.write(describe(settings, state) + '\n');
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (args.command === 'on') {
|
|
170
|
+
const plan = planApply(settings, args, state);
|
|
171
|
+
if (!plan.changes.length && state) {
|
|
172
|
+
process.stdout.write('Already in low power. Nothing to change.\n');
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
if (args.dryRun) {
|
|
176
|
+
process.stdout.write('Would change:\n ' + (plan.changes.join('\n ') || '(nothing)') + '\n');
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
if (!state && fs.existsSync(file)) fs.copyFileSync(file, file + '.usage-limits-backup');
|
|
180
|
+
writeJson(file, plan.settings);
|
|
181
|
+
writeJson(stateFile(), plan.state);
|
|
182
|
+
process.stdout.write(
|
|
183
|
+
'Low power on.\n ' + (plan.changes.join('\n ') || '(nothing to change)') + '\n' +
|
|
184
|
+
'Applies to new sessions. For the session you are in, run /effort ' +
|
|
185
|
+
plan.state.applied.effortLevel + '.\n'
|
|
186
|
+
);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (args.command === 'off') {
|
|
191
|
+
if (!state) {
|
|
192
|
+
process.stdout.write('Low power is not on. Nothing to restore.\n');
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
const plan = planRestore(settings, state);
|
|
196
|
+
if (args.dryRun) {
|
|
197
|
+
process.stdout.write('Would restore:\n ' + (plan.changes.join('\n ') || '(nothing)') + '\n');
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
writeJson(file, plan.settings);
|
|
201
|
+
fs.unlinkSync(stateFile());
|
|
202
|
+
process.stdout.write(
|
|
203
|
+
'Low power off.\n ' + (plan.changes.join('\n ') || '(nothing to change)') + '\n'
|
|
204
|
+
);
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
process.stderr.write('usage: lowpower.js [status|on|off] [--effort level] [--model name] [--dry-run]\n');
|
|
209
|
+
return 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (require.main === module) {
|
|
213
|
+
try {
|
|
214
|
+
process.exitCode = main(process.argv.slice(2));
|
|
215
|
+
} catch (err) {
|
|
216
|
+
process.stderr.write('lowpower: ' + (err && err.message ? err.message : String(err)) + '\n');
|
|
217
|
+
process.exitCode = 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
main,
|
|
223
|
+
EFFORT_LEVELS,
|
|
224
|
+
MANAGED_KEYS,
|
|
225
|
+
parseArgs,
|
|
226
|
+
capture,
|
|
227
|
+
planApply,
|
|
228
|
+
planRestore,
|
|
229
|
+
describe,
|
|
230
|
+
};
|