wisp-agents 0.8.0 → 0.9.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
@@ -5,6 +5,24 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.9.0] - 2026-10-18
9
+
10
+ ### Added
11
+
12
+ - Machines: run agents on your other computers (a dev box, a GPU server, a cloud VM) over SSH. **Machines** in the top bar (also Settings → *Machines*, ⌘K) takes an SSH host (suggested from `~/.ssh/config`) and connects: wisp signs in with your keys, finds Node.js there, starts `npx -y wisp-agents@<this version>` detached (or reuses the wisp already running), and opens an `ssh -L` tunnel back. Each step shows live, and a failure says what to run (`ssh-copy-id`, install Node, restart an older wisp). Then pick projects on the machine (its git repos are listed, folders autocomplete). The launcher's **Run on** button (next to the harness) picks where a prompt goes: this computer, a machine, or several at once. Each place gets its own row of project chips, and one Enter launches on all of them in parallel, as one task card with a row per machine (its own project, badge and status) and a single "ready on 3 machines" toast. A machine picked while disconnected connects. Each run executes the agent on its machine, with worktrees, diffs, rollback, `ask_user`, subagents, compare and Ship it working as usual. Its sessions join your feed with a machine badge, answer questions from your phone like any other, open in VS Code / Cursor / Windsurf over Remote - SSH, and **Take over** opens `ssh -t` in your terminal. Nothing new listens on the network: wisp over there stays on its own loopback, keeps running (agents too) when this computer sleeps, and wisp reconnects when it wakes. API: `/api/machines`, `machine` and `groupId` (to group runs launched in several places) on `POST /sessions`, `machine` on `/projects`; remote sessions are `<id>@<machine>` everywhere (CLI, SDK, MCP).
13
+ - Grok Build harness (`grok`, `--agent grok`). wisp runs `grok --output-format streaming-messages-json` headless. It lists the models from `grok models` with each model's thinking levels (Low to X-High), maps Plan / Edits / Full auto to grok's permission modes, and resumes follow-ups with `--resume`. Grok can lead a team of subagents, and it asks you questions through wisp's `ask_user`. The tool reaches grok through a throwaway `--agent` definition that keeps grok's stock prompt. Diffs come from `search_replace` and `write`. Cost shows in Usage when grok reports it.
14
+
15
+ ### Changed
16
+
17
+ - The launcher's Recipes row no longer has a Manage link. Recipes are still one click away from the Automations button in the top bar.
18
+ - Every text field, textarea, select and checkbox now shares one look from a small form kit in `web/css/app.css` (`.input`, `.textarea`, `.field`, `.check`, plus `.mono` / `.sm` / `.lg`): the same border, radius, height, placeholder colour, hover and focus ring across Settings, Phone, Automations, Ship it, Run recipe, Projects, the model picker search, the Subagents editor and question cards. Checkboxes use the accent colour.
19
+
20
+ ### Fixed
21
+
22
+ - A lead with subagents now uses them instead of its harness's built-in subagents. Claude Code leads run with `Task`, `Agent` and `Workflow` disallowed, opencode leads with `permission.task: "deny"`, and every lead is told to use `delegate` in place of its own subagent tool. Without a team, nothing changes.
23
+ - The selects in the recipe and trigger forms and in the Ship it / Run dialogs had lost their dropdown chevron.
24
+ - Settings and Phone text fields, and every select, showed no focus state.
25
+
8
26
  ## [0.8.0] - 2026-10-17
9
27
 
10
28
  ### Added
@@ -158,7 +176,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
158
176
  - Session queue with follow-ups, answers to agent questions, terminal takeover and mark-as-done.
159
177
  - Older turns are folded, with a floating collapse-all button and back-to-top.
160
178
 
161
- [Unreleased]: https://github.com/M0xoo/wisp/compare/v0.8.0...HEAD
179
+ [Unreleased]: https://github.com/M0xoo/wisp/compare/v0.9.0...HEAD
180
+ [0.9.0]: https://github.com/M0xoo/wisp/compare/v0.8.0...v0.9.0
162
181
  [0.8.0]: https://github.com/M0xoo/wisp/compare/v0.7.0...v0.8.0
163
182
  [0.7.0]: https://github.com/M0xoo/wisp/compare/v0.6.0...v0.7.0
164
183
  [0.6.0]: https://github.com/M0xoo/wisp/compare/v0.5.0...v0.6.0
