wisp-agents 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 M0xoo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,186 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <img src="electron/icon.png" alt="wisp" width="120" />
4
+
5
+ # wisp
6
+
7
+ **Send coding agents off. Read the highlights.**
8
+
9
+ Launch coding agents in the background, skip the transcript, and read the presentation they hand back when they finish.
10
+
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-6a5cff.svg)](#license)
12
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org)
13
+ [![Electron](https://img.shields.io/badge/desktop-Electron-47848f?logo=electron&logoColor=white)](https://www.electronjs.org)
14
+ [![Zero runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)](package.json)
15
+ [![Claude Code](https://img.shields.io/badge/harness-Claude%20Code-d97757)](https://claude.com/claude-code)
16
+
17
+ [Quick start](#quick-start) · [Features](#features) · [CLI](#cli) · [Shortcuts](#keyboard-shortcuts) · [Architecture](#architecture) · [Adding a harness](#adding-a-harness) · [Roadmap](#roadmap)
18
+
19
+ </div>
20
+
21
+ ---
22
+
23
+ ## Why wisp?
24
+
25
+ Agent transcripts are long, and you rarely need them. wisp runs agent harnesses (Claude Code today, with Codex and opencode planned) 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
+
27
+ From there you can **follow up**, **answer its questions**, **take it over** in your terminal, or **mark it done**.
28
+
29
+ ```text
30
+ you ──▶ "fix the flaky auth test" ──▶ agent works in the background
31
+ │
32
+ you ◀── hero · diff · tests · next steps ◀───┘ (no transcript to read)
33
+ ```
34
+
35
+ ## Features
36
+
37
+ | | |
38
+ |---|---|
39
+ | 🚀 **Fire and forget** | Launch a task from the UI or from any folder with `wisp "…"`. Each folder becomes a project. |
40
+ | 🎞️ **Presentations, not transcripts** | Agents end with structured blocks that render as an animated, skimmable report. |
41
+ | ❓ **Live questions** | Agents get an `ask_user` tool, so they can pause mid-run. Your answer goes straight back to the running agent. |
42
+ | 🧾 **Real diffs** | Edits come from the agent's own tool calls, so `diff` blocks show what actually changed. |
43
+ | 📥 **A queue** | Sessions sort into *Needs you*, *Working* (with a live one-line status), and *Done*. |
44
+ | 🖥️ **Take over** | Opens your terminal with `claude --resume <id>` in the project folder. Works with Terminal, iTerm, Ghostty, Warp, kitty, WezTerm, or a custom command. |
45
+ | 📎 **Attachments** | Paste screenshots or drop images, video, logs, or PDFs into the launcher. |
46
+ | 🔐 **Permission modes** | Choose **Plan** (read-only), **Edits** (file edits auto-accepted), or **Full auto** for each launch. |
47
+ | 🪶 **No runtime dependencies** | The server uses only Node built-ins. Electron is needed only for the desktop app. |
48
+
49
+ ## Quick start
50
+
51
+ > **Requirements:** Node.js ≥ 20 and [Claude Code](https://claude.com/claude-code) (`claude`) installed and signed in.
52
+
53
+ ```sh
54
+ npx wisp-agents # → http://localhost:4477
55
+ ```
56
+
57
+ Or install it globally to get the `wisp` command everywhere:
58
+
59
+ ```sh
60
+ npm install -g wisp-agents
61
+ wisp "fix the flaky auth test"
62
+ ```
63
+
64
+ From source:
65
+
66
+ ```sh
67
+ git clone https://github.com/M0xoo/wisp.git
68
+ cd wisp
69
+
70
+ # Browser mode, with nothing to install:
71
+ node bin/wisp.js # → http://localhost:4477
72
+
73
+ # Desktop app:
74
+ npm install # pulls Electron
75
+ npm run app
76
+ ```
77
+
78
+ On first launch, the *Welcome to wisp* session shows a live gallery of every presentation block.
79
+
80
+ ### Development
81
+
82
+ ```sh
83
+ npm run dev # desktop app: live reload for web/, relaunch on server/ and electron/ changes
84
+ npm run dev:web # browser mode, restarts the server when server/ changes
85
+ ```
86
+
87
+ ## CLI
88
+
89
+ The CLI talks to a running wisp, or starts one if none is running. It works from any folder:
90
+
91
+ ```sh
92
+ wisp # open wisp (or focus the running one)
93
+ wisp "fix the flaky auth test" # launch a task in this folder (it becomes a project)
94
+ wisp add [path] # add a folder as a project (default: cwd)
95
+ wisp --mode plan "audit deps" # permission mode: plan | edits | auto
96
+ ```
97
+
98
+ | Option | Description |
99
+ |---|---|
100
+ | `--port`, `-p N` | Port to use (default `4477`, or `WISP_PORT`) |
101
+ | `--mode`, `-m` | `plan` · `edits` · `auto` |
102
+ | `--no-open` | Don't open a browser |
103
+ | `--version`, `-v` | Print the version |
104
+
105
+ > `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.
106
+
107
+ ## Keyboard shortcuts
108
+
109
+ | Key | Action |
110
+ |---|---|
111
+ | `N` | New task (focus the launcher) |
112
+ | `⌘ ↵` | Launch and keep the launcher open for the next task |
113
+ | `J` / `K` | Move down / up through the queue |
114
+ | `O` / `↵` | Open or collapse the focused session |
115
+ | `/` | Reply to the focused session |
116
+ | `1`–`9` | Pick an option in a pending question |
117
+ | `T` | Take over in your terminal |
118
+ | `E` | Mark done and jump to the next session that needs you |
119
+ | `R` | Replay the presentation |
120
+ | `?` | Show all shortcuts |
121
+ | `Esc` | Close, blur, or collapse |
122
+
123
+ File paths in presentations open in your editor. VS Code, Cursor, Zed, and JetBrains IDEs are supported; pick one in **Settings**.
124
+
125
+ ## Architecture
126
+
127
+ ```text
128
+ web/ (UI) ⇄ HTTP + SSE ⇄ server/ ── adapters/claude.js ── claude -p --output-format stream-json
129
+ └─ mcp-ask.js (ask_user MCP tool, injected per run)
130
+ ```
131
+
132
+ ```text
133
+ wisp/
134
+ ├── bin/
135
+ │ └── wisp.js # CLI: starts the server or talks to the running one
136
+ ├── server/
137
+ │ ├── index.js # HTTP API, SSE stream, static UI, uploads
138
+ │ ├── runner.js # session lifecycle, follow-ups, event normalization
139
+ │ ├── protocol.js # system prompt + ```wisp block parser (markdown fallback)
140
+ │ ├── mcp-ask.js # ask_user MCP server
141
+ │ ├── store.js # JSON-file persistence in ~/.wisp
142
+ │ ├── terminal.js # "take over" in your terminal
143
+ │ ├── welcome.js # the gallery session
144
+ │ └── adapters/
145
+ │ ├── index.js # adapter registry + contract
146
+ │ └── claude.js # Claude Code adapter
147
+ ├── web/ # vanilla JS UI, no build step
148
+ │ ├── index.html
149
+ │ ├── css/app.css
150
+ │ └── js/{app,blocks,diff,md}.js
151
+ └── electron/ # desktop shell
152
+ ├── main.js
153
+ └── preload.cjs
154
+ ```
155
+
156
+ - **Protocol** (`server/protocol.js`): a system prompt tells agents to work autonomously and to end with a fenced `wisp` JSON block of presentation blocks. The parser reads that block, and falls back to plain markdown if it is missing.
157
+ - **Adapters** (`server/adapters/`): one file per harness. An adapter turns a run into normalized events (`session`, `tool`, `change`, `text`, `result`, …) and provides a `resumeCommand` for take over.
158
+ - **State** lives in `~/.wisp` (override with `WISP_HOME`): one JSON file per session, plus `config.json` and `uploads/`.
159
+
160
+ ### Presentation blocks
161
+
162
+ `hero` · `markdown` · `steps` · `stats` · `files` · `tree` · `diff` · `code` · `tests` · `commits` · `terminal` · `compare` · `flow` · `chart` · `checklist` · `callout` · `image` · `question` · `next`
163
+
164
+ ## Adding a harness
165
+
166
+ 1. Copy `server/adapters/claude.js`.
167
+ 2. Implement `detect()`, `run()`, and `resumeCommand()`. The full contract is documented at the top of `server/adapters/index.js`.
168
+ 3. Register the adapter in `server/adapters/index.js`.
169
+
170
+ If the harness has no system-prompt flag, prepend `systemPrompt` to the first prompt. If it supports MCP, pass `mcpServers` through so that `ask_user` works.
171
+
172
+ ## Roadmap
173
+
174
+ - [x] Claude Code adapter
175
+ - [x] Live `ask_user` questions
176
+ - [x] Desktop app (Electron)
177
+ - [ ] Codex adapter
178
+ - [ ] opencode adapter
179
+
180
+ ## Contributing
181
+
182
+ Issues and PRs are welcome. wisp has no build step: edit `web/` or `server/`, then run `npm run dev` (or `npm run dev:web`) to see your changes.
183
+
184
+ ## License
185
+
186
+ [MIT](LICENSE) © M0xoo
package/bin/wisp.js ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // wisp — launcher + queue for coding agents.
3
+ //
4
+ // wisp start wisp (or focus the running one) and open the browser
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
+ // --no-open don't open a browser
10
+
11
+ import { exec } from 'node:child_process';
12
+ import path from 'node:path';
13
+
14
+ const argv = process.argv.slice(2);
15
+ const flags = { open: true, port: Number(process.env.WISP_PORT) || 4477, mode: null };
16
+ const words = [];
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const a = argv[i];
19
+ if (a === '--no-open') flags.open = false;
20
+ else if (a === '--port' || a === '-p') flags.port = Number(argv[++i]);
21
+ else if (a === '--mode' || a === '-m') flags.mode = argv[++i];
22
+ else if (a === '--version' || a === '-v') {
23
+ const { readFileSync } = await import('node:fs');
24
+ console.log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url))).version);
25
+ process.exit(0);
26
+ } else if (a === '--help' || a === '-h') {
27
+ 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 --no-open --version');
28
+ process.exit(0);
29
+ } else words.push(a);
30
+ }
31
+
32
+ const base = `http://localhost:${flags.port}`;
33
+ const api = (p, body) =>
34
+ fetch(`${base}/api${p}`, {
35
+ method: body ? 'POST' : 'GET',
36
+ headers: { 'content-type': 'application/json' },
37
+ body: body ? JSON.stringify(body) : undefined,
38
+ }).then(async (r) => {
39
+ const j = await r.json();
40
+ if (!r.ok) throw new Error(j.error || r.statusText);
41
+ return j;
42
+ });
43
+
44
+ let running = false;
45
+ try {
46
+ await api('/config');
47
+ running = true;
48
+ } catch {}
49
+
50
+ if (!running) {
51
+ const { startServer } = await import('../server/index.js');
52
+ await startServer({ port: flags.port });
53
+ console.log(`\n ◌ wisp is floating at ${base}\n`);
54
+ }
55
+
56
+ let openPath = '/';
57
+ if (words[0] === 'add') {
58
+ const dir = path.resolve(words[1] || process.cwd());
59
+ const p = await api('/projects', { path: dir }).catch((e) => console.error(` ✗ ${e.message}`));
60
+ if (p) console.log(` ✓ project added: ${p.name} (${p.path})`);
61
+ if (running) process.exit(0);
62
+ } else if (words.length) {
63
+ const cfg = await api('/config');
64
+ const s = await api('/sessions', { prompt: words.join(' '), cwd: process.cwd(), agent: cfg.defaults.agent, mode: flags.mode || cfg.defaults.mode, model: cfg.defaults.model });
65
+ console.log(` ◌ launched: ${s.title}`);
66
+ openPath = `/#/s/${s.id}`;
67
+ if (running) process.exit(0); // the running wisp will show it in the queue
68
+ }
69
+
70
+ if (flags.open) {
71
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start ""' : 'xdg-open';
72
+ exec(`${cmd} "${base}${openPath}"`);
73
+ }
@@ -0,0 +1,147 @@
1
+ // wisp desktop shell. The Node server runs inside Electron's main process and
2
+ // the window loads the same UI the browser mode serves.
3
+
4
+ import { app, BrowserWindow, shell, nativeTheme, Menu, dialog, ipcMain } from 'electron';
5
+ import { execFileSync } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const here = path.dirname(fileURLToPath(import.meta.url));
11
+ const PORT = Number(process.env.WISP_PORT) || 4477;
12
+ const URL_BASE = `http://localhost:${PORT}`;
13
+ const DEV = process.env.WISP_DEV === '1';
14
+
15
+ // Apps launched from Finder/Dock get a minimal PATH, so `claude` (nvm, homebrew,
16
+ // ~/.local/bin…) wouldn't be found. Borrow the PATH from the user's login shell.
17
+ function inheritShellPath() {
18
+ if (process.platform === 'win32') return;
19
+ try {
20
+ const sh = process.env.SHELL || '/bin/zsh';
21
+ const out = execFileSync(sh, ['-ilc', 'printf "__WISP__%s__WISP__" "$PATH"'], { encoding: 'utf8', timeout: 5000 });
22
+ const m = out.match(/__WISP__(.*)__WISP__/);
23
+ if (m?.[1]) process.env.PATH = m[1];
24
+ } catch {}
25
+ }
26
+
27
+ if (DEV) app.commandLine.appendSwitch('remote-debugging-port', process.env.WISP_DEBUG_PORT || '9339');
28
+ if (!app.requestSingleInstanceLock()) app.quit();
29
+
30
+ let win = null;
31
+ let server = null;
32
+
33
+ async function ensureServer() {
34
+ try {
35
+ await fetch(`${URL_BASE}/api/config`);
36
+ return; // a wisp server (e.g. from the CLI) is already running — just attach to it
37
+ } catch {}
38
+ const { startServer } = await import('../server/index.js');
39
+ server = await startServer({ port: PORT, handleSignals: false });
40
+ }
41
+
42
+ function createWindow() {
43
+ win = new BrowserWindow({
44
+ width: 1380,
45
+ height: 900,
46
+ minWidth: 720,
47
+ minHeight: 520,
48
+ title: 'wisp',
49
+ titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
50
+ trafficLightPosition: { x: 16, y: 18 },
51
+ backgroundColor: nativeTheme.shouldUseDarkColors ? '#111111' : '#ffffff',
52
+ show: false,
53
+ webPreferences: { contextIsolation: true, sandbox: true, preload: path.join(here, 'preload.cjs') },
54
+ });
55
+
56
+ win.loadURL(URL_BASE);
57
+ win.once('ready-to-show', () => win.show());
58
+
59
+ // Editor links (vscode://…), docs links etc. open outside the app.
60
+ win.webContents.setWindowOpenHandler(({ url }) => {
61
+ if (!url.startsWith(URL_BASE)) shell.openExternal(url);
62
+ else return { action: 'allow' };
63
+ return { action: 'deny' };
64
+ });
65
+ win.webContents.on('will-navigate', (e, url) => {
66
+ if (!url.startsWith(URL_BASE)) {
67
+ e.preventDefault();
68
+ shell.openExternal(url);
69
+ }
70
+ });
71
+
72
+ // The page title carries the "needs you" count: "(3) wisp" → dock badge.
73
+ win.on('page-title-updated', (e, title) => {
74
+ e.preventDefault();
75
+ const n = Number(title.match(/^\((\d+)\)/)?.[1] || 0);
76
+ app.setBadgeCount(n);
77
+ });
78
+
79
+ win.on('closed', () => (win = null));
80
+ }
81
+
82
+ ipcMain.handle('wisp:pick-folder', async () => {
83
+ const r = await dialog.showOpenDialog(win, { properties: ['openDirectory', 'createDirectory'], buttonLabel: 'Add project' });
84
+ return r.canceled ? null : r.filePaths[0];
85
+ });
86
+
87
+ app.on('second-instance', () => {
88
+ if (!win) return createWindow();
89
+ if (win.isMinimized()) win.restore();
90
+ win.focus();
91
+ });
92
+
93
+ // Agents keep running with the window closed; quitting stops them.
94
+ app.on('window-all-closed', () => {
95
+ if (process.platform !== 'darwin') app.quit();
96
+ });
97
+ app.on('activate', () => !win && createWindow());
98
+ app.on('before-quit', () => server?.shutdown());
99
+
100
+ // Dev: web/ edits reload the window, server/ or electron/ edits relaunch the app.
101
+ // Agents run inside this process, so when wisp is working on itself a relaunch
102
+ // would kill them mid-task: hold it until nothing is running.
103
+ function watchForDev() {
104
+ const root = path.join(here, '..');
105
+ let timer;
106
+ let pending = null;
107
+ const debounce = (fn) => (clearTimeout(timer), (timer = setTimeout(fn, 120)));
108
+ const busy = () => server?.runner.live.size || 0;
109
+ const relaunch = () => {
110
+ if (busy()) return;
111
+ clearInterval(pending);
112
+ console.log('[wisp dev] relaunching');
113
+ server?.shutdown();
114
+ app.relaunch();
115
+ app.exit(0);
116
+ };
117
+ fs.watch(path.join(root, 'web'), { recursive: true }, () =>
118
+ debounce(() => BrowserWindow.getAllWindows().forEach((w) => w.webContents.reloadIgnoringCache())),
119
+ );
120
+ for (const dir of ['server', 'electron'])
121
+ fs.watch(path.join(root, dir), { recursive: true }, (_, file) =>
122
+ debounce(() => {
123
+ if (!busy()) return relaunch();
124
+ console.log(`[wisp dev] ${dir}/${file} changed — relaunching once ${busy()} running session(s) finish`);
125
+ pending ??= setInterval(relaunch, 2000);
126
+ }),
127
+ );
128
+ console.log('[wisp dev] watching web/ (reload) and server/, electron/ (relaunch when idle)');
129
+ }
130
+
131
+ app.whenReady().then(async () => {
132
+ app.setName('wisp');
133
+ if (process.platform === 'darwin') app.dock?.setIcon(path.join(here, 'icon.png'));
134
+ Menu.setApplicationMenu(
135
+ Menu.buildFromTemplate([
136
+ ...(process.platform === 'darwin' ? [{ role: 'appMenu' }] : []),
137
+ { role: 'fileMenu' },
138
+ { role: 'editMenu' },
139
+ { role: 'viewMenu' },
140
+ { role: 'windowMenu' },
141
+ ]),
142
+ );
143
+ inheritShellPath();
144
+ await ensureServer();
145
+ createWindow();
146
+ if (DEV) watchForDev();
147
+ });
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "wisp-agents",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Launch coding agents, skip the transcript, read the presentation.",
5
+ "type": "module",
6
+ "bin": {
7
+ "wisp": "bin/wisp.js",
8
+ "wisp-agents": "bin/wisp.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "server/",
13
+ "web/"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/wisp.js",
17
+ "app": "electron .",
18
+ "dev": "WISP_DEV=1 electron .",
19
+ "dev:web": "node --watch-path=server bin/wisp.js --no-open",
20
+ "prepublishOnly": "node --check bin/wisp.js && node -e \"import('./server/index.js')\""
21
+ },
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "keywords": [
26
+ "claude",
27
+ "claude-code",
28
+ "coding-agent",
29
+ "ai",
30
+ "agents",
31
+ "cli",
32
+ "launcher"
33
+ ],
34
+ "author": "M0xoo",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/M0xoo/wisp.git"
39
+ },
40
+ "homepage": "https://github.com/M0xoo/wisp#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/M0xoo/wisp/issues"
43
+ },
44
+ "main": "electron/main.js",
45
+ "devDependencies": {
46
+ "electron": "^44.8.0"
47
+ }
48
+ }