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,113 @@
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 os from "node:os";
5
+ import readline from "node:readline/promises";
6
+ import { ADAPTERS, AGENTS_FILE_ERRORS, BUILTIN_ADAPTERS, LEGACY, installCommand, noInstallHint, probe } from "../adapters/index.js";
7
+ import { agentsFiles } from "../adapters/custom.js";
8
+ import { runInteractive, setAgentChoice, skippedAgents } from "./app.js";
9
+ import { C, dot, lineWidth, pad, wrap } from "./term.js";
10
+ function all() {
11
+ const seen = new Map();
12
+ for (const a of [...BUILTIN_ADAPTERS, ...ADAPTERS])
13
+ seen.set(a.id, a); // the roster's version wins (agents.json overrides)
14
+ return [...seen.values()];
15
+ }
16
+ const tilde = (p) => p.replace(os.homedir(), "~");
17
+ const stateText = (r) => r.state === "ready" ? C.green("ready") : r.state === "not_authed" ? C.yellow("not signed in") : C.faint("not installed");
18
+ /** One agent's row: state, whether you use it, and the next command when there is one. */
19
+ function printRow(a, r, skipped) {
20
+ const from = a.source ? C.faint(` · ${tilde(a.source)}`) : "";
21
+ const choice = skipped ? C.faint("skipped") : C.violet("used");
22
+ console.log(` ${dot(a.id)} ${pad(C.bold(a.id), 13)} ${pad(C.faint(a.vendor), 13)} ${pad(stateText(r), 14)} ${from ? pad(choice, 8) + from : choice}`);
23
+ if (skipped)
24
+ return;
25
+ const cmd = installCommand(a);
26
+ // Hints wrap under the row (80 columns when piped), never past the edge.
27
+ const hint = (t) => wrap(t, (lineWidth() ?? 80) - 7, " ").forEach((l) => console.log(` ${C.faint(l)}`));
28
+ if (r.state === "not_installed")
29
+ hint(cmd ? "$ " + cmd : noInstallHint(a));
30
+ else if (r.state === "not_authed")
31
+ hint(r.detail);
32
+ }
33
+ export function printCatalog() {
34
+ console.log(`\n${C.bold("Connectors")} ${C.faint("· any CLI that runs headless and speaks MCP can join")}\n`);
35
+ const skip = skippedAgents();
36
+ for (const a of all())
37
+ printRow(a, probe(a), skip.has(a.id));
38
+ for (const e of AGENTS_FILE_ERRORS)
39
+ console.log(` ${C.yellow("!")} ${C.faint(e)}`);
40
+ const f = agentsFiles(null);
41
+ console.log(`\n ${C.faint("install one:")} huddle agents install <id>`);
42
+ console.log(` ${C.faint("choose:")} huddle agents use <id> ${C.faint("·")} huddle agents skip <id>`);
43
+ console.log(` ${C.faint("add your own:")} ${tilde(f.user)} ${C.faint("(see docs/ADAPTERS.md)")}\n`);
44
+ }
45
+ /** Install a connector's CLI (and offer its sign-in) — only after a yes. */
46
+ export async function installConnector(id) {
47
+ const a = all().find((x) => x.id === id);
48
+ if (!a) {
49
+ console.log(C.red(`✗ no connector called ${id}.`) + " See `huddle agents --all`.");
50
+ return false;
51
+ }
52
+ const cmd = installCommand(a);
53
+ if (!cmd) {
54
+ console.log(`${a.id}: ${noInstallHint(a)}`);
55
+ return false;
56
+ }
57
+ if (!process.stdin.isTTY) {
58
+ console.log(`To install ${a.id}, run:\n ${cmd}`);
59
+ return false;
60
+ }
61
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
62
+ try {
63
+ console.log(`\n ${dot(a.id)} ${C.bold(a.id)} ${C.faint("$ " + cmd)}`);
64
+ const ans = (await rl.question(`Install ${a.id}? ${C.faint("[Y/n]")} `)).trim().toLowerCase();
65
+ if (ans && !ans.startsWith("y"))
66
+ return false;
67
+ const ok = runInteractive(rl, cmd);
68
+ console.log(ok ? C.green(`✓ ${a.id} installed`) : C.red(`✗ ${a.id} install failed — see output above`));
69
+ if (ok && a.install.login) {
70
+ const go = (await rl.question(`Sign in to ${a.id} now? ${C.faint("[Y/n]")} `)).trim().toLowerCase();
71
+ if (!go || go.startsWith("y")) {
72
+ if (a.install.loginNote)
73
+ console.log(C.faint(` ${a.install.loginNote}`));
74
+ runInteractive(rl, a.install.login);
75
+ }
76
+ }
77
+ return ok;
78
+ }
79
+ finally {
80
+ rl.close();
81
+ }
82
+ }
83
+ /** `huddle agents`: the agents Huddle would use here, their state, and your use/skip choice. */
84
+ export function printAgents() {
85
+ const skip = skippedAgents();
86
+ console.log(`\n${C.bold("Your agents")}\n`);
87
+ for (const a of ADAPTERS) {
88
+ const r = probe(a);
89
+ // Opt-in connectors stay out of the list until their CLI is on this machine.
90
+ if (LEGACY.has(a.id) && r.state === "not_installed")
91
+ continue;
92
+ printRow(a, r, skip.has(a.id));
93
+ }
94
+ console.log(`\n ${C.faint("choose:")} huddle agents use claude,codex ${C.faint("·")} huddle agents skip cursor`);
95
+ console.log(` ${C.faint("set up the rest:")} huddle setup`);
96
+ console.log(` ${C.faint("every connector:")} huddle agents --all\n`);
97
+ }
98
+ /** `huddle agents use|skip <ids>`: remembered in ~/.huddle/prefs.json. */
99
+ export function chooseAgents(verb, list) {
100
+ const ids = list.flatMap((x) => x.split(",")).map((s) => s.trim()).filter(Boolean);
101
+ const known = new Set(all().map((a) => a.id));
102
+ const unknown = ids.filter((id) => !known.has(id));
103
+ if (!ids.length || unknown.length) {
104
+ console.log(C.red(`✗ no agent called ${unknown.join(", ") || "(none given)"}.`) + ` Known: ${[...known].join(", ")}`);
105
+ return false;
106
+ }
107
+ setAgentChoice(ids, verb === "use");
108
+ console.log(verb === "use"
109
+ ? `${C.green("✓")} using ${ids.join(", ")} ${C.faint("— `huddle setup` installs or signs in any that aren't ready")}`
110
+ : `${C.green("✓")} skipping ${ids.join(", ")} ${C.faint("— never installed, asked about, or put to work. Undo: huddle agents use " + ids.join(","))}`);
111
+ return true;
112
+ }
113
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,335 @@
1
+ // The live view — the full-screen dashboard from the launch film.
2
+ //
3
+ // ⬢ huddle 1 agents 2 plan 3 board BUILD · ● live · ⏱ 1m12s
4
+ // ╭─ ● claude working 40s · 31k tok ──────╮╭─ ● codex idle · 1 merged ───╮
5
+ // │ ▸ Add greet(name) with tests 60% ││ ✓ merged Add farewell… │
6
+ // │ plans: CommonJS greet.js … ││ │
7
+ // ╰────────────────────────────────────────╯╰───────────────────────────────╯
8
+ // ◆ codex → claude: 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. Every row
13
+ // is cut to the screen's width — a row that wraps scrolls the whole frame.
14
+ import { C, dot, pad, truncate, width, AGENT_COLOR } from "./term.js";
15
+ import { fmtDuration, fmtTokens } from "../adapters/usage.js";
16
+ import { taskViews } from "../room/progress.js";
17
+ import { getContract } from "../room/contract.js";
18
+ import { screenPanel } from "../room/context.js";
19
+ const PHASE_OF = {
20
+ "council.proposal": "COUNCIL",
21
+ "council.critique": "COUNCIL",
22
+ "council.adopt": "COUNCIL",
23
+ "council.flaw": "COUNCIL",
24
+ "council.ratify": "COUNCIL",
25
+ "council.plan": "PLAN",
26
+ "plan.made": "PLAN",
27
+ "contract.set": "PLAN",
28
+ "run.start": "BUILD",
29
+ "qa.round": "QA",
30
+ };
31
+ /**
32
+ * The run's phase: the latest phase-marking event since the session began,
33
+ * and DONE once every task on the board is merged or failed and no agent is busy.
34
+ */
35
+ export function runPhase(db, busy) {
36
+ if (!db)
37
+ return "WAITING";
38
+ let tasks = [];
39
+ let since = 0;
40
+ try {
41
+ tasks = db.listTasks();
42
+ since = (db.currentSession()?.created_at ?? 1) - 1;
43
+ }
44
+ catch {
45
+ return "WAITING"; // a closed db
46
+ }
47
+ const last = db.eventsOfType(Object.keys(PHASE_OF), since, 1).at(-1);
48
+ let phase = last ? PHASE_OF[last.type] : "WAITING";
49
+ // A QA round's fix tasks are BUILD work, but the run is still in QA.
50
+ if (phase !== "QA" && phase !== "WAITING" && db.eventsOfType(["qa.round"], since, 1).length)
51
+ phase = "QA";
52
+ if (!busy && tasks.length && tasks.every((t) => t.status === "merged" || t.status === "failed"))
53
+ return "DONE";
54
+ return phase;
55
+ }
56
+ const hexColor = (id) => C.hex(AGENT_COLOR[id.split("/").pop().split("#")[0]] ?? "#8A857C");
57
+ /** The shared screen's 📎/📌 rows in the view's glyphs (emoji are two cells wide and vary by font). */
58
+ const screenGlyphs = (l) => l.replace(/^📎/u, C.violet("+")).replace(/^📌/u, C.violet("◆"));
59
+ export class Dashboard {
60
+ src;
61
+ tab = "agents";
62
+ open = false;
63
+ timer;
64
+ feed = [];
65
+ onKey;
66
+ constructor(src) {
67
+ this.src = src;
68
+ }
69
+ isOpen() {
70
+ return this.open;
71
+ }
72
+ /** Keep a rolling log whether or not the view is on screen. */
73
+ push(a) {
74
+ this.feed.push({ ...a, ts: Date.now() });
75
+ if (this.feed.length > 400)
76
+ this.feed.splice(0, this.feed.length - 400);
77
+ if (this.open)
78
+ this.render();
79
+ }
80
+ show(tab, onClose) {
81
+ if (!process.stdout.isTTY)
82
+ return;
83
+ if (tab)
84
+ this.tab = tab;
85
+ if (this.open)
86
+ return this.render();
87
+ this.open = true;
88
+ process.stdout.write("\x1b[?1049h\x1b[?25l"); // alternate screen, hide cursor
89
+ this.onKey = (s, key) => {
90
+ if (!this.open)
91
+ return;
92
+ const k = key?.name ?? s;
93
+ if (s === "1")
94
+ this.tab = "agents";
95
+ else if (s === "2")
96
+ this.tab = "plan";
97
+ else if (s === "3")
98
+ this.tab = "board";
99
+ else if (k === "tab")
100
+ this.tab = this.tab === "agents" ? "plan" : this.tab === "plan" ? "board" : "agents";
101
+ else if (s === "q" || k === "escape" || (key?.ctrl && key.name === "t")) {
102
+ this.hide();
103
+ onClose?.();
104
+ return;
105
+ }
106
+ this.render();
107
+ };
108
+ process.stdin.on("keypress", this.onKey);
109
+ process.stdout.on("resize", this.renderBound);
110
+ this.timer = setInterval(() => this.render(), 500);
111
+ this.render();
112
+ }
113
+ hide() {
114
+ if (!this.open)
115
+ return;
116
+ this.open = false;
117
+ if (this.timer)
118
+ clearInterval(this.timer);
119
+ if (this.onKey)
120
+ process.stdin.off("keypress", this.onKey);
121
+ process.stdout.off("resize", this.renderBound);
122
+ process.stdout.write("\x1b[?25h\x1b[?1049l"); // back to the normal screen
123
+ }
124
+ renderBound = () => this.render();
125
+ render() {
126
+ if (!this.open)
127
+ return;
128
+ const lines = this.frame(process.stdout.columns || 100, process.stdout.rows || 30);
129
+ process.stdout.write("\x1b[H" + lines.map((l) => "\x1b[2K" + l).join("\n"));
130
+ }
131
+ /** One full screen, `H` rows each at most `W` cells (also how tests and samples render it). */
132
+ frame(W, H, tab = this.tab, now = Date.now()) {
133
+ const db = this.src.db();
134
+ const stats = this.src.stats();
135
+ const busy = !!stats && [...stats.agents.values()].some((a) => a.busySince);
136
+ const phase = runPhase(db, busy);
137
+ let goal = "";
138
+ try {
139
+ goal = db?.currentSession()?.goal ?? "";
140
+ }
141
+ catch {
142
+ /* a closed db */
143
+ }
144
+ const tabs = ["agents", "plan", "board"]
145
+ .map((t, i) => (t === tab ? C.bold(C.violet(`[${i + 1} ${t}]`)) : C.faint(` ${i + 1} ${t} `)))
146
+ .join(" ");
147
+ const clock = stats ? `${C.faint("⏱")} ${fmtDuration((stats.endedAt ?? now) - stats.startedAt)}` : "";
148
+ const state = phase === "DONE"
149
+ ? C.green("✓ done")
150
+ : phase === "WAITING"
151
+ ? C.faint("○ waiting for a run")
152
+ : `${C.bold(C.violet(phase))} ${C.faint("·")} ${busy ? C.green("● live") : C.faint("○ between steps")}`;
153
+ const right = [state, clock].filter(Boolean).join(C.faint(" · "));
154
+ const bar = ` ${C.violet("⬢")} ${C.bold("huddle")} ${tabs}`;
155
+ // The goal takes whatever room is left between the tabs and the status.
156
+ const room = W - width(bar) - width(right) - 4;
157
+ const goalS = goal && room >= 12 ? C.faint(truncate(goal.replace(/\s+/g, " "), room)) : "";
158
+ const gap = Math.max(1, W - width(bar) - width(goalS) - width(right) - 3);
159
+ const top = goalS ? `${bar} ${goalS}${" ".repeat(gap)}${right}` : `${bar}${" ".repeat(Math.max(1, W - width(bar) - width(right) - 1))}${right}`;
160
+ const bodyH = H - 4;
161
+ const body = tab === "agents" ? this.agents(W, bodyH, stats, db, now) : tab === "plan" ? this.plan(W, bodyH, db) : this.board(W, bodyH, db);
162
+ const lastRoom = [...this.feed].reverse().find((a) => !a.agent);
163
+ const strip = lastRoom ? ` ${lastRoom.text}` : C.faint(" the room is quiet");
164
+ const hint = C.faint(` ${this.src.hint ?? "1 2 3 switch · q back to the prompt · ctrl-c stop agents"} `) + this.src.modeChip();
165
+ const lines = [top, C.faint("─".repeat(W)), ...body.slice(0, bodyH)];
166
+ while (lines.length < H - 2)
167
+ lines.push("");
168
+ lines.push(strip, hint);
169
+ return lines.slice(0, H).map((l) => truncate(l.replace(/[\r\n]+/g, " "), W));
170
+ }
171
+ /** Tab 1: a pane per agent — what each one is doing right now. */
172
+ agents(W, H, stats, db, now) {
173
+ const ids = stats ? [...stats.agents.keys()] : [...new Set(this.feed.filter((a) => a.agent).map((a) => a.agent))];
174
+ if (!ids.length)
175
+ return ["", C.faint(` ${this.src.empty ?? "no agents working — give Huddle a goal"}`)];
176
+ let views = [];
177
+ try {
178
+ views = db ? taskViews(db, now) : [];
179
+ }
180
+ catch {
181
+ /* a closed db: no pinned task line */
182
+ }
183
+ const cols = W >= 110 && ids.length > 1 ? 2 : 1;
184
+ const rows = Math.ceil(ids.length / cols);
185
+ const paneW = Math.floor(W / cols);
186
+ const paneH = Math.max(4, Math.floor(H / rows));
187
+ const out = [];
188
+ for (let r = 0; r < rows; r++) {
189
+ const panes = ids.slice(r * cols, r * cols + cols).map((id) => this.pane(id, paneW, paneH, stats?.agents.get(id), views, now));
190
+ for (let i = 0; i < paneH; i++)
191
+ out.push(panes.map((p) => p[i] ?? " ".repeat(paneW)).join(""));
192
+ }
193
+ return out;
194
+ }
195
+ pane(id, w, h, a, views, now) {
196
+ const color = hexColor(id);
197
+ const status = a?.busySince
198
+ ? `${C.violet("working")} ${fmtDuration(now - a.busySince)}${a.liveTokens > 0 ? " · " + fmtTokens(a.liveTokens) + " tok" : ""}`
199
+ : a && (a.tasks || a.reviews || a.qa)
200
+ ? C.green(`idle · ${a.tasks} merged${a.reviews ? ` · ${a.reviews} review${a.reviews === 1 ? "" : "s"}` : ""}${a.qa ? ` · ${a.qa} QA` : ""}`)
201
+ : C.faint("idle");
202
+ const inner = w - 2;
203
+ const title = truncate(` ${dot(id)} ${C.bold(id)} ${status} `, inner - 1);
204
+ const topLine = color("╭─") + title + color("─".repeat(Math.max(0, inner - 1 - width(title))) + "╮");
205
+ // The task it's on stays pinned at the top: title, %, how long since it last said anything.
206
+ const on = a?.busySince ? views.find((v) => v.owner === id && (v.status === "claimed" || v.status === "in_progress")) : undefined;
207
+ const pinned = [];
208
+ if (on) {
209
+ const pct = on.reported ? C.violet(`${on.progress_pct}%`) : C.faint("no report yet");
210
+ const said = on.last_note ? C.faint(` · note ${fmtDuration(on.last_update_s * 1000)} ago`) : "";
211
+ const tail = ` ${pct}${said}`;
212
+ pinned.push(`${C.violet("▸")} ${C.bold(truncate(on.title, Math.max(8, inner - 4 - width(tail))))}${tail}`);
213
+ }
214
+ const room = h - 2 - pinned.length;
215
+ const lines = [...pinned, ...this.feed.filter((x) => x.agent === id).slice(-room).map((x) => x.text)];
216
+ const body = Array.from({ length: h - 2 }, (_, i) => {
217
+ const l = lines[i] ? " " + truncate(lines[i], inner - 2) : "";
218
+ return color("│") + pad(l, inner) + color("│");
219
+ });
220
+ return [topLine, ...body, color("╰" + "─".repeat(inner) + "╯")];
221
+ }
222
+ /** Tab 2: the plan, and the room talking (side by side when there's room, stacked when not). */
223
+ plan(W, H, db) {
224
+ const left = [C.violet(" PLAN")];
225
+ let tasks = [];
226
+ try {
227
+ tasks = db?.listTasks() ?? [];
228
+ }
229
+ catch {
230
+ /* a closed db */
231
+ }
232
+ const idx = new Map(tasks.map((t, i) => [t.id, i + 1]));
233
+ tasks.forEach((t, i) => {
234
+ const deps = JSON.parse(t.depends_on || "[]");
235
+ const after = deps.length ? C.faint(` after ${deps.map((d) => idx.get(d)).join(",")}`) : "";
236
+ const who = t.claimed_by ? ` ${dot(t.claimed_by)} ${C.faint(t.claimed_by)}` : "";
237
+ left.push(` ${mark(t.status, !!t.claimed_by)} ${C.faint(String(i + 1).padStart(2) + ".")} ${t.title}${after}${who}`);
238
+ });
239
+ if (!tasks.length)
240
+ left.push(C.faint(" no plan yet"));
241
+ let contract = null;
242
+ try {
243
+ contract = db ? getContract(db) : null;
244
+ }
245
+ catch {
246
+ /* a closed db: no contract line */
247
+ }
248
+ if (contract && (contract.text || contract.amendments.length)) {
249
+ left.push("", `${C.violet(" CONTRACT")} ${C.faint(`v${contract.version} · ${contract.amendments.length} amendment(s)`)}`);
250
+ for (const a of contract.amendments.slice(-3))
251
+ left.push(C.faint(` v${a.version} ${a.by}: ${a.section}: ${a.change}`));
252
+ }
253
+ const right = [C.violet(" ROOM") + C.faint(" · agents & people talking")];
254
+ for (const a of this.feed.filter((x) => !x.agent).slice(-(H - 1)))
255
+ right.push(" " + a.text);
256
+ if (right.length === 1)
257
+ right.push(C.faint(" nothing said yet"));
258
+ return columns(left, right, W, H, true);
259
+ }
260
+ /** Tab 3: progress, and the shared screen + memory. */
261
+ board(W, H, db) {
262
+ const left = [C.violet(" BOARD") + C.faint(" · live progress")];
263
+ let views = [];
264
+ try {
265
+ views = db ? taskViews(db) : [];
266
+ }
267
+ catch {
268
+ /* a closed db */
269
+ }
270
+ const bw = W >= 100 ? 14 : 8;
271
+ for (const t of views) {
272
+ const filled = Math.round((t.progress_pct / 100) * bw);
273
+ const barS = t.status === "merged" ? C.green("█".repeat(bw)) : C.violet("█".repeat(filled)) + C.faint("░".repeat(bw - filled));
274
+ // Never reported (no huddle_progress yet): say so instead of showing a made-up 0%.
275
+ const silent = ["claimed", "in_progress"].includes(t.status) && !t.reported;
276
+ const who = t.owner ? ` ${dot(t.owner)} ${C.faint(t.owner)}` : "";
277
+ left.push(` ${mark(t.status, !!t.owner)} ${barS} ${silent ? C.faint(" — ") : String(t.progress_pct).padStart(3) + "%"} ${t.title}${who}`);
278
+ }
279
+ if (!views.length)
280
+ left.push(C.faint(" no tasks yet"));
281
+ const right = [];
282
+ // The shared screen: what the user put up, and the notes pinned on it.
283
+ let screen = [];
284
+ let memory = [];
285
+ try {
286
+ screen = db ? screenPanel(db, Math.min(8, Math.floor(H / 2))) : [];
287
+ memory = db?.recall() ?? [];
288
+ }
289
+ catch {
290
+ /* a closed db */
291
+ }
292
+ if (screen.length)
293
+ right.push(C.violet(" SCREEN") + C.faint(" · pastes & files every agent reads"), ...screen.map((l) => " " + screenGlyphs(l)), "");
294
+ right.push(C.violet(" SHARED MEMORY") + C.faint(" · everyone reads & writes"));
295
+ for (const m of memory.slice(0, H - 1)) {
296
+ right.push(` ${C.violet("◆")} ${m.author ? C.faint(m.author + ": ") : ""}${m.body}`);
297
+ if (m.why)
298
+ right.push(C.faint(` why: ${m.why}`));
299
+ }
300
+ if (!memory.length)
301
+ right.push(C.faint(" nothing yet"));
302
+ return columns(left, right, W, H);
303
+ }
304
+ }
305
+ /** A task's state as one glyph, the same ones the run's output uses. */
306
+ function mark(status, claimed) {
307
+ if (status === "merged")
308
+ return C.green("✓");
309
+ if (status === "failed")
310
+ return C.red("✗");
311
+ if (status === "needs_you")
312
+ return C.yellow("!");
313
+ if (status === "review")
314
+ return C.violet("◎");
315
+ return claimed ? C.violet("▸") : C.faint("○");
316
+ }
317
+ /** Two columns side by side on a wide screen; on a narrow one, the left on top of the right. */
318
+ function columns(left, right, W, H, latest = false) {
319
+ if (W < 100) {
320
+ const top = left.slice(0, Math.max(3, Math.min(left.length, H - Math.min(right.length, Math.floor(H / 2)) - 1)));
321
+ const n = Math.max(0, H - top.length - 1);
322
+ // A feed keeps its newest lines (its header stays).
323
+ const rest = latest && right.length > n ? [right[0], ...right.slice(1).slice(-(n - 1))].slice(0, n) : right.slice(0, n);
324
+ return [...top, C.faint("─".repeat(W)), ...rest].map((l) => truncate(l, W));
325
+ }
326
+ const leftW = Math.floor(W * 0.55);
327
+ const out = [];
328
+ for (let i = 0; i < H; i++) {
329
+ const l = truncate(left[i] ?? "", leftW - 1);
330
+ const r = truncate(right[i] ?? "", W - leftW - 2);
331
+ out.push(pad(l, leftW - 1) + C.faint("│") + " " + r);
332
+ }
333
+ return out;
334
+ }
335
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1,58 @@
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, installLine } from "./app.js";
10
+ import { gitFix } from "../platform.js";
11
+ import { C, dot } from "./term.js";
12
+ export async function runDoctor() {
13
+ let blocked = false;
14
+ const gitV = spawnSync("git", ["--version"], { encoding: "utf8" });
15
+ const gitNum = gitV.status === 0 ? (gitV.stdout.match(/\d+\.\d+(\.\d+)?/)?.[0] ?? "") : null;
16
+ const db = sqliteStatus();
17
+ const parts = [
18
+ `${C.violet("huddle")} ${packageInfo().version}`,
19
+ `node ${process.versions.node}`,
20
+ gitNum === null ? C.red("git missing") : `git ${gitNum}`.trim(),
21
+ db.ok ? `room db: ${db.driver}` : C.red("room db: unavailable"),
22
+ ];
23
+ console.log("\n " + parts.join(C.faint(" · ")));
24
+ if (gitNum === null) {
25
+ blocked = true;
26
+ console.log(C.red("\n✗ git isn't installed.") + " Huddle gives every agent its own git worktree.");
27
+ console.log(C.faint(" " + gitFix()));
28
+ }
29
+ if (!db.ok) {
30
+ blocked = true;
31
+ console.log("\n" + C.red("✗ ") + db.message);
32
+ }
33
+ const results = await connectAgents();
34
+ if (!nextSteps(results))
35
+ blocked = true;
36
+ return blocked ? 1 : 0;
37
+ }
38
+ /** What to do when agents are missing or signed out. Returns true if at least one is ready. */
39
+ export function nextSteps(results, print = console.log) {
40
+ const ready = results.filter((r) => r.state === "ready");
41
+ const pending = results.filter((r) => r.state === "not_authed" || (r.state === "not_installed" && !LEGACY.has(r.adapter.id)));
42
+ if (ready.length) {
43
+ print(C.faint(`\n ${ready.length} agent(s) ready — cd into a repo and run: huddle`));
44
+ if (pending.length)
45
+ print(C.faint(` set up the rest (${pending.map((r) => r.adapter.id).join(", ")}): huddle setup`));
46
+ return true;
47
+ }
48
+ const missing = results.filter((r) => r.state === "not_installed" && !LEGACY.has(r.adapter.id));
49
+ print(`\n${C.yellow("No agents ready yet.")} Huddle runs the coding agents you already use,`);
50
+ print("each signed in to its own account.");
51
+ print(` ${C.bold("huddle setup")} ${C.faint("installs and signs in the ones you pick (asks first). Or by hand:")}`);
52
+ for (const r of missing)
53
+ print(` ${dot(r.adapter.id)} ${C.bold(r.adapter.id.padEnd(12))} ${C.faint("$")} ${installLine(r.adapter)}`);
54
+ print(C.faint(" Then sign in with the agent itself — Huddle never sees your credentials —"));
55
+ print(C.faint(" and run `huddle doctor` again."));
56
+ return false;
57
+ }
58
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,81 @@
1
+ // `huddle lessons`: what Huddle learned across runs on this machine, and how
2
+ // to take a lesson back.
3
+ //
4
+ // huddle lessons list them, most-seen first
5
+ // huddle lessons --forget <id> drop one
6
+ // huddle lessons --clear [--yes] drop all (asks first)
7
+ import readline from "node:readline/promises";
8
+ import os from "node:os";
9
+ import { C } from "./term.js";
10
+ import { allLessons, clearLessons, forgetLesson, lessonsFile, lessonsOff, repoKey } from "../room/lessons.js";
11
+ const ago = (ts) => {
12
+ const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
13
+ return s < 90 ? `${s}s ago` : s < 5400 ? `${Math.round(s / 60)}m ago` : s < 129600 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago`;
14
+ };
15
+ function scopeLabel(l, here) {
16
+ if (l.scope === "all")
17
+ return "every run";
18
+ if (l.scope.startsWith("agent:"))
19
+ return l.scope.slice(6);
20
+ return here && l.scope === `repo:${here}` ? "this repo" : `repo ${l.scope.slice(5, 13)}`;
21
+ }
22
+ export async function runLessons(rest, cwd = process.cwd()) {
23
+ const file = lessonsFile().replace(os.homedir(), "~");
24
+ const fi = rest.indexOf("--forget");
25
+ if (fi >= 0) {
26
+ const id = rest[fi + 1];
27
+ if (!id || id.startsWith("--")) {
28
+ console.log(C.red("✗ --forget needs a lesson id") + C.faint(" (the first column of `huddle lessons`)"));
29
+ process.exitCode = 1;
30
+ return;
31
+ }
32
+ if (!forgetLesson(id)) {
33
+ console.log(C.red(`✗ no lesson ${id}`) + C.faint(" — `huddle lessons` lists them"));
34
+ process.exitCode = 1;
35
+ return;
36
+ }
37
+ return void console.log(`${C.green("✓")} forgot ${id}`);
38
+ }
39
+ if (rest.includes("--clear")) {
40
+ const n = allLessons().length;
41
+ if (!n)
42
+ return void console.log(C.faint("No lessons to clear."));
43
+ if (!rest.includes("--yes")) {
44
+ if (!process.stdin.isTTY) {
45
+ console.log(C.yellow(`! this deletes all ${n} lesson(s) in ${file}.`) + " Run it again with --yes to confirm.");
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
50
+ const a = (await rl.question(`Delete all ${n} lesson(s) in ${file}? ${C.faint("[y/N]")} `)).trim().toLowerCase();
51
+ rl.close();
52
+ if (!a.startsWith("y"))
53
+ return void console.log(C.faint("Kept them."));
54
+ }
55
+ clearLessons();
56
+ return void console.log(`${C.green("✓")} cleared ${n} lesson(s)`);
57
+ }
58
+ const lessons = allLessons().sort((a, b) => b.hits - a.hits || b.created - a.created);
59
+ if (lessonsOff())
60
+ console.log(C.yellow("! HUDDLE_NO_LESSONS is set: runs neither use nor learn lessons right now."));
61
+ if (!lessons.length) {
62
+ console.log(`No lessons yet ${C.faint(`(${file})`)}.`);
63
+ console.log(C.faint(" Huddle learns them at the end of each run — a council turn that timed out, a file agents collided on, QA bugs — and agents add them with huddle_remember(kind=\"lesson\")."));
64
+ return;
65
+ }
66
+ let here = null;
67
+ try {
68
+ here = repoKey(cwd);
69
+ }
70
+ catch {
71
+ /* not in a repo */
72
+ }
73
+ console.log(`${C.violet("lessons")} ${C.faint(`· ${lessons.length} · ${file} · they go into agent and planner prompts`)}\n`);
74
+ for (const l of lessons) {
75
+ console.log(` ${C.bold(l.id)} ${l.text}`);
76
+ const used = l.last_used ? `used ${ago(l.last_used)}` : "not used yet";
77
+ console.log(C.faint(` ${scopeLabel(l, here)} · seen ${l.hits}× · ${used} · from ${l.source || "?"}${l.evidence ? ` · ${l.evidence}` : ""}`));
78
+ }
79
+ console.log(C.faint(`\n huddle lessons --forget <id> · --clear · HUDDLE_NO_LESSONS=1 turns them off`));
80
+ }
81
+ //# sourceMappingURL=lessons.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