huddle-cli 0.1.0 → 0.1.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/QUICKSTART.md +102 -0
  3. package/README.md +23 -4
  4. package/dist/adapters/amp.js +1 -1
  5. package/dist/adapters/antigravity.js +2 -0
  6. package/dist/adapters/claude.js +19 -3
  7. package/dist/adapters/codex.js +6 -1
  8. package/dist/adapters/cursor.js +12 -5
  9. package/dist/adapters/custom.js +4 -2
  10. package/dist/adapters/droid.js +1 -0
  11. package/dist/adapters/goose.js +2 -0
  12. package/dist/adapters/index.js +44 -18
  13. package/dist/adapters/kiro.js +1 -1
  14. package/dist/adapters/opencode.js +1 -1
  15. package/dist/attach/connect.js +4 -1
  16. package/dist/attach/guard.js +21 -2
  17. package/dist/attach/live.js +5 -1
  18. package/dist/attach/plug.js +14 -3
  19. package/dist/attach/screen.js +150 -0
  20. package/dist/attach/session.js +7 -2
  21. package/dist/cli.js +108 -40
  22. package/dist/mcp/context-tools.js +79 -0
  23. package/dist/mcp/server.js +38 -11
  24. package/dist/orchestrator/ask.js +13 -10
  25. package/dist/orchestrator/brain.js +13 -3
  26. package/dist/orchestrator/council.js +427 -86
  27. package/dist/orchestrator/goal.js +196 -0
  28. package/dist/orchestrator/handoff.js +2 -2
  29. package/dist/orchestrator/launcher.js +29 -13
  30. package/dist/orchestrator/learn.js +105 -0
  31. package/dist/orchestrator/planner.js +25 -7
  32. package/dist/orchestrator/qa.js +62 -14
  33. package/dist/orchestrator/radar.js +719 -0
  34. package/dist/orchestrator/scheduler.js +498 -39
  35. package/dist/orchestrator/takeover.js +155 -0
  36. package/dist/platform.js +273 -0
  37. package/dist/room/context.js +1285 -0
  38. package/dist/room/contract.js +132 -0
  39. package/dist/room/db.js +79 -16
  40. package/dist/room/followup.js +233 -0
  41. package/dist/room/identity.js +2 -2
  42. package/dist/room/lessons.js +275 -0
  43. package/dist/room/live.js +75 -20
  44. package/dist/room/mail.js +212 -0
  45. package/dist/room/progress.js +40 -1
  46. package/dist/room/server.js +169 -39
  47. package/dist/room/tunnel.js +6 -5
  48. package/dist/sandbox/sync.js +4 -4
  49. package/dist/sandbox/worktree.js +145 -14
  50. package/dist/ui/app.js +554 -133
  51. package/dist/ui/catalog.js +62 -24
  52. package/dist/ui/dashboard.js +194 -51
  53. package/dist/ui/doctor.js +15 -11
  54. package/dist/ui/lessons.js +81 -0
  55. package/dist/ui/paste.js +515 -0
  56. package/dist/ui/remote.js +11 -5
  57. package/dist/ui/setup.js +152 -0
  58. package/dist/ui/story.js +666 -69
  59. package/dist/ui/term.js +151 -16
  60. package/dist/ui/watch.js +145 -48
  61. package/package.json +6 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 — 2026-10-16
