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 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.
@@ -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);