@zgeoff/atc 0.1.9 → 0.1.11

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/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
  <h1>atc</h1>
3
3
 
4
4
  <p>
5
- Control tower for Claude Code sessions: stock <code>claude</code> instances in PTYs behind a
6
- keyboard-driven session list with hook-driven attention routing — no panes, no tiling, no
7
- mouse.
5
+ Control tower for Claude Code and Grok Build sessions: stock <code>claude</code> and
6
+ <code>grok</code> instances in PTYs behind a keyboard-driven session list with hook-driven
7
+ attention routing — no panes, no tiling, no mouse.
8
8
  </p>
9
9
 
10
10
  <p>
@@ -27,7 +27,8 @@ atc
27
27
  ```
28
28
 
29
29
  Needs [Bun](https://bun.sh) (atc runs from source through it) and the `claude` CLI on your PATH.
30
- From a checkout, `bun src/cli.ts` runs the same thing. atc is built to pair with
30
+ Grok sessions also need the `grok` CLI, and Codex sessions the `codex` CLI. From a checkout,
31
+ `bun src/cli.ts` runs the same thing. atc is built to pair with
31
32
  [zoxide](https://github.com/ajeetdsouza/zoxide): the spawn directory picker feeds on its frecency
32
33
  list, so with zoxide installed every directory you visit is two keystrokes from a session. Without
33
34
  it the picker falls back to atc's own spawn history.
@@ -38,51 +39,101 @@ fine nested inside zellij/tmux (give the pane locked mode so Ctrl-Space reaches
38
39
 
39
40
  ## Keys
40
41
 
41
- | Key | Where | Action |
42
- | --------------- | -------------- | ---------------------------------------------------------------------------------------------- |
43
- | leader | anywhere | toggle session overlay — `Ctrl-Space` by default, configurable (see Config) |
44
- | `n` | home/overlay | spawn: pick dir (zoxide + history, fuzzy) → name → optional first prompt |
45
- | `r` | home/overlay | adopt: pick dir → name → `claude --resume` (Claude's session picker opens in the new PTY) |
46
- | `R` | home | restore last fleet after a daemon death — respawns every session via `claude --resume <id>` |
47
- | `j`/`k`/`↑`/`↓` | overlay/picker | move |
48
- | `Enter` | overlay | attach (auto-acks) |
49
- | `Tab` | overlay | attach the most urgent needs-you session, else the latest turn-done one |
50
- | `/` | overlay | fuzzy filter by name/dir (chars in order), `⏎` attach top match, `esc` clear |
51
- | `a` | overlay | ack notification without attaching |
52
- | `p` | overlay | pin or unpin the selected session — pinned sessions stay at the top of the list |
53
- | `g` | overlay | toggle the grouped view: sessions cluster under repository headers |
54
- | `H` | overlay | eject to headless: the terminal dies, a headless Agent SDK run resumes the same session |
55
- | `P` | overlay | revive: a fresh terminal resumes a headless or killed session in place |
56
- | `y` | overlay | yank `cd <dir> && claude --resume <id>` to clipboard (OSC 52 + clip.exe/wl-copy/xclip) |
57
- | `Y` | overlay | eject: yank the resume command, then kill the session here — paste it in any pane to take over |
58
- | `K` | overlay | kill selected (confirm with `y`) — the entry stays revivable with `P`; a second `K` forgets it |
59
- | `?` | overlay | full key reference — the hint row only shows actions valid for the selected session |
60
- | `u` | overlay | restart an outdated daemon and restore the fleet — offered only while `⟳ update ready` shows |
61
- | `q` | home/overlay | quit the client — sessions keep running in the daemon |
42
+ | Key | Where | Action |
43
+ | --------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | leader | anywhere | toggle session overlay — `Ctrl-Space` by default, configurable (see Config) |
45
+ | `n` | home/overlay | spawn: pick agent → dir (zoxide + history, fuzzy) → name → optional first prompt. Fresh clients default to Claude; last-used is the last deliberate-spawn SessionStart. |
46
+ | `r` | home/overlay | adopt: pick agent → dir → name. Claude opens `claude --resume`, Codex `codex resume`. Grok opens plain `grok`. |
47
+ | `R` | home | restore last fleet after a daemon death — each session with its matching CLI |
48
+ | `j`/`k`/`↑`/`↓` | overlay/picker | move |
49
+ | `Enter` | overlay | attach (auto-acks) |
50
+ | `Tab` | overlay | attach the most urgent needs-you session, else the latest turn-done one |
51
+ | `/` | overlay | fuzzy filter by name/dir (chars in order), `⏎` attach top match, `esc` clear |
52
+ | `a` | overlay | ack notification without attaching |
53
+ | `p` | overlay | pin or unpin the selected session — pinned sessions stay at the top of the list |
54
+ | `g` | overlay | toggle the grouped view: sessions cluster under repository headers |
55
+ | `H` | overlay | eject to headless (Claude only). Hidden and ignored on a Grok row. |
56
+ | `P` | overlay | revive: a fresh terminal resumes a headless or killed session in place |
57
+ | `y` | overlay | yank the resume command (`claude --resume <id>`, `grok --resume <id>`, or `codex resume <id>`) |
58
+ | `Y` | overlay | eject: yank the resume command, then kill the session here |
59
+ | `K` | overlay | kill selected (confirm with `y`) — the entry stays revivable with `P`; a second `K` forgets it |
60
+ | `?` | overlay | full key reference — the hint row only shows actions valid for the selected session |
61
+ | `u` | overlay | restart an outdated daemon and restore the fleet — offered only while `⟳ update ready` shows |
62
+ | `q` | home/overlay | quit the client — sessions keep running in the daemon |
62
63
 
63
64
  The overlay orders sessions by pinned first, then attention state, then most recently attached, so
64
65
  the session you want is nearly always near the top. The grouped view (`g`) keeps that order but
65
66
  clusters sessions under dim repository headers, with pinned sessions leading in their own cluster; a
66
67
  git worktree clusters with its main repository, and a directory outside any repository stands alone.
67
- The `atc_session_update` MCP tool renames and pins sessions, so an agent can organise the fleet for
68
- you.
68
+ A reserved column after the pin mark shows a dim `g` on Grok rows; Claude rows keep a space so names
69
+ stay aligned. The `atc_session_update` MCP tool renames and pins sessions, so an agent can organise
70
+ the fleet for you.
69
71
 
70
- Revive (`P`) and headless eject (`H`) resume the session from its saved transcript, so both need one
71
- to exist: a session killed before its first exchange has nothing on disk yet, and the overlay says
72
- so in its message column instead of resuming.
72
+ Revive (`P`) resumes the session from its saved transcript, so a session killed before its first
73
+ exchange has nothing on disk yet, and the overlay says so in its message column instead of resuming.
74
+ Headless eject (`H`) is Claude-only and uses the same transcript; Grok has no headless handoff.
73
75
 
74
- Everything else is passed through to the focused Claude session, which owns the full screen. Fleet
75
- state renders inside Claude Code's own status line (injected via the same `--settings` file): your
76
+ Everything else is passed through to the focused session, which owns the full screen. Fleet state
77
+ renders inside Claude Code's own status line (injected via the same `--settings` file): your
76
78
  configured statusline runs first, and atc appends `▏● 2 need you: auth-bug`. atc draws its own
77
79
  status bar only on the home and overlay screens.
78
80
 
79
81
  ## How state tracking works
80
82
 
81
- Spawned sessions get a `--settings` file injecting `Notification`, `Stop`, `UserPromptSubmit`, and
82
- `SessionEnd` hooks that report to a unix socket (`$XDG_RUNTIME_DIR/atc.sock`). Your global Claude
83
- settings are untouched; sessions you start outside atc are unaffected. States: red `●` needs you,
84
- cyan `◐` running, green `✓` turn done, gray `✗` exited. The status bar turns red and names the most
85
- urgent session.
83
+ Spawned Claude sessions get a `--settings` file injecting `Notification`, `Stop`,
84
+ `UserPromptSubmit`, and `SessionEnd` hooks that report to a unix socket
85
+ (`$XDG_RUNTIME_DIR/atc.sock`). Your global Claude settings are untouched; sessions you start outside
86
+ atc are unaffected. Grok attention comes from a dedicated hook file at
87
+ `$GROK_HOME/hooks/atc-reporter.json` (`~/.grok` when `GROK_HOME` is unset). atc never writes that
88
+ path. Install it yourself:
89
+
90
+ ```sh
91
+ mkdir -p ~/.grok/hooks
92
+ atc grok-hooks > ~/.grok/hooks/atc-reporter.json
93
+ ```
94
+
95
+ Codex attention comes from hook entries in `$CODEX_HOME/hooks.json` (`~/.codex` when `CODEX_HOME` is
96
+ unset). atc never writes that path either — `atc codex-hooks` prints the entries to merge in:
97
+
98
+ ```sh
99
+ atc codex-hooks
100
+ ```
101
+
102
+ Codex parses new hooks but never runs them until you trust them once: open `codex`, review the atc
103
+ hooks in its hooks list, and approve them. Sessions you start outside atc report events too; the
104
+ reporter exits immediately when no atc session id is present.
105
+
106
+ `atc grok-hooks` prints this file, with the `hook-report` command resolved for this install:
107
+
108
+ ```json
109
+ {
110
+ "hooks": {
111
+ "SessionStart": [
112
+ { "hooks": [{ "type": "command", "command": "atc hook-report", "timeout": 5 }] }
113
+ ],
114
+ "SessionEnd": [
115
+ { "hooks": [{ "type": "command", "command": "atc hook-report", "timeout": 5 }] }
116
+ ],
117
+ "UserPromptSubmit": [
118
+ { "hooks": [{ "type": "command", "command": "atc hook-report", "timeout": 5 }] }
119
+ ],
120
+ "Stop": [{ "hooks": [{ "type": "command", "command": "atc hook-report", "timeout": 5 }] }],
121
+ "StopFailure": [
122
+ { "hooks": [{ "type": "command", "command": "atc hook-report", "timeout": 5 }] }
123
+ ],
124
+ "StopCancelled": [
125
+ { "hooks": [{ "type": "command", "command": "atc hook-report", "timeout": 5 }] }
126
+ ],
127
+ "Notification": [
128
+ { "hooks": [{ "type": "command", "command": "atc hook-report", "timeout": 5 }] }
129
+ ]
130
+ }
131
+ }
132
+ ```
133
+
134
+ A missing file is a Grok PTY without hook-driven attention. States: red `●` needs you, cyan `◐`
135
+ running, green `✓` turn done, gray `✗` exited. The status bar turns red and names the most urgent
136
+ session.
86
137
 
87
138
  ## Config
88
139
 
@@ -92,30 +143,39 @@ urgent session.
92
143
  {
93
144
  "claudeBin": "claude",
94
145
  "claudeArgs": [],
146
+ "grokBin": "grok",
147
+ "grokArgs": [],
148
+ "codexBin": "codex",
149
+ "codexArgs": [],
95
150
  "leader": "ctrl-space"
96
151
  }
97
152
  ```
