wisp-agents 0.4.0 → 0.6.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,32 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.6.0] - 2026-10-17
9
+
10
+ ### Added
11
+
12
+ - Antigravity CLI adapter (`agy --input-format stream-json --output-format stream-json`). Plan → `--mode plan`, Edits → `--mode accept-edits`, Full auto → `--dangerously-skip-permissions`; models from `agy models`, `--effort low|medium|high`, follow-ups via `--conversation`. Like Cursor it has no flag to inject MCP servers, so Antigravity agents ask with a `question` block. Override the binary with `WISP_ANTIGRAVITY_BIN`.
13
+ - Integrations: wisp can now be driven by other agents, editors and scripts. Reference: [docs/integrations.md](docs/integrations.md).
14
+ - `wisp mcp`: a stdio MCP server with `wisp_launch`, `wisp_wait`, `wisp_get`, `wisp_reply`, `wisp_list`, `wisp_stop` and `wisp_agents`, so Claude Code, Codex, opencode, Cursor or Claude Desktop can hand tasks to wisp and read the results.
15
+ - Agent skill (`skills/wisp/SKILL.md`), installed with `wisp skill install [--agent claude|codex|opencode|all] [--project]`.
16
+ - Scripting CLI: `wisp ls`, `show`, `wait`, `reply`, `stop`, plus `--json`, `--wait`, `--cwd`, `--model` and `--effort` on launches. Use `wisp -- …` for a task whose first word is a command name.
17
+ - JS SDK: `import { WispClient } from 'wisp-agents'`. It connects to the running wisp (or starts a headless one) and covers launch, wait, reply, presentation, uploads and the event stream. `wisp-agents/render`, `/protocol` and `/server` are exported too.
18
+ - HTTP API: `GET /api/health`, `GET /api/openapi.json` (OpenAPI 3.1), `GET /api/sessions/:id/wait` (long-poll until it finishes or asks), `GET /api/sessions/:id/presentation` (blocks + markdown, `?format=markdown`), and `state`/`agent`/`project`/`cwd`/`archived`/`limit` filters on `GET /api/sessions`.
19
+ - `~/.wisp/server.json` advertises the running server's URL, port and pid.
20
+ - Webhooks (`PATCH /api/config {"webhooks": [...]}`): POST `session.finished` / `session.waiting` with the presentation as markdown.
21
+
22
+ ### Fixed
23
+
24
+ - Clicking a harness in the harness · model picker (or Enter on it) switches to it with the model and thinking level shown, instead of only previewing its models.
25
+ - Antigravity models are listed with newer `agy` builds that print tab-separated `agy models` output.
26
+ - A harness CLI installed while wisp is running is detected without restarting wisp.
27
+
28
+ ## [0.5.0] - 2026-10-17
29
+
30
+ ### Added
31
+
32
+ - Anonymous usage stats and error reports via [PostHog](https://posthog.com): which agents, models and modes are used, run outcomes and timings, and errors (agent failures, server errors, crashes) with paths scrubbed. Never sends prompts, output, paths or project names. Turn it off in Settings → *Anonymous usage stats*, or with `WISP_TELEMETRY=0` / `DO_NOT_TRACK=1`; always off when `CI` is set. See the Telemetry section of the README.
33
+
8
34
  ## [0.4.0] - 2026-10-17
9
35
 
10
36
  ### Added
@@ -83,7 +109,9 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
83
109
  - Session queue with follow-ups, answers to agent questions, terminal takeover and mark-as-done.
84
110
  - Older turns are folded, with a floating collapse-all button and back-to-top.
85
111
 
86
- [Unreleased]: https://github.com/M0xoo/wisp/compare/v0.4.0...HEAD
112
+ [Unreleased]: https://github.com/M0xoo/wisp/compare/v0.6.0...HEAD
113
+ [0.6.0]: https://github.com/M0xoo/wisp/compare/v0.5.0...v0.6.0
114
+ [0.5.0]: https://github.com/M0xoo/wisp/compare/v0.4.0...v0.5.0
87
115
  [0.4.0]: https://github.com/M0xoo/wisp/compare/v0.3.0...v0.4.0
88
116
  [0.3.0]: https://github.com/M0xoo/wisp/compare/v0.2.2...v0.3.0
89
117
  [0.2.2]: https://github.com/M0xoo/wisp/compare/v0.2.1...v0.2.2
package/README.md CHANGED
@@ -13,9 +13,9 @@ 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-d97757)](#adding-a-harness)
16
+ [![Harnesses](https://img.shields.io/badge/harness-Claude%20Code%20·%20Codex%20·%20opencode%20·%20Cursor%20·%20Antigravity-d97757)](#adding-a-harness)
17
17
 
18
- [Quick start](#quick-start) · [Features](#features) · [CLI](#cli) · [Shortcuts](#keyboard-shortcuts) · [Architecture](#architecture) · [Adding a harness](#adding-a-harness) · [Roadmap](#roadmap)
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
 
20
20
  </div>
21
21
 
@@ -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, and Cursor) 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, 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.
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,15 +44,16 @@ 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, or Cursor 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, 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. |
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
  | 🔐 **Permission modes** | Choose **Plan** (read-only), **Edits** (file edits auto-accepted), or **Full auto** for each launch. `Shift+Tab` in the prompt cycles modes. |
51
+ | 🔌 **Integrations** | An MCP server (`wisp mcp`), an agent skill, a scriptable CLI, a JS SDK, an OpenAPI-described HTTP API, and webhooks. Hand tasks to wisp from Claude Code, Codex, opencode, Cursor, scripts or CI. |
51
52
  | 🪶 **No runtime dependencies** | The server uses only Node built-ins. Electron is needed only for the desktop app. |
52
53
 
53
54
  ## Quick start
54
55
 
55
- > **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`), or [Cursor CLI](https://cursor.com/cli) (`cursor-agent`). Override a binary path with `WISP_CLAUDE_BIN`, `WISP_CODEX_BIN`, `WISP_OPENCODE_BIN`, or `WISP_CURSOR_BIN`.
56
+ > **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`.
56
57
 
57
58
  ```sh
58
59
  npx wisp-agents # open the wisp desktop app
@@ -101,20 +102,59 @@ wisp # open wisp (or focus the running one)
101
102
  wisp "fix the flaky auth test" # launch a task in this folder (it becomes a project)
102
103
  wisp add [path] # add a folder as a project (default: cwd)
103
104
  wisp --mode plan "audit deps" # permission mode: plan | edits | auto
104
- wisp --agent codex "add a test" # harness: claude | codex | opencode | cursor
105
+ wisp --agent codex "add a test" # harness: claude | codex | opencode | cursor | antigravity
106
+ ```
107
+
108
+ Scripting (every command takes `--json`):
109
+
110
+ ```sh
111
+ wisp "add a test" --wait # block until it finishes, print the presentation as markdown
112
+ wisp ls --state running,waiting # list sessions
113
+ wisp show <id> # state + latest presentation
114
+ wisp wait <id> # until it finishes or asks you something
115
+ wisp reply <id> "also do X" # follow up (answers a pending question)
116
+ wisp stop <id>
117
+ wisp mcp # MCP server for other agents and editors
118
+ wisp skill install # install the wisp skill for Claude Code (--agent all for Codex/opencode too)
105
119
  ```
106
120
 
107
121
  | Option | Description |
108
122
  |---|---|
109
123
  | `--port`, `-p N` | Port to use (default `4477`, or `WISP_PORT`) |
110
124
  | `--mode`, `-m` | `plan` · `edits` · `auto` |
111
- | `--agent`, `-a` | `claude` · `codex` · `opencode` · `cursor` (default: the agent last picked in the launcher) |
125
+ | `--agent`, `-a` | `claude` · `codex` · `opencode` · `cursor` · `antigravity` (default: the agent last picked in the launcher) |
126
+ | `--model` · `--effort` | Model id and thinking level for the launch |
127
+ | `--cwd`, `-C DIR` | Project folder (default: the current one) |
128
+ | `--wait`, `-w` | Wait for the run to finish or ask, then print the presentation |
129
+ | `--json` | Machine-readable output |
112
130
  | `--browser`, `-b` | Use the browser instead of the desktop app |
113
131
  | `--no-open` | Just run the server; open nothing |
114
132
  | `--version`, `-v` | Print the version |
115
133
 
116
134
  > `npm install -g wisp-agents` (or `npm link` from a clone) puts `wisp` on your `PATH`. Without installing, use `npx wisp-agents …` with the same arguments.
117
135
 
136
+ ## Integrations
137
+
138
+ wisp is a local server, and the app is just one client of it. Other agents, editors and scripts can launch tasks into the same queue and read the presentations back. Full reference: [docs/integrations.md](docs/integrations.md).
139
+
140
+ ```sh
141
+ claude mcp add wisp -- npx -y wisp-agents mcp # Claude Code can now delegate with wisp_launch / wisp_wait / wisp_reply
142
+ wisp skill install --agent all # teach Claude Code, Codex and opencode when and how to use wisp
143
+ ```
144
+
145
+ ```js
146
+ import { WispClient } from 'wisp-agents';
147
+ const wisp = await WispClient.connect();
148
+ const s = await wisp.launch({ prompt: 'fix the flaky auth test', cwd: '/path/to/repo' });
149
+ console.log((await wisp.wait(s.id)).state, (await wisp.presentation(s.id)).markdown);
150
+ ```
151
+
152
+ - **MCP** (`wisp mcp`): `wisp_launch`, `wisp_wait`, `wisp_get`, `wisp_reply`, `wisp_list`, `wisp_stop`, `wisp_agents`. Config snippets for Claude Code, Codex, opencode, Cursor and Claude Desktop are in the docs.
153
+ - **Skill** ([`skills/wisp/SKILL.md`](skills/wisp/SKILL.md)): when to delegate, how to brief, and to pass a waiting agent's question on to you instead of answering it.
154
+ - **HTTP API**: `GET /api/openapi.json`. The new routes are `/api/health`, `/api/sessions/:id/wait` (long-poll), `/api/sessions/:id/presentation` (blocks + markdown), and filters on `/api/sessions`.
155
+ - **Discovery**: the running server writes `~/.wisp/server.json` (`url`, `port`, `pid`, `version`).
156
+ - **Webhooks**: `PATCH /api/config {"webhooks":[…]}`. wisp POSTs `session.finished` and `session.waiting` with the presentation as markdown.
157
+
118
158
  ## Keyboard shortcuts
119
159
 
120
160
  | Key | Action |
@@ -146,6 +186,7 @@ web/ (UI) ⇄ HTTP + SSE ⇄ server/ ── adapters/claude.js ── claude -
146
186
  ├─ adapters/codex.js ── codex exec --json
147
187
  ├─ adapters/opencode.js ── opencode run --format json
148
188
  ├─ adapters/cursor.js ── cursor-agent -p --output-format stream-json
189
+ ├─ adapters/antigravity.js ── agy --output-format stream-json
149
190
  └─ mcp-ask.js (ask_user MCP tool, injected per run)
150
191
  ```
151
192
 
@@ -155,6 +196,10 @@ wisp/
155
196
  │ └── wisp.js # CLI: starts the server or talks to the running one
156
197
  ├── server/
157
198
  │ ├── index.js # HTTP API, SSE stream, static UI, uploads
199
+ │ ├── openapi.js # OpenAPI document for the public API
200
+ │ ├── client.js # JS SDK (wisp-agents/client), used by the CLI and MCP server
201
+ │ ├── mcp-wisp.js # `wisp mcp`: wisp as tools for other agents
202
+ │ ├── render.js # presentation → markdown
158
203
  │ ├── runner.js # session lifecycle, follow-ups, event normalization
159
204
  │ ├── protocol.js # system prompt + ```wisp block parser (markdown fallback)
160
205
  │ ├── mcp-ask.js # ask_user MCP server
@@ -167,7 +212,9 @@ wisp/
167
212
  │ ├── claude.js # Claude Code adapter
168
213
  │ ├── codex.js # Codex CLI adapter
169
214
  │ ├── opencode.js # opencode adapter
170
- │ └── cursor.js # Cursor CLI adapter
215
+ │ ├── cursor.js # Cursor CLI adapter
216
+ │ └── antigravity.js # Antigravity CLI adapter
217
+ ├── skills/wisp/SKILL.md # agent skill: how other agents use wisp
171
218
  ├── web/ # vanilla JS UI, no build step
172
219
  │ ├── index.html
173
220
  │ ├── css/app.css
@@ -201,6 +248,22 @@ If the harness has no system-prompt flag, prepend `systemPrompt` to the first pr
201
248
  - [x] Codex adapter
202
249
  - [x] opencode adapter
203
250
  - [x] Cursor CLI adapter
251
+ - [x] Antigravity CLI adapter
252
+
253
+ ## Telemetry
254
+
255
+ wisp sends anonymous usage stats and error reports to [PostHog](https://posthog.com) so we can see which agents people use and what breaks. The code is in [`server/telemetry.js`](server/telemetry.js).
256
+
257
+ **What is sent:** a random install id (not tied to you or your machine), the wisp, OS, Node and agent CLI versions, and per run: agent, model, mode, effort, status, duration, token counts and cost, how many files changed, and which presentation block types came back. Errors include a stack trace and message with your home directory and file paths removed, cut to 500 characters.
258
+
259
+ **Never sent:** prompts, replies, agent output, file contents, project names, paths or repo URLs.
260
+
261
+ **Opt out** with any of these:
262
+ - Settings → *Anonymous usage stats*
263
+ - `WISP_TELEMETRY=0`
264
+ - `DO_NOT_TRACK=1`
265
+
266
+ It is also off whenever `CI` is set.
204
267
 
205
268
  ## Contributing
206
269
 
@@ -213,7 +276,7 @@ npm test # adapters, process launching, terminals, and the full ask_user
213
276
  npm run test:real # smoke test against the real CLIs installed on this machine
214
277
  ```
215
278
 
216
- `npm test` needs no network or accounts. A fake CLI (`test/fixtures/fake-agent.mjs`) speaks each harness's real output format, edits files, and calls `ask_user` through the real MCP bridge. On Windows the fakes are installed as npm-style `.cmd` shims, which is how npm installs the real CLIs. CI (`.github/workflows/test.yml`) runs the suite on Linux, macOS, and Windows with Node 20 and 22, then runs `test:real` against freshly installed Claude Code, Codex, opencode, and Cursor CLIs.
279
+ `npm test` needs no network or accounts. A fake CLI (`test/fixtures/fake-agent.mjs`) speaks each harness's real output format, edits files, and calls `ask_user` through the real MCP bridge. On Windows the fakes are installed as npm-style `.cmd` shims, which is how npm installs the real CLIs. CI (`.github/workflows/test.yml`) runs the suite on Linux, macOS, and Windows with Node 20 and 22, then runs `test:real` against freshly installed Claude Code, Codex, opencode, Cursor, and Antigravity CLIs.
217
280
 
218
281
  ## License
219
282
 
package/bin/wisp.js CHANGED
@@ -1,100 +1,213 @@
1
1
  #!/usr/bin/env node
2
2
  // wisp — launcher + queue for coding agents.
3
3
  //
4
- // wisp open the wisp desktop app (or focus the running one)
5
- // wisp "fix the tests" launch a wisp in the current folder
6
- // wisp add [path] add a folder as a project (default: current folder)
7
- // --port N port (default 4477, or WISP_PORT)
8
- // --mode plan|edits|auto permission mode for a launch
9
- // --agent ID claude, codex, opencode or cursor (default: the launcher's pick)
10
- // --browser use the browser instead of the desktop app
11
- // --no-open just run the server, open nothing
4
+ // wisp open the wisp desktop app (or focus the running one)
5
+ // wisp "fix the tests" launch a wisp in the current folder
6
+ // wisp add [path] add a folder as a project (default: current folder)
7
+ // wisp ls | show | wait | reply | stop script sessions (see --help)
8
+ // wisp mcp stdio MCP server, so other agents and editors can use wisp
9
+ // wisp skill [install] the agent skill that teaches other agents to use wisp
12
10
 
13
11
  import { exec, spawn } from 'node:child_process';
14
- import { existsSync } from 'node:fs';
12
+ import fs, { existsSync } from 'node:fs';
13
+ import os from 'node:os';
15
14
  import { createRequire } from 'node:module';
16
15
  import path from 'node:path';
17
16
  import { fileURLToPath } from 'node:url';
18
17
  import { wispAppBinary } from './mac-app.js';
19
18
 
19
+ const HELP = `usage:
20
+ wisp open wisp
21
+ wisp "task" launch a task in this folder
22
+ wisp add [path] add a project
23
+ wisp ls list sessions (--state running,waiting --limit N)
24
+ wisp show <id> state + latest presentation as markdown
25
+ wisp wait <id> block until it finishes or asks you something
26
+ wisp reply <id> "text" follow up (answers a pending question) (--queue)
27
+ wisp stop <id> stop a run
28
+ wisp mcp run the MCP server (stdio) for other agents and editors
29
+ wisp skill [install] print / install the wisp agent skill (--agent claude|codex|opencode|all --project)
30
+ wisp -- "ls the repo" a task that starts with a command name
31
+
32
+ launch options: --mode plan|edits|auto --agent claude|codex|opencode|cursor|antigravity --model ID --effort LEVEL
33
+ --cwd DIR --wait --json
34
+ options: --port N --browser --no-open --json --version`;
35
+
36
+ const COMMANDS = new Set(['add', 'ls', 'list', 'show', 'wait', 'reply', 'stop', 'mcp', 'skill']);
20
37
  const argv = process.argv.slice(2);
21
- const flags = { open: true, browser: false, port: Number(process.env.WISP_PORT) || 4477, mode: null, agent: null };
38
+ const flags = { open: true, browser: false, port: Number(process.env.WISP_PORT) || 4477, mode: null, agent: null, model: null, effort: null, cwd: null, json: false, wait: false, queue: undefined, state: null, limit: null, project: false };
22
39
  const words = [];
40
+ let literal = false;
23
41
  for (let i = 0; i < argv.length; i++) {
24
42
  const a = argv[i];
25
- if (a === '--no-open') flags.open = false;
43
+ if (literal) words.push(a);
44
+ else if (a === '--') literal = words.length === 0 ? 'task' : true;
45
+ else if (a === '--no-open') flags.open = false;
26
46
  else if (a === '--browser' || a === '-b') flags.browser = true;
27
47
  else if (a === '--port' || a === '-p') flags.port = Number(argv[++i]);
28
48
  else if (a === '--mode' || a === '-m') flags.mode = argv[++i];
29
49
  else if (a === '--agent' || a === '-a') flags.agent = argv[++i];
50
+ else if (a === '--model') flags.model = argv[++i];
51
+ else if (a === '--effort') flags.effort = argv[++i];
52
+ else if (a === '--cwd' || a === '-C') flags.cwd = path.resolve(argv[++i].replace(/^~(?=$|[\\/])/, os.homedir()));
53
+ else if (a === '--state') flags.state = argv[++i];
54
+ else if (a === '--limit') flags.limit = Number(argv[++i]);
55
+ else if (a === '--json') flags.json = true;
56
+ else if (a === '--wait' || a === '-w') flags.wait = true;
57
+ else if (a === '--queue') flags.queue = true;
58
+ else if (a === '--project') flags.project = true;
30
59
  else if (a === '--version' || a === '-v') {
31
- const { readFileSync } = await import('node:fs');
32
- console.log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url))).version);
60
+ console.log(JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url))).version);
33
61
  process.exit(0);
34
62
  } else if (a === '--help' || a === '-h') {
35
- console.log('usage:\n wisp open wisp\n wisp "task" launch a task in this folder\n wisp add [path] add a project\n\noptions: --port N --mode plan|edits|auto --agent claude|codex|opencode|cursor --browser --no-open --version');
63
+ console.log(HELP);
36
64
  process.exit(0);
37
65
  } else words.push(a);
38
66
  }
67
+ const command = literal !== 'task' && COMMANDS.has(words[0]) ? words[0] : null;
68
+ const cwd = flags.cwd || process.cwd();
69
+ const die = (msg) => {
70
+ console.error(` ✗ ${msg}`);
71
+ process.exit(1);
72
+ };
39
73
 
40
- const base = `http://localhost:${flags.port}`;
41
- const api = (p, body) =>
42
- fetch(`${base}/api${p}`, {
43
- method: body ? 'POST' : 'GET',
44
- headers: { 'content-type': 'application/json' },
45
- body: body ? JSON.stringify(body) : undefined,
46
- }).then(async (r) => {
47
- const j = await r.json();
48
- if (!r.ok) throw new Error(j.error || r.statusText);
49
- return j;
50
- });
51
-
52
- let running = false;
53
- try {
54
- await api('/config');
55
- running = true;
56
- } catch {}
57
-
58
- // The desktop app (Electron is an optional dependency) runs the server in its own process, so
59
- // it outlives this terminal. Without Electron, or with --browser/--no-open, serve from here.
60
- // A task or `add` sent to an already-running wisp just goes over the API, no window.
61
- const electron = flags.open && !flags.browser && (!running || !words.length) ? electronBinary() : null;
62
- if (electron) {
63
- const child = spawn(electron, [fileURLToPath(new URL('..', import.meta.url))], {
64
- detached: true,
65
- stdio: 'ignore',
66
- env: { ...process.env, WISP_PORT: String(flags.port) },
67
- });
68
- child.unref();
69
- if (!running) await waitForServer();
70
- console.log(`\n ◌ wisp is open\n`);
71
- } else if (!running) {
72
- const { startServer } = await import('../server/index.js');
73
- await startServer({ port: flags.port });
74
- console.log(`\n ◌ wisp is floating at ${base}\n`);
74
+ // --- commands that only talk to the API (and start a headless wisp if none is running) ---
75
+
76
+ if (command === 'mcp') {
77
+ const { serveMcp } = await import('../server/mcp-wisp.js');
78
+ serveMcp(); // stdout belongs to the MCP protocol from here on
79
+ } else if (command === 'skill') {
80
+ skill(words[1]);
81
+ } else if (command && command !== 'add') {
82
+ await scripted(command, words.slice(1)).catch((e) => die(e.message));
83
+ } else if (!command && words.length && (flags.json || flags.wait)) {
84
+ await scripted('launch', words).catch((e) => die(e.message));
85
+ } else {
86
+ await interactive();
75
87
  }
76
88
 
77
- let openPath = '/';
78
- if (words[0] === 'add') {
79
- const dir = path.resolve(words[1] || process.cwd());
80
- const p = await api('/projects', { path: dir }).catch((e) => console.error(` ✗ ${e.message}`));
81
- if (p) console.log(` ✓ project added: ${p.name} (${p.path})`);
82
- if (running || electron) process.exit(0);
83
- } else if (words.length) {
84
- const cfg = await api('/config');
85
- const agent = flags.agent || cfg.defaults.agent;
86
- // Models are remembered per agent (ids aren't portable between harnesses).
87
- const model = cfg.defaults.models?.[agent] ?? (agent === cfg.defaults.agent ? cfg.defaults.model : '');
88
- const s = await api('/sessions', { prompt: words.join(' '), cwd: process.cwd(), agent, mode: flags.mode || cfg.defaults.mode, model });
89
- console.log(` ◌ launched: ${s.title}`);
90
- openPath = `/#/s/${s.id}`;
91
- if (running || electron) process.exit(0); // the running wisp will show it in the queue
89
+ async function scripted(cmd, args) {
90
+ const { WispClient } = await import('../server/client.js');
91
+ const wisp = await WispClient.connect({ port: argv.some((a) => a === '--port' || a === '-p') ? flags.port : undefined });
92
+ const out = (v, text) => console.log(flags.json ? JSON.stringify(v, null, 2) : text);
93
+ const need = (v, what) => v || die(`${what} required — see wisp --help`);
94
+ const show = async (id, summary) => {
95
+ const p = await wisp.presentation(id);
96
+ const s = summary || (await wisp.sessions()).find((x) => x.id === id);
97
+ out({ ...s, presentation: p }, `◌ ${s?.title} [${s?.state}${summary?.timedOut ? ', still running' : ''}] ${id}\n\n${p.markdown}`);
98
+ };
99
+ switch (cmd) {
100
+ case 'launch': {
101
+ const s = await wisp.launch({ prompt: args.join(' '), cwd, agent: flags.agent || undefined, mode: flags.mode || undefined, model: flags.model ?? undefined, effort: flags.effort ?? undefined });
102
+ if (!flags.wait) return out(s, ` ◌ launched: ${s.title} (${s.id})`);
103
+ if (!flags.json) console.error(` ◌ launched: ${s.title} (${s.id}) — waiting…`);
104
+ return show(s.id, await wisp.wait(s.id));
105
+ }
106
+ case 'ls':
107
+ case 'list': {
108
+ const list = await wisp.sessions({ state: flags.state, limit: flags.limit, cwd: flags.cwd || undefined, archived: flags.state ? undefined : false });
109
+ return out(list, list.map((s) => `${s.id} ${s.state.padEnd(7)} ${s.agent.padEnd(8)} ${s.project.padEnd(16).slice(0, 16)} ${s.title}${s.question ? ` — asks: ${s.question}` : ''}`).join('\n') || 'No sessions.');
110
+ }
111
+ case 'show':
112
+ return show(need(args[0], 'session id'));
113
+ case 'wait': {
114
+ const id = need(args[0], 'session id');
115
+ return show(id, await wisp.wait(id));
116
+ }
117
+ case 'reply': {
118
+ const id = need(args[0], 'session id');
119
+ const r = await wisp.reply(id, need(args.slice(1).join(' ').trim(), 'text'), { queue: flags.queue });
120
+ if (!flags.wait) return out(r, r.answered ? ' ✓ answered' : r.interrupted ? ' ✓ interrupted; resuming with your message' : r.queued ? ' ✓ queued' : ' ✓ sent');
121
+ return show(id, await wisp.wait(id));
122
+ }
123
+ case 'stop':
124
+ return out(await wisp.stop(need(args[0], 'session id')), ' ✓ stopped');
125
+ }
92
126
  }
93
127
 
94
- if (electron) process.exit(0);
95
- if (flags.open) {
96
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start ""' : 'xdg-open';
97
- exec(`${cmd} "${base}${openPath}"`);
128
+ // Installs skills/wisp/SKILL.md where each harness looks for skills.
129
+ function skill(sub) {
130
+ const src = fileURLToPath(new URL('../skills/wisp/SKILL.md', import.meta.url));
131
+ if (sub !== 'install') return process.stdout.write(fs.readFileSync(src, 'utf8'));
132
+ const home = os.homedir();
133
+ const base = flags.project ? cwd : null;
134
+ const targets = {
135
+ claude: base ? path.join(base, '.claude', 'skills') : path.join(home, '.claude', 'skills'),
136
+ codex: base ? path.join(base, '.codex', 'skills') : path.join(process.env.CODEX_HOME || path.join(home, '.codex'), 'skills'),
137
+ opencode: base ? path.join(base, '.opencode', 'skill') : path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'opencode', 'skill'),
138
+ };
139
+ const which = !flags.agent ? ['claude'] : flags.agent === 'all' ? Object.keys(targets) : [flags.agent];
140
+ for (const a of which) {
141
+ if (!targets[a]) die(`no skill location for ${a} (try claude, codex, opencode or all)`);
142
+ const dir = path.join(targets[a], 'wisp');
143
+ fs.mkdirSync(dir, { recursive: true });
144
+ fs.copyFileSync(src, path.join(dir, 'SKILL.md'));
145
+ console.log(` ✓ ${a}: ${path.join(dir, 'SKILL.md')}`);
146
+ }
147
+ }
148
+
149
+ // --- the original flow: open the app, launch from the terminal, add projects ---
150
+
151
+ async function interactive() {
152
+ const base = `http://localhost:${flags.port}`;
153
+ const api = (p, body) =>
154
+ fetch(`${base}/api${p}`, {
155
+ method: body ? 'POST' : 'GET',
156
+ headers: { 'content-type': 'application/json' },
157
+ body: body ? JSON.stringify(body) : undefined,
158
+ }).then(async (r) => {
159
+ const j = await r.json();
160
+ if (!r.ok) throw new Error(j.error || r.statusText);
161
+ return j;
162
+ });
163
+
164
+ let running = false;
165
+ try {
166
+ await api('/config');
167
+ running = true;
168
+ } catch {}
169
+
170
+ // The desktop app (Electron is an optional dependency) runs the server in its own process, so
171
+ // it outlives this terminal. Without Electron, or with --browser/--no-open, serve from here.
172
+ // A task or `add` sent to an already-running wisp just goes over the API, no window.
173
+ const electron = flags.open && !flags.browser && (!running || !words.length) ? electronBinary() : null;
174
+ if (electron) {
175
+ const child = spawn(electron, [fileURLToPath(new URL('..', import.meta.url))], {
176
+ detached: true,
177
+ stdio: 'ignore',
178
+ env: { ...process.env, WISP_PORT: String(flags.port) },
179
+ });
180
+ child.unref();
181
+ if (!running) await waitForServer(api);
182
+ console.log(`\n ◌ wisp is open\n`);
183
+ } else if (!running) {
184
+ const { startServer } = await import('../server/index.js');
185
+ await startServer({ port: flags.port });
186
+ console.log(`\n ◌ wisp is floating at ${base}\n`);
187
+ }
188
+
189
+ let openPath = '/';
190
+ if (command === 'add') {
191
+ const dir = path.resolve(words[1] || process.cwd());
192
+ const p = await api('/projects', { path: dir }).catch((e) => console.error(` ✗ ${e.message}`));
193
+ if (p) console.log(` ✓ project added: ${p.name} (${p.path})`);
194
+ if (running || electron) process.exit(0);
195
+ } else if (words.length) {
196
+ const cfg = await api('/config');
197
+ const agent = flags.agent || cfg.defaults.agent;
198
+ // Models are remembered per agent (ids aren't portable between harnesses).
199
+ const model = flags.model ?? cfg.defaults.models?.[agent] ?? (agent === cfg.defaults.agent ? cfg.defaults.model : '');
200
+ const s = await api('/sessions', { prompt: words.join(' '), cwd, agent, mode: flags.mode || cfg.defaults.mode, model, effort: flags.effort || '' });
201
+ console.log(` ◌ launched: ${s.title}`);
202
+ openPath = `/#/s/${s.id}`;
203
+ if (running || electron) process.exit(0); // the running wisp will show it in the queue
204
+ }
205
+
206
+ if (electron) process.exit(0);
207
+ if (flags.open) {
208
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start ""' : 'xdg-open';
209
+ exec(`${cmd} "${base}${openPath}"`);
210
+ }
98
211
  }
99
212
 
100
213
  function electronBinary() {
@@ -107,7 +220,7 @@ function electronBinary() {
107
220
  }
108
221
  }
109
222
 
110
- async function waitForServer() {
223
+ async function waitForServer(api) {
111
224
  for (let i = 0; i < 150; i++) {
112
225
  try {
113
226
  return await api('/config');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wisp-agents",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Launch coding agents, skip the transcript, read the presentation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "CHANGELOG.md",
13
13
  "electron/",
14
14
  "server/",
15
+ "skills/",
15
16
  "web/"
16
17
  ],
17
18
  "scripts": {
@@ -19,9 +20,9 @@
19
20
  "app": "node bin/wisp.js",
20
21
  "dev": "WISP_DEV=1 node bin/wisp.js",
21
22
  "dev:web": "node --watch-path=server bin/wisp.js --no-open",
22
- "test": "node --test test/proc.test.js test/terminal.test.js test/adapters.test.js test/e2e.test.js test/fuzzy.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",
23
24
  "test:real": "node test/real-clis.mjs",
24
- "prepublishOnly": "node --check bin/wisp.js && node --check bin/mac-app.js && node -e \"import('./server/index.js')\" && node --check electron/main.js"
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"
25
26
  },
26
27
  "engines": {
27
28
  "node": ">=20"
@@ -33,7 +34,9 @@
33
34
  "ai",
34
35
  "agents",
35
36
  "cli",
36
- "launcher"
37
+ "launcher",
38
+ "mcp",
39
+ "sdk"
37
40
  ],
38
41
  "author": "M0xoo",
39
42
  "license": "MIT",
@@ -46,6 +49,14 @@
46
49
  "url": "https://github.com/M0xoo/wisp/issues"
47
50
  },
48
51
  "main": "electron/main.js",
52
+ "exports": {
53
+ ".": "./server/client.js",
54
+ "./client": "./server/client.js",
55
+ "./server": "./server/index.js",
56
+ "./protocol": "./server/protocol.js",
57
+ "./render": "./server/render.js",
58
+ "./package.json": "./package.json"
59
+ },
49
60
  "optionalDependencies": {
50
61
  "electron": "^44.8.0"
51
62
  }