wisp-agents 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +191 -2
- package/bin/wisp.js +113 -0
- package/electron/icon.png +0 -0
- package/electron/icon.svg +13 -0
- package/electron/main.js +147 -0
- package/electron/preload.cjs +6 -0
- package/package.json +47 -4
- package/server/adapters/claude.js +230 -0
- package/server/adapters/index.js +36 -0
- package/server/index.js +364 -0
- package/server/mcp-ask.js +82 -0
- package/server/protocol.js +182 -0
- package/server/runner.js +283 -0
- package/server/store.js +132 -0
- package/server/terminal.js +73 -0
- package/server/welcome.js +168 -0
- package/web/css/app.css +893 -0
- package/web/favicon.svg +5 -0
- package/web/index.html +85 -0
- package/web/js/app.js +1452 -0
- package/web/js/blocks.js +557 -0
- package/web/js/diff.js +68 -0
- package/web/js/md.js +139 -0
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,192 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/wisp-agents)
|
|
12
|
+
[](#license)
|
|
13
|
+
[](https://nodejs.org)
|
|
14
|
+
[](https://www.electronjs.org)
|
|
15
|
+
[](package.json)
|
|
16
|
+
[](https://claude.com/claude-code)
|
|
17
|
+
|
|
18
|
+
[Quick start](#quick-start) · [Features](#features) · [CLI](#cli) · [Shortcuts](#keyboard-shortcuts) · [Architecture](#architecture) · [Adding a harness](#adding-a-harness) · [Roadmap](#roadmap)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Why wisp?
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
From there you can **follow up**, **answer its questions**, **take it over** in your terminal, or **mark it done**.
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
you ──▶ "fix the flaky auth test" ──▶ agent works in the background
|
|
32
|
+
│
|
|
33
|
+
you ◀── hero · diff · tests · next steps ◀───┘ (no transcript to read)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
| | |
|
|
39
|
+
|---|---|
|
|
40
|
+
| 🚀 **Fire and forget** | Launch a task from the UI or from any folder with `wisp "…"`. Each folder becomes a project. |
|
|
41
|
+
| 🎞️ **Presentations, not transcripts** | Agents end with structured blocks that render as an animated, skimmable report. |
|
|
42
|
+
| ❓ **Live questions** | Agents get an `ask_user` tool, so they can pause mid-run. Your answer goes straight back to the running agent. |
|
|
43
|
+
| 🧾 **Real diffs** | Edits come from the agent's own tool calls, so `diff` blocks show what actually changed. |
|
|
44
|
+
| 📥 **A queue** | Sessions sort into *Needs you*, *Working* (with a live one-line status), and *Done*. |
|
|
45
|
+
| 🖥️ **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. |
|
|
46
|
+
| 📎 **Attachments** | Paste screenshots or drop images, video, logs, or PDFs into the launcher. |
|
|
47
|
+
| 🔐 **Permission modes** | Choose **Plan** (read-only), **Edits** (file edits auto-accepted), or **Full auto** for each launch. |
|
|
48
|
+
| 🪶 **No runtime dependencies** | The server uses only Node built-ins. Electron is needed only for the desktop app. |
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
> **Requirements:** Node.js ≥ 20 and [Claude Code](https://claude.com/claude-code) (`claude`) installed and signed in.
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npx wisp-agents # open the wisp desktop app
|
|
56
|
+
npx wisp-agents "fix the flaky auth test" # launch a task in the current folder
|
|
57
|
+
npx wisp-agents --browser # or use it in the browser → http://localhost:4477
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The first run downloads Electron for the desktop app; if that isn't possible, wisp opens in the browser instead.
|
|
61
|
+
|
|
62
|
+
Or install it globally to get the `wisp` command everywhere:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npm install -g wisp-agents
|
|
66
|
+
wisp "fix the flaky auth test"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
From source:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
git clone https://github.com/M0xoo/wisp.git
|
|
73
|
+
cd wisp
|
|
74
|
+
|
|
75
|
+
# Browser mode, with nothing to install:
|
|
76
|
+
node bin/wisp.js # → http://localhost:4477
|
|
77
|
+
|
|
78
|
+
# Desktop app:
|
|
79
|
+
npm install # pulls Electron
|
|
80
|
+
npm run app
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
On first launch, the *Welcome to wisp* session shows a live gallery of every presentation block.
|
|
84
|
+
|
|
85
|
+
### Development
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
npm run dev # desktop app: live reload for web/, relaunch on server/ and electron/ changes
|
|
89
|
+
npm run dev:web # browser mode, restarts the server when server/ changes
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## CLI
|
|
93
|
+
|
|
94
|
+
The CLI talks to a running wisp, or starts one if none is running. It works from any folder:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
wisp # open wisp (or focus the running one)
|
|
98
|
+
wisp "fix the flaky auth test" # launch a task in this folder (it becomes a project)
|
|
99
|
+
wisp add [path] # add a folder as a project (default: cwd)
|
|
100
|
+
wisp --mode plan "audit deps" # permission mode: plan | edits | auto
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| Option | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `--port`, `-p N` | Port to use (default `4477`, or `WISP_PORT`) |
|
|
106
|
+
| `--mode`, `-m` | `plan` · `edits` · `auto` |
|
|
107
|
+
| `--browser`, `-b` | Use the browser instead of the desktop app |
|
|
108
|
+
| `--no-open` | Just run the server; open nothing |
|
|
109
|
+
| `--version`, `-v` | Print the version |
|
|
110
|
+
|
|
111
|
+
> `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.
|
|
112
|
+
|
|
113
|
+
## Keyboard shortcuts
|
|
114
|
+
|
|
115
|
+
| Key | Action |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `N` | New task (focus the launcher) |
|
|
118
|
+
| `⌘ ↵` | Launch and keep the launcher open for the next task |
|
|
119
|
+
| `J` / `K` | Move down / up through the queue |
|
|
120
|
+
| `O` / `↵` | Open or collapse the focused session |
|
|
121
|
+
| `/` | Reply to the focused session |
|
|
122
|
+
| `1`–`9` | Pick an option in a pending question |
|
|
123
|
+
| `T` | Take over in your terminal |
|
|
124
|
+
| `E` | Mark done and jump to the next session that needs you |
|
|
125
|
+
| `R` | Replay the presentation |
|
|
126
|
+
| `?` | Show all shortcuts |
|
|
127
|
+
| `Esc` | Close, blur, or collapse |
|
|
128
|
+
|
|
129
|
+
File paths in presentations open in your editor. VS Code, Cursor, Zed, and JetBrains IDEs are supported; pick one in **Settings**.
|
|
130
|
+
|
|
131
|
+
## Architecture
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
web/ (UI) ⇄ HTTP + SSE ⇄ server/ ── adapters/claude.js ── claude -p --output-format stream-json
|
|
135
|
+
└─ mcp-ask.js (ask_user MCP tool, injected per run)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
wisp/
|
|
140
|
+
├── bin/
|
|
141
|
+
│ └── wisp.js # CLI: starts the server or talks to the running one
|
|
142
|
+
├── server/
|
|
143
|
+
│ ├── index.js # HTTP API, SSE stream, static UI, uploads
|
|
144
|
+
│ ├── runner.js # session lifecycle, follow-ups, event normalization
|
|
145
|
+
│ ├── protocol.js # system prompt + ```wisp block parser (markdown fallback)
|
|
146
|
+
│ ├── mcp-ask.js # ask_user MCP server
|
|
147
|
+
│ ├── store.js # JSON-file persistence in ~/.wisp
|
|
148
|
+
│ ├── terminal.js # "take over" in your terminal
|
|
149
|
+
│ ├── welcome.js # the gallery session
|
|
150
|
+
│ └── adapters/
|
|
151
|
+
│ ├── index.js # adapter registry + contract
|
|
152
|
+
│ └── claude.js # Claude Code adapter
|
|
153
|
+
├── web/ # vanilla JS UI, no build step
|
|
154
|
+
│ ├── index.html
|
|
155
|
+
│ ├── css/app.css
|
|
156
|
+
│ └── js/{app,blocks,diff,md}.js
|
|
157
|
+
└── electron/ # desktop shell
|
|
158
|
+
├── main.js
|
|
159
|
+
└── preload.cjs
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- **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.
|
|
163
|
+
- **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.
|
|
164
|
+
- **State** lives in `~/.wisp` (override with `WISP_HOME`): one JSON file per session, plus `config.json` and `uploads/`.
|
|
165
|
+
|
|
166
|
+
### Presentation blocks
|
|
167
|
+
|
|
168
|
+
`hero` · `markdown` · `steps` · `stats` · `files` · `tree` · `diff` · `code` · `tests` · `commits` · `terminal` · `compare` · `flow` · `chart` · `checklist` · `callout` · `image` · `question` · `next`
|
|
169
|
+
|
|
170
|
+
## Adding a harness
|
|
171
|
+
|
|
172
|
+
1. Copy `server/adapters/claude.js`.
|
|
173
|
+
2. Implement `detect()`, `run()`, and `resumeCommand()`. The full contract is documented at the top of `server/adapters/index.js`.
|
|
174
|
+
3. Register the adapter in `server/adapters/index.js`.
|
|
175
|
+
|
|
176
|
+
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.
|
|
177
|
+
|
|
178
|
+
## Roadmap
|
|
179
|
+
|
|
180
|
+
- [x] Claude Code adapter
|
|
181
|
+
- [x] Live `ask_user` questions
|
|
182
|
+
- [x] Desktop app (Electron)
|
|
183
|
+
- [ ] Codex adapter
|
|
184
|
+
- [ ] opencode adapter
|
|
185
|
+
|
|
186
|
+
## Contributing
|
|
187
|
+
|
|
188
|
+
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.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
[MIT](LICENSE) © M0xoo
|
package/bin/wisp.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// wisp — launcher + queue for coding agents.
|
|
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
|
+
// --browser use the browser instead of the desktop app
|
|
10
|
+
// --no-open just run the server, open nothing
|
|
11
|
+
|
|
12
|
+
import { exec, spawn } from 'node:child_process';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const argv = process.argv.slice(2);
|
|
19
|
+
const flags = { open: true, browser: false, port: Number(process.env.WISP_PORT) || 4477, mode: null };
|
|
20
|
+
const words = [];
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
if (a === '--no-open') flags.open = false;
|
|
24
|
+
else if (a === '--browser' || a === '-b') flags.browser = true;
|
|
25
|
+
else if (a === '--port' || a === '-p') flags.port = Number(argv[++i]);
|
|
26
|
+
else if (a === '--mode' || a === '-m') flags.mode = argv[++i];
|
|
27
|
+
else if (a === '--version' || a === '-v') {
|
|
28
|
+
const { readFileSync } = await import('node:fs');
|
|
29
|
+
console.log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url))).version);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
} else if (a === '--help' || a === '-h') {
|
|
32
|
+
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 --browser --no-open --version');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
} else words.push(a);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const base = `http://localhost:${flags.port}`;
|
|
38
|
+
const api = (p, body) =>
|
|
39
|
+
fetch(`${base}/api${p}`, {
|
|
40
|
+
method: body ? 'POST' : 'GET',
|
|
41
|
+
headers: { 'content-type': 'application/json' },
|
|
42
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
43
|
+
}).then(async (r) => {
|
|
44
|
+
const j = await r.json();
|
|
45
|
+
if (!r.ok) throw new Error(j.error || r.statusText);
|
|
46
|
+
return j;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
let running = false;
|
|
50
|
+
try {
|
|
51
|
+
await api('/config');
|
|
52
|
+
running = true;
|
|
53
|
+
} catch {}
|
|
54
|
+
|
|
55
|
+
// The desktop app (Electron is an optional dependency) runs the server in its own process, so
|
|
56
|
+
// it outlives this terminal. Without Electron, or with --browser/--no-open, serve from here.
|
|
57
|
+
// A task or `add` sent to an already-running wisp just goes over the API, no window.
|
|
58
|
+
const electron = flags.open && !flags.browser && (!running || !words.length) ? electronBinary() : null;
|
|
59
|
+
if (electron) {
|
|
60
|
+
const child = spawn(electron, [fileURLToPath(new URL('..', import.meta.url))], {
|
|
61
|
+
detached: true,
|
|
62
|
+
stdio: 'ignore',
|
|
63
|
+
env: { ...process.env, WISP_PORT: String(flags.port) },
|
|
64
|
+
});
|
|
65
|
+
child.unref();
|
|
66
|
+
if (!running) await waitForServer();
|
|
67
|
+
console.log(`\n ◌ wisp is open\n`);
|
|
68
|
+
} else if (!running) {
|
|
69
|
+
const { startServer } = await import('../server/index.js');
|
|
70
|
+
await startServer({ port: flags.port });
|
|
71
|
+
console.log(`\n ◌ wisp is floating at ${base}\n`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let openPath = '/';
|
|
75
|
+
if (words[0] === 'add') {
|
|
76
|
+
const dir = path.resolve(words[1] || process.cwd());
|
|
77
|
+
const p = await api('/projects', { path: dir }).catch((e) => console.error(` ✗ ${e.message}`));
|
|
78
|
+
if (p) console.log(` ✓ project added: ${p.name} (${p.path})`);
|
|
79
|
+
if (running || electron) process.exit(0);
|
|
80
|
+
} else if (words.length) {
|
|
81
|
+
const cfg = await api('/config');
|
|
82
|
+
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 });
|
|
83
|
+
console.log(` ◌ launched: ${s.title}`);
|
|
84
|
+
openPath = `/#/s/${s.id}`;
|
|
85
|
+
if (running || electron) process.exit(0); // the running wisp will show it in the queue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (electron) process.exit(0);
|
|
89
|
+
if (flags.open) {
|
|
90
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start ""' : 'xdg-open';
|
|
91
|
+
exec(`${cmd} "${base}${openPath}"`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function electronBinary() {
|
|
95
|
+
try {
|
|
96
|
+
const bin = createRequire(import.meta.url)('electron'); // the electron package exports its binary path
|
|
97
|
+
return typeof bin === 'string' && existsSync(bin) ? bin : null;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function waitForServer() {
|
|
104
|
+
for (let i = 0; i < 150; i++) {
|
|
105
|
+
try {
|
|
106
|
+
return await api('/config');
|
|
107
|
+
} catch {
|
|
108
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
console.error(' ✗ the wisp app did not start in time — try `wisp --browser`');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1d1c22"/><stop offset="1" stop-color="#0e0e10"/></linearGradient>
|
|
4
|
+
<radialGradient id="glow" cx="50%" cy="50%" r="50%"><stop offset="0" stop-color="#6a5cff" stop-opacity=".55"/><stop offset="1" stop-color="#6a5cff" stop-opacity="0"/></radialGradient>
|
|
5
|
+
<radialGradient id="orb" cx="36%" cy="32%" r="68%"><stop offset="0" stop-color="#fff"/><stop offset=".16" stop-color="#d9fff6"/><stop offset=".38" stop-color="#2fd0b4"/><stop offset=".78" stop-color="#6a5cff"/><stop offset="1" stop-color="#4b3fe0"/></radialGradient>
|
|
6
|
+
</defs>
|
|
7
|
+
<rect x="100" y="100" width="824" height="824" rx="190" fill="url(#bg)"/>
|
|
8
|
+
<circle cx="512" cy="512" r="330" fill="url(#glow)"/>
|
|
9
|
+
<circle cx="512" cy="512" r="210" fill="url(#orb)"/>
|
|
10
|
+
<circle cx="742" cy="360" r="16" fill="#fff" opacity=".9"/>
|
|
11
|
+
<circle cx="300" cy="650" r="10" fill="#bff" opacity=".7"/>
|
|
12
|
+
<circle cx="690" cy="720" r="7" fill="#fff" opacity=".6"/>
|
|
13
|
+
</svg>
|
package/electron/main.js
ADDED
|
@@ -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,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wisp-agents",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.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
|
+
"electron/",
|
|
13
|
+
"server/",
|
|
14
|
+
"web/"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node bin/wisp.js",
|
|
18
|
+
"app": "electron .",
|
|
19
|
+
"dev": "WISP_DEV=1 electron .",
|
|
20
|
+
"dev:web": "node --watch-path=server bin/wisp.js --no-open",
|
|
21
|
+
"prepublishOnly": "node --check bin/wisp.js && node -e \"import('./server/index.js')\" && node --check electron/main.js"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"claude",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"coding-agent",
|
|
30
|
+
"ai",
|
|
31
|
+
"agents",
|
|
32
|
+
"cli",
|
|
33
|
+
"launcher"
|
|
34
|
+
],
|
|
35
|
+
"author": "M0xoo",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/M0xoo/wisp.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/M0xoo/wisp#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/M0xoo/wisp/issues"
|
|
44
|
+
},
|
|
45
|
+
"main": "electron/main.js",
|
|
46
|
+
"optionalDependencies": {
|
|
47
|
+
"electron": "^44.8.0"
|
|
48
|
+
}
|
|
49
|
+
}
|