claude-usage-limits 1.40.5 → 1.41.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "usage-limits",
3
3
  "displayName": "Usage Limits",
4
- "version": "1.40.5",
4
+ "version": "1.41.0",
5
5
  "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
6
6
  "author": {
7
7
  "name": "Ridelink",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usage-limits",
3
- "version": "1.40.5",
3
+ "version": "1.41.0",
4
4
  "description": "Reports how much of your Codex usage limit is left as turns of work rather than a percentage, prices a job before you start it, and counts the other agents sharing the same budget.",
5
5
  "author": {
6
6
  "name": "Ridelink",
package/README.md CHANGED
@@ -19,6 +19,21 @@ opens with the answer instead:
19
19
 
20
20
  Nobody read a chart to get that. The numbers reached the model, not you.
21
21
 
22
+ ![The usage report: the 5-hour window is binding with about 44 turns left, and at the current pace it runs out 38 minutes before the reset.](docs/img/usage-report.png)
23
+
24
+ <sub>Real output of `npx claude-usage-limits` on the author's machine, 2026-09-26: the Claude Code windows and the closing verdict, with the Codex and per-model sections left out. The plugin puts the same reading into Claude's context, as the budget line, before every prompt.</sub>
25
+
26
+ Install it as a Claude Code plugin:
27
+
28
+ ```
29
+ /plugin marketplace add ridelink0/claude-code-usage-limits
30
+ /plugin install usage-limits@usage-limits
31
+ ```
32
+
33
+ Or see the report without installing anything: `npx claude-usage-limits`.
34
+ Codex, the plain-skill route and keeping it updated are under
35
+ [Install](#install).
36
+
22
37
  ## What it runs on your machine, and what it never does
23
38
 
24
39
  Installing this plugin registers six Claude Code hooks, which means Node runs on
@@ -1337,18 +1352,6 @@ percentage and any pressure, the `max` line must never be longer than the
1337
1352
  worse, and every mode path, alias, bound and guard is exercised before
1338
1353
  `settings.json` is compared byte for byte.
1339
1354
 
1340
- ## Status
1341
-
1342
- It works and I use it daily.
1343
-
1344
- What I am not doing is fielding feature requests or support questions. If you
1345
- want it to behave differently, fork it and change it, which is what the MIT
1346
- licence is there for. Do not wait on me to add something for you.
1347
-
1348
- ## License
1349
-
1350
- MIT. See [LICENSE](LICENSE).
1351
-
1352
1355
  ## The wall is not the end of the budget
1353
1356
 
1354
1357
  A full window is not always a reason to stop, and the plugin now says which.
@@ -1379,119 +1382,20 @@ line said the budget was nearly gone, and the work stopped - with the 5-hour
1379
1382
  window at 46 and every other model untouched. One command would have carried it
1380
1383
  on.
1381
1384
 
1382
- ## 1.23.0 - the ceiling, and three hosts that can be told no
1383
-
1384
- Everything before this release reported. This one intervenes.
1385
-
1386
- ### The Codex meter was going blind about a third of the time
1387
-
1388
- Codex writes more than one meter into its rollouts, and they are not successive
1389
- readings of one thing. A ChatGPT Plus session carries `limit_id: "codex"`, which
1390
- holds the 5-hour and weekly windows, interleaved with `limit_id: "premium"`,
1391
- which holds the credit balance and has `primary` and `secondary` set to `null`.
1392
- The reader took whichever was written last.
1393
-
1394
- On the machine this was found on, two of every six rollouts ended on a `premium`
1395
- payload - and when one did, the plugin reported no windows at all. Measured on a
1396
- real session: the final line was a `premium` payload two lines after a `codex`
1397
- payload reading **99 per cent of the 5-hour window**, and what reached the agent
1398
- was a meter with nothing in it.
1399
-
1400
- That is the whole of "Codex does not slow down when the limit is close": nothing
1401
- ever told it the limit was close. The reader now keeps the newest reading of each
1402
- meter and prefers the one that actually describes a window. Nothing is merged or
1403
- synthesised - the payload returned is one Codex really wrote.
1404
-
1405
- ### An old snapshot is an estimate, not a reading
1406
-
1407
- There was already a warning for a reading spent past its own remainder. It could
1408
- never fire for a snapshot taken at the start of a window, because everything
1409
- spent since is still inside the remainder.
1410
-
1411
- So a session ran for most of an hour being told **7 per cent** while the account
1412
- was at **41**: Claude Code's own cache had not moved in fifty minutes, the
1413
- plugin's live reading was rate-limited into backoff, and the correction was
1414
- quietly carrying the entire difference on its own. A correction is a good
1415
- adjustment to a recent snapshot and a bad substitute for an old one, because the
1416
- pricing error compounds with every point it has to bridge. Past fifteen minutes
1417
- the brief now says the figure is an estimate that can run high or low and points
1418
- at `/usage`. It is only called a floor when the correction has been refused
1419
- outright and the raw snapshot is all that is shown, since that really is a
1420
- lower bound; a snapshot plus a correction is not, and on 2026-09-20 it ran 13
1421
- to 17 points high.
1422
-
1423
- ### The ceiling
1424
-
1425
- node bin/cli.js mode --cap 60
1426
- node bin/cli.js mode --cap off
1427
-
1428
- Past the ceiling, **fan-out calls are refused at the hook** - `Agent`, `Task`,
1429
- `Workflow` and their equivalents on each host. Everything else keeps working at
1430
- any percentage: reads, edits, tests, commands. The work still finishes, just
1431
- sequentially, in one session, which is where the saving is. A measured fan-out
1432
- costs between 2.6x and 5.9x the same work done in sequence, because every agent
1433
- warms its own cache from cold and none can report back until they all stop.
1434
-
1435
- It is off until you set a number, it never fires without a reading behind it, and
1436
- the refusal says what to do instead - a denial that only says "over budget" gets
1437
- retried.
1438
-
1439
- It is judged on the fullest window this session can spend into, and the refusal
1440
- names that window. A weekly scoped to one model counts only while that model
1441
- runs - by the setting, or by the model the session's last reply came from, so a
1442
- `/model` switch is seen - and a window whose reset has passed does not count.
1443
- Until 1.39.0 it took the highest number on disk, and an Opus session was refused
1444
- on the Fable weekly with a message calling it "the binding window".
1445
-
1446
- **Why a refusal rather than a sentence.** On Codex the reported figure
1447
- demonstrably does not change behaviour, and the reason is not stubbornness.
1448
- `gpt-6-astra`'s own system prompt, shipped in `models_cache.json`, says: *"Do not
1449
- settle for a partial or 'helpful enough' solution that does not fully satisfy the
1450
- user's task to save time, effort or tokens"* - and ranks the live user
1451
- instruction above anything an `AGENTS.md` or a skill says. A line asking it to
1452
- economise is arguing with its own instructions, and losing.
1453
-
1454
- ### The Codex subagent clamp
1455
-
1456
- `lowpower on --host codex` now also bounds `[agents]`. Two facts read out of the
1457
- model catalog Codex itself caches, not out of documentation:
1458
-
1459
- gpt-6-astra: default_reasoning_level = "low"
1460
- multi_agent_reasoning_effort = "xhigh"
1461
-
1462
- Astra's own default effort is the cheapest one, and its subagents run at the
1463
- dearest one **no matter what the session is set to**. Lowering effort without
1464
- bounding them leaves the most expensive path in the product untouched. The clamp
1465
- is a marked block, removed exactly by `lowpower off`, and it refuses outright
1466
- rather than writing a second `[agents]` table over one you wrote yourself.
1467
- `--no-agents` declines just that half.
1468
-
1469
- ### Antigravity
1470
-
1471
- node skills/usage-limits/scripts/install-antigravity.js on
1472
-
1473
- Installs into `~/.gemini/config/plugins/usage-limits/`. `PreInvocation` carries
1474
- the budget line as an injected ephemeral message; `PreToolUse` carries the
1475
- ceiling, which Antigravity implements as a real `decision: "deny"`.
1476
-
1477
- One thing is said plainly rather than papered over: **Antigravity publishes no
1478
- remaining quota anywhere readable on disk.** It refreshes quota - its own log
1479
- says so - and keeps it in memory. A previous version filled that gap by reporting
1480
- *Claude's* meter under a `cross-agent-claude` source, next to a hardcoded plan
1481
- and a model name invented for a settings file that has no model key in it. All of
1482
- that is gone. Where there is nothing to read, the report says the quota is
1483
- unreadable.
1484
-
1485
- ### Caveman, and why it is not here
1486
-
1487
- It was on the list for this release and it is not in it. JetBrains ran a
1488
- controlled A/B of the caveman skill - 86 tasks, paired, ~240 billed trials - and
1489
- measured output tokens down **8.5 per cent** against an advertised 65, with
1490
- quality differences indistinguishable from noise (p=0.82). An independent
1491
- benchmark found the literal instruction `be brief.` matched or beat it, and
1492
- because agentic cost is input-dominated, the skill's own rules riding along every
1493
- turn can cost more than they save. Its engine is also BSL-licensed, not open.
1494
-
1495
- Claude Code already ships a built-in **Concise** output style that does the same
1496
- job in the cached system-prompt layer at no marginal cost. Use that. This plugin
1497
- will not ship a measured 8 per cent as a 65 per cent saving.
1385
+ ## Release notes
1386
+
1387
+ What changed in each version is in the [GitHub Releases](https://github.com/ridelink0/claude-code-usage-limits/releases).
1388
+ The long notes for 1.23.0, the release that started intervening rather than
1389
+ only reporting, are in [docs/1.23.0.md](docs/1.23.0.md).
1390
+
1391
+ ## Status
1392
+
1393
+ It works and I use it daily.
1394
+
1395
+ What I am not doing is fielding feature requests or support questions. If you
1396
+ want it to behave differently, fork it and change it, which is what the MIT
1397
+ licence is there for. Do not wait on me to add something for you.
1398
+
1399
+ ## License
1400
+
1401
+ MIT. See [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.40.5",
3
+ "version": "1.41.0",
4
4
  "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
5
5
  "keywords": [
6
6
  "claude",
@@ -1253,15 +1253,20 @@ function briefText(input) {
1253
1253
  // (autoContinueAtUsageLimit, code.claude.com/docs/en/settings-reference). In
1254
1254
  // place, with the context intact, that is strictly better than a wake: no
1255
1255
  // hand-off file to re-read and nothing lost. So the wake is the route for a
1256
- // session that will be CLOSED, and saying so is what stops both firing for the
1257
- // same reset - which would run the work twice and spend the weekly twice.
1256
+ // session that will be CLOSED. Both firing for the same reset would run the
1257
+ // work twice and spend the weekly twice, so since 1.41.0 the wake looks for
1258
+ // this session's process first and stands down when it is open and working
1259
+ // again (wake.js, liveSession). What the CLI does NOT do is continue a session
1260
+ // that stopped short of the wall, at a cap, so that case is said too.
1258
1261
  if (carry && carry.armed && carry.armed.mode === 'resume' && parts.autoContinue && parts.autoContinue.value) {
1259
1262
  relaySentences.push(
1260
1263
  'Claude Code\'s own "Continue automatically at usage limit" is on (' + parts.autoContinue.source +
1261
- '), so if this terminal is still open at the reset the CLI carries THIS session across by ' +
1262
- 'itself, in place, with its context intact - better than any wake. The wake is the route for a ' +
1263
- 'session that is closed by then. Both firing for the same reset would start the work twice and ' +
1264
- 'spend the weekly twice, so if the terminal is staying open, one of the two is worth standing down.'
1264
+ '), so if this terminal is still open when the limit is hit, the CLI carries THIS session across ' +
1265
+ 'the reset by itself, in place, with its context intact - better than any wake. The wake is the ' +
1266
+ 'route for a session that is closed by then. Both firing for the same reset would start the work ' +
1267
+ 'twice and spend the weekly twice, so the wake checks for this session first and opens no second ' +
1268
+ 'window while it is open and working again. The CLI does not continue a session that stopped short ' +
1269
+ 'of the limit (at a cap); the wake gives such a session a few minutes, then opens it in one window.'
1265
1270
  );
1266
1271
  }
1267
1272
  if (carry && carry.last) {
@@ -904,19 +904,50 @@ function verifyRegistration(state, next, wanted, now) {
904
904
  return { ok: true, nextRun: at };
905
905
  }
906
906
 
907
- const HIDDEN_HOST = path.join(process.env.SystemRoot || 'C:' + path.sep + 'Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
907
+ const SYSTEM32 = path.join(process.env.SystemRoot || 'C:' + path.sep + 'Windows', 'System32');
908
+ const HIDDEN_HOST = path.join(SYSTEM32, 'WindowsPowerShell', 'v1.0', 'powershell.exe');
909
+ const HEADLESS_HOST = path.join(SYSTEM32, 'conhost.exe');
910
+
911
+ // conhost --headless arrived with the pseudoconsole in Windows 10 1809 (build
912
+ // 17763). Older builds, and anything that is not Windows, keep the PowerShell
913
+ // route alone.
914
+ function headlessAvailable() {
915
+ if (process.platform !== 'win32') return false;
916
+ const build = Number(String(os.release()).split('.')[2]);
917
+ return Number.isFinite(build) && build >= 17763 && fs.existsSync(HEADLESS_HOST);
918
+ }
908
919
 
909
920
  // The task used to run node.exe directly, and node.exe is a console program:
910
921
  // under the scheduler, in an interactive session, it gets a console window
911
922
  // of its own. That was the "blank terminal that says Claude" - the wake's own
912
923
  // console, shared by its headless child - and closing it killed both (task
913
- // result 0xC000013A), so the wake never wrote its outcome. PowerShell can
914
- // start hidden and node inherits that; the session the wake opens for the
915
- // user is a new window (cmd /c start) and stays visible.
916
- function hiddenAction(launcher, cwd) {
924
+ // result 0xC000013A), so the wake never wrote its outcome.
925
+ //
926
+ // PowerShell's -WindowStyle Hidden was the first fix, and it only works where
927
+ // the old console host draws the window. Windows 11 hands every new console to
928
+ // Windows Terminal by default, and Terminal ignores the flag: measured
929
+ // 2026-09-27, the task opened a "powershell.exe" Terminal window beside the
930
+ // Claude window the wake started, so the user saw two terminals for one relay
931
+ // and closing the wrong one killed the wake again (0xC000013A the same
932
+ // evening). conhost --headless gives the console no window at all, so there
933
+ // is nothing to hand to Terminal; measured the same evening, the same task
934
+ // under it opened no window, and the session window the wake opens (cmd /c
935
+ // start) still appeared, alone.
936
+ function hiddenAction(launcher, cwd, headless) {
937
+ const useHeadless = headless === undefined ? headlessAvailable() : headless;
938
+ const run = '-Command "& ' + psQuote(launcher) + '"';
939
+ if (useHeadless) {
940
+ // No -WindowStyle here: there is no window to style, and the 20 characters
941
+ // matter to the schtasks fallback, which refuses a /TR over 261.
942
+ return {
943
+ execute: HEADLESS_HOST,
944
+ argument: '--headless "' + HIDDEN_HOST + '" -NoProfile -NonInteractive ' + run,
945
+ cwd: cwd || os.homedir(),
946
+ };
947
+ }
917
948
  return {
918
949
  execute: HIDDEN_HOST,
919
- argument: '-NoProfile -NonInteractive -WindowStyle Hidden -Command "& ' + psQuote(launcher) + '"',
950
+ argument: '-NoProfile -NonInteractive -WindowStyle Hidden ' + run,
920
951
  cwd: cwd || os.homedir(),
921
952
  };
922
953
  }
@@ -1884,7 +1915,7 @@ if (require.main === module) {
1884
1915
  );
1885
1916
  }
1886
1917
 
1887
- module.exports = { workWithContinuation, reapLost, hiddenAction, BUGCHECK_LINE,
1918
+ module.exports = { workWithContinuation, reapLost, hiddenAction, headlessAvailable, HEADLESS_HOST, HIDDEN_HOST, BUGCHECK_LINE,
1888
1919
  taskAction, wakeLauncherFile, wakeLauncherScript, writeWakeLauncher, sweepWakeLaunchers, batchArg, describeSpawn, PS_ROUTE_MS, SCHTASKS_MS,
1889
1920
  records, armedFor, putRecord, dropRecord, preflightPrompts, claudeJsonFile, projectKeys, isHome, launchDirFor, applySetting, settingIs,
1890
1921
  DEFAULTS,
@@ -308,6 +308,106 @@ function transcriptExists(id) {
308
308
  return false;
309
309
  }
310
310
 
311
+ function transcriptFile(id) {
312
+ const root = path.join(relay.configDir(), 'projects');
313
+ try {
314
+ for (const dir of fs.readdirSync(root)) {
315
+ const file = path.join(root, dir, id + '.jsonl');
316
+ if (fs.existsSync(file)) return file;
317
+ }
318
+ } catch (err) {
319
+ // No projects folder at all.
320
+ }
321
+ return null;
322
+ }
323
+
324
+ // ONE TERMINAL.
325
+ //
326
+ // The session a relay was armed for is often still open. Claude Code's own
327
+ // autoContinueAtUsageLimit (on by default) carries an open session across the
328
+ // reset in place, and a wake that opened `claude --resume` beside it put two
329
+ // terminals on the screen running the same conversation - the same work
330
+ // started twice and the weekly spent twice. Gev, 2026-09-27: "fix it to where
331
+ // it makes 1 terminal and not 2".
332
+ //
333
+ // Claude Code keeps one file per running process in <config>/sessions,
334
+ // named for the pid: { pid, sessionId, procStart, kind, cwd, ... }. procStart
335
+ // is the process creation time (a Windows FILETIME, read against
336
+ // Get-Process on 2026-09-27: 134350239768728478 on both sides), which is what
337
+ // tells a live session from a dead one whose pid Windows has handed to
338
+ // something else. A file whose process is gone is left where it is: it is
339
+ // Claude Code's, not the relay's.
340
+ function pidAlive(pid) {
341
+ try {
342
+ process.kill(pid, 0);
343
+ return true;
344
+ } catch (err) {
345
+ return err.code === 'EPERM';
346
+ }
347
+ }
348
+
349
+ function processStart(pid) {
350
+ if (process.platform !== 'win32') return null;
351
+ const shell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
352
+ const run = spawnSync(shell, ['-NoProfile', '-NonInteractive', '-Command', '(Get-Process -Id ' + Number(pid) + ' -ErrorAction Stop).StartTime.ToFileTimeUtc()'], {
353
+ encoding: 'utf8',
354
+ timeout: 30000,
355
+ windowsHide: true,
356
+ });
357
+ const said = String(run.stdout || '').trim();
358
+ return run.status === 0 && /^\d+$/.test(said) ? said : null;
359
+ }
360
+
361
+ function liveSession(id, io) {
362
+ const ops = Object.assign({ alive: pidAlive, started: processStart }, io || null);
363
+ const dir = path.join(relay.configDir(), 'sessions');
364
+ let names;
365
+ try {
366
+ names = fs.readdirSync(dir);
367
+ } catch (err) {
368
+ return null;
369
+ }
370
+ for (const name of names) {
371
+ if (!/^\d+\.json$/.test(name)) continue;
372
+ let held;
373
+ try {
374
+ held = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
375
+ } catch (err) {
376
+ continue;
377
+ }
378
+ if (!held || held.sessionId !== id) continue;
379
+ const pid = Number(held.pid);
380
+ if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) continue;
381
+ if (!ops.alive(pid)) continue;
382
+ // A start time that can be read and does not match is a recycled pid.
383
+ // One that cannot be read (not Windows, or the query failed) leaves the
384
+ // pid's word as the answer.
385
+ if (held.procStart) {
386
+ const started = ops.started(pid);
387
+ if (started !== null && String(started) !== String(held.procStart)) continue;
388
+ }
389
+ return { pid, kind: held.kind || null, cwd: held.cwd || null };
390
+ }
391
+ return null;
392
+ }
393
+
394
+ // Written to since `since`: the open session is working again, which is what
395
+ // the CLI's own continue looks like from outside.
396
+ function transcriptActiveSince(id, since) {
397
+ const file = transcriptFile(id);
398
+ if (!file) return false;
399
+ try {
400
+ return fs.statSync(file).mtimeMs >= since;
401
+ } catch (err) {
402
+ return false;
403
+ }
404
+ }
405
+
406
+ // How long a wake that finds the session open but quiet waits for it to start
407
+ // working by itself before opening the one window it would have opened anyway.
408
+ const LIVE_WAIT_MS = 3 * MINUTE;
409
+ const LIVE_POLL_MS = 15 * 1000;
410
+
311
411
  function readExit(file) {
312
412
  try {
313
413
  const code = parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
@@ -569,6 +669,7 @@ async function run(now, argv, overrides) {
569
669
  {
570
670
  windowReopened, deliverClaude, deliverCodex, toast, userIsPresent,
571
671
  arm: relay.arm, capabilities: relay.capabilities, reachable: net.reachable,
672
+ liveSession, transcriptActiveSince, sleep: sleepMs, now: Date.now,
572
673
  },
573
674
  overrides || null
574
675
  );
@@ -615,6 +716,35 @@ async function run(now, argv, overrides) {
615
716
  return { outcome: 'rescheduled', attempt };
616
717
  }
617
718
 
719
+ // ONE TERMINAL (see liveSession). An open session that is working again -
720
+ // written to since the window reset - has been carried across by the CLI,
721
+ // and a second window would run the same work twice, so the wake stands
722
+ // down. One that is open but quiet gets LIVE_WAIT_MS to start by itself
723
+ // (the CLI's continue, or the session's own timer); if it is closed in that
724
+ // time, or stays quiet, the wake opens its one window, because nothing
725
+ // outside a session can type into it and the plan still has to run.
726
+ if (record.host !== host.CODEX) {
727
+ const since = Number.isFinite(record.wakeAt) ? record.wakeAt - (config.graceMinutes || 0) * MINUTE : now - 5 * MINUTE;
728
+ const started = deps.now();
729
+ let open = deps.liveSession(record.id);
730
+ while (open) {
731
+ if (deps.transcriptActiveSince(record.id, since)) {
732
+ deps.toast(
733
+ 'Usage limits: carried on in place',
734
+ (record.project || path.basename(record.cwd)) + ' is still open and working again in its own terminal, so no second window was opened.'
735
+ );
736
+ finish(state, record, 'live', 'the session is open in process ' + open.pid + ' and working again; no second window', now);
737
+ return { outcome: 'live', pid: open.pid };
738
+ }
739
+ if (deps.now() - started >= LIVE_WAIT_MS) break;
740
+ deps.sleep(LIVE_POLL_MS);
741
+ open = deps.liveSession(record.id);
742
+ }
743
+ if (open) {
744
+ relay.note('wake ' + record.id + ': the session is open in process ' + open.pid + ' but idle since the reset; opening it in a window', now);
745
+ }
746
+ }
747
+
618
748
  // THE PREFLIGHT.
619
749
  //
620
750
  // The retry above is the right shape but the wrong budget for the failure it
@@ -848,4 +978,4 @@ if (require.main === module) {
848
978
  );
849
979
  }
850
980
 
851
- module.exports = { launcherScriptPosix, shQuote, visibleArgs, launcherScript, pointerPrompt, transcriptExists, wakePromptFile, LAUNCH_GRACE_MS, run, toast, userIsPresent, claudeArgs, deliverClaude, deliverCodex, windowReopened, argOf, appendRun, spawnOptionsFor };
981
+ module.exports = { liveSession, transcriptActiveSince, pidAlive, LIVE_WAIT_MS, LIVE_POLL_MS, launcherScriptPosix, shQuote, visibleArgs, launcherScript, pointerPrompt, transcriptExists, wakePromptFile, LAUNCH_GRACE_MS, run, toast, userIsPresent, claudeArgs, deliverClaude, deliverCodex, windowReopened, argOf, appendRun, spawnOptionsFor };