huddle-cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +357 -2
  3. package/dist/adapters/amp.js +40 -0
  4. package/dist/adapters/antigravity.js +52 -0
  5. package/dist/adapters/claude.js +70 -0
  6. package/dist/adapters/codex.js +59 -0
  7. package/dist/adapters/copilot.js +69 -0
  8. package/dist/adapters/cursor.js +85 -0
  9. package/dist/adapters/custom.js +340 -0
  10. package/dist/adapters/droid.js +57 -0
  11. package/dist/adapters/gemini.js +61 -0
  12. package/dist/adapters/goose.js +55 -0
  13. package/dist/adapters/index.js +135 -0
  14. package/dist/adapters/kiro.js +41 -0
  15. package/dist/adapters/opencode.js +63 -0
  16. package/dist/adapters/qwen.js +67 -0
  17. package/dist/adapters/types.js +6 -0
  18. package/dist/adapters/usage.js +141 -0
  19. package/dist/attach/connect.js +409 -0
  20. package/dist/attach/guard.js +64 -0
  21. package/dist/attach/live.js +222 -0
  22. package/dist/attach/plug.js +174 -0
  23. package/dist/attach/session.js +172 -0
  24. package/dist/cli.js +176 -0
  25. package/dist/mcp/server.js +240 -0
  26. package/dist/orchestrator/ask.js +51 -0
  27. package/dist/orchestrator/brain.js +164 -0
  28. package/dist/orchestrator/council.js +274 -0
  29. package/dist/orchestrator/handoff.js +117 -0
  30. package/dist/orchestrator/launcher.js +155 -0
  31. package/dist/orchestrator/planner.js +194 -0
  32. package/dist/orchestrator/qa.js +350 -0
  33. package/dist/orchestrator/scheduler.js +748 -0
  34. package/dist/orchestrator/takeover.js +85 -0
  35. package/dist/preflight.js +31 -0
  36. package/dist/relay/server.js +113 -0
  37. package/dist/room/db.js +617 -0
  38. package/dist/room/identity.js +122 -0
  39. package/dist/room/live.js +293 -0
  40. package/dist/room/progress.js +110 -0
  41. package/dist/room/relaylink.js +85 -0
  42. package/dist/room/server.js +752 -0
  43. package/dist/room/sqlite.js +156 -0
  44. package/dist/room/transport.js +107 -0
  45. package/dist/room/tunnel.js +85 -0
  46. package/dist/sandbox/sync.js +104 -0
  47. package/dist/sandbox/worktree.js +261 -0
  48. package/dist/types.js +4 -0
  49. package/dist/ui/app.js +1534 -0
  50. package/dist/ui/catalog.js +75 -0
  51. package/dist/ui/dashboard.js +192 -0
  52. package/dist/ui/doctor.js +54 -0
  53. package/dist/ui/logo.js +40 -0
  54. package/dist/ui/remote.js +332 -0
  55. package/dist/ui/story.js +932 -0
  56. package/dist/ui/term.js +114 -0
  57. package/dist/ui/watch.js +174 -0
  58. package/install.sh +44 -0
  59. package/package.json +60 -4