package/README.md CHANGED
@@ -13,7 +13,7 @@ Launch coding agents in the background, skip the transcript, and read the presen
13
13
  [![Node](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org)
14
14
  [![Electron](https://img.shields.io/badge/desktop-Electron-47848f?logo=electron&logoColor=white)](https://www.electronjs.org)
15
15
  [![Zero runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)](package.json)
16
- [![Harnesses](https://img.shields.io/badge/harness-Claude%20Code%20·%20Codex%20·%20opencode%20·%20Cursor%20·%20Antigravity-d97757)](#adding-a-harness)
16
+ [![Harnesses](https://img.shields.io/badge/harness-Claude%20Code%20·%20Codex%20·%20opencode%20·%20Cursor%20·%20Antigravity%20·%20Grok-d97757)](#adding-a-harness)
17
17
 
18
18
  [Quick start](#quick-start) · [Features](#features) · [CLI](#cli) · [Integrations](#integrations) · [Shortcuts](#keyboard-shortcuts) · [Architecture](#architecture) · [Adding a harness](#adding-a-harness) · [Roadmap](#roadmap)
19
19
 
@@ -23,7 +23,7 @@ Launch coding agents in the background, skip the transcript, and read the presen
23
23
 
24
24
  ## Why wisp?
25
25
 
26
- Agent transcripts are long, and you rarely need them. wisp runs agent harnesses (Claude Code, Codex, opencode, Cursor, and Antigravity) on your projects in the background. When an agent finishes, it hands you a short, animated **presentation**: diffs, test results, commits, flows, and decisions. The session then waits in your queue.
26
+ Agent transcripts are long, and you rarely need them. wisp runs agent harnesses (Claude Code, Codex, opencode, Cursor, Antigravity, and Grok) on your projects in the background. When an agent finishes, it hands you a short, animated **presentation**: diffs, test results, commits, flows, and decisions. The session then waits in your queue.
27
27
 
28
28
  From there you can **follow up**, **answer its questions**, **take it over** in your terminal, or **mark it done**.
29
29
 
@@ -44,7 +44,7 @@ From there you can **follow up**, **answer its questions**, **take it over** in
44
44
  | 📥 **A queue** | Sessions sort into *Needs you*, *Working* (with a live one-line status), and *Done*. Archive what you don't need to see. |
45
45
  | 💬 **Follow-ups that interrupt** | A follow-up sent mid-run interrupts the agent and resumes it with your message. Turn on **Queue** to wait for the run to finish instead; queued messages can be cancelled. |
46
46
  | 🔎 **Command palette** | `⌘K` / `Ctrl+K` fuzzy-searches sessions, projects, and actions. |
47
- | 🤖 **Any harness** | Claude Code, Codex, opencode, Cursor CLI, or Antigravity CLI. Pick the harness, model and thinking level together in the launcher; both are remembered per harness. Each card shows the harness it ran on. |
47
+ | 🤖 **Any harness** | Claude Code, Codex, opencode, Cursor CLI, Antigravity CLI, or Grok Build. Pick the harness, model and thinking level together in the launcher; both are remembered per harness. Each card shows the harness it ran on. |
48
48
  | 🖥️ **Take over** | Opens your terminal with the harness's resume command (`claude --resume <id>`, `codex resume <id>`, …) in the project folder. Works with Terminal, iTerm, Ghostty, Warp, cmux, kitty, WezTerm, GNOME Terminal, Konsole, Alacritty, Windows Terminal, PowerShell, or a custom command. |
49
49
  | 📎 **Attachments** | Paste screenshots or drop images, video, logs, or PDFs into the launcher. |
50
50
  | 🧠 **Learns as you go** | After a session goes quiet, a background retro records your preferences and the agent's lessons (a mistake and the way that worked), globally and per project. Later runs get them in their prompt. See [Learning](#learning). |
@@ -53,7 +53,7 @@ From there you can **follow up**, **answer its questions**, **take it over** in
53
53
  | 🤝 **Subagents** | Give the lead agent a team on other harnesses (e.g. Claude Code hands tests to Codex ×3). It delegates through a `delegate` tool; their edits land in its worktree. See [Subagents](#subagents). |
54
54
  | ⏪ **Checkpoints & rollback** | Every turn is snapshotted first. Restore to before any turn, revert a single file, or re-run a command block from the presentation. |
55
55
  | ⚖️ **Compare** | Run one task on two harnesses side by side, and keep the better run. |
56
- | 🧾 **Recipes & triggers** | Saved prompts with `{params}` (`wisp run release 0.8.0`), run by hand, on a schedule, when CI fails, or when an issue is labelled. |
56
+ | 🧾 **Recipes & triggers** | Saved prompts with `{params}` (`wisp run release 0.9.0`), run by hand, on a schedule, when CI fails, or when an issue is labelled. |
57
57
  | 📊 **Usage** | Spend, success rate and questions per project and per harness · model. |
58
58
  | 📱 **Phone** | Questions and finished tasks on ntfy or Telegram; answer from there. |
59
59
  | 🔐 **Permission modes** | Choose **Plan** (read-only), **Edits** (file edits auto-accepted), or **Full auto** for each launch. `Shift+Tab` in the prompt cycles modes. |
@@ -62,7 +62,7 @@ From there you can **follow up**, **answer its questions**, **take it over** in
62
62
 
63
63
  ## Quick start
64
64
 
65
- > **Requirements:** macOS, Linux, or Windows; Node.js ≥ 20; and at least one harness installed and signed in: [Claude Code](https://claude.com/claude-code) (`claude`), [Codex](https://github.com/openai/codex) (`codex`), [opencode](https://opencode.ai) (`opencode`), [Cursor CLI](https://cursor.com/cli) (`cursor-agent`), or [Antigravity CLI](https://antigravity.google/docs/cli/headless/) (`agy`). Override a binary path with `WISP_CLAUDE_BIN`, `WISP_CODEX_BIN`, `WISP_OPENCODE_BIN`, `WISP_CURSOR_BIN`, or `WISP_ANTIGRAVITY_BIN`.
65
+ > **Requirements:** macOS, Linux, or Windows; Node.js ≥ 20; and at least one harness installed and signed in: [Claude Code](https://claude.com/claude-code) (`claude`), [Codex](https://github.com/openai/codex) (`codex`), [opencode](https://opencode.ai) (`opencode`), [Cursor CLI](https://cursor.com/cli) (`cursor-agent`), [Antigravity CLI](https://antigravity.google/docs/cli/headless/) (`agy`), or [Grok Build](https://x.ai/cli) (`grok`). Override a binary path with `WISP_CLAUDE_BIN`, `WISP_CODEX_BIN`, `WISP_OPENCODE_BIN`, `WISP_CURSOR_BIN`, or `WISP_ANTIGRAVITY_BIN`.
66
66
 
67
67
  ```sh
68
68
  npx wisp-agents # open the wisp desktop app
@@ -111,7 +111,7 @@ wisp # open wisp (or focus the running one)
111
111
  wisp "fix the flaky auth test" # launch a task in this folder (it becomes a project)
112
112
  wisp add [path] # add a folder as a project (default: cwd)
113
113
  wisp --mode plan "audit deps" # permission mode: plan | edits | auto
114
- wisp --agent codex "add a test" # harness: claude | codex | opencode | cursor | antigravity
114
+ wisp --agent codex "add a test" # harness: claude | codex | opencode | cursor | antigravity | grok
115
115
  ```
116
116
 
117
117
  Scripting (every command takes `--json`):
@@ -125,7 +125,7 @@ wisp reply <id> "also do X" # follow up (answers a pending question)
125
125
  wisp stop <id>
126
126
  wisp ship <id> [--draft] # commit, push and open a PR for a worktree task
127
127
  wisp merge <id> # squash-merge its PR, fast-forward your local main
128
- wisp run release 0.8.0 # run a recipe; values fill its {params} (or version=0.8.0)
128
+ wisp run release 0.9.0 # run a recipe; values fill its {params} (or version=0.9.0)
129
129
  wisp recipes # list recipes
130
130
  wisp usage [--days 7] # spend and outcomes
131
131
  wisp mcp # MCP server for other agents and editors
@@ -136,7 +136,7 @@ wisp skill install # install the wisp skill for Claude Code (--ag
136
136
  |---|---|
137
137
  | `--port`, `-p N` | Port to use (default `4477`, or `WISP_PORT`) |
138
138
  | `--mode`, `-m` | `plan` · `edits` · `auto` |
139
- | `--agent`, `-a` | `claude` · `codex` · `opencode` · `cursor` · `antigravity` (default: the agent last picked in the launcher) |
139
+ | `--agent`, `-a` | `claude` · `codex` · `opencode` · `cursor` · `antigravity` · `grok` (default: the agent last picked in the launcher) |
140
140
  | `--model` · `--effort` | Model id and thinking level for the launch |
141
141
  | `--cwd`, `-C DIR` | Project folder (default: the current one) |
142
142
  | `--wait`, `-w` | Wait for the run to finish or ask, then print the presentation |
@@ -206,6 +206,7 @@ web/ (UI) ⇄ HTTP + SSE ⇄ server/ ── adapters/claude.js ── claude -
206
206
  ├─ adapters/opencode.js ── opencode run --format json
207
207
  ├─ adapters/cursor.js ── cursor-agent -p --output-format stream-json
208
208
  ├─ adapters/antigravity.js ── agy --output-format stream-json
209
+ ├─ adapters/grok.js ── grok --output-format streaming-messages-json
209
210
  └─ mcp-ask.js (ask_user MCP tool, injected per run)
210
211
  ```
211
212
 
@@ -228,6 +229,7 @@ wisp/
228
229
  │ ├── recipes.js # saved prompts with {params}
229
230
  │ ├── triggers.js # cron schedules and GitHub watchers
230
231
  │ ├── remote.js # phone: ntfy and Telegram, out and back
232
+ │ ├── machines.js # other computers: SSH, a tunnel to wisp there, relayed sessions
231
233
  │ ├── usage.js # spend and outcomes
232
234
  │ ├── terminal.js # "take over" in your terminal
233
235
  │ ├── welcome.js # the gallery session
@@ -238,7 +240,8 @@ wisp/
238
240
  │ ├── codex.js # Codex CLI adapter
239
241
  │ ├── opencode.js # opencode adapter
240
242
  │ ├── cursor.js # Cursor CLI adapter
241
- │ └── antigravity.js # Antigravity CLI adapter
243
+ │ ├── antigravity.js # Antigravity CLI adapter
244
+ │ └── grok.js # Grok Build adapter
242
245
  ├── skills/wisp/SKILL.md # agent skill: how other agents use wisp
243
246
  ├── web/ # vanilla JS UI, no build step
244
247
  │ ├── index.html
@@ -274,6 +277,7 @@ If the harness has no system-prompt flag, prepend `systemPrompt` to the first pr
274
277
  - [x] opencode adapter
275
278
  - [x] Cursor CLI adapter
276
279
  - [x] Antigravity CLI adapter
280
+ - [x] Grok Build adapter
277
281
 
278
282
  ## Worktrees and shipping
279
283
 
@@ -287,7 +291,7 @@ Turn worktrees off in Settings → *Worktree per task*, with the launcher's **Wo
287
291
 
288
292
  ## Subagents
289
293
 
290
- The launcher's **Subagents** button gives the lead agent a team: up to 6 members, each with its own harness, model, thinking level, name, a **Use for** note that tells the lead when to pick it, and a **×N** copies count (`Codex ×3` → `codex#1`…`codex#3`). The lead gets a `delegate` tool on wisp's MCP server; tasks sent in one call run in parallel in the lead's folder, and delegating to the same member again resumes its conversation. Claude Code, Codex and opencode can lead; Cursor and Antigravity can be members. **Save as preset** stores a team for later. Subagent edits count as the lead's, so they show up in its diffs, rollback and Ship it. API: `subagents` / `preset` on `POST /sessions`, `/teams`.
294
+ The launcher's **Subagents** button gives the lead agent a team: up to 6 members, each with its own harness, model, thinking level, name, a **Use for** note that tells the lead when to pick it, and a **×N** copies count (`Codex ×3` → `codex#1`…`codex#3`). The lead gets a `delegate` tool on wisp's MCP server; tasks sent in one call run in parallel in the lead's folder, and delegating to the same member again resumes its conversation. Claude Code, Codex, opencode and Grok can lead; Cursor and Antigravity can be members. **Save as preset** stores a team for later. Subagent edits count as the lead's, so they show up in its diffs, rollback and Ship it. API: `subagents` / `preset` on `POST /sessions`, `/teams`.
291
295
 
292
296
  ## Checkpoints and rollback
293
297
 
@@ -301,7 +305,7 @@ Before each turn, wisp snapshots the git working tree (pinned at `refs/wisp/chec
301
305
  Prepare release {version}: update CHANGELOG, README and docs/, bump package.json, tag v{version}.
302
306
  ```
303
307
 
304
- Make one from ⌘K → *Recipes*, or *Save as recipe…* on any card. A recipe may pin a project, harness, model and mode. Run it from ⌘K (*Run: Release*), from the Recipes view, or from a terminal: `wisp run release 0.8.0` fills the fields in order, `wisp run release version=0.8.0` by name.
308
+ Make one from ⌘K → *Recipes*, or *Save as recipe…* on any card. A recipe may pin a project, harness, model and mode. Run it from ⌘K (*Run: Release*), from the Recipes view, or from a terminal: `wisp run release 0.9.0` fills the fields in order, `wisp run release version=0.9.0` by name.
305
309
 
306
310
  **Triggers** launch sessions on their own while wisp is open (⌘K → *Triggers*):
307
311
 
@@ -315,7 +319,7 @@ GitHub triggers poll with your `gh` login (every 2–60 minutes), so nothing has
315
319
 
316
320
  ## Usage
317
321
 
318
- ⌘K → *Usage* shows spend per day and, per project and per harness · model: spend, turns, how often the run worked (finished, not partial or blocked), how often the agent had to ask you, and the average time. Claude Code and opencode report cost; Codex, Cursor and Antigravity only report tokens, so their turns are counted but not priced. Also `wisp usage` and `GET /api/usage?days=30`.
322
+ ⌘K → *Usage* shows spend per day and, per project and per harness · model: spend, turns, how often the run worked (finished, not partial or blocked), how often the agent had to ask you, and the average time. Claude Code, opencode and Grok report cost (Grok only when its plan does); Codex, Cursor and Antigravity only report tokens, so their turns are counted but not priced. Also `wisp usage` and `GET /api/usage?days=30`.
319
323
 
320
324
  ## Phone
321
325
 
@@ -326,6 +330,18 @@ Settings → *Phone* sends questions and finished tasks to your phone and takes
326
330
 
327
331
  They receive titles, summaries and questions (with their context), never code or diffs.
328
332
 
333
+ ## Machines
334
+
335
+ **Machines** (top bar, Settings → *Machines*, or ⌘K) runs agents on your other computers over SSH. Give it `user@host` or a `Host` from `~/.ssh/config` and press **Connect**:
336
+
337
+ 1. wisp signs in with `ssh` using your keys, agent and config (never a password; `BatchMode=yes`, new host keys are trusted on first use).
338
+ 2. On the machine it finds Node.js 20+ and starts `npx -y wisp-agents@<this version> --no-open` detached, or reuses the wisp already answering on its port (4477 by default).
339
+ 3. It forwards a free local port to that wisp with `ssh -L` and relays its event stream.
340
+
341
+ Then **Pick projects on it**: the machine's git repos are listed, and folders autocomplete. In the launcher, **Run on** (next to the harness) picks where the prompt goes: this computer, any machine, or several at once. Each place shows its own project chips; Enter launches on all of them in parallel, and the runs land as one task with a row per machine. Each run executes on its machine, with worktrees, diffs, rollback, `ask_user`, subagents, compare and Ship it all working on that machine. Its sessions join your feed with a badge, answer from your phone like any other, open in VS Code, Cursor or Windsurf through Remote - SSH, and **Take over** opens `ssh -t <host>` resuming the agent in your terminal.
342
+
343
+ Nothing new listens on the network: wisp on the machine stays on its own `127.0.0.1`, and it keeps running (agents too) when this computer sleeps or disconnects; wisp reconnects on its own. **Stop wisp there** (the machine's ⋯ menu) stops it. The machine needs Node.js 20+ and the harness CLIs, signed in. Sessions there are `<id>@<machine>` in the API, CLI, SDK and MCP; recipes and schedules work with machine projects, while CI and issue triggers, usage and memory stay per machine.
344
+
329
345
  ## Learning
330
346
 
331
347
  After a session has been quiet for 90 seconds (or right away when you mark it done or archive it), wisp runs a short **retro**: the same harness, read-only and without tools (Claude Code runs it on Haiku), reads a digest of the turns it hasn't seen yet. The digest holds your requests and follow-ups, questions and answers, failed tool calls and the commands run around them, and how each run ended. The retro then updates four small lists:
package/bin/wisp.js CHANGED
@@ -35,7 +35,7 @@ const HELP = `usage:
35
35
  wisp skill [install] print / install the wisp agent skill (--agent claude|codex|opencode|all --project)
36
36
  wisp -- "ls the repo" a task that starts with a command name
37
37
 
38
- launch options: --mode plan|edits|auto --agent claude|codex|opencode|cursor|antigravity --model ID --effort LEVEL
38
+ launch options: --mode plan|edits|auto --agent claude|codex|opencode|cursor|antigravity|grok --model ID --effort LEVEL
39
39
  --cwd DIR --wait --json --no-worktree --compare agent[:model],…
40
40
  options: --port N --browser --no-open --json --version`;
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wisp-agents",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Launch coding agents, skip the transcript, read the presentation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "app": "node bin/wisp.js",
21
21
  "dev": "WISP_DEV=1 node bin/wisp.js",
22
22
  "dev:web": "node --watch-path=server bin/wisp.js --no-open",
23
- "test": "node --test test/proc.test.js test/terminal.test.js test/adapters.test.js test/e2e.test.js test/fuzzy.test.js test/integrations.test.js test/update.test.js test/learn.test.js test/ship.test.js test/subagents.test.js",
23
+ "test": "node --test test/proc.test.js test/terminal.test.js test/adapters.test.js test/e2e.test.js test/fuzzy.test.js test/integrations.test.js test/update.test.js test/learn.test.js test/ship.test.js test/subagents.test.js test/machines.test.js",
24
24
  "test:real": "node test/real-clis.mjs",
25
25
  "prepublishOnly": "node --check bin/wisp.js && node --check bin/mac-app.js && node -e \"import('./server/index.js').then(() => import('./server/mcp-wisp.js'))\" && node --check electron/main.js"
26
26
  },
@@ -35,7 +35,7 @@ export default {
35
35
  },
36
36
 
37
37
  // Returns { stop(), done: Promise<void> }. Everything else is reported through onEvent.
38
- run({ prompt, attachments = [], cwd, resumeId, mode, model, effort, systemPrompt, mcpServers = {}, onEvent }) {
38
+ run({ prompt, attachments = [], cwd, resumeId, mode, model, effort, systemPrompt, mcpServers = {}, delegating = false, onEvent }) {
39
39
  const args = [
40
40
  '-p',
41
41
  '--output-format', 'stream-json',
@@ -49,6 +49,8 @@ export default {
49
49
  args.push('--mcp-config', JSON.stringify({ mcpServers }));
50
50
  args.push('--allowedTools', ...Object.keys(mcpServers).map((n) => `mcp__${n}`));
51
51
  }
52
+ // Its built-in subagents (Task, or Agent in newer versions) would bypass the user's team.
53
+ if (delegating) args.push('--disallowedTools', 'Task', 'Agent', 'Workflow');
52
54
  const dirs = [...new Set(attachments.map((a) => path.dirname(a.path)))];
53
55
  if (dirs.length) args.push('--add-dir', ...dirs);
54
56
  args.push('--verbose'); // terminates the variadic flags above
@@ -0,0 +1,251 @@
1
+ // Grok Build adapter — drives `grok --prompt-file … --output-format streaming-messages-json`
2
+ // (Claude Code's stream-json shape, with grok's own tool names).
3
+ //
4
+ // grok has no flag for an extra MCP server (and its GROK_CONFIG overlay can't add one), but an
5
+ // agent definition passed with `--agent <file>` can declare `mcpServers`. So each turn writes a
6
+ // throwaway agent file that inherits the full stock prompt and only adds wisp's server; the model
7
+ // reaches it through grok's `use_tool` as `wisp__ask_user`. The protocol goes in with `--rules`.
8
+
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { ASK_TIMEOUT_MS, attachmentNote, clip, detectVersion, fileTracker, flatten, quietOutput, rel, spawnJsonl } from './common.js';
13
+
14
+ const BIN = process.env.WISP_GROK_BIN || 'grok';
15
+
16
+ const PERMISSION_MODES = {
17
+ plan: 'plan',
18
+ edits: 'acceptEdits',
19
+ auto: 'bypassPermissions',
20
+ };
21
+ const EFFORT_ORDER = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
22
+
23
+ export default {
24
+ id: 'grok',
25
+ name: 'Grok',
26
+ askTool: 'wisp__ask_user',
27
+ models: [{ id: '', label: 'Default', hint: 'Whatever Grok is set to' }],
28
+ efforts: ['low', 'medium', 'high', 'xhigh'],
29
+
30
+ resumeCommand({ resumeId, model, effort }) {
31
+ return [BIN, ...(resumeId ? ['--resume', resumeId] : []), ...(model ? ['--model', model] : []), ...(effort ? ['--reasoning-effort', effort] : [])];
32
+ },
33
+
34
+ async detect() {
35
+ const found = await detectVersion(BIN);
36
+ if (!found.available) return found;
37
+ // `grok models` prints " * grok-4.7 (default)" / " - grok-4.6" lines.
38
+ const ids = [...(await quietOutput(BIN, ['models'])).matchAll(/^\s+[*-]\s+([\w.\-/:]+)/gm)].map((m) => m[1]);
39
+ if (!ids.length) return found;
40
+ const meta = cachedModels();
41
+ const models = ids.map((id) => {
42
+ const info = meta[id]?.info || {};
43
+ const efforts = info.supports_reasoning_effort === false ? [] : effortsOf(info.reasoning_efforts);
44
+ return { id, label: info.name || id, hint: clip(info.description || id, 60), ...(efforts && { efforts }) };
45
+ });
46
+ return { ...found, models: [this.models[0], ...models] };
47
+ },
48
+
49
+ run({ prompt, attachments = [], cwd, resumeId, mode, model, effort, systemPrompt, mcpServers = {}, onEvent }) {
50
+ // grok -p doesn't read stdin, and a prompt in argv is capped on Windows: hand it over as a file.
51
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'wisp-grok-'));
52
+ const promptFile = path.join(tmp, 'prompt.md');
53
+ fs.writeFileSync(promptFile, prompt + attachmentNote(attachments));
54
+
55
+ const args = [
56
+ '--prompt-file', promptFile,
57
+ '--output-format', 'streaming-messages-json',
58
+ '--permission-mode', PERMISSION_MODES[mode] || 'acceptEdits',
59
+ '--rules', systemPrompt,
60
+ // A headless run can't answer grok's own question tool; wisp's ask_user (or a question block) replaces it.
61
+ '--disallowed-tools', 'ask_user_question',
62
+ '--no-auto-update',
63
+ ];
64
+ if (model) args.push('--model', model);
65
+ if (effort) args.push('--reasoning-effort', effort);
66
+ if (resumeId) args.push('--resume', resumeId);
67
+ if (Object.keys(mcpServers).length) {
68
+ const agentFile = path.join(tmp, 'wisp.md');
69
+ fs.writeFileSync(agentFile, agentDefinition(mcpServers));
70
+ args.push('--agent', agentFile);
71
+ }
72
+
73
+ const files = fileTracker(cwd);
74
+ const pending = new Map(); // tool_use_id -> { name, input }
75
+ let session = null;
76
+ let sawResult = false;
77
+ let error = null;
78
+
79
+ const handle = spawnJsonl({
80
+ bin: BIN,
81
+ args,
82
+ cwd,
83
+ env: { ...process.env, GROK_DISABLE_AUTOUPDATER: '1' },
84
+ onEvent,
85
+ onJson(msg) {
86
+ if (msg.session_id && msg.session_id !== session) onEvent({ type: 'session', id: (session = msg.session_id) });
87
+ // Subagent traffic carries its parent's tool id; only the main conversation is shown.
88
+ if (msg.parent_tool_use_id) return;
89
+ switch (msg.type) {
90
+ case 'assistant':
91
+ for (const part of msg.message?.content || []) {
92
+ if (part.type === 'text' && part.text?.trim()) onEvent({ type: 'text', text: part.text });
93
+ else if (part.type === 'thinking') onEvent({ type: 'thinking' });
94
+ else if (part.type === 'server_tool_use') onEvent({ type: 'tool', kind: 'web', label: `Searching the web: ${clip(part.input?.query, 50)}` });
95
+ else if (part.type === 'tool_use') {
96
+ const input = part.input || {};
97
+ pending.set(part.id, { name: part.name, input });
98
+ if (part.name === 'write' && input.file_path) files.remember(input.file_path);
99
+ // Streamed once answered, so "waiting" would land after "You answered"; the trail has both.
100
+ if (part.name === 'use_tool' && input.tool_name === 'wisp__ask_user') continue;
101
+ onEvent({ type: 'tool', ...describeTool(part.name, input, cwd) });
102
+ }
103
+ }
104
+ break;
105
+ case 'user':
106
+ for (const part of msg.message?.content || []) {
107
+ if (part.type !== 'tool_result') continue;
108
+ const call = pending.get(part.tool_use_id);
109
+ pending.delete(part.tool_use_id);
110
+ if (!call) continue;
111
+ if (part.is_error) onEvent({ type: 'tool_error', name: call.name, text: clip(flatten(part.content), 400) });
112
+ else {
113
+ const change = captureChange(call.name, call.input, cwd, files);
114
+ if (change) onEvent({ type: 'change', ...change });
115
+ }
116
+ }
117
+ break;
118
+ case 'result':
119
+ sawResult = true;
120
+ onEvent({
121
+ type: 'result',
122
+ text: msg.is_error ? (msg.errors || []).join('\n') || msg.result || `grok stopped: ${msg.subtype}` : msg.result ?? '',
123
+ isError: !!msg.is_error || (msg.subtype && msg.subtype !== 'success'),
124
+ stats: {
125
+ costUsd: msg.total_cost_usd || undefined, // 0 means "not reported", never free
126
+ durationMs: msg.duration_ms,
127
+ turns: msg.num_turns,
128
+ inputTokens: msg.usage?.input_tokens,
129
+ outputTokens: msg.usage?.output_tokens,
130
+ },
131
+ });
132
+ break;
133
+ case 'error':
134
+ error = msg.message || 'grok reported an error.';
135
+ break;
136
+ }
137
+ },
138
+ onClose({ code, signal, stderr, stopped }) {
139
+ if (sawResult) return;
140
+ onEvent({ type: 'result', isError: true, stopped, text: error || stderr || `grok exited with ${signal || `code ${code}`}` });
141
+ },
142
+ });
143
+ handle.done.finally(() => fs.rmSync(tmp, { recursive: true, force: true }));
144
+ return handle;
145
+ },
146
+ };
147
+
148
+ // `prompt_mode: full` keeps grok's stock system prompt; the body adds nothing.
149
+ function agentDefinition(mcpServers) {
150
+ const servers = Object.entries(mcpServers).map(([name, srv]) => ({
151
+ name,
152
+ command: srv.command,
153
+ args: srv.args || [],
154
+ env: srv.env || {},
155
+ tool_timeout_sec: ASK_TIMEOUT_MS / 1000,
156
+ startup_timeout_sec: 60,
157
+ }));
158
+ // YAML is a superset of JSON, so JSON values need no escaping of their own.
159
+ return `---\nname: wisp\ndescription: A task launched by wisp\nprompt_mode: full\nagents_md: true\nmcpServers: ${JSON.stringify(servers)}\n---\n`;
160
+ }
161
+
162
+ // grok caches the model catalog, with each model's reasoning levels, in $GROK_HOME.
163
+ function cachedModels() {
164
+ try {
165
+ const home = process.env.GROK_HOME || path.join(os.homedir(), '.grok');
166
+ return JSON.parse(fs.readFileSync(path.join(home, 'models_cache.json'), 'utf8')).models || {};
167
+ } catch {
168
+ return {};
169
+ }
170
+ }
171
+
172
+ function effortsOf(list) {
173
+ if (!Array.isArray(list)) return undefined;
174
+ const rank = (id) => (EFFORT_ORDER.includes(id) ? EFFORT_ORDER.indexOf(id) : EFFORT_ORDER.length);
175
+ return list.map((e) => e.id).filter(Boolean).sort((a, b) => rank(a) - rank(b));
176
+ }
177
+
178
+ function describeTool(name, input, cwd) {
179
+ const f = rel(input.target_file || input.file_path || input.path || input.target_directory, cwd);
180
+ switch (name) {
181
+ case 'read_file':
182
+ return { kind: 'read', label: `Reading ${f}`, file: f };
183
+ case 'search_replace':
184
+ return { kind: 'edit', label: `Editing ${f}`, file: f };
185
+ case 'write':
186
+ return { kind: 'edit', label: `Writing ${f}`, file: f };
187
+ case 'run_terminal_command':
188
+ case 'run_terminal_cmd':
189
+ return { kind: 'run', label: input.description ? clip(input.description) : `Running ${clip(input.command, 60)}`, detail: input.command };
190
+ case 'grep':
191
+ return { kind: 'search', label: `Searching for “${clip(input.pattern, 40)}”` };
192
+ case 'list_dir':
193
+ return { kind: 'search', label: `Listing ${isDir(input.target_directory, cwd) ? '.' : f || '.'}` };
194
+ case 'web_search':
195
+ return { kind: 'web', label: `Searching the web: ${clip(input.query, 50)}` };
196
+ case 'web_fetch':
197
+ return { kind: 'web', label: `Reading ${clip(input.url, 60)}` };
198
+ case 'spawn_subagent':
199
+ case 'task':
200
+ return { kind: 'agent', label: `Delegating: ${clip(input.description || input.prompt, 60)}` };
201
+ case 'todo_write': {
202
+ const active = input.todos?.find((t) => t.status === 'in_progress' && t.content);
203
+ return { kind: 'plan', label: active ? clip(active.content) : 'Planning' };
204
+ }
205
+ case 'enter_plan_mode':
206
+ case 'exit_plan_mode':
207
+ return { kind: 'plan', label: 'Planning' };
208
+ case 'search_tool':
209
+ return { kind: 'search', label: `Looking up tools: ${clip(input.query, 50)}` };
210
+ case 'use_tool':
211
+ return describeMcp(String(input.tool_name || ''), input.tool_input || {});
212
+ default:
213
+ return { kind: 'tool', label: `Using ${name.replace(/_/g, ' ')}` };
214
+ }
215
+ }
216
+
217
+ // grok passes absolute paths, also for the project root itself (which rel() leaves as is).
218
+ function isDir(dir, cwd) {
219
+ try {
220
+ return !!dir && fs.realpathSync(path.resolve(cwd, dir)) === fs.realpathSync(cwd);
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+
226
+ // MCP tools are called through `use_tool` with a `server__tool` name.
227
+ function describeMcp(tool, input) {
228
+ switch (tool) {
229
+ case 'wisp__ask_user':
230
+ return { kind: 'wait', label: 'Waiting for your answer' };
231
+ case 'wisp__delegate':
232
+ return { kind: 'agent', label: `Delegating to ${clip([...new Set((input.tasks || []).map((t) => t.subagent))].join(', ') || 'subagents', 60)}` };
233
+ case 'wisp__subagent_results':
234
+ return { kind: 'agent', label: 'Waiting on subagents' };
235
+ default:
236
+ return { kind: 'tool', label: `Using ${tool.split('__').join(' · ') || 'an integration'}` };
237
+ }
238
+ }
239
+
240
+ // Called once a tool succeeded. grok may stream a batch of tool calls after running them, so the
241
+ // "before" side of a write can't come from disk at tool_use time: the tracker supplies it.
242
+ function captureChange(name, input, cwd, files) {
243
+ const file = input.file_path;
244
+ if (!file) return null;
245
+ if (name === 'search_replace' && typeof input.old_string === 'string' && input.old_string) {
246
+ files.change(file); // keep the tracker's snapshot current for later writes
247
+ return { file: rel(file, cwd), op: 'edit', edits: [{ before: input.old_string, after: input.new_string ?? '' }] };
248
+ }
249
+ if (name === 'search_replace' || name === 'write') return files.change(file);
250
+ return null;
251
+ }
@@ -29,8 +29,9 @@ import codex from './codex.js';
29
29
  import opencode from './opencode.js';
30
30
  import cursor from './cursor.js';
31
31
  import antigravity from './antigravity.js';
32
+ import grok from './grok.js';
32
33
 
33
- export const adapters = Object.fromEntries([claude, codex, opencode, cursor, antigravity].map((a) => [a.id, a]));
34
+ export const adapters = Object.fromEntries([claude, codex, opencode, cursor, antigravity, grok].map((a) => [a.id, a]));
34
35
 
35
36
  const detected = new Map();
36
37
 
@@ -38,7 +38,7 @@ export default {
38
38
  return { ...found, models: [this.models[0], ...models] };
39
39
  },
40
40
 
41
- run({ prompt, attachments = [], cwd, resumeId, mode, model, effort, systemPrompt, mcpServers = {}, onEvent }) {
41
+ run({ prompt, attachments = [], cwd, resumeId, mode, model, effort, systemPrompt, mcpServers = {}, delegating = false, onEvent }) {
42
42
  // The prompt goes on stdin; `run` appends piped stdin to the (empty) message.
43
43
  const args = ['run', '--format', 'json'];
44
44
  if (mode === 'plan') args.push('--agent', 'plan');
@@ -60,6 +60,8 @@ export default {
60
60
  ...inline,
61
61
  mcp: { ...inline.mcp, ...mcp },
62
62
  experimental: { ...inline.experimental, mcp_timeout: ASK_TIMEOUT_MS },
63
+ // Its built-in `task` subagents would bypass the user's team.
64
+ ...(delegating && { permission: { ...(typeof inline.permission === 'object' ? inline.permission : inline.permission && { '*': inline.permission }), task: 'deny' } }),
63
65
  });
64
66
  }
65
67