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 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.0...HEAD
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.0`), run by hand, on a schedule, when CI fails, or when an issue is labelled. |
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.0 bash`).
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.0 # run a recipe; values fill its {params} (or version=0.24.0)
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.0` fills the fields in order, `wisp run release version=0.24.0` by name.
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.0",
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 from the same folder
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
- // git a clone: we only tell you to pull
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.enabled = !/^(0|false|no|off)$/i.test(env.WISP_UPDATE_CHECK || '') && !env.CI;
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
- const s = this.state;
64
- const command = { global: `npm install -g ${PKG.name}@latest`, npx: `npx -y ${PKG.name}@latest`, git: 'git pull' }[s.method];
65
- return { ...s, command, auto: s.method !== 'git' };
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 (global) then hands off to a detached helper that waits for this server's port to
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 checkout: run `git pull` to update.');
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 === 'global') await run(`npm install -g ${PKG.name}@latest`);
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(ROOT, 'bin', 'wisp.js'))}${args}`;
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 = u.auto ? `wisp ${u.current} → ${u.latest}: install and restart` : `wisp ${u.latest} is out. Run \`${u.command}\` to update`;
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) => {