4
+
5
+ The agents now work *together*, not just side by side. Verified live with real Claude, Codex and Cursor building an app from an empty repo (capstone: 29/29 checks, rounds 4 and 6).
6
+
7
+ ### Collaboration
8
+ - **Design council v2.** Every agent proposes a design, then reviews the others: what it *adopts* from each teammate and what *flaw* it sees. The agent whose ideas teammates adopted most writes the plan (not always Claude). Everyone else ratifies it or objects; objections are taken before anyone codes.
9
+ - **Shared contract.** The plan comes with `HUDDLE_CONTRACT.md` — file owners, function names, data fields, exact error messages — committed before work starts. `huddle_amend_contract` changes it and tells every working agent.
10
+ - **The goal reaches every agent verbatim** and wins over the plan and the contract; a check makes sure every exact string in the goal lands in some task.
11
+ - **Mail mid-task.** Messages arrive inside every Huddle tool result, and Claude also gets them after *every* tool call (a hook). Agents check in at ~25/50/75%.
12
+ - **Buddy checks.** At ~50% (or after 2 minutes without a progress report) another agent looks at the work in progress and sends notes. Notes must be answered — fixed, or "won't fix, because…" — before the task can finish.
13
+ - **Radar.** Every 20s Huddle compares the agents' unfinished work and warns both sides about overlapping edits or name mismatches (`getItems` vs `listItems`).
14
+ - **Pairing.** A free agent takes a separable part of a task that's behind (or writes its tests) unless the author says no.
15
+ - **QA checks the goal item by item**, and a suspiciously quick "clean" round gets a second opinion from another agent.
16
+ - **Reviews say what they verified**; a bare APPROVE is marked shallow.
17
+
18
+ ### New
19
+ - `huddle setup` — checks Node and git, installs the agents you pick, walks you through each sign-in.
20
+ - `huddle agents use claude,codex` / `huddle agents skip cursor` — choose your agents, even ones that are installed.
21
+ - **The shared screen.** Paste a long log or doc and it becomes one item (`ctx-1`) instead of one command per line; `add <file|folder>` or drag a file in; screenshots and PDFs too. Agents read, search and pin notes on it. Works in attach mode: `huddle add <file>`, `pbpaste | huddle add -`, `huddle ctx`. Secrets are caught before anything goes to an agent.
22
+ - `huddle join "<goal>" --with <file>` and piped input (`cat log | huddle join "fix this"`).
23
+ - **Lessons.** Huddle remembers what went wrong in earlier runs and tells the next run's agents (`huddle lessons`).
24
+ - `npm run evolve` — Huddle runs on its own repo to fix its weaknesses, kept only if the capstone score holds.
25
+ - The planner (brain) defaults to Claude Opus.
26
+
27
+ ### Better
28
+ - Live output in clear phases (COUNCIL → PLAN → BUILD → QA → RESULT), one status line per agent, a TEAMWORK summary; redesigned `huddle view` and `huddle story --html`.
29
+ - Windows: PowerShell install commands, agent CLIs launched correctly, Windows paths, process cleanup, no duplicated rows in `huddle doctor`. `HUDDLE_ASCII=1` for old consoles.
30
+ - Fixed: lost reviewer answers and agent output when Huddle was busy; a log-stream crash on Linux; flaky tests.
31
+ - `QUICKSTART.md`: install, first run, teammates, on one page.
package/QUICKSTART.md ADDED
@@ -0,0 +1,102 @@
1
+ # Huddle quickstart
2
+
3
+ Two commands on any computer, then you type what you want built.
4
+
5
+ ## 1. Install (once per computer)
6
+
7
+ You need **Node.js 20+** and **git**. Check with `node -v` and `git --version`.
8
+ Missing one? Node: [nodejs.org](https://nodejs.org) (or `nvm install 22`). git on macOS: `xcode-select --install`. On Windows, see [below](#windows).
9
+
10
+ ```bash
11
+ npm install -g huddle-cli && huddle setup
12
+ ```
13
+
14
+ `huddle setup` checks Node and git, asks which agents you want (say no to any you don't use — e.g. skip Cursor), installs the missing ones with the right command for your computer, then opens each one's own sign-in, one at a time. You need at least one agent. `huddle setup --yes` skips the questions (sign-ins still need you: they open a browser).
15
+
16
+ By hand instead:
17
+
18
+ | Agent | Install (macOS / Linux) | Install (Windows PowerShell) | Sign in (once) |
19
+ |---|---|---|---|
20
+ | Claude Code | `curl -fsSL https://claude.ai/install.sh \| bash` | `irm https://claude.ai/install.ps1 \| iex` | `claude auth login` |
21
+ | Codex | `npm install -g @openai/codex` | same | `codex login` |
22
+ | Cursor | `curl https://cursor.com/install -fsS \| bash` | `irm 'https://cursor.com/install?win32=true' \| iex` | `cursor-agent login` |
23
+ | Antigravity | `curl -fsSL https://antigravity.google/cli/install.sh \| bash` | `irm https://antigravity.google/cli/install.ps1 \| iex` | run `agy`, sign in, then ctrl-c twice |
24
+
25
+ Each agent uses its own account; Huddle never sees a password or key. Changed your mind? `huddle agents skip cursor` keeps Cursor out of every run (even when it's installed and signed in); `huddle agents use cursor` brings it back; `huddle agents` shows who's ready and who's skipped.
26
+
27
+ ### Windows
28
+
29
+ Huddle runs natively in PowerShell (Windows Terminal or VS Code's terminal):
30
+
31
+ ```powershell
32
+ winget install OpenJS.NodeJS.LTS # if `node -v` fails (then open a new terminal)
33
+ winget install --id Git.Git -e --source winget # if `git --version` fails
34
+ npm install -g huddle-cli
35
+ huddle setup
36
+ ```
37
+
38
+ `huddle setup` uses the PowerShell installers above. If PowerShell says "running scripts is disabled", run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once.
39
+ **If anything fails on Windows, WSL is the fallback:** `wsl --install`, restart, open "Ubuntu", then follow the macOS / Linux steps inside it (install Node there too, and keep your repo inside the Linux file system, e.g. `~/code`).
40
+
41
+ ## 2. Use it
42
+
43
+ ```bash
44
+ cd my-project # any git repo (not one? huddle offers to set it up)
45
+ huddle
46
+ ```
47
+
48
+ At the `huddle ›` prompt, type the goal, e.g. `build a REST API for todos with tests`.
49
+ Huddle plans it, every agent works at once in its own copy of the repo, and work merges only when tests pass.
50
+
51
+ One-shot instead of the prompt: `huddle join "build a REST API for todos with tests"`.
52
+
53
+ **Pasting and files.** Paste anything long (a stack trace, a spec, code) at the prompt: it goes on the *shared screen* as one item, every agent can read it, and what you type next is the goal. Drag a file or screenshot into the terminal, or type `add notes.md` / `add src/` / `add *.png`. `ctx` shows what's on the screen; `ctx rm ctx-2` takes one off. Prompts only carry a short index, so a huge paste doesn't get copied to every agent. Keys and tokens in a paste are caught first: Huddle asks before showing them to agents, and redacts them unless you say no.
54
+ One-shot: `huddle join "fix this" --with error.log --with screenshot.png`, or `cat error.log | huddle join "fix this"`.
55
+
56
+ **While it runs** (second terminal, same folder):
57
+
58
+ ```bash
59
+ huddle view # live: who's doing what, messages, merges
60
+ ```
61
+
62
+ **After it runs:**
63
+
64
+ ```bash
65
+ huddle story # replay: plan, messages between agents, reviews, merges
66
+ huddle story --html story.html # the same as a page you can open or share
67
+ ```
68
+
69
+ ## 3. Working with teammates (e.g. a hackathon)
70
+
71
+ Everyone installs Huddle as in step 1, on their own machine, with their own agents.
72
+
73
+ 1. Put the repo on GitHub and have each teammate clone it.
74
+ 2. In your `huddle`, type `sync on` so merged work gets pushed for them.
75
+ 3. Type `invite @their-github-name`. Huddle prints a one-time `huddle join <address> <CODE>` line (valid 10 minutes).
76
+ 4. They run that line inside their clone. Their agents now take tasks off your board, using their own accounts.
77
+
78
+ Not on the same Wi-Fi? Install [Tailscale](https://tailscale.com) on both machines, or run `huddle relay` on a server and type `relay <url>` in your huddle.
79
+
80
+ ## Everyday commands
81
+
82
+ | Command | What it does |
83
+ |---|---|
84
+ | `huddle` | open Huddle in this repo |
85
+ | `huddle join "<goal>" --plan` | dry run: show who would do what |
86
+ | `huddle setup` | install and sign in to your agents (asks first) |
87
+ | `huddle doctor` | check Node, git and your agents |
88
+ | `huddle agents` · `huddle agents skip <id>` · `use <id>` | who's ready, and which agents Huddle uses |
89
+ | `huddle view` | watch a run live |
90
+ | `huddle story` | replay the last run |
91
+ | `huddle board` | print the task board |
92
+ | `add <file>` / `ctx` | (at the prompt) put a file on the shared screen / see what's on it |
93
+ | `huddle add <file\|->` / `huddle ctx` | running claude/codex/cursor yourself (`huddle connect`): put a file, folder or stdin (`cat err.log \| huddle add -`) on the shared screen every session reads / see it |
94
+ | `npm install -g huddle-cli` | update to the newest version |
95
+ | `npm uninstall -g huddle-cli` | remove it (your repos keep their work) |
96
+
97
+ ## If something goes wrong
98
+
99
+ - **`EACCES` when installing:** your npm global folder is owned by root. Use [nvm](https://github.com/nvm-sh/nvm), or run `npm config set prefix ~/.npm-global` and add `~/.npm-global/bin` to your `PATH`. Don't use `sudo`.
100
+ - **An agent says "not signed in":** `huddle setup` walks you through it, or run the sign-in command `huddle doctor` shows next to it.
101
+ - **"Node.js 20 or newer":** `nvm install 22`, then install Huddle again.
102
+ - **Anything else:** `HUDDLE_DEBUG=1 huddle …` prints the full error.
package/README.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  `npm install -g huddle-cli` → `huddle`
10
10
 
11
+ **New here? [QUICKSTART.md](QUICKSTART.md)** — install, first run, teammates, in one page.
12
+
11
13
  </div>
12
14
 
13
15
  ---
@@ -18,19 +20,21 @@ You need **Node.js 20+** and **git**. Then:
18
20
 
19
21
  ```bash
20
22
  npm install -g huddle-cli # puts `huddle` on your PATH
21
- huddle doctor # checks node, git, and which agents you have
23
+ huddle setup # checks node + git, installs and signs in the agents you pick
22
24
 
23
25
  cd ~/some-git-repo
24
26
  huddle # welcome → connects your agents → huddle ›
25
27
  ```
26
28
 
27
- Prefer a script? `install.sh` checks Node, npm, git and a root-owned npm folder first, then runs the same `npm install -g` (works once the GitHub repo is public):
29
+ Prefer a script? `install.sh` checks Node, npm, git and a root-owned npm folder first, then runs the same `npm install -g` (this URL works only once the GitHub repo is public; until then use the npm command above):
28
30
 
29
31
  ```bash
30
32
  curl -fsSL https://raw.githubusercontent.com/Preet37/Huddle/main/install.sh | sh
31
33
  ```
32
34
 
33
- **Agents.** Huddle drives the coding-agent CLIs you already use — Claude Code (`claude`), Codex (`codex`), Antigravity (`agy`), Cursor (`cursor-agent`), and Gemini CLI if it's installed. You need at least one. `huddle` offers to install any that are missing and to sign you in, **always asking first**; `huddle doctor` prints the exact install commands. Each agent signs in with its own account and bills to it — Huddle never sees or stores a credential.
35
+ **Agents.** Huddle drives the coding-agent CLIs you already use — Claude Code (`claude`), Codex (`codex`), Antigravity (`agy`), Cursor (`cursor-agent`), and Gemini CLI if it's installed. You need at least one. `huddle setup` (and the first `huddle`) offers to install any that are missing and to sign you in, **always asking first**; `huddle doctor` prints the exact install commands for your OS. Don't want one? `huddle agents skip cursor` keeps it off every run, even when installed; `huddle agents use cursor` undoes it.
36
+
37
+ **Windows:** works natively in PowerShell — the same two commands; `huddle setup` uses each agent's PowerShell installer. Fallback if anything fails: WSL (`wsl --install`, then the Linux steps). Details in [QUICKSTART.md](QUICKSTART.md#windows). Each agent signs in with its own account and bills to it — Huddle never sees or stores a credential.
34
38
 
35
39
  **Not a git repo?** `huddle` offers to set one up (git init + a first commit). One-shot `huddle join "<goal>"` won't touch a non-repo folder.
36
40
 
@@ -64,6 +68,21 @@ At the `huddle ›` prompt, type what you want built. Huddle plans it into paral
64
68
  - keep typing while agents work: `board`, questions, and new goals (queued) all work mid-run
65
69
  - `cd`, `!cmd`, `clear` behave like a shell; not a git repo? Huddle offers to set it up
66
70
 
71
+ **How the agents work together on a big goal:**
72
+
73
+ - **Council.** Each agent proposes a design, then reads the others' and answers `ADOPT` (what a teammate got right that it missed), `FLAW` (what a teammate got wrong, and why) and `REVISED`. The agent whose ideas were adopted by the most teammates writes the plan (ties: the most adoptions in all, then the fewest flaws found, then whoever has written fewest plans); the brain writes it if nobody adopted anything. Each task records whose idea it was.
74
+ - **Contract.** The plan comes with a contract: the exact shared names (files and owners, signatures, data fields, routes, error strings). The other agents reply `OK` or `OBJECT: <change>`, with one revision round. It's committed as `HUDDLE_CONTRACT.md` and included in every task prompt. To change a name mid-task an agent calls `huddle_amend_contract`, and everyone still working is told.
75
+ - **Mid-task mail.** Every Plug call result includes the agent's new messages. Claude also gets them after any tool use, through a hook. Agents report progress at about 25/50/75%.
76
+ - **Radar.** About every 20s Huddle compares the uncommitted work of the agents in flight. It looks for edits to the same lines, and for imports of a name the other agent's module doesn't export (`getItems` vs `listItems`). It tells both agents while they can still fix it. Turn it off with `HUDDLE_RADAR_SEC=off`.
77
+ - **Buddy checks.** At about 40% (and again at 75% if there were notes), another agent reads the work in progress. It checks it against the task, the contract and the other tasks in flight, and its notes go to the author's inbox. An agent that finishes while a check is still being written waits for it. An agent that never reports progress gets its check after 2 minutes instead (`HUDDLE_BUDDY_AFTER_SEC`). Turn it off with `HUDDLE_BUDDY=off`.
78
+ - **Notes need an answer.** A buddy note or a radar finding stays open until the author fixes the file it names or replies to it (`won't fix: <why>` counts). `huddle_done` with notes still open is refused once, with the list; the second `huddle_done` always goes through, and the note is recorded as ignored.
79
+ - **Pairing.** When an agent is free and a task is under 50% done (one that never reports progress only after twice the help wait, with little of its file list started), the free agent proposes taking a separate part of it, or its tests. The author has 60s to say no.
80
+ - `huddle story` shows all of this afterwards: who adopted whose idea, who objected, contract versions, what the radar caught, buddy notes (addressed or ignored), and pairs.
81
+
82
+ None of these steps blocks the run. If an agent doesn't answer or doesn't follow the format, the run continues as it would without that step.
83
+
84
+ **Pasting and files: one shared screen, not N copies.** Paste a stack trace, a doc or a block of code at the prompt and it becomes one item on the Room's *shared screen* (`📎 pasted 214 lines (ctx-3 'TypeError…')`) instead of 214 commands; what you type next is the goal. Drag a file or screenshot into the terminal, or `add <file|folder|glob>`. Prompts carry only a one-line index per item; the planner links items to the tasks that need them, and agents open them with `huddle_context_read` (400 lines a call, images and PDFs as a file path; Codex also gets linked screenshots via `--image`), search them, and pin notes on them that everyone sees. `ctx` lists the screen (`ctx show|rm|note|clear`). Likely secrets (API keys, tokens, private keys, `.env` lines) are flagged before anything is shown to agents, and redacted unless you say no. One-shot: `huddle join "fix this" --with error.log --with shot.png`, or `cat log.txt | huddle join "fix this"`. Items live in `.huddle/context/` (git-ignored); caps: 2 MB of text, 25 MB per file, 200 MB in all (`HUDDLE_CTX_MAX_*_MB`).
85
+
67
86
  **Running claude, codex and cursor yourself, in one repo at once (attach mode):**
68
87
 
69
88
  ```bash
@@ -73,7 +92,7 @@ claude # open your agents as usual, in as many terminals as you lik
73
92
  huddle peers # who's live, what each is doing, which files each one holds
74
93
  ```
75
94
 
76
- Each session checks in before editing, claims the files it's about to change, and gets warned (Claude: asked through its own permission prompt) when another session holds them. Sessions can leave each other notes. Want each session on its own branch instead? `huddle attach codex` opens it in a worktree, and `huddle land` tests and merges it. Live-verified for Claude and Codex; Cursor checks in but hasn't finished a live attach run yet (see STATUS).
95
+ Each session checks in before editing, claims the files it's about to change, and gets warned (Claude: asked through its own permission prompt) when another session holds them. Sessions can leave each other notes. The shared screen works here too: `huddle add err.log` (or `cat err.log | huddle add -`) from any terminal puts it up, every session hears `📎 new on the screen: ctx-4 …` on its next Huddle call and reads it with `huddle_context_read`; `huddle ctx` lists it (`show|rm|note|clear`). Want each session on its own branch instead? `huddle attach codex` opens it in a worktree, and `huddle land` tests and merges it. Live-verified for Claude and Codex; Cursor checks in but hasn't finished a live attach run yet (see STATUS).
77
96
 
78
97
  **With a team:** `invite @harsh` in your Huddle; Harsh installs Huddle the same way and runs the `huddle join …` line it prints, from his clone of the repo. His agents take tasks off your board, work on his machine with his accounts, and push branches you test, review and merge. Different networks? `huddle relay` somewhere and `relay <url>`.
79
98
 
@@ -24,7 +24,7 @@ export const ampAdapter = {
24
24
  accountNote: "your Amp account",
25
25
  homepage: "https://ampcode.com/docs/cli",
26
26
  optional: true,
27
- install: { cmd: "curl -fsSL https://ampcode.com/install.sh | bash", login: ["amp", "login"] },
27
+ install: { cmd: "curl -fsSL https://ampcode.com/install.sh | bash", win: null, login: ["amp", "login"] },
28
28
  authProbe: {
29
29
  cmd: ["amp", "--version"],
30
30
  ok: (r) => r.code === 0,
@@ -17,7 +17,9 @@ export const antigravityAdapter = {
17
17
  models: { orchestrator: "", worker: "", fast: "", reviewer: "" },
18
18
  accountNote: "personal Google",
19
19
  install: {
20
+ // https://antigravity.google/docs/getting-started?tab=cli
20
21
  cmd: "curl -fsSL https://antigravity.google/cli/install.sh | bash",
22
+ win: "irm https://antigravity.google/cli/install.ps1 | iex",
21
23
  login: ["agy"],
22
24
  // agy has no login subcommand — signing in happens inside its own chat UI.
23
25
  loginNote: "Antigravity opens its own chat to sign you in — once your email shows at the top, press ctrl-c twice to come back to Huddle",
@@ -18,7 +18,12 @@ export const claudeAdapter = {
18
18
  // CLI aliases, not dated ids — Claude Code resolves them to the latest model.
19
19
  models: { orchestrator: "opus", worker: "opus", fast: "haiku", reviewer: "opus" },
20
20
  accountNote: "personal Anthropic",
21
- install: { cmd: "curl -fsSL https://claude.ai/install.sh | bash" },
21
+ // https://code.claude.com/docs/en/setup · sign-in: https://code.claude.com/docs/en/cli-reference
22
+ install: {
23
+ cmd: "curl -fsSL https://claude.ai/install.sh | bash",
24
+ win: "irm https://claude.ai/install.ps1 | iex",
25
+ login: ["claude", "auth", "login"],
26
+ },
22
27
  authProbe: {
23
28
  // `claude auth status` (JSON by default, verified on 2.1.300): exit 0 +
24
29
  // "loggedIn": true when signed in (OAuth or ANTHROPIC_API_KEY), exit 1 +
@@ -26,7 +31,7 @@ export const claudeAdapter = {
26
31
  // a signed-out Claude as ready.
27
32
  cmd: ["claude", "auth", "status"],
28
33
  ok: (r) => r.code === 0 && !/"loggedIn"\s*:\s*false/.test(r.stdout),
29
- fixHint: "run `claude` once and sign in (OAuth) or set ANTHROPIC_API_KEY",
34
+ fixHint: "run `claude auth login` (or set ANTHROPIC_API_KEY)",
30
35
  },
31
36
  plug: {
32
37
  file: { path: "claude-mcp.json", in: "run-dir" },
@@ -46,12 +51,22 @@ export const claudeAdapter = {
46
51
  ],
47
52
  cwd,
48
53
  }),
54
+ // `claude -p` with no prompt argument reads it from stdin.
55
+ stdinPrompt: "",
49
56
  usage: claudeUsage,
50
- launch: ({ worktree, prompt, model, mcpConfigPath, approvals }) => ({
57
+ // A PostToolUse hook, loaded with --settings (a file in .huddle/run/, never
58
+ // the user's settings): after any tool call, `huddle mail --hook` prints
59
+ // {hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext}} when
60
+ // a teammate wrote, and nothing otherwise. Schema per
61
+ // https://code.claude.com/docs/en/hooks; --settings per `claude --help` (2.1.300).
62
+ mailHook: (command) => ({ hooks: { PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command, timeout: 5 }] }] } }),
63
+ launch: ({ worktree, prompt, model, mcpConfigPath, approvals, context, hookSettingsPath }) => ({
51
64
  cmd: [
52
65
  "claude",
53
66
  "-p",
54
67
  prompt,
68
+ // The shared screen lives outside the worktree; Read opens its images and PDFs.
69
+ ...(context ? ["--add-dir", context.dir] : []),
55
70
  ...(model ? ["--model", model] : []),
56
71
  ...PERMISSIONS[approvals],
57
72
  "--mcp-config",
@@ -59,6 +74,7 @@ export const claudeAdapter = {
59
74
  // Only Huddle's Plug — not your personal MCP servers. Smaller context
60
75
  // every turn, and a headless agent can't reach your Gmail/Drive/etc.
61
76
  "--strict-mcp-config",
77
+ ...(hookSettingsPath ? ["--settings", hookSettingsPath] : []),
62
78
  // JSON events so Huddle can count tokens live (logs stay in .huddle/logs)
63
79
  "--output-format",
64
80
  "stream-json",
@@ -26,11 +26,16 @@ export const codexAdapter = {
26
26
  cwd,
27
27
  usesOutFile: true,
28
28
  }),
29
+ // `codex exec -`: "instructions are read from stdin".
30
+ stdinPrompt: "-",
29
31
  usage: codexUsage,
30
- launch: ({ worktree, prompt, model, plug, approvals }) => ({
32
+ launch: ({ worktree, prompt, model, plug, approvals, context }) => ({
31
33
  cmd: [
32
34
  "codex",
33
35
  "exec",
36
+ // `codex exec -i/--image <FILE>`: screenshots the planner linked to this task, attached to the prompt.
37
+ // One flag per file, before the other flags, so the list never swallows the prompt.
38
+ ...(context?.images ?? []).flatMap((f) => ["--image", f]),
34
39
  "--json", // JSON events so Huddle can count tokens live
35
40
  ...(model ? ["--model", model] : []),
36
41
  "-c",
@@ -1,4 +1,4 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawnToolSync } from "../platform.js";
2
2
  import { cursorUsage } from "./usage.js";
3
3
  /**
4
4
  * The model for quick prompt-only answers (council turns, cross-reviews), or
@@ -24,7 +24,7 @@ export function cursorFastModel(list = listCursorModels) {
24
24
  catch {
25
25
  /* not installed / signed out: keep the default */
26
26
  }
27
- const ids = out.split("\n").map((l) => /^\s*([\w.-]+)\s+-\s/.exec(l)?.[1]).filter((x) => !!x);
27
+ const ids = out.split(/\r?\n/).map((l) => /^\s*([\w.-]+)\s+-\s/.exec(l)?.[1]).filter((x) => !!x);
28
28
  fastPick = { model: ids.find((id) => /^composer-[\d.]+$/.test(id)) };
29
29
  return fastPick.model;
30
30
  }
@@ -33,7 +33,7 @@ export function resetCursorFastModel() {
33
33
  fastPick = undefined;
34
34
  }
35
35
  function listCursorModels() {
36
- const r = spawnSync("cursor-agent", ["--list-models"], { encoding: "utf8", timeout: 15_000 });
36
+ const r = spawnToolSync(["cursor-agent", "--list-models"], { encoding: "utf8", timeout: 15_000 });
37
37
  return r.status === 0 ? r.stdout : "";
38
38
  }
39
39
  // Cursor CLI (cursor-agent) — headless coding agent. Personal, team or SSO
@@ -47,7 +47,12 @@ export const cursorAdapter = {
47
47
  prefers: ["in-editor-edits", "frontend", "quickfix"],
48
48
  models: { orchestrator: "", worker: "", fast: "", reviewer: "" },
49
49
  accountNote: "your Cursor account (SSO handled by cursor-agent itself)",
50
- install: { cmd: "curl https://cursor.com/install -fsS | bash", login: ["cursor-agent", "login"] },
50
+ // https://cursor.com/docs/cli/installation (native Windows: PowerShell)
51
+ install: {
52
+ cmd: "curl https://cursor.com/install -fsS | bash",
53
+ win: "irm 'https://cursor.com/install?win32=true' | iex",
54
+ login: ["cursor-agent", "login"],
55
+ },
51
56
  authProbe: {
52
57
  cmd: ["cursor-agent", "status"],
53
58
  ok: (r) => r.code === 0 && !/not logged in/i.test(r.stdout + r.stderr),
@@ -64,11 +69,13 @@ export const cursorAdapter = {
64
69
  return { cmd: ["cursor-agent", "-p", prompt, "--mode", "ask", "--trust", ...(m ? ["--model", m] : [])], cwd };
65
70
  },
66
71
  usage: cursorUsage,
67
- launch: ({ worktree, prompt, model, approvals }) => ({
72
+ launch: ({ worktree, prompt, model, approvals, context }) => ({
68
73
  cmd: [
69
74
  "cursor-agent",
70
75
  "-p",
71
76
  prompt,
77
+ // No image flag on cursor-agent: the shared screen's folder, so its tools can open items by path.
78
+ ...(context ? ["--add-dir", context.dir] : []),
72
79
  ...(model ? ["--model", model] : []),
73
80
  // headless: trust the worktree and the Plug without prompting
74
81
  "--trust",
@@ -28,7 +28,7 @@ export const USAGE_PARSERS = {
28
28
  qwen: qwenUsage,
29
29
  };
30
30
  const SPEC_KEYS = new Set([
31
- "id", "bin", "launch", "vendor", "approvals", "ask", "askOutFile", "plug", "env", "install", "login", "loginNote",
31
+ "id", "bin", "launch", "vendor", "approvals", "ask", "askOutFile", "plug", "env", "install", "installWin", "login", "loginNote",
32
32
  "auth", "usage", "roles", "prefers", "models", "accountNote", "homepage", "optional", "$comment",
33
33
  ]);
34
34
  const ROLES = new Set(["orchestrator", "worker", "reviewer", "fast"]);
@@ -124,6 +124,8 @@ export function validateSpec(raw) {
124
124
  errs.push(`"login" must be an array of strings`);
125
125
  if (s.install !== undefined && typeof s.install !== "string")
126
126
  errs.push(`"install" must be a shell command string`);
127
+ if (s.installWin !== undefined && typeof s.installWin !== "string")
128
+ errs.push(`"installWin" must be a PowerShell command string`);
127
129
  if (s.auth !== undefined) {
128
130
  const a = s.auth;
129
131
  if (!a || typeof a !== "object")
@@ -216,7 +218,7 @@ export function specToAdapter(spec, source) {
216
218
  roles: spec.roles ?? (spec.ask ? ["worker", "reviewer"] : ["worker"]),
217
219
  prefers: spec.prefers ?? [],
218
220
  models: { orchestrator: "", worker: "", fast: "", reviewer: "", ...spec.models },
219
- install: { cmd: spec.install ?? "", login: spec.login, loginNote: spec.loginNote },
221
+ install: { cmd: spec.install ?? "", ...(spec.installWin !== undefined ? { win: spec.installWin } : {}), login: spec.login, loginNote: spec.loginNote },
220
222
  authProbe: {
221
223
  cmd: authCmd,
222
224
  ok: (r) => r.code === 0 && !(fail && fail.test(r.stdout + r.stderr)),
@@ -34,6 +34,7 @@ export const droidAdapter = {
34
34
  optional: true,
35
35
  install: {
36
36
  cmd: "curl -fsSL https://app.factory.ai/cli | sh",
37
+ win: "irm https://app.factory.ai/cli/windows | iex",
37
38
  login: ["droid"],
38
39
  loginNote: "Droid signs you in from its own screen — once you're in, press ctrl-c to come back to Huddle",
39
40
  },
@@ -37,6 +37,8 @@ export const gooseAdapter = {
37
37
  optional: true,
38
38
  install: {
39
39
  cmd: "curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash",
40
+ // Windows has a download_cli.ps1 that saves itself into the current folder: point at the docs instead.
41
+ win: null,
40
42
  login: ["goose", "configure"],
41
43
  loginNote: "pick a provider and paste its key in goose's own setup — Huddle never sees it",
42
44
  },
@@ -2,7 +2,7 @@
2
2
  // uses to decide who's in the roster for a session. Because we only ever ask
3
3
  // "installed?" and "authed?", expiring subscriptions and account swaps are a
4
4
  // non-event: a failing probe just drops that agent, the session runs on.
5
- import { execFile, execFileSync } from "node:child_process";
5
+ import { spawn } from "node:child_process";
6
6
  import path from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { claudeAdapter } from "./claude.js";
@@ -18,6 +18,7 @@ import { qwenAdapter } from "./qwen.js";
18
18
  import { droidAdapter } from "./droid.js";
19
19
  import { kiroAdapter } from "./kiro.js";
20
20
  import { loadAgentsFiles } from "./custom.js";
21
+ import { onPath, spawnToolSync, toolCommand } from "../platform.js";
21
22
  export const ADAPTERS = [
22
23
  claudeAdapter,
23
24
  codexAdapter,
@@ -80,23 +81,18 @@ export function registerAdapter(adapter) {
80
81
  else if (adapter.id !== "gemini")
81
82
  LEGACY.delete(adapter.id);
82
83
  }
83
- function onPath(bin) {
84
- try {
85
- execFileSync(process.platform === "win32" ? "where" : "which", [bin], { stdio: "pipe" });
86
- return true;
87
- }
88
- catch {
89
- return false;
90
- }
84
+ /** Mark the agents you skipped, keeping what the probe found in `was`. */
85
+ export function applySkips(results, skip) {
86
+ const s = new Set(skip);
87
+ return results.map((r) => s.has(r.adapter.id) && r.state !== "skipped" ? { ...r, state: "skipped", was: r.state, detail: `skipped by you — huddle agents use ${r.adapter.id}` } : r);
88
+ }
89
+ /** Undo applySkips for `ids` — `--agents=` on a run overrides a skip for that run. */
90
+ export function unskip(results, ids) {
91
+ return results.map((r) => (r.state === "skipped" && r.was && ids.includes(r.adapter.id) ? { ...r, state: r.was, was: undefined } : r));
91
92
  }
92
93
  function run(cmd) {
93
- try {
94
- const stdout = execFileSync(cmd[0], cmd.slice(1), { encoding: "utf8", stdio: "pipe", timeout: 8000 });
95
- return { code: 0, stdout, stderr: "" };
96
- }
97
- catch (e) {
98
- return { code: e?.status ?? 1, stdout: e?.stdout?.toString() ?? "", stderr: e?.stderr?.toString() ?? "" };
99
- }
94
+ const r = spawnToolSync(cmd, { encoding: "utf8", stdio: "pipe", timeout: 8000 });
95
+ return { code: r.error ? 1 : (r.status ?? 1), stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") };
100
96
  }
101
97
  /** Detect + auth-probe a single adapter. */
102
98
  export function probe(adapter) {
@@ -109,9 +105,21 @@ export function probe(adapter) {
109
105
  }
110
106
  return { adapter, state: "ready", detail: `${adapter.vendor} · ${adapter.accountNote ?? "ready"}` };
111
107
  }
112
- function runAsync(cmd) {
108
+ function runAsync(cmd, timeoutMs = 8000) {
113
109
  return new Promise((resolve) => {
114
- execFile(cmd[0], cmd.slice(1), { encoding: "utf8", timeout: 8000 }, (err, stdout, stderr) => resolve({ code: err ? (typeof err.code === "number" ? err.code : 1) : 0, stdout, stderr }));
110
+ const t = toolCommand(cmd);
111
+ const child = spawn(t.file, t.args, { stdio: ["ignore", "pipe", "pipe"], windowsVerbatimArguments: t.verbatim });
112
+ let stdout = "";
113
+ let stderr = "";
114
+ child.stdout.on("data", (d) => (stdout += d));
115
+ child.stderr.on("data", (d) => (stderr += d));
116
+ const timer = setTimeout(() => child.kill(), timeoutMs);
117
+ const done = (code) => {
118
+ clearTimeout(timer);
119
+ resolve({ code, stdout, stderr });
120
+ };
121
+ child.on("error", () => done(1));
122
+ child.on("close", (code) => done(code ?? 1));
115
123
  });
116
124
  }
117
125
  /** Non-blocking probe, so the welcome screen can animate while agents connect. */
@@ -125,6 +133,24 @@ export async function probeAsync(adapter) {
125
133
  }
126
134
  return { adapter, state: "ready", detail: `${adapter.vendor} · ${adapter.accountNote ?? "ready"}` };
127
135
  }
136
+ /**
137
+ * The install command for this platform, or null when there's no native one.
138
+ * An `npm install -g …` works everywhere; curl-pipe-bash doesn't on Windows.
139
+ */
140
+ export function installCommand(a, platform = process.platform) {
141
+ if (platform !== "win32")
142
+ return a.install.cmd || null;
143
+ if (a.install.win !== undefined)
144
+ return a.install.win;
145
+ return /^npm (install|i) /.test(a.install.cmd) ? a.install.cmd : null;
146
+ }
147
+ /** What to say when installCommand() is null. */
148
+ export function noInstallHint(a, platform = process.platform) {
149
+ const docs = a.homepage ? `see ${a.homepage}` : `install ${a.bin} the way its docs say`;
150
+ return platform === "win32" && a.install.cmd ? `no Windows installer — ${docs}, or use it from WSL` : docs;
151
+ }
152
+ /** The sign-in command as one line, for messages ("codex login"). */
153
+ export const loginLine = (a) => (a.install.login ? a.install.login.join(" ") : null);
128
154
  /** Probe the whole roster. */
129
155
  export function probeAll() {
130
156
  return ADAPTERS.map(probe);
@@ -23,7 +23,7 @@ export const kiroAdapter = {
23
23
  accountNote: "your Kiro login",
24
24
  homepage: "https://kiro.dev/docs/cli/",
25
25
  optional: true,
26
- install: { cmd: "curl -fsSL https://cli.kiro.dev/install | bash", login: ["kiro-cli", "login"] },
26
+ install: { cmd: "curl -fsSL https://cli.kiro.dev/install | bash", win: null, login: ["kiro-cli", "login"] },
27
27
  authProbe: {
28
28
  cmd: ["kiro-cli", "whoami"],
29
29
  ok: (r) => r.code === 0 && !/not logged in/i.test(r.stdout + r.stderr),
@@ -35,7 +35,7 @@ export const opencodeAdapter = {
35
35
  accountNote: "your OpenCode providers",
36
36
  homepage: "https://opencode.ai/docs/",
37
37
  optional: true,
38
- install: { cmd: "curl -fsSL https://opencode.ai/install | bash", login: ["opencode", "auth", "login"] },
38
+ install: { cmd: "curl -fsSL https://opencode.ai/install | bash", win: "npm install -g opencode-ai", login: ["opencode", "auth", "login"] },
39
39
  authProbe: {
40
40
  cmd: ["opencode", "--version"],
41
41
  ok: (r) => r.code === 0,
@@ -35,7 +35,10 @@ const CLAUDE_ALLOW = "mcp__huddle";
35
35
  export function livePlug(repo, agent) {
36
36
  return { command: process.execPath, args: [CLI_PATH, "plug"], env: { HUDDLE_LIVE: "1", HUDDLE_AGENT_ID: agent, HUDDLE_REPO: repo } };
37
37
  }
38
- export const guardCommand = () => `"${process.execPath}" "${CLI_PATH}" guard`;
38
+ // Quoted (paths with spaces: C:\Users\Some Name\…) and with forward slashes on
39
+ // Windows, which node accepts and which survive both bash and cmd/PowerShell.
40
+ const hookPath = (p) => (process.platform === "win32" ? p.replace(/\\/g, "/") : p);
41
+ export const guardCommand = () => `"${hookPath(process.execPath)}" "${hookPath(CLI_PATH)}" guard`;
39
42
  /** What `huddle connect` would write for one agent. */
40
43
  export function wiresFor(adapter, repo, opts = {}) {
41
44
  const plug = livePlug(repo, adapter.id);
@@ -14,13 +14,17 @@
14
14
  import { execFileSync } from "node:child_process";
15
15
  import { liveCall, mainRoot, readLive } from "./live.js";
16
16
  /** This process's ancestors (the hook's shell, then Claude itself, …). */
17
- export function ancestors(max = 8) {
17
+ export function ancestors(max = 8, platform = process.platform) {
18
18
  const out = [];
19
+ // Windows has no `ps`: one PowerShell call lists every process's parent.
20
+ const parents = platform === "win32" ? windowsParents() : null;
19
21
  let pid = process.ppid;
20
22
  for (let i = 0; i < max && pid > 1; i++) {
21
23
  out.push(pid);
22
24
  try {
23
- pid = Number(execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
25
+ pid = parents
26
+ ? (parents.get(pid) ?? 0)
27
+ : Number(execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
24
28
  }
25
29
  catch {
26
30
  break;
@@ -28,6 +32,21 @@ export function ancestors(max = 8) {
28
32
  }
29
33
  return out;
30
34
  }
35
+ /** pid → parent pid, from `Get-CimInstance Win32_Process`. Empty if that fails. */
36
+ export function windowsParents(run = () => execFileSync("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId) $($_.ParentProcessId)\" }"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000, windowsHide: true })) {
37
+ const m = new Map();
38
+ try {
39
+ for (const l of run().split(/\r?\n/)) {
40
+ const [a, b] = l.trim().split(/\s+/).map(Number);
41
+ if (a > 0 && b >= 0)
42
+ m.set(a, b);
43
+ }
44
+ }
45
+ catch {
46
+ /* no PowerShell: the guard still works, just without pid matching */
47
+ }
48
+ return m;
49
+ }
31
50
  async function readStdin() {
32
51
  let s = "";
33
52
  for await (const c of process.stdin)
@@ -103,7 +103,7 @@ export async function pingLive(st) {
103
103
  return false;
104
104
  }
105
105
  }
106
- function ensureHuddleDir(root) {
106
+ export function ensureHuddleDir(root) {
107
107
  const f = liveFiles(root);
108
108
  fs.mkdirSync(path.dirname(f.log), { recursive: true });
109
109
  const gi = path.join(f.dir, ".gitignore");
@@ -142,6 +142,7 @@ export async function ensureLive(root, timeoutMs = 8000) {
142
142
  const child = spawn(process.execPath, [CLI_PATH, "up", "--serve"], {
143
143
  cwd: root,
144
144
  detached: true,
145
+ windowsHide: true, // detached on Windows = a new console window, unless hidden
145
146
  stdio: ["ignore", out, out],
146
147
  env: { ...process.env, HUDDLE_LIVE_ROOT: root },
147
148
  });
@@ -217,6 +218,9 @@ export async function stopLive(root) {
217
218
  process.kill(st.pid, "SIGTERM");
218
219
  for (let i = 0; i < 50 && alive(st.pid); i++)
219
220
  await new Promise((r) => setTimeout(r, 100));
221
+ // On Windows "SIGTERM" is TerminateProcess: the daemon's handler never runs, so tidy up for it.
222
+ if (!alive(st.pid))
223
+ fs.rmSync(liveFiles(root).state, { force: true });
220
224
  return true;
221
225
  }
222
226
  //# sourceMappingURL=live.js.map