@ucsandman/legcli 0.7.0 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 (2026-09-16)
4
+
5
+ - **Grok is a supervised and handoff agent.** `leg grok` starts Grok the same
6
+ way `leg claude`, `leg codex` and `leg agy` start those agents: board
7
+ alongside, usage tracking, and a live context handoff bundle. At the limit
8
+ the next agent continues in the same terminal.
9
+ - **Auto-approve is on by default** for new sessions. Leg injects each agent's
10
+ fully-permissive flag at spawn (`--dangerously-skip-permissions` for Claude
11
+ and agy, `--ask-for-approval never` for Codex, `--always-approve` for Grok).
12
+ Opt out with `--no-auto-approve`, `LEG_AUTO_APPROVE=0` /
13
+ `LEG_NO_AUTO_APPROVE=1`, or `"auto_approve": false` in `preferences.json`.
14
+
3
15
  ## 0.7.0 (2026-09-15)
4
16
 
5
17
  - **Rebranded to Leg.** Package is now `legcli` on npm, binary command is `leg`,
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # leg
2
2
 
3
- **Type `leg claude`, `leg codex` or `leg agy` instead of the bare command. You get the same interactive agent; Leg opens a board next to it, watches the usage limit, keeps a handoff bundle current, and when the limit hits it starts the next agent in the same terminal from that bundle.**
3
+ **Type `leg claude`, `leg codex`, `leg agy` or `leg grok` instead of the bare command. You get the same interactive agent; Leg opens a board next to it, watches the usage limit, keeps a handoff bundle current, and when the limit hits it starts the next agent in the same terminal from that bundle.**
4
4
 
