huddle-cli 0.0.0-stage → 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 (75) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/QUICKSTART.md +102 -0
  4. package/README.md +376 -2
  5. package/dist/adapters/amp.js +40 -0
  6. package/dist/adapters/antigravity.js +54 -0
  7. package/dist/adapters/claude.js +86 -0
  8. package/dist/adapters/codex.js +64 -0
  9. package/dist/adapters/copilot.js +69 -0
  10. package/dist/adapters/cursor.js +92 -0
  11. package/dist/adapters/custom.js +342 -0
  12. package/dist/adapters/droid.js +58 -0
  13. package/dist/adapters/gemini.js +61 -0
  14. package/dist/adapters/goose.js +57 -0
  15. package/dist/adapters/index.js +161 -0
  16. package/dist/adapters/kiro.js +41 -0
  17. package/dist/adapters/opencode.js +63 -0
  18. package/dist/adapters/qwen.js +67 -0
  19. package/dist/adapters/types.js +6 -0
  20. package/dist/adapters/usage.js +141 -0
  21. package/dist/attach/connect.js +412 -0
  22. package/dist/attach/guard.js +83 -0
  23. package/dist/attach/live.js +226 -0
  24. package/dist/attach/plug.js +185 -0
  25. package/dist/attach/screen.js +150 -0
  26. package/dist/attach/session.js +177 -0
  27. package/dist/cli.js +244 -0
  28. package/dist/mcp/context-tools.js +79 -0
  29. package/dist/mcp/server.js +267 -0
  30. package/dist/orchestrator/ask.js +54 -0
  31. package/dist/orchestrator/brain.js +174 -0
  32. package/dist/orchestrator/council.js +615 -0
  33. package/dist/orchestrator/goal.js +196 -0
  34. package/dist/orchestrator/handoff.js +117 -0
  35. package/dist/orchestrator/launcher.js +171 -0
  36. package/dist/orchestrator/learn.js +105 -0
  37. package/dist/orchestrator/planner.js +212 -0
  38. package/dist/orchestrator/qa.js +398 -0
  39. package/dist/orchestrator/radar.js +719 -0
  40. package/dist/orchestrator/scheduler.js +1207 -0
  41. package/dist/orchestrator/takeover.js +240 -0
  42. package/dist/platform.js +273 -0
  43. package/dist/preflight.js +31 -0
  44. package/dist/relay/server.js +113 -0
  45. package/dist/room/context.js +1285 -0
  46. package/dist/room/contract.js +132 -0
  47. package/dist/room/db.js +680 -0
  48. package/dist/room/followup.js +233 -0
  49. package/dist/room/identity.js +122 -0
  50. package/dist/room/lessons.js +275 -0
  51. package/dist/room/live.js +348 -0
  52. package/dist/room/mail.js +212 -0
  53. package/dist/room/progress.js +149 -0
  54. package/dist/room/relaylink.js +85 -0
  55. package/dist/room/server.js +882 -0
  56. package/dist/room/sqlite.js +156 -0
  57. package/dist/room/transport.js +107 -0
  58. package/dist/room/tunnel.js +86 -0
  59. package/dist/sandbox/sync.js +104 -0
  60. package/dist/sandbox/worktree.js +392 -0
  61. package/dist/types.js +4 -0
  62. package/dist/ui/app.js +1955 -0
  63. package/dist/ui/catalog.js +113 -0
  64. package/dist/ui/dashboard.js +335 -0
  65. package/dist/ui/doctor.js +58 -0
  66. package/dist/ui/lessons.js +81 -0
  67. package/dist/ui/logo.js +40 -0
  68. package/dist/ui/paste.js +515 -0
  69. package/dist/ui/remote.js +338 -0
  70. package/dist/ui/setup.js +152 -0
  71. package/dist/ui/story.js +1529 -0
  72. package/dist/ui/term.js +249 -0
  73. package/dist/ui/watch.js +271 -0
  74. package/install.sh +44 -0
  75. package/package.json +63 -4
