autolimit 0.2.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 +366 -0
- package/bin/autolimit.js +52 -0
- package/lib/autolimit.py +1215 -0
- package/package.json +39 -0
- package/scripts/github-star.js +179 -0
- package/scripts/postinstall.js +134 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 autolimit contributors
|
|
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,366 @@
|
|
|
1
|
+
# autolimit
|
|
2
|
+
|
|
3
|
+
`autolimit` is a terminal-agnostic PTY wrapper for interactive command-line runners.
|
|
4
|
+
|
|
5
|
+
It was built for Claude Code style session-limit messages such as:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
You've hit your session limit · resets 7:40pm (Asia/Seoul)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
When a configured limit message appears, `autolimit` parses the reset time. After that reset time plus a small grace period, it automatically sends the contents of `~/.config/autolimit/message.txt` to the still-running interactive session. If the same limit message is still on screen after the send (the session did not actually unblock), it retries a few times with backoff (60s / 120s / 240s). Time-of-day resets (`resets 7:40pm`, `resets 19:40`) and dated resets (`resets Jul 8 at 10:59am`) are both recognized.
|
|
12
|
+
|
|
13
|
+
If the wrapped process exits after printing the limit message — or exits right after an auto-send, before the message could be processed — `autolimit` can run a runner-specific fallback command after the reset time. Built-in fallback profiles are included for `claude` and `cc`.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
From this project directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The install step creates:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
~/.config/autolimit/message.txt
|
|
27
|
+
~/.config/autolimit/config.json
|
|
28
|
+
~/.local/bin/autolimit -> <this package>/bin/autolimit.js
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
When installation runs in an interactive terminal, `autolimit` may ask whether
|
|
32
|
+
you want to star the GitHub repository. Answering `y` uses an authenticated
|
|
33
|
+
`gh` CLI session, or `GH_TOKEN` / `GITHUB_TOKEN`, to star
|
|
34
|
+
`https://github.com/betive37/autolimit`. Set
|
|
35
|
+
`AUTOLIMIT_GITHUB_STAR_PROMPT=0` to skip the prompt. With npm versions that run
|
|
36
|
+
lifecycle scripts in the background, use `npm install --foreground-scripts` if
|
|
37
|
+
you want to see and answer the prompt.
|
|
38
|
+
|
|
39
|
+
If `autolimit` is not found after installation, add `~/.local/bin` to your shell PATH:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
|
|
43
|
+
source ~/.zshrc
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requirements:
|
|
47
|
+
|
|
48
|
+
- Node.js 16+
|
|
49
|
+
- Python 3.9+
|
|
50
|
+
- macOS or Linux interactive terminal
|
|
51
|
+
|
|
52
|
+
The Python runtime uses only the standard library.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
Wrap `cc`:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
autolimit --cc
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Pass arguments to `cc`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
autolimit --cc --model sonnet
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Wrap Claude Code directly:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
autolimit --claude
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Wrap any command:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
autolimit -- npx some-runner --flag
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
or:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
autolimit python3 my_repl.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Why `--cc` works
|
|
87
|
+
|
|
88
|
+
The first non-wrapper argument of the form `--NAME` is interpreted as the command `NAME`.
|
|
89
|
+
|
|
90
|
+
So this:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
autolimit --cc --model sonnet
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
runs this inside the wrapper:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cc --model sonnet
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Similarly:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
autolimit --claude --dangerously-skip-permissions
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
runs:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
claude --dangerously-skip-permissions
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Shell aliases and functions
|
|
115
|
+
|
|
116
|
+
By default, `autolimit` runs the target command through your interactive shell:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
$SHELL -i -c '<command>'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This means shell aliases and functions usually work. For example, if your `.zshrc` contains:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
alias cc='claude --dangerously-skip-permissions'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
then:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
autolimit --cc
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
uses that alias.
|
|
135
|
+
|
|
136
|
+
If you want exact binary execution without shell alias expansion, put `--direct` before the runner selector:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
autolimit --direct --cc
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
To force shell mode explicitly:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
autolimit --shell --cc
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Message configuration
|
|
149
|
+
|
|
150
|
+
Edit the message that gets sent after reset:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
nano ~/.config/autolimit/message.txt
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Default:
|
|
157
|
+
|
|
158
|
+
```text
|
|
159
|
+
이어서 계속 진행해줘.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
You can override it per run:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
AUTOLIMIT_MESSAGE='continue from where you left off' autolimit --cc
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Fallback after process exit
|
|
169
|
+
|
|
170
|
+
Live auto-send works only while the wrapped process is still running.
|
|
171
|
+
|
|
172
|
+
If the process exits after printing the limit message, `autolimit` uses a fallback profile if one is configured. The default `config.json` includes:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"runners": {
|
|
177
|
+
"claude": {
|
|
178
|
+
"fallback": ["{runner}", "{args}", "-c", "-p", "{message}"]
|
|
179
|
+
},
|
|
180
|
+
"cc": {
|
|
181
|
+
"fallback": ["{runner}", "{args}", "-c", "-p", "{message}"]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
So `autolimit --cc --model sonnet` falls back to roughly:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
cc --model sonnet -c -p '<message>'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The actual fallback is run through the same mode as the original invocation. In default shell mode, aliases and functions are respected.
|
|
194
|
+
|
|
195
|
+
The fallback also covers two exit-time edge cases:
|
|
196
|
+
|
|
197
|
+
- The process exits within ~10 seconds of a live auto-send: the message was probably never processed, so the fallback delivers it instead.
|
|
198
|
+
- The live auto-send itself fails because the process is already gone.
|
|
199
|
+
|
|
200
|
+
The wait before a fallback is capped by `max_fallback_wait_hours` (default 26). Press Ctrl-C during the wait to abort cleanly.
|
|
201
|
+
|
|
202
|
+
If your original command already contains a positional prompt or `-p`, adjust the runner's fallback template so the resumed invocation stays valid (`{args}` re-inserts all original arguments).
|
|
203
|
+
|
|
204
|
+
Disable fallback:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
autolimit --no-fallback --cc
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
or:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
AUTOLIMIT_DISABLE_FALLBACK=1 autolimit --cc
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Config file
|
|
217
|
+
|
|
218
|
+
Default config path:
|
|
219
|
+
|
|
220
|
+
```text
|
|
221
|
+
~/.config/autolimit/config.json
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"version": 1,
|
|
229
|
+
"message_file": "/Users/you/.config/autolimit/message.txt",
|
|
230
|
+
"log_file": "/Users/you/.config/autolimit/limit-watch.log",
|
|
231
|
+
"default_timezone": "Asia/Seoul",
|
|
232
|
+
"send_grace_seconds": 5,
|
|
233
|
+
"max_schedule_ahead_hours": 12,
|
|
234
|
+
"max_fallback_wait_hours": 26,
|
|
235
|
+
"patterns": [
|
|
236
|
+
{
|
|
237
|
+
"name": "Claude-style session limit",
|
|
238
|
+
"regex": "You[\\u2019']?ve\\s+hit\\s+your(?:\\s+[\\w-]+)?\\s+limit.{0,120}?resets\\s+(?P<hour>\\d{1,2})(?::(?P<minute>\\d{2}))?\\s*(?P<ampm>[aApP]\\.?[mM]\\.?)?\\s*\\((?P<tzname>[^)]{1,64})\\)"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"name": "Claude-style dated limit",
|
|
242
|
+
"regex": "You[\\u2019']?ve\\s+hit\\s+your(?:\\s+[\\w-]+)?\\s+limit.{0,120}?resets\\s+(?P<month>[A-Za-z]{3,9})\\.?\\s+(?P<day>\\d{1,2})(?:st|nd|rd|th)?,?\\s*(?:at\\s+)?(?P<hour>\\d{1,2})(?::(?P<minute>\\d{2}))?\\s*(?P<ampm>[aApP]\\.?[mM]\\.?)?\\s*\\((?P<tzname>[^)]{1,64})\\)"
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
"runners": {
|
|
246
|
+
"claude": {
|
|
247
|
+
"fallback": ["{runner}", "{args}", "-c", "-p", "{message}"]
|
|
248
|
+
},
|
|
249
|
+
"cc": {
|
|
250
|
+
"fallback": ["{runner}", "{args}", "-c", "-p", "{message}"]
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Settings:
|
|
257
|
+
|
|
258
|
+
- `send_grace_seconds`: extra seconds to wait after the parsed reset time before sending.
|
|
259
|
+
- `max_schedule_ahead_hours`: a parsed reset time further ahead than this is treated as stale scrollback text and ignored (date-carrying patterns are allowed up to 8 days). This prevents an already-handled message from re-scheduling a next-day send.
|
|
260
|
+
- `max_fallback_wait_hours`: upper bound on how long the post-exit fallback will wait for a reset.
|
|
261
|
+
|
|
262
|
+
A `config.json` that still contains the v0.1 default pattern is upgraded to the current defaults automatically on the next run (custom patterns are left untouched).
|
|
263
|
+
|
|
264
|
+
### Custom runner fallback
|
|
265
|
+
|
|
266
|
+
Add a runner profile if a tool can resume non-interactively.
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"runners": {
|
|
271
|
+
"myrunner": {
|
|
272
|
+
"fallback": ["{runner}", "resume", "--message", "{message}"]
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Template tokens:
|
|
279
|
+
|
|
280
|
+
- `{runner}`: the executable name, such as `cc`
|
|
281
|
+
- `{args}`: all original arguments after the executable
|
|
282
|
+
- `{command}`: the full original command list
|
|
283
|
+
- `{message}`: the configured resume message
|
|
284
|
+
|
|
285
|
+
For commands with complex spacing, prefer JSON array templates instead of string templates.
|
|
286
|
+
|
|
287
|
+
### Custom limit patterns
|
|
288
|
+
|
|
289
|
+
Patterns are Python regular expressions. A pattern must provide these captures, either by name or by positional group:
|
|
290
|
+
|
|
291
|
+
- `hour`, required
|
|
292
|
+
- `minute`, optional
|
|
293
|
+
- `ampm`, optional; omit it to accept 24-hour clocks
|
|
294
|
+
- `tzname`, optional; an IANA name (`Asia/Seoul`), a common abbreviation (`KST`, `PST`, ...), or an offset (`UTC+9`); unknown names fall back to `default_timezone` and are logged
|
|
295
|
+
- `month` / `day`, optional named groups for date-carrying messages such as `resets Jul 8 at 10:59am`
|
|
296
|
+
|
|
297
|
+
Named captures are recommended. Keep the gap between the anchor text and the time bounded (for example `.{0,120}?` instead of `.*?`) so unrelated output cannot bridge a match:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"patterns": [
|
|
302
|
+
{
|
|
303
|
+
"name": "example",
|
|
304
|
+
"regex": "resets at (?P<hour>\\d{1,2}):(?P<minute>\\d{2})(?P<ampm>[ap]m) \\((?P<tzname>[^)]+)\\)"
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Utility commands
|
|
311
|
+
|
|
312
|
+
Show help:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
autolimit --help
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Show resolved configuration:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
autolimit --show-config
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
List fallback runner profiles:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
autolimit --list-runners
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Test the default parser:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
autolimit --test-parser
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Show how a command would be wrapped:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
autolimit --dry-run --cc --model sonnet
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Logs
|
|
343
|
+
|
|
344
|
+
Default log path:
|
|
345
|
+
|
|
346
|
+
```text
|
|
347
|
+
~/.config/autolimit/limit-watch.log
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Follow logs:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
tail -f ~/.config/autolimit/limit-watch.log
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Disable logs:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
AUTOLIMIT_LOG=0 autolimit --cc
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Notes
|
|
363
|
+
|
|
364
|
+
`autolimit` does not bypass usage limits. It only waits until the reset time reported by the wrapped tool and then sends a configured message.
|
|
365
|
+
|
|
366
|
+
The wrapper must start the process itself. It cannot attach to an already-running terminal process.
|
package/bin/autolimit.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const script = path.join(__dirname, '..', 'lib', 'autolimit.py');
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
function candidatePythons() {
|
|
11
|
+
const out = [];
|
|
12
|
+
if (process.env.AUTOLIMIT_PYTHON) out.push(process.env.AUTOLIMIT_PYTHON);
|
|
13
|
+
out.push('python3', 'python');
|
|
14
|
+
return [...new Set(out)];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Probe before running: a Python 2 `python` would otherwise die with a
|
|
18
|
+
// confusing SyntaxError instead of a version message.
|
|
19
|
+
function pythonUsable(python) {
|
|
20
|
+
const probe = spawnSync(python, ['-c', 'import sys; sys.exit(0 if sys.version_info >= (3, 9) else 3)'], {
|
|
21
|
+
stdio: 'ignore'
|
|
22
|
+
});
|
|
23
|
+
if (probe.error) return probe.error.code === 'ENOENT' ? 'missing' : 'error';
|
|
24
|
+
return probe.status === 0 ? 'ok' : 'too-old';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const skipped = [];
|
|
28
|
+
for (const python of candidatePythons()) {
|
|
29
|
+
const usable = pythonUsable(python);
|
|
30
|
+
if (usable !== 'ok') {
|
|
31
|
+
skipped.push(`${python} (${usable})`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const result = spawnSync(python, [script, ...args], { stdio: 'inherit' });
|
|
35
|
+
if (result.error) {
|
|
36
|
+
console.error(`[autolimit] Failed to run ${python}: ${result.error.message}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (typeof result.status === 'number') {
|
|
40
|
+
process.exit(result.status);
|
|
41
|
+
}
|
|
42
|
+
if (result.signal) {
|
|
43
|
+
console.error(`[autolimit] Python wrapper terminated by signal ${result.signal}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.error('[autolimit] Python 3.9+ is required, but no suitable interpreter was found.');
|
|
50
|
+
if (skipped.length) console.error(`[autolimit] Tried: ${skipped.join(', ')}`);
|
|
51
|
+
console.error('[autolimit] Set AUTOLIMIT_PYTHON to a Python 3.9+ executable to override.');
|
|
52
|
+
process.exit(127);
|