@@ -0,0 +1,75 @@
1
+ // `huddle agents --all`: every connector Huddle knows — built in, or declared
2
+ // in agents.json — with whether it's on this machine and how to get it.
3
+ // `huddle agents install <id>` installs one, always after asking.
4
+ import readline from "node:readline/promises";
5
+ import { spawnSync } from "node:child_process";
6
+ import { ADAPTERS, AGENTS_FILE_ERRORS, BUILTIN_ADAPTERS, probe } from "../adapters/index.js";
7
+ import { agentsFiles } from "../adapters/custom.js";
8
+ import { C, dot, pad } from "./term.js";
9
+ function all() {
10
+ const seen = new Map();
11
+ for (const a of [...BUILTIN_ADAPTERS, ...ADAPTERS])
12
+ seen.set(a.id, a); // the roster's version wins (agents.json overrides)
13
+ return [...seen.values()];
14
+ }
15
+ const tilde = (p) => p.replace(process.env.HOME ?? "\0", "~");
16
+ export function printCatalog() {
17
+ console.log(`\n${C.bold("Connectors")} ${C.faint("· any CLI that runs headless and speaks MCP can join")}\n`);
18
+ for (const a of all()) {
19
+ const r = probe(a);
20
+ const state = r.state === "ready" ? C.green("ready") : r.state === "not_authed" ? C.yellow("not signed in") : C.faint("not installed");
21
+ const from = a.source ? C.faint(` · ${tilde(a.source)}`) : "";
22
+ console.log(` ${dot(a.id)} ${pad(C.bold(a.id), 13)} ${pad(C.faint(a.vendor), 13)} ${pad(state, 14)}${from}`);
23
+ if (r.state === "not_installed" && a.install.cmd)
24
+ console.log(` ${C.faint("$ " + a.install.cmd)}`);
25
+ else if (r.state === "not_authed")
26
+ console.log(` ${C.faint(r.detail)}`);
27
+ }
28
+ for (const e of AGENTS_FILE_ERRORS)
29
+ console.log(` ${C.yellow("!")} ${C.faint(e)}`);
30
+ const f = agentsFiles(null);
31
+ console.log(`\n ${C.faint("install one:")} huddle agents install <id>`);
32
+ console.log(` ${C.faint("add your own:")} ${tilde(f.user)} ${C.faint("(see docs/ADAPTERS.md)")}\n`);
33
+ }
34
+ /** Install a connector's CLI (and offer its sign-in) — only after a yes. */
35
+ export async function installConnector(id) {
36
+ const a = all().find((x) => x.id === id);
37
+ if (!a) {
38
+ console.log(C.red(`✗ no connector called ${id}.`) + " See `huddle agents --all`.");
39
+ return false;
40
+ }
41
+ if (!a.install.cmd) {
42
+ console.log(`${a.id} has no install command — install ${a.bin} the way its docs say${a.homepage ? `: ${a.homepage}` : "."}`);
43
+ return false;
44
+ }
45
+ if (!process.stdin.isTTY) {
46
+ console.log(`To install ${a.id}, run:\n ${a.install.cmd}`);
47
+ return false;
48
+ }
49
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
50
+ try {
51
+ console.log(`\n ${dot(a.id)} ${C.bold(a.id)} ${C.faint("$ " + a.install.cmd)}`);
52
+ const ans = (await rl.question(`Install ${a.id}? ${C.faint("[Y/n]")} `)).trim().toLowerCase();
53
+ if (ans && !ans.startsWith("y"))
54
+ return false;
55
+ rl.pause();
56
+ const ok = spawnSync("sh", ["-c", a.install.cmd], { stdio: "inherit" }).status === 0;
57
+ rl.resume();
58
+ console.log(ok ? C.green(`✓ ${a.id} installed`) : C.red(`✗ ${a.id} install failed — see output above`));
59
+ if (ok && a.install.login) {
60
+ const go = (await rl.question(`Sign in to ${a.id} now? ${C.faint("[Y/n]")} `)).trim().toLowerCase();
61
+ if (!go || go.startsWith("y")) {
62
+ if (a.install.loginNote)
63
+ console.log(C.faint(` ${a.install.loginNote}`));
64
+ rl.pause();
65
+ spawnSync(a.install.login[0], a.install.login.slice(1), { stdio: "inherit" });
66
+ rl.resume();
67
+ }
68
+ }
69
+ return ok;
70
+ }
71
+ finally {
72
+ rl.close();
73
+ }
74
+ }
75
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,192 @@
1
+ // The live view — the full-screen dashboard from the launch film.
2
+ //
3
+ // ⬢ huddle 1 agents 2 plan 3 board room: <goal> ● live ⏱ 1m12s
4
+ // ┌ ● claude anthropic ─ working 40s · 31k ┐┌ ● codex openai ─ idle ───────┐
5
+ // │ ▸ Add greet(name) with tests ││ ✓ merged Add farewell… │
6
+ // │ plans: CommonJS greet.js … ││ │
7
+ // └─────────────────────────────────────────┘└────────────────────────────────┘
8
+ // ◆ harsh/codex → room what shape is the Order type?
9
+ // 1 2 3 switch · q back to the prompt · ctrl-c stop agents
10
+ //
11
+ // It only reads: the Room's board and memory, the run's live stats, and the
12
+ // activity feed. Pressing q hands the terminal back to the prompt.
13
+ import { C, dot, pad, truncate, width, AGENT_COLOR } from "./term.js";
14
+ import { fmtDuration, fmtTokens } from "../adapters/usage.js";
15
+ const hexColor = (id) => C.hex(AGENT_COLOR[id.split("/").pop().split("#")[0]] ?? "#8A857C");
16
+ export class Dashboard {
17
+ src;
18
+ tab = "agents";
19
+ open = false;
20
+ timer;
21
+ feed = [];
22
+ onKey;
23
+ constructor(src) {
24
+ this.src = src;
25
+ }
26
+ isOpen() {
27
+ return this.open;
28
+ }
29
+ /** Keep a rolling log whether or not the view is on screen. */
30
+ push(a) {
31
+ this.feed.push({ ...a, ts: Date.now() });
32
+ if (this.feed.length > 400)
33
+ this.feed.splice(0, this.feed.length - 400);
34
+ if (this.open)
35
+ this.render();
36
+ }
37
+ show(tab, onClose) {
38
+ if (!process.stdout.isTTY)
39
+ return;
40
+ if (tab)
41
+ this.tab = tab;
42
+ if (this.open)
43
+ return this.render();
44
+ this.open = true;
45
+ process.stdout.write("\x1b[?1049h\x1b[?25l"); // alternate screen, hide cursor
46
+ this.onKey = (s, key) => {
47
+ if (!this.open)
48
+ return;
49
+ const k = key?.name ?? s;
50
+ if (s === "1")
51
+ this.tab = "agents";
52
+ else if (s === "2")
53
+ this.tab = "plan";
54
+ else if (s === "3")
55
+ this.tab = "board";
56
+ else if (k === "tab")
57
+ this.tab = this.tab === "agents" ? "plan" : this.tab === "plan" ? "board" : "agents";
58
+ else if (s === "q" || k === "escape" || (key?.ctrl && key.name === "t")) {
59
+ this.hide();
60
+ onClose?.();
61
+ return;
62
+ }
63
+ this.render();
64
+ };
65
+ process.stdin.on("keypress", this.onKey);
66
+ process.stdout.on("resize", this.renderBound);
67
+ this.timer = setInterval(() => this.render(), 500);
68
+ this.render();
69
+ }
70
+ hide() {
71
+ if (!this.open)
72
+ return;
73
+ this.open = false;
74
+ if (this.timer)
75
+ clearInterval(this.timer);
76
+ if (this.onKey)
77
+ process.stdin.off("keypress", this.onKey);
78
+ process.stdout.off("resize", this.renderBound);
79
+ process.stdout.write("\x1b[?25h\x1b[?1049l"); // back to the normal screen
80
+ }
81
+ renderBound = () => this.render();
82
+ render() {
83
+ if (!this.open)
84
+ return;
85
+ const W = process.stdout.columns || 100;
86
+ const H = process.stdout.rows || 30;
87
+ const db = this.src.db();
88
+ const stats = this.src.stats();
89
+ const goal = db?.currentSession()?.goal ?? "no goal yet — press q and type one";
90
+ const tabs = ["agents", "plan", "board"]
91
+ .map((t, i) => (t === this.tab ? C.bold(C.violet(` ${i + 1} ${t} `)) : C.faint(` ${i + 1} ${t} `)))
92
+ .join(" ");
93
+ const right = `${C.faint("room:")} ${truncate(goal, Math.max(10, W - 70))} ` +
94
+ (stats ? `${C.green("● live")} ${C.faint("⏱")} ${fmtDuration(Date.now() - stats.startedAt)}` : C.faint("○ idle"));
95
+ const bar = ` ${C.violet("⬢")} ${C.bold("huddle")} ${tabs}`;
96
+ const top = bar + " ".repeat(Math.max(1, W - width(bar) - width(right) - 1)) + right;
97
+ const bodyH = H - 4;
98
+ const body = this.tab === "agents" ? this.agents(W, bodyH, stats) : this.tab === "plan" ? this.plan(W, bodyH, db) : this.board(W, bodyH, db);
99
+ const lastRoom = [...this.feed].reverse().find((a) => !a.agent);
100
+ const strip = lastRoom ? ` ${truncate(lastRoom.text, W - 2)}` : C.faint(" the room is quiet");
101
+ const hint = C.faint(` ${this.src.hint ?? "1 2 3 switch · q back to the prompt · ctrl-c stop agents"} ${this.src.modeChip()}`);
102
+ const lines = [truncate(top, W), C.faint("─".repeat(W)), ...body.slice(0, bodyH), strip, hint];
103
+ while (lines.length < H)
104
+ lines.splice(lines.length - 2, 0, "");
105
+ process.stdout.write("\x1b[H" + lines.slice(0, H).map((l) => "\x1b[2K" + truncate(l, W)).join("\n"));
106
+ }
107
+ /** Tab 1: a pane per agent — what each one is doing right now. */
108
+ agents(W, H, stats) {
109
+ const ids = stats ? [...stats.agents.keys()] : [...new Set(this.feed.filter((a) => a.agent).map((a) => a.agent))];
110
+ if (!ids.length)
111
+ return [C.faint(" no agents working — give Huddle a goal")];
112
+ const cols = W >= 110 && ids.length > 1 ? 2 : 1;
113
+ const rows = Math.ceil(ids.length / cols);
114
+ const paneW = Math.floor(W / cols);
115
+ const paneH = Math.max(4, Math.floor(H / rows));
116
+ const out = [];
117
+ for (let r = 0; r < rows; r++) {
118
+ const panes = ids.slice(r * cols, r * cols + cols).map((id) => this.pane(id, paneW, paneH, stats?.agents.get(id)));
119
+ for (let i = 0; i < paneH; i++)
120
+ out.push(panes.map((p) => p[i] ?? " ".repeat(paneW)).join(""));
121
+ }
122
+ return out;
123
+ }
124
+ pane(id, w, h, a) {
125
+ const color = hexColor(id);
126
+ const status = a?.busySince
127
+ ? `${C.violet("working")} ${fmtDuration(Date.now() - a.busySince)}${a.liveTokens > 0 ? " · " + fmtTokens(a.liveTokens) + " tok" : ""}`
128
+ : a && (a.tasks || a.reviews)
129
+ ? C.green(`done · ${a.tasks} task${a.tasks === 1 ? "" : "s"}${a.reviews ? ` + ${a.reviews} review` : ""}`)
130
+ : C.faint("idle");
131
+ const title = ` ${dot(id)} ${C.bold(id)} ${status} `;
132
+ const inner = w - 2;
133
+ const topLine = color("╭─") + title + color("─".repeat(Math.max(0, inner - 1 - width(title))) + "╮");
134
+ const lines = this.feed.filter((x) => x.agent === id).slice(-(h - 2));
135
+ const body = Array.from({ length: h - 2 }, (_, i) => {
136
+ const l = lines[i] ? " " + truncate(lines[i].text, inner - 2) : "";
137
+ return color("│") + pad(l, inner) + color("│");
138
+ });
139
+ return [topLine, ...body, color("╰" + "─".repeat(inner) + "╯")];
140
+ }
141
+ /** Tab 2: the plan on the left, the room talking on the right. */
142
+ plan(W, H, db) {
143
+ const leftW = Math.floor(W * 0.55);
144
+ const left = [C.violet(" PLAN")];
145
+ const tasks = db?.listTasks() ?? [];
146
+ const idx = new Map(tasks.map((t, i) => [t.id, i + 1]));
147
+ tasks.forEach((t, i) => {
148
+ const deps = JSON.parse(t.depends_on || "[]");
149
+ const after = deps.length ? C.faint(` after ${deps.map((d) => idx.get(d)).join(",")}`) : "";
150
+ const who = t.claimed_by ? ` ${dot(t.claimed_by)} ${C.faint(t.claimed_by)}` : "";
151
+ left.push(` ${C.faint(String(i + 1) + ".")} ${t.title}${after}${who}`);
152
+ left.push(C.faint(` ${t.status}`));
153
+ });
154
+ if (!tasks.length)
155
+ left.push(C.faint(" no plan yet"));
156
+ const right = [C.violet(" ROOM") + C.faint(" · agents & people talking")];
157
+ for (const a of this.feed.filter((x) => !x.agent).slice(-(H - 1)))
158
+ right.push(" " + a.text);
159
+ return columns(left, right, leftW, W, H);
160
+ }
161
+ /** Tab 3: progress on the left, shared memory on the right. */
162
+ board(W, H, db) {
163
+ const leftW = Math.floor(W * 0.55);
164
+ const left = [C.violet(" BOARD") + C.faint(" · live progress")];
165
+ for (const t of db?.listTasks() ?? []) {
166
+ const bw = 14;
167
+ const filled = Math.round((t.progress_pct / 100) * bw);
168
+ const barS = t.status === "merged" ? C.green("█".repeat(bw)) : C.violet("█".repeat(filled)) + C.faint("░".repeat(bw - filled));
169
+ const st = t.status === "merged" ? C.green("✓") : t.status === "failed" ? C.red("✗") : t.status === "needs_you" ? C.yellow("!") : t.claimed_by ? C.violet("▸") : C.faint("○");
170
+ left.push(` ${st} ${barS} ${String(t.progress_pct).padStart(3)}% ${t.title}`);
171
+ if (t.claimed_by)
172
+ left.push(C.faint(` ${t.claimed_by}`));
173
+ }
174
+ const right = [C.violet(" SHARED MEMORY") + C.faint(" · everyone reads & writes")];
175
+ for (const m of (db?.recall() ?? []).slice(0, H - 1)) {
176
+ right.push(` ${C.faint("•")} ${m.body}`);
177
+ if (m.why)
178
+ right.push(C.faint(` why: ${m.why}`));
179
+ }
180
+ return columns(left, right, leftW, W, H);
181
+ }
182
+ }
183
+ function columns(left, right, leftW, W, H) {
184
+ const out = [];
185
+ for (let i = 0; i < H; i++) {
186
+ const l = truncate(left[i] ?? "", leftW - 1);
187
+ const r = truncate(right[i] ?? "", W - leftW - 2);
188
+ out.push(pad(l, leftW - 1) + C.faint("│") + " " + r);
189
+ }
190
+ return out;
191
+ }
192
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1,54 @@
1
+ // `huddle doctor`: is this machine ready? Huddle itself (Node, git, the room
2
+ // database), then your agents — and, when something's missing, the exact next
3
+ // command. Read-only: it never installs or signs in; `huddle` offers that (and
4
+ // asks first). Exit code 1 when Huddle can't run a goal here yet.
5
+ import { spawnSync } from "node:child_process";
6
+ import { LEGACY } from "../adapters/index.js";
7
+ import { packageInfo } from "../preflight.js";
8
+ import { sqliteStatus } from "../room/sqlite.js";
9
+ import { connectAgents } from "./app.js";
10
+ import { C, dot } from "./term.js";
11
+ export async function runDoctor() {
12
+ let blocked = false;
13
+ const gitV = spawnSync("git", ["--version"], { encoding: "utf8" });
14
+ const gitNum = gitV.status === 0 ? (gitV.stdout.match(/\d+\.\d+(\.\d+)?/)?.[0] ?? "") : null;
15
+ const db = sqliteStatus();
16
+ const parts = [
17
+ `${C.violet("huddle")} ${packageInfo().version}`,
18
+ `node ${process.versions.node}`,
19
+ gitNum === null ? C.red("git missing") : `git ${gitNum}`.trim(),
20
+ db.ok ? `room db: ${db.driver}` : C.red("room db: unavailable"),
21
+ ];
22
+ console.log("\n " + parts.join(C.faint(" · ")));
23
+ if (gitNum === null) {
24
+ blocked = true;
25
+ console.log(C.red("\n✗ git isn't installed.") + " Huddle gives every agent its own git worktree.");
26
+ console.log(C.faint(" macOS: xcode-select --install · Debian/Ubuntu: sudo apt install git · https://git-scm.com"));
27
+ }
28
+ if (!db.ok) {
29
+ blocked = true;
30
+ console.log("\n" + C.red("✗ ") + db.message);
31
+ }
32
+ const results = await connectAgents();
33
+ if (!nextSteps(results))
34
+ blocked = true;
35
+ return blocked ? 1 : 0;
36
+ }
37
+ /** What to do when agents are missing or signed out. Returns true if at least one is ready. */
38
+ function nextSteps(results) {
39
+ const ready = results.filter((r) => r.state === "ready");
40
+ if (ready.length) {
41
+ console.log(C.faint(`\n ${ready.length} agent(s) ready — cd into a repo and run: huddle`));
42
+ return true;
43
+ }
44
+ const missing = results.filter((r) => r.state === "not_installed" && !LEGACY.has(r.adapter.id));
45
+ console.log(`\n${C.yellow("No agents ready yet.")} Huddle runs the coding agents you already use, each signed in to its own account.`);
46
+ if (missing.length) {
47
+ console.log(C.faint(" Install one or more (or run `huddle` — it offers to, and asks first):"));
48
+ for (const r of missing)
49
+ console.log(` ${dot(r.adapter.id)} ${C.bold(r.adapter.id.padEnd(12))} ${C.faint("$")} ${r.adapter.install.cmd}`);
50
+ }
51
+ console.log(C.faint(" Then sign in with the agent itself — Huddle never sees your credentials — and run `huddle doctor` again."));
52
+ return false;
53
+ }
54
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,40 @@
1
+ // The Huddle mark (teaser/assets/huddle-logo.svg: four heavy rings in a plus)
2
+ // rasterized to half-block characters (▀ ▄ █). Each cell is one pixel wide and
3
+ // two tall, so pixels come out square and the rings render solid in any font.
4
+ const RINGS = [
5
+ [0, -43.5],
6
+ [43.5, 0],
7
+ [0, 43.5],
8
+ [-43.5, 0],
9
+ ];
10
+ const R = 56;
11
+ const HALF_STROKE = 11;
12
+ const VIEW = 224; // svg viewBox is -112..112
13
+ const inRing = (x, y) => RINGS.some(([cx, cy]) => Math.abs(Math.hypot(x - cx, y - cy) - R) <= HALF_STROKE);
14
+ /** Fraction of pixel (px, py) on a W×H grid covered by the rings. */
15
+ function coverage(px, py, W, H, ss = 6) {
16
+ let hits = 0;
17
+ for (let i = 0; i < ss; i++)
18
+ for (let j = 0; j < ss; j++) {
19
+ const x = -VIEW / 2 + ((px + (i + 0.5) / ss) / W) * VIEW;
20
+ const y = -VIEW / 2 + ((py + (j + 0.5) / ss) / H) * VIEW;
21
+ if (inRing(x, y))
22
+ hits++;
23
+ }
24
+ return hits / (ss * ss);
25
+ }
26
+ /** Render the logo as `rows` lines of `cols` cells. 16×8 is the tuned size. */
27
+ export function logoLines(cols = 16, rows = 8, threshold = 0.42) {
28
+ const out = [];
29
+ for (let r = 0; r < rows; r++) {
30
+ let line = "";
31
+ for (let c = 0; c < cols; c++) {
32
+ const top = coverage(c, r * 2, cols, rows * 2) >= threshold;
33
+ const bot = coverage(c, r * 2 + 1, cols, rows * 2) >= threshold;
34
+ line += top && bot ? "█" : top ? "▀" : bot ? "▄" : " ";
35
+ }
36
+ out.push(line);
37
+ }
38
+ return out;
39
+ }
40
+ //# sourceMappingURL=logo.js.map