@@ -0,0 +1,177 @@
1
+ // Attach mode, isolation: `huddle attach <agent>` opens the agent's normal
2
+ // interactive CLI in its OWN worktree branch (huddle/<agent>-live-N), already
3
+ // connected to the live room. Two sessions can then never overwrite each
4
+ // other's files mid-edit; `huddle land` brings a session's branch home through
5
+ // the merge engine (commit → tests → rebase → fast-forward), after you say so.
6
+ //
7
+ // Plus `huddle up` / `huddle down` / `huddle peers`, the human's view of the room
8
+ // (`huddle add` / `huddle ctx`, its shared screen, are in screen.ts).
9
+ import { spawnSync } from "node:child_process";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { getAdapter } from "../adapters/index.js";
13
+ import { spawnToolSync } from "../platform.js";
14
+ import { createWorktree, landMerge, prepareMerge, removeWorktree } from "../sandbox/worktree.js";
15
+ import { C, dot } from "../ui/term.js";
16
+ import { detectTestCmd } from "../ui/app.js";
17
+ import { applyConnect, confirm, disconnect, livePlug, planConnect } from "./connect.js";
18
+ import { ensureLive, liveCall, liveFiles, mainRoot, readLive, serveLive, stopLive, pingLive, checkoutRoot } from "./live.js";
19
+ const attachFile = (repo) => path.join(liveFiles(repo).dir, "attach.json");
20
+ export function readAttached(repo) {
21
+ try {
22
+ return JSON.parse(fs.readFileSync(attachFile(repo), "utf8"));
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ }
28
+ function writeAttached(repo, list) {
29
+ fs.mkdirSync(path.dirname(attachFile(repo)), { recursive: true });
30
+ fs.writeFileSync(attachFile(repo), JSON.stringify(list, null, 2));
31
+ }
32
+ function needRepo() {
33
+ const root = mainRoot();
34
+ if (!root) {
35
+ console.log(C.red("✗ Not a git repo.") + " Run this inside the repo your agents work in.");
36
+ process.exit(1);
37
+ }
38
+ return root;
39
+ }
40
+ /**
41
+ * Make a session worktree for `agent`, wired to the live room. Returns the
42
+ * worktree and the interactive command to run in it.
43
+ */
44
+ export function prepareAttach(repo, agentId) {
45
+ const adapter = getAdapter(agentId);
46
+ if (!adapter)
47
+ throw new Error(`unknown agent "${agentId}"`);
48
+ const taken = new Set(readAttached(repo).map((e) => e.branch));
49
+ let n = 1;
50
+ while (taken.has(`huddle/${agentId}-live-${n}`) || fs.existsSync(path.join(repo, ".huddle", "worktrees", `${agentId}-live-${n}`)))
51
+ n++;
52
+ const wt = createWorktree(repo, agentId, `live-${n}`);
53
+ // The same project files `huddle connect` writes, inside the session's own worktree.
54
+ const plan = planConnect(wt.path, { agents: [agentId], repo, adapters: [adapter] });
55
+ applyConnect(wt.path, plan, repo);
56
+ const excludes = plan.filter((w) => w.action !== "skip").map((w) => w.file);
57
+ const cmd = [adapter.bin];
58
+ if (adapter.id === "codex") {
59
+ // A new worktree isn't a trusted project yet, and Codex ignores .codex/ in
60
+ // untrusted ones — so hand the Plug over with -c too (verified: `codex --help`).
61
+ const plug = livePlug(repo, "codex");
62
+ const env = `{${Object.entries(plug.env).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(",")}}`;
63
+ cmd.push("-c", `mcp_servers.huddle.command=${JSON.stringify(plug.command)}`, "-c", `mcp_servers.huddle.args=${JSON.stringify(plug.args)}`, "-c", `mcp_servers.huddle.env=${env}`);
64
+ }
65
+ writeAttached(repo, [...readAttached(repo), { agent: agentId, branch: wt.branch, path: wt.path, excludes, started_at: Date.now() }]);
66
+ return { wt, cmd, excludes };
67
+ }
68
+ export async function runAttach(args) {
69
+ const repo = needRepo();
70
+ const agentId = args.find((a) => !a.startsWith("-"));
71
+ if (!agentId) {
72
+ console.log(C.red("✗ Which agent?") + " huddle attach claude (or codex, cursor, …)");
73
+ process.exit(1);
74
+ }
75
+ const passthrough = args.includes("--") ? args.slice(args.indexOf("--") + 1) : [];
76
+ const dirty = spawnSync("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: repo, encoding: "utf8" }).stdout.trim();
77
+ if (dirty)
78
+ console.log(C.yellow("! You have uncommitted changes in the main checkout — the session starts from the last commit, without them."));
79
+ const { state } = await ensureLive(repo);
80
+ const { wt, cmd } = prepareAttach(repo, agentId);
81
+ console.log(`${dot(agentId)} ${C.bold(agentId)} in its own worktree ${C.faint(path.relative(repo, wt.path))} on ${C.bold(wt.branch)}`);
82
+ console.log(C.faint(` live room ${state.url} · the others see it in huddle_peers · when it's done: huddle land ${wt.branch}\n`));
83
+ const r = spawnToolSync([...cmd, ...passthrough], { cwd: wt.path, stdio: "inherit" });
84
+ if (r.error)
85
+ console.log(C.red(`✗ couldn't start ${cmd[0]}: ${r.error.message}`));
86
+ const changed = spawnSync("git", ["status", "--porcelain"], { cwd: wt.path, encoding: "utf8" }).stdout.trim();
87
+ const ahead = spawnSync("git", ["rev-list", "--count", `HEAD..${wt.branch}`], { cwd: repo, encoding: "utf8" }).stdout.trim();
88
+ console.log(`\n${agentId} session ended. ${changed || (ahead && ahead !== "0") ? `Its work is on ${C.bold(wt.branch)} — ${C.bold(`huddle land ${wt.branch}`)} tests it and merges it.` : "No changes."}`);
89
+ }
90
+ /** `huddle land [branch|agent]`: the merge engine for one attach session. */
91
+ export async function landSession(repo, e, opts = {}) {
92
+ const wt = { branch: e.branch, path: e.path };
93
+ const testCmd = opts.testCmd !== undefined ? opts.testCmd : detectTestCmd(e.path);
94
+ const prep = prepareMerge(repo, wt, testCmd, e.excludes, `huddle: ${e.agent} session (${e.branch})`);
95
+ // Attach mode has no scheduler to hand a conflict to a resolver agent: the
96
+ // session's own agent (or you) merges main in, then lands again.
97
+ if (prep.conflict)
98
+ return { merged: false, reason: `main changed the same lines — in ${e.path} run \`git merge main\`, resolve, then \`huddle land\` again` };
99
+ if (!prep.ok)
100
+ return { merged: false, reason: prep.reason };
101
+ if (!prep.diffstat.trim())
102
+ return { merged: false, reason: "nothing to land — the branch matches main" };
103
+ if (!opts.yes) {
104
+ console.log(prep.diffstat + "\n" + C.faint(prep.reason));
105
+ if (!(await confirm(`Merge ${e.branch} into your current branch?`)))
106
+ return { merged: false, reason: "not now", diffstat: prep.diffstat };
107
+ }
108
+ const land = landMerge(repo, wt);
109
+ if (land.merged && !opts.keep) {
110
+ disconnect(e.path, repo);
111
+ removeWorktree(repo, e.path);
112
+ spawnSync("git", ["branch", "-d", e.branch], { cwd: repo, stdio: "ignore" });
113
+ writeAttached(repo, readAttached(repo).filter((x) => x.branch !== e.branch));
114
+ }
115
+ return { merged: land.merged, reason: land.reason, diffstat: prep.diffstat };
116
+ }
117
+ export async function runLand(args) {
118
+ const repo = needRepo();
119
+ const list = readAttached(repo);
120
+ const want = args.find((a) => !a.startsWith("-"));
121
+ const here = checkoutRoot();
122
+ const pick = want
123
+ ? list.filter((e) => e.branch === want || e.agent === want || path.basename(e.path) === want)
124
+ : list.filter((e) => e.path === here).length
125
+ ? list.filter((e) => e.path === here)
126
+ : list;
127
+ if (!pick.length)
128
+ return void console.log(want ? C.red(`✗ no attach session "${want}"`) : "No attach sessions. Start one with huddle attach <agent>.");
129
+ if (pick.length > 1) {
130
+ console.log("Which one?\n" + pick.map((e) => ` huddle land ${e.branch} ${C.faint(e.agent)}`).join("\n"));
131
+ return;
132
+ }
133
+ const r = await landSession(repo, pick[0], { yes: args.includes("--yes") || args.includes("-y"), keep: args.includes("--keep") });
134
+ console.log(r.merged ? `${C.green("✓")} landed ${pick[0].branch}` : `${C.yellow("·")} not landed: ${r.reason}`);
135
+ if (!r.merged)
136
+ process.exitCode = 1;
137
+ }
138
+ // ---- up / down / peers ---------------------------------------------------------
139
+ export async function runUp(args) {
140
+ if (args.includes("--serve"))
141
+ return serveLive(process.env.HUDDLE_LIVE_ROOT || needRepo());
142
+ const repo = needRepo();
143
+ const { state, started } = await ensureLive(repo);
144
+ console.log(`${C.green("✓")} live room ${started ? "started" : "already running"} for ${C.bold(path.basename(repo))} ${C.faint(`(${state.url}, pid ${state.pid})`)}`);
145
+ const connected = fs.existsSync(path.join(liveFiles(repo).dir, "connect.json"));
146
+ if (!connected)
147
+ console.log(` next: ${C.bold("huddle connect")} so your claude / codex / cursor sessions join it`);
148
+ console.log(C.faint(" huddle peers — who's in · huddle down — stop it"));
149
+ }
150
+ export async function runDown() {
151
+ const repo = needRepo();
152
+ console.log((await stopLive(repo)) ? `${C.green("✓")} live room stopped` : "No live room running for this repo.");
153
+ }
154
+ export async function runPeers() {
155
+ const repo = needRepo();
156
+ const st = readLive(repo);
157
+ if (!st || !(await pingLive(st)))
158
+ return void console.log(`No live room. ${C.bold("huddle up")} starts one.`);
159
+ // Anonymous: a person looking in isn't a session.
160
+ const r = await liveCall(st, "peers", {}, "");
161
+ const sessions = (r.body.sessions ?? []).filter((s) => !s.you);
162
+ console.log(`${C.violet("huddle")} live room · ${path.basename(repo)} · ${sessions.length} session${sessions.length === 1 ? "" : "s"}\n`);
163
+ for (const s of sessions) {
164
+ console.log(` ${dot(s.agent)} ${C.bold(s.id.padEnd(12))} ${s.doing}`);
165
+ console.log(C.faint(` ${s.branch || "?"} · ${s.worktree} · holds ${s.holds.length ? s.holds.join(", ") : "nothing"} · seen ${s.seen_s_ago}s ago`));
166
+ }
167
+ const sc = r.body.screen;
168
+ if (sc?.items)
169
+ console.log(`\n ${C.bold("screen")} ${sc.items} item${sc.items === 1 ? "" : "s"}${sc.latest_note ? ` · latest note: ${sc.latest_note}` : ""} ${C.faint("· huddle ctx")}`);
170
+ const notes = r.body.notes ?? [];
171
+ if (notes.length) {
172
+ console.log(`\n ${C.bold("notes")}`);
173
+ for (const n of notes)
174
+ console.log(` ${C.faint(`${n.ago} ago`.padStart(8))} ${n.from}${n.from_live ? "" : C.faint(" (gone)")} → ${n.to}: ${n.body}`);
175
+ }
176
+ }
177
+ //# sourceMappingURL=session.js.map
package/dist/cli.js ADDED
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ // huddle — the multiplayer layer for AI coding agents.
3
+ //
4
+ // huddle open Huddle (welcome, connect agents, prompt)
5
+ // huddle join "<goal>" plan a goal and run your agents on it
6
+ // huddle join "<goal>" --plan dry run: show the plan, create/spawn nothing
7
+ // --approvals=safe|auto|yolo what agents may do without asking (default auto)
8
+ // --agents=claude,codex only these agents work on this run
9
+ // --council / --no-council every agent brainstorms the design first (default: big goals)
10
+ // --qa / --no-qa a QA round after the build (default: on for big goals)
11
+ // --with <file|dir|glob> put files on the shared screen first (repeatable); piped stdin too
12
+ // huddle setup install + sign in to the agents you pick (asks first)
13
+ // huddle doctor is this machine ready: node, git, room db, your agents
14
+ // huddle agents your agents: ready / not signed in / not installed, used or skipped
15
+ // huddle agents use|skip <ids> choose who works with you (remembered)
16
+ // huddle --version print the version
17
+ // huddle agents --all every connector (built in + agents.json) and how to get it
18
+ // huddle agents install <id> install one connector's CLI, after asking
19
+ // huddle board print the board + shared memory
20
+ // huddle view [repo] the live view (ctrl-t) from another terminal, read-only
21
+ // huddle story [--html f] replay the latest run: who built on whose idea, who said what to whom, merges
22
+ // huddle lessons what Huddle learned across runs (--forget <id>, --clear)
23
+ // huddle init set up .huddle/ in this repo (automatic on first run)
24
+ // huddle up | down attach mode: start/stop this repo's live room
25
+ // huddle connect | disconnect wire your own claude/codex/cursor sessions into it
26
+ // huddle peers who's live in this repo and which files they hold
27
+ // huddle add <file|dir|glob|-> put it on the live room's shared screen (- = stdin)
28
+ // huddle ctx [show|rm|note|clear] what's on that screen; take things off, pin notes
29
+ // huddle attach <agent> open an agent in its own worktree, connected
30
+ // huddle land [branch] tests -> rebase -> merge an attach session's branch
31
+ // huddle plug (internal) the MCP server each agent talks to
32
+ // huddle guard (internal) Claude's pre-edit hook in attach mode
33
+ // huddle mail --hook (internal) Claude's post-tool hook: teammates' new messages
34
+ import { packageInfo } from "./preflight.js"; // first: refuses old Node before anything else loads
35
+ import { C, useAsciiOutput } from "./ui/term.js";
36
+ import { SqliteUnavailableError } from "./room/sqlite.js";
37
+ // Everything heavier loads per command: `huddle mail --hook` runs after every
38
+ // one of Claude's tool calls and has to start in well under 300ms.
39
+ const app = () => import("./ui/app.js");
40
+ const B = C.bold;
41
+ const F = C.faint;
42
+ // Every line fits 80 columns.
43
+ const USAGE = `${C.violet("huddle")} — your agents, in the same room
44
+
45
+ ${B("huddle")} open Huddle
46
+ ${B('huddle join "<goal>"')} plan the goal and run your agents
47
+ ${B("--plan")} dry run: show who'd do what
48
+ ${B("--agents=claude,codex")} only these agents work on this run
49
+ ${B("--approvals=safe|auto|yolo")} what agents may do unasked ${F("(default auto)")}
50
+ ${B("--council")} ${F("/")} ${B("--no-council")} design council first ${F("(default: big goals)")}
51
+ ${B("--qa")} ${F("/")} ${B("--no-qa")} agents re-check the build ${F("(default: big goals)")}
52
+ ${B("--with <file>")} put a file, folder or glob on the shared screen
53
+ ${F("(repeatable; or pipe: cat log | huddle join …)")}
54
+ ${B("huddle join <address> <CODE>")} join someone's room from an invite
55
+
56
+ ${F("watch and replay")}
57
+ ${B("huddle view")} ${F("[repo]")} watch a run live, read-only, from a 2nd terminal
58
+ ${B("huddle story")} ${F("[--html f]")} replay the last run: ideas, contract, merges
59
+ ${F("(--list, --run n)")}
60
+ ${B("huddle board")} print the board
61
+ ${B("huddle lessons")} ${F("[--forget id]")} what earlier runs taught your agents ${F("(--clear)")}
62
+
63
+ ${F("your agents")}
64
+ ${B("huddle setup")} install + sign in to your agents ${F("(--yes)")}
65
+ ${B("huddle doctor")} check this machine: node, git, your agents
66
+ ${B("huddle agents")} your agents: ready or not, used or skipped
67
+ ${B("huddle agents use|skip <ids>")} choose who works with you ${F("(e.g. skip cursor)")}
68
+ ${B("huddle agents --all")} every connector Huddle knows, installed or not
69
+ ${B("huddle agents install <id>")} install one ${F("(asks first)")}
70
+ ${B("huddle relay")} ${F("[--port 8080]")} run a relay so rooms work across networks
71
+ ${B("huddle --version")} print the version
72
+
73
+ ${F("run claude, codex and cursor yourself — in one repo, safely")}
74
+ ${B("huddle connect")} wire your agents into this repo's live room
75
+ ${B("huddle peers")} who's live, what they're on, their files
76
+ ${B("huddle add <file|->")} put a file, folder, glob or stdin on the screen
77
+ ${F("(cat err.log | huddle add -)")}
78
+ ${B("huddle ctx")} ${F("[show|rm|note|clear]")} what's on the screen every session reads
79
+ ${B("huddle attach <agent>")} open an agent in its own worktree branch
80
+ ${B("huddle land")} ${F("[branch]")} test + merge that session's work
81
+ ${B("huddle up")} ${F("/")} ${B("down")} ${F("/")} ${B("disconnect")} start · stop the live room · unwire
82
+ `;
83
+ async function main() {
84
+ const [cmd, ...rest] = process.argv.slice(2);
85
+ // Legacy Windows consoles get ASCII glyphs. Not for the machine-read channels: the Plug's JSON-RPC and Claude's hooks.
86
+ if (!["plug", "guard", "mail"].includes(cmd ?? ""))
87
+ useAsciiOutput();
88
+ switch (cmd) {
89
+ case undefined:
90
+ if (!process.stdin.isTTY)
91
+ return void console.log(USAGE);
92
+ return (await app()).runApp();
93
+ case "plug":
94
+ return (await import("./mcp/server.js")).runPlug();
95
+ case "init": {
96
+ const { ensureInit, huddleDir } = await app();
97
+ if (!ensureInit()) {
98
+ console.log(C.red("✗ Not a git repo.") + " Huddle sandboxes are git worktrees — run `git init` first.");
99
+ process.exit(1);
100
+ }
101
+ return void console.log(`${C.green("✓")} Huddle initialized in ${C.faint(huddleDir())}`);
102
+ }
103
+ case "setup":
104
+ process.exitCode = await (await import("./ui/setup.js")).runSetup(rest);
105
+ return;
106
+ case "agents": {
107
+ const { chooseAgents, installConnector, printAgents, printCatalog } = await import("./ui/catalog.js");
108
+ if (rest[0] === "install" && rest[1])
109
+ return void (await installConnector(rest[1]));
110
+ if ((rest[0] === "use" || rest[0] === "skip") && rest.length > 1)
111
+ return void (process.exitCode = chooseAgents(rest[0], rest.slice(1)) ? 0 : 1);
112
+ if (rest.includes("--all"))
113
+ return printCatalog();
114
+ if (!rest.length)
115
+ return printAgents();
116
+ process.exitCode = await (await import("./ui/doctor.js")).runDoctor();
117
+ return;
118
+ }
119
+ case "doctor":
120
+ process.exitCode = await (await import("./ui/doctor.js")).runDoctor();
121
+ return;
122
+ case "join": {
123
+ // `huddle join <address> <CODE>` joins someone's room; anything else is a goal.
124
+ const { INVITE_CODE, joinRoom } = await import("./ui/remote.js");
125
+ if (rest.length === 2 && INVITE_CODE.test(rest[1]))
126
+ return joinRoom(rest[0], rest[1]);
127
+ const { connectAgents, dbPath, ensureInit, runGoal } = await app();
128
+ const { addForRun, readPipedStdin, takeWith } = await import("./ui/paste.js");
129
+ const withs = takeWith(rest);
130
+ const goal = withs.rest.filter((a) => !a.startsWith("--")).join(" ");
131
+ if (!goal) {
132
+ console.log(C.red("✗ Give a goal:") + ' huddle join "build the login page"');
133
+ process.exit(1);
134
+ }
135
+ // The shared screen first (--with files, piped stdin), so the planner sees its index.
136
+ const piped = await readPipedStdin();
137
+ if (withs.files.length || piped.trim()) {
138
+ if (!ensureInit()) {
139
+ console.log(C.red("✗ Not a git repo.") + " The shared screen lives in a repo's .huddle/ — run `git init` first.");
140
+ process.exit(1);
141
+ }
142
+ if (await addForRun(dbPath(), withs.files, piped, (l = "") => console.log(l))) {
143
+ console.log(C.red("✗ couldn't put everything on the shared screen (above) — nothing was run."));
144
+ process.exit(1);
145
+ }
146
+ }
147
+ const flag = rest.find((a) => a.startsWith("--approvals="))?.split("=")[1];
148
+ const approvals = (["safe", "auto", "yolo"].includes(flag ?? "") ? flag : "auto");
149
+ let probed = await connectAgents();
150
+ let results = probed;
151
+ // --agents=claude,codex: run with just these (the rest sit this one out) — even ones you skipped.
152
+ const pick = rest.find((a) => a.startsWith("--agents="))?.split("=")[1];
153
+ if (pick !== undefined) {
154
+ const ids = pick.split(",").map((s) => s.trim()).filter(Boolean);
155
+ results = probed = (await import("./adapters/index.js")).unskip(probed, ids);
156
+ const unknown = ids.filter((id) => !results.some((r) => r.adapter.id === id));
157
+ if (!ids.length || unknown.length) {
158
+ console.log(C.red(`✗ --agents: no agent called ${unknown.join(", ") || "(empty)"}.`) + ` Known: ${results.map((r) => r.adapter.id).join(", ")}`);
159
+ process.exit(1);
160
+ }
161
+ const notReady = results.filter((r) => ids.includes(r.adapter.id) && r.state !== "ready").map((r) => r.adapter.id);
162
+ if (notReady.length)
163
+ console.log(C.yellow(`! not ready, skipped: ${notReady.join(", ")}`));
164
+ results = results.filter((r) => ids.includes(r.adapter.id));
165
+ }
166
+ // Non-interactive: nobody to ask, so it's plan (--plan) or auto.
167
+ // --council: every agent brainstorms the design first; --no-council: the brain plans alone.
168
+ const council = rest.includes("--no-council") ? false : rest.includes("--council") ? true : undefined;
169
+ const qa = rest.includes("--no-qa") ? false : rest.includes("--qa") ? true : undefined;
170
+ return runGoal(goal, { results, probed, mode: rest.includes("--plan") ? "plan" : "auto", approvals, council, qa });
171
+ }
172
+ case "board":
173
+ return (await app()).printBoard();
174
+ case "view":
175
+ case "watch":
176
+ return (await import("./ui/watch.js")).runView(rest);
177
+ case "story":
178
+ return (await import("./ui/story.js")).runStory(rest);
179
+ case "lessons":
180
+ return (await import("./ui/lessons.js")).runLessons(rest);
181
+ case "up":
182
+ return (await import("./attach/session.js")).runUp(rest);
183
+ case "down":
184
+ return (await import("./attach/session.js")).runDown();
185
+ case "peers":
186
+ return (await import("./attach/session.js")).runPeers();
187
+ case "add":
188
+ return (await import("./attach/screen.js")).runAdd(rest);
189
+ case "ctx":
190
+ case "context":
191
+ return (await import("./attach/screen.js")).runCtx(rest);
192
+ case "attach":
193
+ return (await import("./attach/session.js")).runAttach(rest);
194
+ case "land":
195
+ return (await import("./attach/session.js")).runLand(rest);
196
+ case "connect":
197
+ return (await import("./attach/connect.js")).runConnect(rest);
198
+ case "disconnect":
199
+ return (await import("./attach/connect.js")).runDisconnect();
200
+ case "guard":
201
+ return (await import("./attach/guard.js")).runGuard();
202
+ case "mail":
203
+ if (rest.includes("--hook"))
204
+ return (await import("./room/mail.js")).runMailHook(rest);
205
+ console.log(C.red("✗ huddle mail is internal:") + " agents read mail through the Plug (huddle_inbox).");
206
+ process.exit(1);
207
+ case "relay": {
208
+ // Run a relay so rooms work across networks. Host it anywhere Node runs.
209
+ const i = rest.indexOf("--port");
210
+ const port = Number(i >= 0 ? rest[i + 1] : process.env.PORT ?? 8080);
211
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
212
+ console.log(C.red("✗ --port needs a number from 0 to 65535,") + " e.g. huddle relay --port 8080");
213
+ process.exit(1);
214
+ }
215
+ const { startRelay } = await import("./relay/server.js");
216
+ const r = await startRelay(port, (l) => console.log(C.faint(new Date().toISOString().slice(11, 19) + " " + l)));
217
+ console.log(`${C.violet("huddle relay")} listening on :${r.port} ${C.faint("· people use it with: relay ws://<this-host>:" + r.port)}`);
218
+ return;
219
+ }
220
+ case "--version":
221
+ case "-v":
222
+ case "-V":
223
+ case "version":
224
+ return void console.log(packageInfo().version);
225
+ case "help":
226
+ case "--help":
227
+ case "-h":
228
+ return void console.log(USAGE);
229
+ default:
230
+ console.log(C.red(`✗ unknown command: ${cmd}\n`) + USAGE);
231
+ process.exit(1);
232
+ }
233
+ }
234
+ main().catch((e) => {
235
+ // A message for the user, not a stack trace. HUDDLE_DEBUG=1 shows the stack.
236
+ if (e instanceof SqliteUnavailableError)
237
+ console.error(C.red("✗ ") + e.message);
238
+ else
239
+ console.error(C.red("huddle error:"), e?.message ?? e);
240
+ if (process.env.HUDDLE_DEBUG && e?.stack)
241
+ console.error(C.faint(e.stack));
242
+ process.exit(1);
243
+ });
244
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,79 @@
1
+ // The Plug's shared-screen verbs (src/room/context.ts is the Room side).
2
+ // Kept out of server.ts so the screen is one file to read.
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ export function contextTools(room) {
7
+ return [
8
+ {
9
+ name: "huddle_context",
10
+ description: "The SHARED SCREEN: everything the user pasted, dropped or uploaded for the whole team (logs, docs, code, screenshots, PDFs) — one copy in the Room, not in your prompt. Lists each item's id, kind, title, size, summary, sections and the notes pinned on it.",
11
+ inputSchema: { type: "object", properties: {} },
12
+ handler: () => room("/context", "GET"),
13
+ },
14
+ {
15
+ name: "huddle_context_read",
16
+ description: "Read one item from the shared screen. Text comes back in chunks of up to 400 lines: pass from_line/to_line, or section (a number or title from huddle_context). Images and PDFs come back as a file path — open it with your image/file reading tool.",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: {
20
+ id: { type: "string", description: "the item id, e.g. ctx-3" },
21
+ from_line: { type: "number" },
22
+ to_line: { type: "number" },
23
+ section: { type: "string", description: "optional: a section number or title" },
24
+ },
25
+ required: ["id"],
26
+ },
27
+ handler: (a) => room("/context_read", "POST", { id: a.id, from_line: a.from_line, to_line: a.to_line, section: a.section }),
28
+ },
29
+ {
30
+ name: "huddle_context_search",
31
+ description: "Search every text item on the shared screen. Returns matching lines with the item id and line number, so you can huddle_context_read around them.",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: { query: { type: "string" }, regex: { type: "boolean", description: "treat query as a regular expression" } },
35
+ required: ["query"],
36
+ },
37
+ handler: (a) => room("/context_search", "POST", { query: a.query, regex: !!a.regex }),
38
+ },
39
+ {
40
+ name: "huddle_context_note",
41
+ description: "Pin a short comment on a shared-screen item — what it means, what you found in it, which part you're handling. Every agent and the user sees it next to the item. This is how the team talks about the same document.",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: { id: { type: "string" }, note: { type: "string" } },
45
+ required: ["id", "note"],
46
+ },
47
+ handler: (a) => room("/context_note", "POST", { id: a.id, note: a.note }),
48
+ },
49
+ ];
50
+ }
51
+ /**
52
+ * A Room result as MCP content. Screenshots also go as an image block (hosts
53
+ * that render MCP images show it to the model directly). Bytes sent across
54
+ * machines (a teammate's agent) are saved here and replaced by a local path.
55
+ */
56
+ export function toContent(result) {
57
+ if (!result || typeof result !== "object" || Array.isArray(result))
58
+ return [{ type: "text", text: JSON.stringify(result, null, 2) }];
59
+ const { image, file, ...rest } = result;
60
+ if (file?.data_base64 && file?.name) {
61
+ try {
62
+ const dir = path.join(os.tmpdir(), "huddle-context");
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ const p = path.join(dir, String(file.name).replace(/[^\w.-]/g, "_"));
65
+ fs.writeFileSync(p, Buffer.from(String(file.data_base64), "base64"));
66
+ rest.path = p;
67
+ rest.note = `Open this file with your image/file reading tool (e.g. Read ${p}).`;
68
+ }
69
+ catch (e) {
70
+ rest.note = `couldn't save the file here: ${e?.message ?? e}`;
71
+ }
72
+ }
73
+ const out = [{ type: "text", text: JSON.stringify(rest, null, 2) }];
74
+ // HUDDLE_CTX_MCP_IMAGES=0: path only, for a host that can't take image blocks.
75
+ if (image?.data_base64 && image?.mime && process.env.HUDDLE_CTX_MCP_IMAGES !== "0")
76
+ out.push({ type: "image", data: String(image.data_base64), mimeType: String(image.mime) });
77
+ return out;
78
+ }
79
+ //# sourceMappingURL=context-tools.js.map