wisp-agents 0.24.0 → 0.24.1
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 +8 -1
- package/README.md +4 -4
- package/package.json +1 -1
- package/server/openapi.js +1 -1
- package/server/update.js +50 -16
- package/web/js/features/update.js +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.24.1] - 2026-10-20
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Update now updates, however you installed wisp.** **Update to X** installs the new version and restarts wisp for every install route: `npx`, `npm install -g`, the `install.sh` script, and now Homebrew. On a Homebrew install it used to run `npm install -g` and then restart the old keg. Now it runs `brew upgrade m0xoo/tap/wisp-agents` and restarts from the new keg. If the install leaves wisp on the same version (the tap doesn't have the release yet, or npm installed into a different folder), the button says so instead of restarting into the old version and then reporting that wisp didn't come back. A git clone of the repo no longer shows the button or tells you to run `git pull`. `GET /update` no longer returns `auto`, and the method can now be `brew`.
|
|
13
|
+
|
|
8
14
|
## [0.24.0] - 2026-10-20
|
|
9
15
|
|
|
10
16
|
### Added
|
|
@@ -436,7 +442,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
436
442
|
- Session queue with follow-ups, answers to agent questions, terminal takeover and mark-as-done.
|
|
437
443
|
- Older turns are folded, with a floating collapse-all button and back-to-top.
|
|
438
444
|
|
|
439
|
-
[Unreleased]: https://github.com/M0xoo/wisp/compare/v0.24.
|
|
445
|
+
[Unreleased]: https://github.com/M0xoo/wisp/compare/v0.24.1...HEAD
|
|
446
|
+
[0.24.1]: https://github.com/M0xoo/wisp/compare/v0.24.0...v0.24.1
|
|
440
447
|
[0.24.0]: https://github.com/M0xoo/wisp/compare/v0.23.0...v0.24.0
|
|
441
448
|
[0.23.0]: https://github.com/M0xoo/wisp/compare/v0.22.0...v0.23.0
|
|
442
449
|
[0.22.0]: https://github.com/M0xoo/wisp/compare/v0.21.0...v0.22.0
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ From there you can **follow up**, **answer its questions**, **take it over** in
|
|
|
56
56
|
| 🤝 **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). |
|
|
57
57
|
| ⏪ **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. |
|
|
58
58
|
| ⚖️ **Compare** | Run one task on two harnesses side by side, and keep the better run. |
|
|
59
|
-
| 🧾 **Recipes & triggers** | Saved prompts with `{params}` (`wisp run release 0.24.
|
|
59
|
+
| 🧾 **Recipes & triggers** | Saved prompts with `{params}` (`wisp run release 0.24.1`), run by hand, on a schedule, when CI fails, or when an issue is labelled. |
|
|
60
60
|
| 📊 **Usage** | Spend, success rate and questions per project and per harness · model. |
|
|
61
61
|
| 📱 **Phone** | Questions and finished tasks on ntfy or Telegram; answer from there, or start a task with `/new`. |
|
|
62
62
|
| 🔐 **Permission modes** | Choose **Plan** (read-only), **Edits** (file edits auto-accepted), or **Full auto** (no permission prompts, the default) for each launch. `Shift+Tab` in the prompt cycles modes. In Edits mode, anything the permissions block comes to you: Claude Code waits for **Allow / Always / Deny**, and the other harnesses show an **Allow & retry** card. |
|
|
@@ -85,7 +85,7 @@ brew install m0xoo/tap/wisp-agents # Homebrew: CLI and browser mode,
|
|
|
85
85
|
wisp "fix the flaky auth test"
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
The Homebrew formula lives in [M0xoo/homebrew-tap](https://github.com/M0xoo/homebrew-tap). `install.sh` honors `WISP_VERSION` to pin a release (`curl -fsSL https://trywisp.dev/install.sh | WISP_VERSION=0.24.
|
|
88
|
+
The Homebrew formula lives in [M0xoo/homebrew-tap](https://github.com/M0xoo/homebrew-tap). `install.sh` honors `WISP_VERSION` to pin a release (`curl -fsSL https://trywisp.dev/install.sh | WISP_VERSION=0.24.1 bash`).
|
|
89
89
|
|
|
90
90
|
From source:
|
|
91
91
|
|
|
@@ -135,7 +135,7 @@ wisp btw <id> "did you do X?" # ask on the side without interrupting the run
|
|
|
135
135
|
wisp stop <id>
|
|
136
136
|
wisp ship <id> [--draft] # commit, push and open a PR for a worktree task
|
|
137
137
|
wisp merge <id> # squash-merge its PR, fast-forward your local main
|
|
138
|
-
wisp run release 0.24.
|
|
138
|
+
wisp run release 0.24.1 # run a recipe; values fill its {params} (or version=0.24.1)
|
|
139
139
|
wisp recipes # list recipes
|
|
140
140
|
wisp usage [--days 7] # spend and outcomes
|
|
141
141
|
wisp mcp # MCP server for other agents and editors
|
|
@@ -354,7 +354,7 @@ Before each turn, wisp snapshots the git working tree (pinned at `refs/wisp/chec
|
|
|
354
354
|
Prepare release {version}: update CHANGELOG, README and docs/, bump package.json, tag v{version}.
|
|
355
355
|
```
|
|
356
356
|
|
|
357
|
-
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.24.
|
|
357
|
+
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.24.1` fills the fields in order, `wisp run release version=0.24.1` by name.
|
|
358
358
|
|
|
359
359
|
**Triggers** launch sessions on their own while wisp is open (⌘K → *Triggers*):
|
|
360
360
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wisp-agents",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "Run Claude Code, Codex, Cursor and other coding agents in parallel, in the background, on any of your machines. Skip the transcript, read the report.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server/openapi.js
CHANGED
|
@@ -25,7 +25,7 @@ export function openapi({ version, port }) {
|
|
|
25
25
|
paths: {
|
|
26
26
|
'/health': { get: { summary: 'Liveness, version and pid', responses: { 200: json(ref('Health')) } } },
|
|
27
27
|
'/update': {
|
|
28
|
-
get: { summary: 'Current and latest published version, and how this copy updates (global | npx | git). ?check forces a registry check', responses: { 200: json({ type: 'object' }) } },
|
|
28
|
+
get: { summary: 'Current and latest published version, and how this copy updates (global | npx | brew | git; a git clone never offers one). ?check forces a registry check', responses: { 200: json({ type: 'object' }) } },
|
|
29
29
|
post: { summary: 'Install the latest version and relaunch. 409 while agents run unless { force: true }', responses: { 202: json({ type: 'object' }), 409: json({ type: 'object' }) } },
|
|
30
30
|
},
|
|
31
31
|
'/preferences': {
|
package/server/update.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Update check: polls the npm registry for a newer wisp-agents and, when asked, installs it and
|
|
2
2
|
// relaunches wisp. How depends on how this copy was installed:
|
|
3
|
-
// global npm install -g wisp-agents@latest, then rerun bin/wisp.js
|
|
3
|
+
// global npm install -g wisp-agents@latest (also what install.sh does), then rerun bin/wisp.js
|
|
4
4
|
// npx nothing to install: relaunch through `npx -y wisp-agents@latest`
|
|
5
|
-
//
|
|
5
|
+
// brew brew upgrade m0xoo/tap/wisp-agents, then rerun it from the keg's opt/ link
|
|
6
|
+
// git a clone of the repo, i.e. working on wisp itself: never offers an update
|
|
6
7
|
// Opt out with WISP_UPDATE_CHECK=0.
|
|
7
8
|
|
|
8
9
|
import { spawn } from 'node:child_process';
|
|
@@ -16,6 +17,9 @@ const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'))
|
|
|
16
17
|
const REGISTRY = `https://registry.npmjs.org/${PKG.name}/latest`;
|
|
17
18
|
const DAY = 24 * 3600_000;
|
|
18
19
|
const TICK = 30 * 60_000; // timers stall while the machine sleeps, so poll the wall clock instead
|
|
20
|
+
const TAP = 'm0xoo/tap/wisp-agents';
|
|
21
|
+
// <prefix>/Cellar/wisp-agents/<version>/libexec/lib/node_modules/wisp-agents
|
|
22
|
+
const CELLAR = /^(.*)[\\/]Cellar[\\/]wisp-agents[\\/]/;
|
|
19
23
|
|
|
20
24
|
export function newer(a, b) {
|
|
21
25
|
const p = (v) => String(v).replace(/^v/, '').split(/[-+]/)[0].split('.').map(Number);
|
|
@@ -25,20 +29,23 @@ export function newer(a, b) {
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export function installMethod(root = ROOT) {
|
|
32
|
+
if (CELLAR.test(root)) return 'brew';
|
|
28
33
|
if (fs.existsSync(path.join(root, '.git'))) return 'git';
|
|
29
34
|
if (/[\\/]_npx[\\/]/.test(root)) return 'npx';
|
|
30
35
|
return 'global';
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export class Updater {
|
|
34
|
-
constructor({ port, onChange = () => {}, env = process.env, fetch = globalThis.fetch, now = Date.now } = {}) {
|
|
39
|
+
constructor({ port, onChange = () => {}, env = process.env, fetch = globalThis.fetch, now = Date.now, root = ROOT } = {}) {
|
|
35
40
|
this.port = port;
|
|
36
41
|
this.onChange = onChange;
|
|
37
42
|
this.fetch = fetch;
|
|
38
43
|
this.now = now;
|
|
39
44
|
this.checkedAt = -Infinity;
|
|
40
|
-
this.
|
|
41
|
-
this.method = installMethod();
|
|
45
|
+
this.root = root;
|
|
46
|
+
this.method = installMethod(root);
|
|
47
|
+
this.brew = root.match(CELLAR)?.[1]; // Homebrew's prefix, e.g. /opt/homebrew
|
|
48
|
+
this.enabled = this.method !== 'git' && !/^(0|false|no|off)$/i.test(env.WISP_UPDATE_CHECK || '') && !env.CI;
|
|
42
49
|
this.state = { current: PKG.version, latest: null, available: false, method: this.method, status: 'idle', error: null };
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -60,9 +67,20 @@ export class Updater {
|
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
view() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
return { ...this.state, command: this.command() };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// What installs the latest version; shown to copy if it fails. A clone has none.
|
|
74
|
+
command() {
|
|
75
|
+
if (this.method === 'brew') return `${JSON.stringify(path.join(this.brew, 'bin', 'brew'))} upgrade ${TAP}`;
|
|
76
|
+
return { global: `npm install -g ${PKG.name}@latest`, npx: `npx -y ${PKG.name}@latest` }[this.method];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Where the updated copy lives: the same folder, except Homebrew puts each version in its own
|
|
80
|
+
// keg and keeps opt/wisp-agents pointing at the current one.
|
|
81
|
+
home() {
|
|
82
|
+
if (this.method !== 'brew') return this.root;
|
|
83
|
+
return path.join(this.brew, 'opt', 'wisp-agents', 'libexec', 'lib', 'node_modules', PKG.name);
|
|
66
84
|
}
|
|
67
85
|
|
|
68
86
|
async check() {
|
|
@@ -71,20 +89,28 @@ export class Updater {
|
|
|
71
89
|
const r = await this.fetch(REGISTRY, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(10_000) });
|
|
72
90
|
if (!r.ok) return this.view();
|
|
73
91
|
const { version } = await r.json();
|
|
74
|
-
const available = newer(version, PKG.version);
|
|
92
|
+
const available = this.method !== 'git' && newer(version, PKG.version);
|
|
75
93
|
if (version !== this.state.latest || available !== this.state.available) this.set({ latest: version, available });
|
|
76
94
|
} catch {} // offline: try again next round
|
|
77
95
|
return this.view();
|
|
78
96
|
}
|
|
79
97
|
|
|
80
|
-
// Installs (
|
|
81
|
-
// free up and starts the new version. The caller shuts this process down.
|
|
98
|
+
// Installs (npm or brew; npx fetches on relaunch) then hands off to a detached helper that waits
|
|
99
|
+
// for this server's port to free up and starts the new version. The caller shuts this process down.
|
|
82
100
|
async install() {
|
|
83
|
-
if (this.method === 'git') throw bad('This is a git
|
|
101
|
+
if (this.method === 'git') throw bad('This copy of wisp is a git clone, so it does not update itself.');
|
|
84
102
|
if (this.state.status === 'installing') throw bad('Already updating.', 409);
|
|
85
103
|
this.set({ status: 'installing', error: null });
|
|
86
104
|
try {
|
|
87
|
-
if (this.method
|
|
105
|
+
if (this.method !== 'npx') {
|
|
106
|
+
// Old kegs stay until brew's own periodic cleanup: this process still runs from one.
|
|
107
|
+
await run(this.command(), this.method === 'brew' ? { HOMEBREW_NO_INSTALL_CLEANUP: '1' } : {});
|
|
108
|
+
// Relaunching a copy that didn't change would leave the UI waiting for a version that never comes.
|
|
109
|
+
const got = installedVersion(this.home());
|
|
110
|
+
if (!newer(got, PKG.version)) {
|
|
111
|
+
throw bad(this.method === 'brew' ? `Homebrew doesn't have wisp ${this.state.latest} yet. Try again later.` : `npm installed wisp somewhere else: this copy is still ${got || PKG.version}.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
88
114
|
} catch (err) {
|
|
89
115
|
this.set({ status: 'idle', error: err.message });
|
|
90
116
|
throw err;
|
|
@@ -97,13 +123,21 @@ export class Updater {
|
|
|
97
123
|
relaunchCommand(electron = !!process.versions.electron) {
|
|
98
124
|
const args = `${electron ? '' : ' --browser --no-open'} --port ${this.port}`; // desktop: reopen the app; browser: keep the tab
|
|
99
125
|
const node = JSON.stringify(electron ? 'node' : process.execPath);
|
|
100
|
-
return this.method === 'npx' ? `npx -y ${PKG.name}@latest${args}` : `${node} ${JSON.stringify(path.join(
|
|
126
|
+
return this.method === 'npx' ? `npx -y ${PKG.name}@latest${args}` : `${node} ${JSON.stringify(path.join(this.home(), 'bin', 'wisp.js'))}${args}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function installedVersion(dir) {
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
101
135
|
}
|
|
102
136
|
}
|
|
103
137
|
|
|
104
|
-
function run(cmd) {
|
|
138
|
+
function run(cmd, env = {}) {
|
|
105
139
|
return new Promise((resolve, reject) => {
|
|
106
|
-
const child = spawn(cmd, { shell: true, stdio: ['ignore', 'ignore', 'pipe'], env: withoutElectronNode() });
|
|
140
|
+
const child = spawn(cmd, { shell: true, stdio: ['ignore', 'ignore', 'pipe'], env: { ...withoutElectronNode(), ...env } });
|
|
107
141
|
let err = '';
|
|
108
142
|
child.stderr.on('data', (d) => (err = (err + d).slice(-2000)));
|
|
109
143
|
child.on('error', reject);
|
|
@@ -18,12 +18,11 @@ export function renderUpdate(u) {
|
|
|
18
18
|
b.disabled = busy;
|
|
19
19
|
b.classList.toggle('busy', busy);
|
|
20
20
|
b.querySelector('span').textContent = u.status === 'installing' ? 'Updating…' : u.status === 'restarting' ? 'Restarting…' : `Update to ${u.latest}`;
|
|
21
|
-
b.title =
|
|
21
|
+
b.title = `wisp ${u.current} → ${u.latest}: install and restart`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export async function installUpdate() {
|
|
25
25
|
if (!update?.available) return;
|
|
26
|
-
if (!update.auto) return toast(`wisp <b>${esc(update.latest)}</b> is out. Run <code>${esc(update.command)}</code>`, { action: { label: 'Copy', run: () => copy(update.command) }, timeout: 8000 });
|
|
27
26
|
const post = (force) => api('/update', { method: 'POST', body: { force } });
|
|
28
27
|
try {
|
|
29
28
|
await post(false).catch(async (err) => {
|