98
153
 
99
- | Field | Default | Meaning |
100
- | ------------ | -------------- | ------------------------------------------------------------------------------------- |
101
- | `claudeBin` | `"claude"` | The binary spawned for every session. |
102
- | `claudeArgs` | `[]` | Prepended to every spawn, e.g. `["--model", "opus"]`. |
103
- | `leader` | `"ctrl-space"` | The overlay toggle: `ctrl-` plus a letter or one of `\` `]` `^` `_`, e.g. `"ctrl-]"`. |
154
+ | Field | Default | Meaning |
155
+ | ------------ | -------------- | ----------------------------------------------------------------------------------------------------------- |
156
+ | `claudeBin` | `"claude"` | The binary spawned for Claude sessions. |
157
+ | `claudeArgs` | `[]` | Prepended to every Claude spawn, e.g. `["--model", "opus"]`. |
158
+ | `grokBin` | `"grok"` | The binary spawned for Grok sessions. |
159
+ | `grokArgs` | `[]` | Prepended to every Grok spawn. A user `--leader` in this list is dropped; atc always appends `--no-leader`. |
160
+ | `codexBin` | `"codex"` | The binary spawned for Codex sessions. |
161
+ | `codexArgs` | `[]` | Prepended to every Codex spawn. |
162
+ | `leader` | `"ctrl-space"` | The overlay toggle: `ctrl-` plus a letter or one of `\` `]` `^` `_`, e.g. `"ctrl-]"`. |
104
163
 
105
164
  Pick a different leader when `Ctrl-Space` is taken on your machine — Raycast on macOS claims it, and
106
165
  `ctrl-]` is a solid replacement that no common terminal, multiplexer, or OS shortcut wants. An
107
166
  unknown or reserved value falls back to the default.
108
167
 
109
168
  `atc mcp` exposes the fleet as MCP tools (list, spawn, drive, organise) to any MCP client, wrangled
110
- sessions included:
169
+ sessions included. `atc_session_spawn` takes an optional `agent` (`claude`, `grok`, or `codex`) and
170
+ defaults to Claude; it never reads the TUI last-used value.
111
171
 
112
172
  ```sh