5
5
  [![License: commercial](https://img.shields.io/badge/license-commercial-blue.svg)](https://legcli.com/license)
6
6
  [![Node 22+](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://legcli.com/docs/getting-started)
@@ -26,15 +26,15 @@ settings file and never edits yours. `leg claude --model opus` is
26
26
  own worktree and a **Land** button instead of writing over the first.
27
27
  2. **Usage tracking** per agent and account, from what each CLI already
28
28
  exposes: Claude Code's usage endpoint and its `StopFailure` hook, Codex's
29
- read-only app-server rate-limit read, agy's log.
29
+ read-only app-server rate-limit read, agy's log, and Grok's billing proxy endpoint.
30
30
  3. **A context handoff bundle** ([context-handoff-bundle](https://pypi.org/project/context-handoff-bundle/))
31
31
  refreshed as the session goes, so the work is always ready to hand off.
32
32
  4. **The handoff itself.** Near the limit you get a warning. At the limit Leg
33
33
  saves the bundle, stops the agent, and starts the next option in the same
34
34
  terminal from that bundle: another login of the same agent if you added
35
35
  one, otherwise the next agent in the order shown on the terminal card.
36
- The default is claude codex agy, and Settings changes the default for
37
- new terminals. Nothing is retyped.
36
+ The default is claude -> codex -> agy (with grok supported in handoff order),
37
+ and Settings changes the default for new terminals. Nothing is retyped.
38
38
  When every option is out, it tells you which resets first and when, waits
39
39
  for that reset with a countdown, and starts that agent from the bundle.
40
40
 
@@ -43,9 +43,10 @@ Subscription logins only: Leg strips `ANTHROPIC_API_KEY`,
43
43
  `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_API_BASE`, `GEMINI_API_KEY`,
44
44
  `GOOGLE_API_KEY`, `GOOGLE_GEMINI_BASE_URL`, `GOOGLE_GENAI_USE_VERTEXAI`,
45
45
  `GOOGLE_GENAI_USE_ENTERPRISE`, `GOOGLE_CLOUD_PROJECT`,
46
- `GOOGLE_CLOUD_LOCATION`, `GOOGLE_APPLICATION_CREDENTIALS`, `CLAUDECODE`,
47
- `CLAUDE_CODE_*`, `CLAUDE_EFFORT`, and `CLAUDE_PLUGIN_DATA` before any agent
48
- starts. It then sets `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0` for a detached
46
+ `GOOGLE_CLOUD_LOCATION`, `GOOGLE_APPLICATION_CREDENTIALS`, `GROK_API_KEY`,
47
+ `XAI_API_KEY`, `CLAUDECODE`, `CLAUDE_CODE_*`, `CLAUDE_EFFORT`, and
48
+ `CLAUDE_PLUGIN_DATA` before any agent starts. It then sets
49
+ `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0` for a detached
49
50
  Claude print session. Leg never edits `~/.claude/settings.json` or any other
50
51
  settings file of yours; its hooks ride in a separate per-session `--settings`
51
52
  file. The one thing it does write outside `~/.leg` is the folder-trust
@@ -103,7 +104,7 @@ yourself.
103
104
  ## 60-second run
104
105
 
105
106
  Prerequisites: Node 22 or newer, git, Python 3 with pip, and at least one
106
- logged-in agent CLI (`claude`, `codex` or `agy`).
107
+ logged-in agent CLI (`claude`, `codex`, `agy` or `grok`).
107
108
 
108
109
  ```
109
110
  npm install -g @ucsandman/legcli
@@ -136,6 +137,7 @@ which.
136
137
  | claude | `GET api.anthropic.com/api/oauth/usage` with the login Claude Code stored, the same data as `/usage` and the built-in status line (`five_hour`, `seven_day`, `utilization`, `resets_at`); polled every 60 s | `StopFailure` hook with `error: rate_limit` ([docs](https://code.claude.com/docs/en/hooks#stopfailure)) | one extra settings file per session via `--settings`, carrying only Leg's own hooks; `autoContinueAtUsageLimit` is set to `false` because Leg owns the handoff | observed live |
137
138
  | codex | read-only `account/rateLimits/read` through the app-server, polled every 60 s by the board and active attach; windows are identified by duration (300 minutes = 5h, 10080 = 7d) | `task_complete.error.codex_error_info: usage_limit_exceeded`, message "You've hit your usage limit … try again at …" (`codex-rs/protocol/src/error.rs`) | no model turn and no hook are injected; the board reads the CLI backend and records only returned windows | verified by source and regression tests |
138
139
  | agy | none exposed (agy's own status line fetches a quota summary that is written nowhere) | `RESOURCE_EXHAUSTED`, "it resets in …", "out of quota" in the log | `--log-file` per session; `~/.gemini/antigravity-cli/history.jsonl` gives the prompts and conversation id | observed live (a real `RESOURCE_EXHAUSTED` with its reset was read from the log on 2026-09-11) |
140
+ | grok | `GET cli-chat-proxy.grok.com/v1/billing?format=credits` and `GET cli-chat-proxy.grok.com/v1/user?include=subscription` with OAuth token from `~/.grok/auth.json` (`creditUsagePercent`, `currentPeriod` weekly reset); polled every 60 s | `-32003` rate limit error, "You've hit the rate limit for your plan. Try again later.", "Rate limited (429)", `StopFailureKind::RateLimit` (`xai-org/grok-build`) | `--debug-file grok.log` per session; `~/.grok/sessions/<encoded-cwd>/prompt_history.jsonl` gives prompts and session id | verified by source and proxy endpoint |
139
141
 
140
142
  Why not Claude Code's status line JSON (`rate_limits.five_hour.used_percentage`):
141
143
  on 2.1.268 the custom `statusLine` Leg passes through `--settings` did not
@@ -387,15 +389,15 @@ happens after you run `leg accounts add`; that is your call.
387
389
  ## CLI reference
388
390
 
389
391
  ```
390
- leg claude|codex|agy [agent args…] the interactive agent, board alongside, handoff on limit
392
+ leg claude|codex|agy|grok [agent args…] the interactive agent, board alongside, handoff on limit
391
393
  [--no-worktree] share the checkout with a live session instead of a worktree
392
394
  leg sessions ls [--json] every session and its usage
393
395
  leg sessions show|events <id>
394
396
  leg sessions handoff|end <id> same as the board buttons
395
397
  leg sessions rm <id> forget an ended session
396
- leg sessions simulate-limit <id> the real limit path without a real wall (claude, agy)
398
+ leg sessions simulate-limit <id> the real limit path without a real wall (claude, agy, grok)
397
399
  leg accounts ls logins and their 5h/7d usage
398
- leg accounts add <claude|codex> <name> | rm <agent> <name> | terms
400
+ leg accounts add <claude|codex|grok> <name> | rm <agent> <name> | terms
399
401
  leg license the license on this machine, or where to buy one
400
402
  leg license activate <key> | deactivate | refresh (refresh renews a Team key)
401
403
  leg share who is on the board (off by default; Team plan)
@@ -409,9 +411,9 @@ Environment, all optional: `LEG_HOME` (default `~/.leg`), `LEG_PORT`
409
411
  (4747), `LEG_ACCOUNT` (start on a named login), `LEG_WARN_PCT` (85),
410
412
  `LEG_NO_HANDOFF=1` (warn and record, never switch), `LEG_NO_OPEN=1` (do not
411
413
  open the browser), `LEG_USAGE_POLL_MS` (60000), `LEG_CLAUDE_ARGS` /
412
- `LEG_CODEX_ARGS` / `LEG_AGY_ARGS` (extra args for a leg Leg starts after
414
+ `LEG_CODEX_ARGS` / `LEG_AGY_ARGS` / `LEG_GROK_ARGS` (extra args for a leg Leg starts after
413
415
  a hand-off, e.g. `-m gpt-5.3-codex-spark`), `LEG_CLAUDE_BIN`,
414
- `LEG_CODEX_BIN`, `LEG_AGY_BIN`, `LEG_CHB_BIN`, `LEG_PERSON` (whose
416
+ `LEG_CODEX_BIN`, `LEG_AGY_BIN`, `LEG_GROK_BIN`, `LEG_CHB_BIN`, `LEG_PERSON` (whose
415
417
  terminal this is when the board is shared), `LEG_RATE_MAX` (600 requests a
416
418
  minute per human) and `LEG_RATE_MAX_FAILURES` (20 wrong tokens per address).
417
419
 
@@ -542,7 +544,7 @@ Leg is commercial software under the [Leg License Agreement](LICENSE).
542
544
  It ships as readable JavaScript so you can see what it does on your machine,
543
545
  and you may modify it for your own use, but not redistribute it or work
544
546
  around the license check. Versions 0.2.0 and 0.3.0 were published under MIT
545
- and remain available. The version in this source tree is 0.7.0; see
547
+ and remain available. The version in this source tree is 0.8.0; see
546
548
  [npm](https://www.npmjs.com/package/legcli) for published versions and
547
549
  [CHANGELOG.md](CHANGELOG.md) for release notes.
548
550
 
package/bin/leg.mjs CHANGED
@@ -20,7 +20,7 @@ import { availableActions } from '../src/chain.mjs'
20
20
  import { up, down, stopBoard, status, openBoard } from '../src/launcher.mjs'
21
21
  import { attach, ensureBoard } from '../src/attach.mjs'
22
22
  import { readShare, addPerson, removePerson, rotate as rotateToken, turnOn, turnOff, linkFor, personNamed } from '../src/share.mjs'
23
- import { AGENTS, listSessions, readSession, readEvents as readSessionEvents, requestControl, removeSession, isActive, readLand, sessionDir, appendEvent } from '../src/sessions.mjs'
23
+ import { SUPERVISED_AGENTS, listSessions, readSession, readEvents as readSessionEvents, requestControl, removeSession, isActive, readLand, sessionDir, appendEvent } from '../src/sessions.mjs'
24
24
  import { addAccount, removeAccount, listAccountRows, LAYOUT } from '../src/accounts.mjs'
25
25
  import { listUsage, fmtReset } from '../src/usage.mjs'
26
26
  import { home } from '../src/store.mjs'
@@ -88,7 +88,12 @@ function simulateLimit(s) {
88
88
  appendEvent(s.session_id, { type: 'status', summary: 'simulated RESOURCE_EXHAUSTED appended to the session log' })
89
89
  return out(`simulated: RESOURCE_EXHAUSTED appended to ${join(sessionDir(s.session_id), 'agy.log')}; the runner reads it within ${process.env.BATON_ATTACH_POLL_MS || 2000} ms and hands off to ${s.chain?.[0]?.agent ?? 'nothing'}`)
90
90
  }
91
- die(2, `simulate-limit drives the claude hook path (and the agy log); codex's wall comes from its own rollout file, which Baton never writes. Use "leg sessions handoff ${s.session_id}" to force the switch.`)
91
+ if (s.agent === 'grok') {
92
+ appendFileSync(join(sessionDir(s.session_id), 'grok.log'), "\nRate limited (429): You've hit the rate limit for your plan. Try again later. (simulated by leg sessions simulate-limit)\n")
93
+ appendEvent(s.session_id, { type: 'status', summary: 'simulated rate limit appended to the grok log' })
94
+ return out(`simulated: rate limit appended to ${join(sessionDir(s.session_id), 'grok.log')}; the runner reads it within ${process.env.BATON_ATTACH_POLL_MS || 2000} ms and hands off to ${s.chain?.[0]?.agent ?? 'nothing'}`)
95
+ }
96
+ die(2, `simulate-limit drives the claude hook path (and the agy/grok log); codex's wall comes from its own rollout file, which Baton never writes. Use "leg sessions handoff ${s.session_id}" to force the switch.`)
92
97
  }
93
98
 
94
99
  function fmtCard(c) {
@@ -97,34 +102,49 @@ function fmtCard(c) {
97
102
  return `${c.card_id} [${c.status}] ${c.station}${leg} leases=${(c.leases?.length ? c.leases : ['**']).join(',')} ${String(c.title ?? c.task).slice(0, 60)}`
98
103
  }
99
104
 
100
- const TERMS = `Terms check (fetched 2026-09-11): Anthropic Consumer Terms forbid sharing account credentials and "bypassing any of our systems or protective measures"; the Anthropic Usage Policy forbids coordinating across multiple accounts to circumvent product guardrails; OpenAI's Terms of Use forbid sharing credentials and "circumvent any rate limits or restrictions". Two paid logins you own are not banned by name, but rotating to a second account of the same vendor because the first is rate-limited is close to that wording. Leg's default chain switches vendors (claude -> codex -> agy); a second account of one vendor is your call.`
105
+ const TERMS = `Terms check (fetched 2026-09-11): Anthropic Consumer Terms forbid sharing account credentials and "bypassing any of our systems or protective measures"; the Anthropic Usage Policy forbids coordinating across multiple accounts to circumvent product guardrails; OpenAI's Terms of Use forbid sharing credentials and "circumvent any rate limits or restrictions". Two paid logins you own are not banned by name, but rotating to a second account of the same vendor because the first is rate-limited is close to that wording. Leg's default chain switches vendors (claude -> codex -> agy -> grok); a second account of one vendor is your call.`
101
106
 
102
107
  async function main() {
103
108
  const [group, cmd, ...rest] = process.argv.slice(2)
104
109
  const args = parseArgs(rest)
105
110
  if (group === '--version' || group === '-v') return out(VERSION)
106
111
  if (group === '🦿' || group === 'prosthetic' || group === 'easter-egg') {
107
- out(' 🦿 LegCli — The mechanical relay runner for coding agents.')
108
- out(`
109
- .--------.
110
- | ____ |
111
- | | | |
112
- | |____| |
113
- '--------'
114
- ||
115
- .--||--.
116
- | || | knee servo
117
- '--||--'
118
- ||
119
- ||
120
- ___||___
121
- |________|
122
- `)
123
- out(' Passing the leg to the next runner when limits hit.')
112
+ const ORANGE = '\x1b[38;5;208m'
113
+ const RESET = '\x1b[0m'
114
+ const LEG_ART = [
115
+ ' ███████',
116
+ ' ███████',
117
+ ' ███████',
118
+ ' ███████',
119
+ ' ███████',
120
+ ' ███████',
121
+ ' ███████',
122
+ ' ███████',
123
+ ' ███████',
124
+ ' ███████',
125
+ ' ███████',
126
+ ' ███████████',
127
+ ' █████████████',
128
+ ` ██████(${ORANGE}00${RESET})███ ← knee servo`,
129
+ ' █████████████',
130
+ ' ███████████',
131
+ ' ███████',
132
+ ' █████',
133
+ ' █████',
134
+ ' █████',
135
+ ' █████',
136
+ ' █████',
137
+ ' █████',
138
+ ' ██████████████████████████',
139
+ ' ████ ████ ████ ████ ████ ████',
140
+ ].join('\n')
141
+ out('🦿 LegCli: the mechanical relay runner for coding agents.\n')
142
+ out(LEG_ART)
143
+ out('\nPassing the leg to the next runner when limits hit.')
124
144
  return
125
145
  }
126
- if (AGENTS.includes(group)) {
127
- // leg claude|codex|agy [agent args...]: everything after the agent name
146
+ if (SUPERVISED_AGENTS.includes(group)) {
147
+ // leg claude|codex|agy|grok [agent args...]: everything after the agent name
128
148
  // goes straight through.
129
149
  const code = await attach(group, [cmd, ...rest].filter((x) => x !== undefined), { open: process.env.BATON_NO_OPEN !== '1' })
130
150
  process.exit(code)
@@ -427,11 +447,12 @@ async function main() {
427
447
  out(openBoard(url) ? `opened ${url}` : `could not open a browser; visit ${url}`)
428
448
  return
429
449
  }
430
- if (group && group !== '--help' && group !== 'help') die(2, `unknown command "${group}" (claude|codex|agy|sessions|resume|accounts|license|share|up|down|status|open|card|scheduler|uninstall)`)
450
+ if (group && group !== '--help' && group !== 'help') die(2, `unknown command "${group}" (claude|codex|agy|grok|sessions|resume|accounts|license|share|up|down|status|open|card|scheduler|uninstall)`)
431
451
  out(`leg ${VERSION}, your coding agents, with a board alongside and a handoff when one hits its limit
432
- claude|codex|agy [args...] the normal interactive agent in this terminal; args pass straight through
433
- the board opens once, the session shows as a card, usage is tracked, a limit hands off
434
- a second live session in one checkout gets its own worktree (--no-worktree to share)
452
+ claude|codex|agy|grok [args...] the normal interactive agent in this terminal; args pass straight through
453
+ the board opens once, the session shows as a card, usage is tracked, a limit hands off
454
+ a second live session in one checkout gets its own worktree (--no-worktree to share)
455
+ auto-approve mode (--no-auto-approve to opt out)
435
456
  sessions ls|show|events|handoff|end|rm|simulate-limit <id>
436
457
  resume [--check] [--json] [--path <dir>] the hand-off waiting in this checkout, and whether it is still true
437
458
  freshness is recomputed from git at read time; --check prints only the verdict
package/docs/adapters.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Adapters
2
2
 
3
3
  Two things per agent: what Leg reads from an interactive session
4
- (`leg claude|codex|agy`), and the headless argv the v0.1 pipeline spawns.
4
+ (`leg claude|codex|agy|grok`), and the headless argv the v0.1 pipeline spawns.
5
5
  Every fact here was written against `src/taps/*.mjs`, `src/attach.mjs` and
6
6
  `src/adapters/*.mjs`; the evidence trail, including which lines an artifact
7
7
  backs, is [cli-contracts.md](cli-contracts.md).
@@ -120,7 +120,7 @@ documentation say docs-only.
120
120
  - **The wall**: `RESOURCE_EXHAUSTED`, "it resets in %s" and "out of quota" in
121
121
  the log. Those strings are present in `agy.exe`, and `scanLog()` also reads a
122
122
  relative reset out of "resets in \<n>\<s|m|h|d>". Status:
123
- **docs-only** <!-- live:agy/agy-resource-exhausted --> —
123
+ **observed-live 2026-09-16** <!-- live:agy/agy-resource-exhausted --> —
124
124
  `RESOURCE_EXHAUSTED (code 429): Individual quota reached … Resets in
125
125
  71h19m42s.` appeared in a session's `agy.log` at 08:02:42Z and walled the
126
126
  agent. No payload was kept: the capture call in `src/attach.mjs` was added
@@ -133,11 +133,25 @@ documentation say docs-only.
133
133
  - **One account only**: agy 1.2.0 has no config-directory override, so
134
134
  `leg accounts add agy …` is refused.
135
135
 
136
+ ### grok
137
+
138
+ - **How Leg attaches**: `grok <your args>` with `--debug-file <~/.leg/sessions/<id>/grok.log>` passed by Leg (`src/attach.mjs` `spawnSpec`).
139
+ - **Usage percentages**: `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits` and `GET https://cli-chat-proxy.grok.com/v1/user?include=subscription` (`LEG_GROK_BILLING_URL` and `LEG_GROK_USER_URL` override), reading the OAuth token stored in `~/.grok/auth.json`. The billing endpoint reports `creditUsagePercent` and `currentPeriod` (with weekly resets). Polled every 60 s (`LEG_USAGE_POLL_MS`). If the token expires or returns 401, usage is marked unknown without crashing, and re-reads `auth.json` on the next poll.
140
+ - **The wall**: rate limit signals cited directly from `xai-org/grok-build`:
141
+ - `SamplingError::Api { status: StatusCode::TOO_MANY_REQUESTS }` (`crates/codegen/xai-grok-sampling-types/src/error.rs:304`)
142
+ - `RATE_LIMITED_ERROR_CODE = -32003` and user messages `RATE_LIMITED_USER_MESSAGE_OAUTH` ("You've hit the rate limit for your plan. Try again later.") and `RATE_LIMITED_USER_MESSAGE_API_KEY` ("You've hit the rate limit for your API key. Try again later.") (`crates/codegen/xai-grok-shell/src/sampling/error.rs:15, 18-21`)
143
+ - Headline "Rate limited (429)" and "You've hit the rate limit for your plan" (`crates/codegen/xai-grok-pager/src/app/error_display.rs:263-267`)
144
+ - `StopFailureKind::RateLimit` ("rate_limit") (`crates/codegen/xai-grok-hooks/src/event.rs:306-315`)
145
+ - Free usage exhausted: `FREE_USAGE_USER_MESSAGE` and `FREE_USAGE_EXHAUSTED_ERROR_CODE` ("subscription:free-usage-exhausted") (`crates/codegen/xai-grok-shell/src/sampling/error.rs:30, 33`)
146
+ `src/taps/grok.mjs` scans `grok.log` for these exact signals and extracts reset durations when available.
147
+ - **Prompts and session id**: `~/.grok/sessions/<url-encoded-cwd>/prompt_history.jsonl`, recorded per prompt with timestamp, `session_id`, and `prompt`.
148
+ - **Accounts**: Supports `GROK_HOME` override. `leg accounts add grok <name>` creates junctioned directories copying `config.toml`.
149
+
136
150
  ### Resume prompt per agent
137
151
 
138
152
  After a hand-off the next agent starts in the same terminal with the pointer
139
153
  prompt as its first positional argument: `claude "<prompt>"`,
140
- `codex "<prompt>"`, `agy -i "<prompt>"` (`src/attach.mjs` `spawnSpec`).
154
+ `codex "<prompt>"`, `agy -i "<prompt>"`, `grok "<prompt>"` (`src/attach.mjs` `spawnSpec`).
141
155
 
142
156
  ## Headless adapters (the v0.1 pipeline)
143
157
 
@@ -191,7 +191,7 @@ stderr are 0 bytes, and codex's is the one stdin notice. observed-live.
191
191
 
192
192
  ## Interactive taps
193
193
 
194
- What `leg claude|codex|agy` reads while the real interactive CLI runs. Same
194
+ What `leg claude|codex|agy|grok` reads while the real interactive CLI runs. Same
195
195
  tagging rule: `observed-live 2026-09-11` means the build machine did it;
196
196
  `docs-only` means the CLI's own source or documentation says so and Leg has
197
197
  not seen it happen. Machine: Claude Code 2.1.268, codex-cli 0.153.4, agy 1.2.0.
@@ -295,7 +295,7 @@ variable and `LEG_SESSION` (source: src/attach.mjs, src/env.mjs).
295
295
  "quota exhausted/exceeded" in the log, plus a relative reset parsed out of
296
296
  "resets in \<n>\<s|m|h|d>" (source: strings present in `agy.exe`;
297
297
  src/taps/agy.mjs `scanLog`).
298
- **docs-only** <!-- live:agy/agy-resource-exhausted -->:
298
+ **observed-live 2026-09-16** <!-- live:agy/agy-resource-exhausted -->:
299
299
  `RESOURCE_EXHAUSTED (code 429): Individual quota reached … Resets in
300
300
  71h19m42s.` appeared in a session's `agy.log` at 08:02:42Z, `scanLog` read
301
301
  the relative reset, and the agent was walled. No payload was kept, the
@@ -310,6 +310,38 @@ variable and `LEG_SESSION` (source: src/attach.mjs, src/env.mjs).
310
310
  `LAYOUT.agy.env` is `null` and `accounts add agy` is refused (source:
311
311
  src/accounts.mjs).
312
312
 
313
+ ### grok tap
314
+
315
+ - Attach: `grok <args> --debug-file <LEG_HOME>/sessions/<id>/grok.log` (source:
316
+ src/attach.mjs `spawnSpec`).
317
+ - Usage: `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits` and
318
+ `GET https://cli-chat-proxy.grok.com/v1/user?include=subscription` with
319
+ `Authorization: Bearer <token>` read from `~/.grok/auth.json` (per-scope
320
+ key map or direct object). The billing endpoint reports `creditUsagePercent`
321
+ and `currentPeriod` (with weekly reset timestamp `end`). Polled every 60 s
322
+ (`LEG_USAGE_POLL_MS`). Token expiry and HTTP 401 are handled gracefully
323
+ without throwing, marking usage unknown until re-authenticated.
324
+ (source: src/taps/grok.mjs; probe against xAI proxy).
325
+ - The wall: exact rate-limit signals cited from `xai-org/grok-build` (Rust):
326
+ - `SamplingError::Api { status: StatusCode::TOO_MANY_REQUESTS }`
327
+ (`crates/codegen/xai-grok-sampling-types/src/error.rs:304`)
328
+ - `RATE_LIMITED_ERROR_CODE = -32003` and messages
329
+ `RATE_LIMITED_USER_MESSAGE_OAUTH` ("You've hit the rate limit for your plan. Try again later.")
330
+ and `RATE_LIMITED_USER_MESSAGE_API_KEY` ("You've hit the rate limit for your API key. Try again later.")
331
+ (`crates/codegen/xai-grok-shell/src/sampling/error.rs:15, 18-21`)
332
+ - Headline "Rate limited (429)" and "You've hit the rate limit for your plan"
333
+ (`crates/codegen/xai-grok-pager/src/app/error_display.rs:263-267`)
334
+ - `StopFailureKind::RateLimit` ("rate_limit")
335
+ (`crates/codegen/xai-grok-hooks/src/event.rs:306-315`)
336
+ - Free usage exhausted: `FREE_USAGE_USER_MESSAGE` and `FREE_USAGE_EXHAUSTED_ERROR_CODE`
337
+ ("subscription:free-usage-exhausted")
338
+ (`crates/codegen/xai-grok-shell/src/sampling/error.rs:30, 33`)
339
+ `src/taps/grok.mjs` scans `grok.log` for these patterns and parses reset duration.
340
+ - Prompts and session id: `~/.grok/sessions/<url-encoded-cwd>/prompt_history.jsonl`,
341
+ one `{ timestamp, session_id, prompt }` per entry (source: src/taps/grok.mjs `promptsSince`).
342
+ - Accounts: `GROK_HOME` override supported; `leg accounts add grok <name>` creates
343
+ junctioned directories with copied `config.toml` (source: src/accounts.mjs).
344
+
313
345
  ### Usage store and the chooser
314
346
 
315
347
  - `<LEG_HOME>/usage/<agent>--<account>.json`:
@@ -361,7 +393,7 @@ non-zero exit → `failed`. Every outcome except `completed`, `auth_failed` and
361
393
  `killed` asks the chain to hand off.
362
394
 
363
395
  <!-- limits-table:start -->
364
- Generated by `node scripts/limits-table.mjs` from 21 fixtures (4 observed-live, 17 docs-only). Classification `limit` hands the card to the next agent as a usage limit; `auth` is a failed launch (never a limit); `launch` is a failed launch that the next agent may still try; `budget` is a turn or spend cap set by Leg itself; `info` must never classify as a limit.
396
+ Generated by `node scripts/limits-table.mjs` from 22 fixtures (4 observed-live, 18 docs-only). Classification `limit` hands the card to the next agent as a usage limit; `auth` is a failed launch (never a limit); `launch` is a failed launch that the next agent may still try; `budget` is a turn or spend cap set by Baton itself; `info` must never classify as a limit.
365
397
 
366
398
  | id | adapter | class | where | source | text (excerpt) | produced by |
367
399
  |----|---------|-------|-------|--------|----------------|-------------|
@@ -383,6 +415,7 @@ Generated by `node scripts/limits-table.mjs` from 21 fixtures (4 observed-live,
383
415
  | generic-resource-exhausted | * | limit | any | **docs-only** | RESOURCE_EXHAUSTED | generic matcher (gRPC RESOURCE_EXHAUSTED); lowest priority |
384
416
  | generic-usage-limit | * | limit | any | **docs-only** | usage limit | generic matcher; lowest priority |
385
417
  | grok-not-logged-in | grok | auth | stderr | **observed-live** | To sign in, open this URL in your browser: https://accounts.x.ai/oauth2/device?user_cod | fixtures/live/grok/err.log from scripts/probe.mjs --adapter grok, 2026-09-10 (stdout JSON stopReason: Cancelled, exit 0) |
418
+ | grok-rate-limit | grok | limit | any | **docs-only** | You've hit the rate limit for your plan. Try again later. | crates/codegen/xai-grok-shell/src/sampling/error.rs:18 (RATE_LIMITED_USER_MESSAGE_OAUTH: "You've hit the rate limit for your plan. Try again later.") |
386
419
  | auth-source-set | * | auth | stderr | **docs-only** | another auth source is set | project brief (Wes, 2026-09-10): stderr saying "another auth source is set" counts as a failed launch; wording not yet observed live |
387
420
  | compile-error | * | info | stderr | **docs-only** | SyntaxError: Unexpected token ) at compileSourceTextModule (node:internal/modules/esm/ | synthetic negative fixture (a crashed agent is not a limit) |
388
421
  | empty-stdout-exit-0 | * | info | stdout | **observed-live** | | fixtures/live/grok (exit 0, no work): silence is not a limit |
@@ -20,11 +20,13 @@ variables yourself in that case.
20
20
 
21
21
  ## Interactive sessions
22
22
 
23
- These apply to `leg claude|codex|agy`.
23
+ These apply to `leg claude|codex|agy|grok`.
24
24
 
25
25
  | variable | default | meaning | read in |
26
26
  |----------|---------|---------|---------|
27
27
  | `LEG_ACCOUNT` | `default` | start the session on a named login instead of the CLI's own home | `src/attach.mjs` |
28
+ | `LEG_AUTO_APPROVE` | `1` | launch interactive agents in auto-approve mode (set to `0`, `false`, or `off` to disable) | `src/preferences.mjs` |
29
+ | `LEG_NO_AUTO_APPROVE` | (unset) | set to `1` to opt out of auto-approve mode | `src/preferences.mjs` |
28
30
  | `LEG_WARN_PCT` | `85` | the percentage of either usage window that turns the card amber, records a `warning` event and rings the terminal bell once | `src/usage.mjs` |
29
31
  | `LEG_NO_HANDOFF` | (unset, hand-off on) | set to `1` to warn and record but never switch agents | `src/attach.mjs` |
30
32
  | `LEG_NO_OPEN` | (unset, opens once) | set to `1` to start the board without opening a browser | `bin/leg.mjs` |
@@ -45,6 +47,23 @@ record under `$LEG_HOME/sessions/`.
45
47
  `CLAUDE_CONFIG_DIR` and `CODEX_HOME` are set for the child when the session
46
48
  runs on a named account; see [Accounts](#accounts) below.
47
49
 
50
+ ### Auto-approve launch mode
51
+
52
+ By default, Leg starts interactive sessions (`leg claude`, `leg codex`, `leg agy`, and `leg grok`) in permissive auto-approve mode so you never sit through repetitive tool permission prompts:
53
+
54
+ - Claude: `--dangerously-skip-permissions`
55
+ - Codex: `--ask-for-approval never`
56
+ - agy: `--dangerously-skip-permissions`
57
+ - Grok: `--always-approve`
58
+
59
+ These flags are injected at spawn time and only affect sessions launched through Leg. Your global CLI configurations (`~/.claude.json`, `~/.codex/config.toml`, `~/.gemini/settings.json`) remain untouched.
60
+
61
+ To opt out and keep standard approval prompts:
62
+
63
+ 1. CLI flag: pass `--no-auto-approve` when starting a session (e.g. `leg claude --no-auto-approve`).
64
+ 2. Environment variable: set `LEG_AUTO_APPROVE=0` or `LEG_NO_AUTO_APPROVE=1` (or legacy `BATON_AUTO_APPROVE=0` / `BATON_NO_AUTO_APPROVE=1`).
65
+ 3. Persistent preference: set `"auto_approve": false` in `~/.leg/preferences.json`.
66
+
48
67
  ## Core
49
68
 
50
69
  | variable | default | meaning | read in |
@@ -10,9 +10,9 @@ it. The v0.1 pipeline flow is section 7 onward.
10
10
  - git.
11
11
  - Python 3 with pip, for the `context-handoff-bundle` CLI. This is what writes
12
12
  and reads the handoff bundles.
13
- - At least one coding-agent CLI, logged in: `claude`, `codex` or `agy`. You do
14
- not need all three. With one installed you get the board and the usage
15
- tracking; with two or more you also get the hand-off.
13
+ - At least one coding-agent CLI, logged in: `claude`, `codex`, `agy` or
14
+ `grok`. You do not need all four. With one installed you get the board
15
+ and the usage tracking; with two or more you also get the hand-off.
16
16
 
17
17
  ## 2. Install
18
18
 
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "grok-rate-limit",
3
+ "adapter": "grok",
4
+ "source": "docs-only",
5
+ "produced_by": "crates/codegen/xai-grok-shell/src/sampling/error.rs:18 (RATE_LIMITED_USER_MESSAGE_OAUTH: \"You've hit the rate limit for your plan. Try again later.\")",
6
+ "where": "any",
7
+ "text": "You've hit the rate limit for your plan. Try again later.",
8
+ "pattern": "You've hit the rate limit for your plan|You've hit the rate limit for your API key|Rate limited \\(429\\)|subscription:free-usage-exhausted|You've used all of your free queries",
9
+ "exit_code": null,
10
+ "classification": "limit"
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "agent": "agy",
3
+ "signal": "agy-resource-exhausted",
4
+ "captured_at": "2026-09-16T07:17:41.481Z",
5
+ "session_id": "s-20260916-065617-agy-ef0c",
6
+ "source": "observed-live",
7
+ "payload": {
8
+ "log_excerpt": "invalid syntax I0916 03:17:39.470463 43949 run.go:389] Run: attempt 1 failed (RESOURCE_EXHAUSTED (code 429): Individual quota reached. Please upgrade your subscription to increase your limits. Resets in 2h35m6s.), retrying in 4s",
9
+ "resets_at": 1789552367
10
+ }
11
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ucsandman/legcli",
3
- "version": "0.7.0",
4
- "description": "Claude Code and Codex usage limit monitor with an automatic handoff. Type leg claude|codex|agy and get the same interactive agent with a board alongside, usage tracking per agent and account, a live context handoff bundle, and at the limit the next agent continuing in the same terminal. $79 once, 30-day money-back guarantee.",
3
+ "version": "0.8.0",
4
+ "description": "Usage-limit monitor and automatic handoff for Claude Code, Codex, agy and Grok. Type leg claude|codex|agy|grok and get the same interactive agent with a board alongside, auto-approve on by default, usage tracking per agent and account, a live context handoff bundle, and at the limit the next agent continuing in the same terminal. $79 once, 30-day money-back guarantee.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "homepage": "https://legcli.com",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "scripts": {
52
52
  "start": "node --env-file-if-exists=.env bin/leg.mjs up",
53
- "test": "node --test --test-concurrency=1 && node scripts/privacy-check.mjs && node scripts/check-claims.mjs",
53
+ "test": "node --test --test-concurrency=4 && node scripts/privacy-check.mjs && node scripts/check-claims.mjs",
54
54
  "lint": "eslint .",
55
55
  "privacy": "node scripts/privacy-check.mjs",
56
56
  "claims": "node scripts/check-claims.mjs",
@@ -185,12 +185,13 @@ function page ({ slug, title, description, body, headings }) {
185
185
  <meta property="og:url" content="${url}">
186
186
  <meta property="og:site_name" content="LegCli">
187
187
  <meta property="og:title" content="${escapeHtml(title)}">
188
- <meta property="og:description" content="${escapeHtml(description)}">
189
- <meta property="og:image" content="${ORIGIN}/og.png?v=2">
188
+ <meta property="og:image" content="${ORIGIN}/og.png?v=3">
189
+ <meta property="og:image:secure_url" content="${ORIGIN}/og.png?v=3">
190
+ <meta property="og:image:type" content="image/png">
190
191
  <meta name="twitter:card" content="summary_large_image">
191
192
  <meta name="twitter:title" content="${escapeHtml(title)}">
192
193
  <meta name="twitter:description" content="${escapeHtml(description)}">
193
- <meta name="twitter:image" content="${ORIGIN}/og.png?v=2">
194
+ <meta name="twitter:image" content="${ORIGIN}/og.png?v=3">
194
195
  <link rel="preload" href="/fonts/atkinson-hyperlegible-next-var.woff2" as="font" type="font/woff2" crossorigin>
195
196
  <link rel="preload" href="/fonts/azeret-mono-var.woff2" as="font" type="font/woff2" crossorigin>
196
197
  <link rel="stylesheet" href="/style.css">
package/src/accounts.mjs CHANGED
@@ -31,6 +31,13 @@ export const LAYOUT = {
31
31
  login: (dir) => `$env:CODEX_HOME='${dir}'; codex login`,
32
32
  },
33
33
  agy: { env: null, home: () => join(homedir(), '.gemini', 'antigravity-cli'), share: [], copy: [], login: null },
34
+ grok: {
35
+ env: 'GROK_HOME',
36
+ home: () => process.env.GROK_HOME || join(homedir(), '.grok'),
37
+ share: ['installed-plugins', 'skills', 'workflows'],
38
+ copy: ['config.toml'],
39
+ login: (dir) => `$env:GROK_HOME='${dir}'; grok login`,
40
+ },
34
41
  }
35
42
 
36
43
  export function accountsFile() { return join(home(), 'accounts.json') }
@@ -42,6 +49,7 @@ export function readAccounts() {
42
49
  try {
43
50
  const j = JSON.parse(readFileSync(accountsFile(), 'utf8'))
44
51
  for (const k of Object.keys(base)) if (Array.isArray(j[k])) base[k] = ['default', ...j[k].filter((n) => n !== 'default')]
52
+ if (Array.isArray(j?.grok)) base.grok = ['default', ...j.grok.filter((n) => n !== 'default')]
45
53
  } catch {}
46
54
  return base
47
55
  }
@@ -69,7 +77,7 @@ function junction(target, link) {
69
77
  export function addAccount(agent, name) {
70
78
  if (!/^[a-z0-9][a-z0-9_-]{0,29}$/i.test(name) || name === 'default') throw new Error(`invalid account name "${name}" (letters, digits, - and _; not "default")`)
71
79
  const l = LAYOUT[agent]
72
- if (!l) throw new Error(`unknown agent "${agent}" (claude|codex|agy)`)
80
+ if (!l) throw new Error(`unknown agent "${agent}" (claude|codex|agy|grok)`)
73
81
  if (!l.env) throw new Error(`${agent} has no config-dir override in the installed version; extra accounts are not possible`)
74
82
  const dir = accountDir(agent, name)
75
83
  mkdirSync(dir, { recursive: true })
@@ -78,6 +86,7 @@ export function addAccount(agent, name) {
78
86
  for (const d of l.share) if (junction(join(src, d), join(dir, d))) shared.push(d)
79
87
  refreshAccount(agent, name)
80
88
  const acc = readAccounts()
89
+ if (!acc[agent]) acc[agent] = ['default']
81
90
  if (!acc[agent].includes(name)) { acc[agent].push(name); writeAccounts(acc) }
82
91
  return { dir, shared, login: l.login(dir), env: l.env }
83
92
  }
@@ -1,8 +1,5 @@
1
- // grok adapter Grok CLI headless (`grok -p <prompt> --output-format json`).
2
- // NOT REGISTERED in index.mjs: the phase-3 probe could not verify it because
3
- // the build machine had no grok login (device-code prompt, exit "Cancelled").
4
- // Built from `grok --help` only; see docs/cli-contracts.md § grok. Register it
5
- // after `grok` is logged in and `node scripts/probe.mjs --adapter grok` passes.
1
+ // grok adapter - Grok CLI headless (`grok -p <prompt> --output-format json`).
2
+ // Registered in index.mjs alongside claude, codex, and agy.
6
3
  import { join } from 'node:path'
7
4
  import { homedir } from 'node:os'
8
5
  import { sanitizeEnv } from '../env.mjs'
@@ -38,8 +35,8 @@ const adapter = {
38
35
  const j = JSON.parse(s.slice(i))
39
36
  return {
40
37
  session_id: j.session_id ?? j.sessionId ?? null,
41
- last_message: typeof j.response === 'string' ? j.response : (typeof j.result === 'string' ? j.result : null),
42
- stop_reason: j.stop_reason ?? null,
38
+ last_message: typeof j.response === 'string' ? j.response : (typeof j.result === 'string' ? j.result : (typeof j.text === 'string' ? j.text : null)),
39
+ stop_reason: j.stop_reason ?? j.stopReason ?? null,
43
40
  raw: j,
44
41
  }
45
42
  } catch { continue }