wisp-agents 0.8.0 → 0.10.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 +35 -1
- package/README.md +28 -12
- package/bin/wisp.js +1 -1
- package/package.json +2 -2
- package/server/adapters/claude.js +3 -1
- package/server/adapters/grok.js +251 -0
- package/server/adapters/index.js +2 -1
- package/server/adapters/opencode.js +3 -1
- package/server/git.js +6 -3
- package/server/index.js +212 -19
- package/server/machines.js +736 -0
- package/server/mcp-ask.js +1 -1
- package/server/mcp-wisp.js +2 -2
- package/server/openapi.js +18 -2
- package/server/remote.js +8 -4
- package/server/runner.js +3 -0
- package/server/store.js +11 -6
- package/server/subagents.js +2 -1
- package/server/update.js +8 -4
- package/skills/wisp/SKILL.md +2 -2
- package/web/css/app.css +173 -39
- package/web/index.html +5 -0
- package/web/js/app.js +649 -86
- package/web/logos/grok.svg +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.10.0] - 2026-10-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Machines finds hosts for you. **Found on this computer** lists the hosts this computer already reaches: `Host` entries in `~/.ssh/config`, your Tailscale peers (`tailscale status --json`, with an online dot; phones and tablets are skipped) and hosts in `~/.ssh/known_hosts` (code hosts such as github.com, hashed entries and non-default ports are skipped). Hosts you've already added aren't listed. Click one to fill in the form, then press Connect. The SSH host field suggests the same hosts. API: `GET /api/machines/discover`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Ship it only shows when there is something to ship. It used to sit on every finished worktree task, even one that changed nothing or whose work was already on its PR. Now it appears (on the card bar, next to the composer, in the right-click menu, in ⌘K and on `S`) only if the worktree has uncommitted changes or commits its PR doesn't have yet. **Push to PR #N** shows only when there are new changes to push. wisp checks the worktree when a turn ends, after Ship it, a revert, a rollback or an action command, and at startup. Summaries carry it as `toShip: { dirty, ahead, unpushed }`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Updating from the top-bar Update button now restarts wisp on the port it was running on. Before, a wisp started with `--port` came back on 4477 and the open tab said "wisp did not come back".
|
|
21
|
+
|
|
22
|
+
## [0.9.0] - 2026-10-18
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- 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).
|
|
27
|
+
- 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.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- 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.
|
|
32
|
+
- 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.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- 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.
|
|
37
|
+
- The selects in the recipe and trigger forms and in the Ship it / Run dialogs had lost their dropdown chevron.
|
|
38
|
+
- Settings and Phone text fields, and every select, showed no focus state.
|
|
39
|
+
|
|
8
40
|
## [0.8.0] - 2026-10-17
|
|
9
41
|
|
|
10
42
|
### Added
|
|
@@ -158,7 +190,9 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
158
190
|
- Session queue with follow-ups, answers to agent questions, terminal takeover and mark-as-done.
|
|
159
191
|
- Older turns are folded, with a floating collapse-all button and back-to-top.
|
|
160
192
|
|
|
161
|
-
[Unreleased]: https://github.com/M0xoo/wisp/compare/v0.
|
|
193
|
+
[Unreleased]: https://github.com/M0xoo/wisp/compare/v0.10.0...HEAD
|
|
194
|
+
[0.10.0]: https://github.com/M0xoo/wisp/compare/v0.9.0...v0.10.0
|
|
195
|
+
[0.9.0]: https://github.com/M0xoo/wisp/compare/v0.8.0...v0.9.0
|
|
162
196
|
[0.8.0]: https://github.com/M0xoo/wisp/compare/v0.7.0...v0.8.0
|
|
163
197
|
[0.7.0]: https://github.com/M0xoo/wisp/compare/v0.6.0...v0.7.0
|
|
164
198
|
[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
|
[](https://nodejs.org)
|
|
14
14
|
[](https://www.electronjs.org)
|
|
15
15
|
[](package.json)
|
|
16
|
-
[](#adding-a-harness)
|
|
16
|
+
[](#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
|
|
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
|
|
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.
|
|
56
|
+
| 🧾 **Recipes & triggers** | Saved prompts with `{params}` (`wisp run release 0.10.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`),
|
|
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.
|
|
128
|
+
wisp run release 0.10.0 # run a recipe; values fill its {params} (or version=0.10.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
|
-
│
|
|
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
|
|
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.
|
|
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.10.0` fills the fields in order, `wisp run release version=0.10.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
|
|
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.
|
|
3
|
+
"version": "0.10.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
|
+
}
|
package/server/adapters/index.js
CHANGED
|
@@ -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
|
|
package/server/git.js
CHANGED
|
@@ -93,12 +93,15 @@ export async function ensureWorktree(wt) {
|
|
|
93
93
|
await git(wt.root, ['worktree', 'add', '--quiet', wt.path, wt.branch]);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// { dirty: changed paths, ahead: commits past the base, exists }
|
|
96
|
+
// { dirty: changed paths, ahead: commits past the base, unpushed: commits not on the remote branch, exists }
|
|
97
97
|
export async function worktreeStatus(wt) {
|
|
98
|
-
if (!wt || !fs.existsSync(wt.path)) return { exists: false, dirty: 0, ahead: 0 };
|
|
98
|
+
if (!wt || !fs.existsSync(wt.path)) return { exists: false, dirty: 0, ahead: 0, unpushed: 0 };
|
|
99
99
|
const porcelain = await quiet(git(wt.path, ['status', '--porcelain']));
|
|
100
100
|
const ahead = Number(await quiet(git(wt.path, ['rev-list', '--count', `${wt.baseRef}..HEAD`]))) || 0;
|
|
101
|
-
|
|
101
|
+
const pushed = wt.remote && `refs/remotes/${wt.remote}/${wt.branch}`;
|
|
102
|
+
const tracked = pushed && (await quiet(git(wt.path, ['rev-parse', '--verify', '--quiet', pushed])));
|
|
103
|
+
const unpushed = tracked ? Number(await quiet(git(wt.path, ['rev-list', '--count', `${pushed}..HEAD`]))) || 0 : ahead;
|
|
104
|
+
return { exists: true, dirty: porcelain ? porcelain.split('\n').length : 0, ahead, unpushed };
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
// force: drop uncommitted work too. The branch goes only when nothing would be lost with it.
|