113
173
  claude mcp add --scope user atc -- atc mcp
114
174
  ```
115
175
 
116
- Daemon state — the restorable fleet, spawn-dir history, and the hook-event trail — lives in
117
- `~/.local/state/atc/atc.db` (SQLite), next to `status.json` (read by the injected statusline); the
118
- daemon's pid file sits in `$XDG_RUNTIME_DIR/atc-daemon.pid`, beside its sockets.
176
+ Daemon state — the restorable fleet, spawn-dir history, last-used agent, and the hook-event trail —
177
+ lives in `~/.local/state/atc/atc.db` (SQLite), next to `status.json` (read by the injected
178
+ statusline); the daemon's pid file sits in `$XDG_RUNTIME_DIR/atc-daemon.pid`, beside its sockets.
119
179
 
120
180
  ## Crash safety
121
181
 
@@ -124,13 +184,13 @@ A client crash or closed window costs nothing: the daemon keeps hosting the flee
124
184
  would kill every hosted session — and shows `⟳ update ready` in the status bar; `u` in the overlay
125
185
  restarts the daemon and restores the fleet at a moment you choose. Only a protocol mismatch, where
126
186
  the two could miscommunicate, forces the restart immediately. The daemon continuously writes the
127
- live fleet (name, cwd, Claude session id) to its SQLite store. If the daemon itself dies — crash,
128
- SIGKILL, reboot — the child claude processes die with it, but every session's transcript is already
129
- on disk. Start atc and press `R`: the whole fleet respawns via `claude --resume`. Only deliberate
130
- kills (`K`, `Y` eject) remove entries from the fleet, so it stays restorable.
187
+ live fleet (name, cwd, agent, session id) to its SQLite store. If the daemon itself dies — crash,
188
+ SIGKILL, reboot — the child processes die with it, but every session's transcript is already on
189
+ disk. Start atc and press `R`: the whole fleet respawns with the matching CLI. Only deliberate kills
190
+ (`K`, `Y` eject) remove entries from the fleet, so it stays restorable.
131
191
 
132
192
  Restoring shows the whole fleet immediately — every incoming session appears in the list marked
133
- "waiting to restore" — and revives one at a time, most recently active first: the next
134
- `claude --resume` starts only once the previous one has reported it is up (its `SessionStart` hook),
135
- so bringing back a dozen sessions no longer launches a dozen Claude processes at the same instant
136
- and pins the machine. Each row flips live as its session comes back.
193
+ "waiting to restore" — and revives one at a time, most recently active first: the next resume starts
194
+ only once the previous one has reported it is up (its `SessionStart` hook), so bringing back a dozen
195
+ sessions no longer launches a dozen agent processes at the same instant and pins the machine. Each
196
+ row flips live as its session comes back.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/atc",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Terminal control tower for Claude Code sessions",
5
5
  "homepage": "https://github.com/zgeoff/atc#readme",
6
6
  "bugs": "https://github.com/zgeoff/atc/issues",
@@ -1,5 +1,15 @@
1
1
  import type { HookEvent } from './hooks';
2
2
 
3
+ export type AgentKind = 'claude' | 'grok' | 'codex';
4
+
5
+ /**
6
+ * Missing, empty, and unknown values become Claude so a fleet written
7
+ * before the agent column still restores as Claude.
8
+ */
9
+ export function toAgentKind(raw: unknown): AgentKind {
10
+ return raw === 'grok' || raw === 'codex' ? raw : 'claude';
11
+ }
12
+
3
13
  export interface SpawnOptions {
4
14
  readonly prompt: string;
5
15
 
@@ -24,6 +34,10 @@ export interface AdapterEvent {
24
34
 
25
35
  // Opaque handle the adapter can later pull a session name from.
26
36
  nameSource?: string;
37
+
38
+ // Claude resume-existence path. Distinct from nameSource: a naming
39
+ // handle is not a resume gate.
40
+ transcriptSource?: string;
27
41
  }
28
42
 
29
43
  export interface NameUpdate {
@@ -42,12 +56,41 @@ interface ScreenDetector {
42
56
  readonly detectAttention: (screen: string) => AttentionJudgment | null;
43
57
  }
44
58
 
59
+ export interface ResumeCheck {
60
+ readonly agentSessionID?: string;
61
+ readonly transcriptSource?: string;
62
+ }
63
+
64
+ interface HeadlessRunRequest {
65
+ readonly cwd: string;
66
+ readonly prompt: string;
67
+ readonly resume?: string;
68
+ readonly permissionMode?: string;
69
+ }
70
+
71
+ interface HeadlessRunEvents {
72
+ readonly onOutput: (text: string) => void;
73
+ readonly onDone: (summary: string) => void;
74
+ readonly onNeedsYou: (msg: string) => void;
75
+ }
76
+
77
+ export type HeadlessRunner = (
78
+ opts: HeadlessRunRequest,
79
+ hooks: HeadlessRunEvents,
80
+ ) => { readonly stop: () => void };
81
+
45
82
  /**
46
83
  * Everything specific to one agent CLI: how to spawn it, how to read its
47
84
  * hook payloads, where its session names come from, and how to resume a
48
85
  * session outside atc. The session core never sees past this interface.
49
86
  */
50
87
  export interface AgentAdapter {
88
+ readonly kind: AgentKind;
89
+
90
+ // Runs one headless turn over a session; null means eject is unsupported
91
+ // for this agent.
92
+ readonly headlessRunner: HeadlessRunner | null;
93
+
51
94
  // The detector stack's screen tier; null when hooks are authoritative.
52
95
  readonly screenDetector: ScreenDetector | null;
53
96
  readonly planSpawn: (opts: SpawnOptions) => SpawnPlan;
@@ -56,5 +99,6 @@ export interface AgentAdapter {
56
99
  source: string,
57
100
  namedBy: 'user' | 'auto' | 'agent',
58
101
  ) => Promise<NameUpdate | null>;
102
+ readonly canResume: (session: ResumeCheck) => boolean;
59
103
  readonly buildResumeCommand: (cwd: string, agentSessionID: string | undefined) => string | null;
60
104
  }
@@ -1,6 +1,7 @@
1
1
  import { spawn as spawnChild } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { basename, join } from 'node:path';
4
+ import type { AgentKind } from './agent-adapter';
4
5
  import { daemonPidFile, daemonSocketPath } from './config';
5
6
  import { DaemonClient } from './daemon-client';
6
7
  import { getBuild } from './get-build';
@@ -9,6 +10,7 @@ import { isRecord } from './report';
9
10
  export interface DaemonBoot {
10
11
  readonly client: DaemonClient;
11
12
  readonly stale: boolean;
13
+ readonly lastUsedAgent: AgentKind;
12
14
  }
13
15
 
14
16
  /**
@@ -30,7 +32,11 @@ export async function bootDaemonClient(): Promise<DaemonBoot> {
30
32
  try {
31
33
  const hello = await client.sendHello(build);
32
34
 
33
- return { client, stale: hello['daemon'] !== build };
35
+ return {
36
+ client,
37
+ stale: hello['daemon'] !== build,
38
+ lastUsedAgent: hello['lastUsedAgent'] === 'grok' ? 'grok' : 'claude',
39
+ };
34
40
  } catch (error) {
35
41
  client.stop();
36
42
 
@@ -0,0 +1,16 @@
1
+ import { basename, join } from 'node:path';
2
+
3
+ /**
4
+ * Command line that wrangled sessions invoke for atc subcommands: under bun
5
+ * the CLI entry path is part of the command; a compiled binary is itself
6
+ * the entry.
7
+ */
8
+ export function buildCLICommand(subcommand: string): string {
9
+ const exec = process.execPath;
10
+
11
+ if (basename(exec) === 'bun' || basename(exec) === 'bun.exe') {
12
+ return `"${exec}" "${join(import.meta.dir, 'cli.ts')}" ${subcommand}`;
13
+ }
14
+
15
+ return `"${exec}" ${subcommand}`;
16
+ }
@@ -1,13 +1,16 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { basename, join } from 'node:path';
3
+ import { join } from 'node:path';
4
4
  import type {
5
5
  AdapterEvent,
6
6
  AgentAdapter,
7
+ HeadlessRunner,
7
8
  NameUpdate,
9
+ ResumeCheck,
8
10
  SpawnOptions,
9
11
  SpawnPlan,
10
12
  } from './agent-adapter';
13
+ import { buildCLICommand } from './build-cli-command';
11
14
  import type { Config } from './config';
12
15
  import { stateDir } from './config';
13
16
  import type { HookEvent } from './hooks';
@@ -18,19 +21,26 @@ import { isRecord } from './report';
18
21
  * resume semantics, transcript name-pulling, and statusline chaining.
19
22
  */
20
23
  export class ClaudeAdapter implements AgentAdapter {
24
+ readonly kind = 'claude';
25
+
26
+ readonly headlessRunner: HeadlessRunner | null;
27
+
21
28
  // Claude's hooks are authoritative; no screen heuristics needed.
22
29
  readonly screenDetector = null;
23
30
 
24
31
  private readonly config: Config;
25
32
 
26
- private readonly settingsFile: string;
33
+ // Written on first spawn so constructing the adapter touches no state.
34
+ private settingsFile: string | undefined;
27
35
 
28
- constructor(config: Config) {
36
+ constructor(config: Config, headlessRunner: HeadlessRunner | null = null) {
29
37
  this.config = config;
30
- this.settingsFile = writeHookSettings();
38
+ this.headlessRunner = headlessRunner;
31
39
  }
32
40
 
33
41
  planSpawn(opts: SpawnOptions): SpawnPlan {
42
+ this.settingsFile ??= writeHookSettings();
43
+
34
44
  return {
35
45
  bin: this.config.claudeBin,
36
46
  args: [
@@ -55,7 +65,9 @@ export class ClaudeAdapter implements AgentAdapter {
55
65
 
56
66
  const named: AdapterEvent = {
57
67
  ...base,
58
- ...(typeof transcript === 'string' ? { nameSource: transcript } : {}),
68
+ ...(typeof transcript === 'string'
69
+ ? { nameSource: transcript, transcriptSource: transcript }
70
+ : {}),
59
71
  };
60
72
 
61
73
  switch (e.event) {
@@ -157,6 +169,14 @@ export class ClaudeAdapter implements AgentAdapter {
157
169
  }
158
170
  }
159
171
 
172
+ canResume(session: ResumeCheck): boolean {
173
+ if (session.transcriptSource === undefined) {
174
+ return true;
175
+ }
176
+
177
+ return existsSync(session.transcriptSource);
178
+ }
179
+
160
180
  // Shell command that re-opens this session outside atc (or anywhere).
161
181
  buildResumeCommand(cwd: string, agentSessionID: string | undefined): string | null {
162
182
  const resume =
@@ -221,15 +241,3 @@ function writeHookSettings(): string {
221
241
  function truncateDetail(text: string): string {
222
242
  return text.length <= 600 ? text : `${text.slice(0, 599)}…`;
223
243
  }
224
-
225
- // Wrangled sessions invoke atc subcommands: under bun the CLI entry path is
226
- // part of the command; a compiled binary is itself the entry.
227
- function buildCLICommand(subcommand: string): string {
228
- const exec = process.execPath;
229
-
230
- if (basename(exec) === 'bun' || basename(exec) === 'bun.exe') {
231
- return `"${exec}" "${join(import.meta.dir, 'cli.ts')}" ${subcommand}`;
232
- }
233
-
234
- return `"${exec}" ${subcommand}`;
235
- }
package/src/cli.ts CHANGED
@@ -45,6 +45,8 @@ const main = defineCommand({
45
45
  const daemon = await import('./daemon');
46
46
  const config = await import('./config');
47
47
  const claude = await import('./claude-adapter');
48
+ const grok = await import('./grok-adapter');
49
+ const codex = await import('./codex-adapter');
48
50
  const headless = await import('./start-headless-run');
49
51
 
50
52
  // Test harnesses shrink the outbound queue to force overflow
@@ -60,12 +62,19 @@ const main = defineCommand({
60
62
  const restoreBootTimeoutMs =
61
63
  Number.isFinite(capOverride) && capOverride >= 0 ? capOverride : 15_000;
62
64
 
65
+ const claudeAdapter = new claude.ClaudeAdapter(cfg, (runOpts, hooks) =>
66
+ headless.startHeadlessRun(runOpts, hooks),
67
+ );
68
+
69
+ const grokAdapter = new grok.GrokAdapter(cfg);
70
+ const codexAdapter = new codex.CodexAdapter(cfg);
71
+
63
72
  const handle = daemon.startDaemon({
64
- headlessRunner: (runOpts, hooks) => headless.startHeadlessRun(runOpts, hooks),
65
73
  socketPath: config.daemonSocketPath,
66
74
  reporterSocketPath: config.socketPath,
67
75
  build: getBuild(),
68
- adapter: new claude.ClaudeAdapter(cfg),
76
+ adapter: claudeAdapter,
77
+ adapters: { claude: claudeAdapter, grok: grokAdapter, codex: codexAdapter },
69
78
  dbPath: config.dbFile,
70
79
  legacyFleetPath: config.legacyFleetFile,
71
80
  pidPath: config.daemonPidFile,
@@ -80,6 +89,31 @@ const main = defineCommand({
80
89
  });
81
90
  },
82
91
  }),
92
+ 'codex-hooks': () =>
93
+ defineCommand({
94
+ meta: {
95
+ name: 'codex-hooks',
96
+ description:
97
+ 'Print the Codex hook entries to merge into $CODEX_HOME/hooks.json (trust them once in the Codex TUI)',
98
+ },
99
+ async run() {
100
+ const hook = await import('./print-codex-hook-file');
101
+
102
+ hook.printCodexHookFile();
103
+ },
104
+ }),
105
+ 'grok-hooks': () =>
106
+ defineCommand({
107
+ meta: {
108
+ name: 'grok-hooks',
109
+ description: 'Print the Grok hook file to install at $GROK_HOME/hooks/atc-reporter.json',
110
+ },
111
+ async run() {
112
+ const hook = await import('./print-grok-hook-file');
113
+
114
+ hook.printGrokHookFile();
115
+ },
116
+ }),
83
117
  'hook-report': () =>
84
118
  defineCommand({
85
119
  meta: {