huddle-cli 0.1.0 → 0.1.3

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 (73) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/QUICKSTART.md +135 -0
  3. package/README.md +26 -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 +9 -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/app/live.js +142 -0
  16. package/dist/app/page.js +701 -0
  17. package/dist/app/server.js +262 -0
  18. package/dist/args.js +18 -0
  19. package/dist/attach/connect.js +7 -4
  20. package/dist/attach/guard.js +21 -2
  21. package/dist/attach/live.js +5 -1
  22. package/dist/attach/plug.js +21 -3
  23. package/dist/attach/screen.js +150 -0
  24. package/dist/attach/session.js +7 -2
  25. package/dist/cli.js +134 -42
  26. package/dist/mcp/context-tools.js +79 -0
  27. package/dist/mcp/server.js +57 -11
  28. package/dist/orchestrator/ask.js +13 -10
  29. package/dist/orchestrator/brain.js +16 -4
  30. package/dist/orchestrator/budget.js +283 -0
  31. package/dist/orchestrator/busy.js +106 -0
  32. package/dist/orchestrator/council.js +434 -87
  33. package/dist/orchestrator/goal.js +196 -0
  34. package/dist/orchestrator/handoff.js +2 -2
  35. package/dist/orchestrator/launcher.js +31 -13
  36. package/dist/orchestrator/learn.js +101 -0
  37. package/dist/orchestrator/planner.js +54 -9
  38. package/dist/orchestrator/qa.js +71 -15
  39. package/dist/orchestrator/radar.js +719 -0
  40. package/dist/orchestrator/scheduler.js +575 -50
  41. package/dist/orchestrator/takeover.js +155 -0
  42. package/dist/platform.js +377 -0
  43. package/dist/room/context.js +1285 -0
  44. package/dist/room/contract.js +132 -0
  45. package/dist/room/db.js +79 -16
  46. package/dist/room/followup.js +233 -0
  47. package/dist/room/identity.js +2 -2
  48. package/dist/room/lessons.js +275 -0
  49. package/dist/room/live.js +75 -20
  50. package/dist/room/mail.js +225 -0
  51. package/dist/room/progress.js +40 -1
  52. package/dist/room/server.js +220 -39
  53. package/dist/room/tunnel.js +6 -5
  54. package/dist/room/user.js +54 -0
  55. package/dist/sandbox/sync.js +4 -4
  56. package/dist/sandbox/worktree.js +145 -14
  57. package/dist/ui/app.js +963 -159
  58. package/dist/ui/brief.js +494 -0
  59. package/dist/ui/catalog.js +62 -24
  60. package/dist/ui/dashboard.js +269 -55
  61. package/dist/ui/doctor.js +15 -11
  62. package/dist/ui/export.js +1194 -0
  63. package/dist/ui/lessons.js +81 -0
  64. package/dist/ui/narrator.js +56 -0
  65. package/dist/ui/paste.js +515 -0
  66. package/dist/ui/remote.js +11 -5
  67. package/dist/ui/setup.js +152 -0
  68. package/dist/ui/story.js +716 -94
  69. package/dist/ui/talk.js +176 -0
  70. package/dist/ui/term.js +182 -16
  71. package/dist/ui/transcript.js +434 -0
  72. package/dist/ui/watch.js +149 -48
  73. package/package.json +6 -3
package/dist/ui/remote.js CHANGED
@@ -22,7 +22,8 @@ import { transportFor } from "../room/transport.js";
22
22
  import { signAs, githubUser } from "../room/identity.js";
23
23
  import { canonicalRemote, pushBranch, remoteInfo } from "../sandbox/sync.js";
24
24
  import { connectAgents, readPrefs, workersFrom, ensureInit } from "./app.js";
25
- import { launchAgent } from "../orchestrator/launcher.js";
25
+ import { launchAgent, stopChild } from "../orchestrator/launcher.js";
26
+ import { looksLikePaths, remoteScreen } from "./paste.js";
26
27
  import { removeWorktree } from "../sandbox/worktree.js";
27
28
  import { fmtDuration, fmtTokens } from "../adapters/usage.js";
28
29
  export const INVITE_CODE = /^[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/i;
@@ -199,7 +200,7 @@ export async function joinRoom(address, code) {
199
200
  // Worktree off the room's pushed main (or the branch itself, on a fix round).
200
201
  const f = git(root, ["fetch", "--quiet", "origin"]);
201
202
  if (!f.ok)
202
- return deliver({ ok: false, reason: `couldn't fetch: ${f.err.split("\n")[0]}` });
203
+ return deliver({ ok: false, reason: `couldn't fetch: ${f.err.split(/\r?\n/)[0]}` });
203
204
  const wtPath = path.join(root, ".huddle", "worktrees", branch.replace(/[^\w.-]/g, "_"));
204
205
  if (!fs.existsSync(wtPath)) {
205
206
  const hasRemoteBranch = git(root, ["rev-parse", "--verify", "--quiet", `origin/${branch}`]).ok;
@@ -207,7 +208,7 @@ export async function joinRoom(address, code) {
207
208
  ? git(root, ["worktree", "add", "-B", branch, wtPath, `origin/${branch}`])
208
209
  : git(root, ["worktree", "add", "-B", branch, wtPath, p.base]);
209
210
  if (!add.ok)
210
- return deliver({ ok: false, reason: `couldn't prepare a worktree: ${add.err.split("\n")[0]}` });
211
+ return deliver({ ok: false, reason: `couldn't prepare a worktree: ${add.err.split(/\r?\n/)[0]}` });
211
212
  }
212
213
  say(` ${dot(w.id)} ${pad(w.id, 10)} ${C.violet("▸")} ${p.task.title} ${C.faint("· on your machine")}`);
213
214
  const prefs = readPrefs();
@@ -250,7 +251,7 @@ export async function joinRoom(address, code) {
250
251
  return;
251
252
  leaving = true;
252
253
  for (const a of assigned.values())
253
- a.handle?.child?.kill("SIGTERM");
254
+ stopChild(a.handle?.child);
254
255
  await t.request("POST", "/leave", {}, token).catch(() => { });
255
256
  proxy?.close();
256
257
  t.close();
@@ -304,7 +305,7 @@ export async function joinRoom(address, code) {
304
305
  process.stdout.write("\r\x1b[2K");
305
306
  console.log(code === 4403 ? C.yellow("you were removed from the room") : C.faint("the room closed"));
306
307
  for (const a of assigned.values())
307
- a.handle?.child?.kill("SIGTERM");
308
+ stopChild(a.handle?.child);
308
309
  proxy?.close();
309
310
  process.exit(0);
310
311
  });
@@ -321,6 +322,11 @@ export async function joinRoom(address, code) {
321
322
  printBoard(b.body);
322
323
  return rl.prompt();
323
324
  }
325
+ // The host's shared screen: `ctx` lists it, `add <file>` puts a text file up.
326
+ if (/^(ctx|context)$/.test(line) || /^add\s+\S/.test(line) && looksLikePaths(line.slice(3))) {
327
+ await remoteScreen((m, p, b) => t.request(m, p, b, token), line, say);
328
+ return rl.prompt();
329
+ }
324
330
  const r = await t.request("POST", "/message", { body: line }, token);
325
331
  if (r.status !== 200)
326
332
  say(C.red(` ✗ ${r.body.error ?? "not sent"}`));
@@ -0,0 +1,152 @@
1
+ // `huddle setup`: everything a person would otherwise do by hand before the
2
+ // first goal, in one command — check Node and git, pick which agents you use,
3
+ // install the missing ones with this platform's command, then hand the
4
+ // terminal to each one's own sign-in, and check again.
5
+ //
6
+ // huddle setup asks: use each agent? install the missing ones?
7
+ // huddle setup --yes no questions for choices and installs
8
+ // (sign-ins still need you: they open a browser)
9
+ // huddle setup --agents=claude,codex only these (and they're marked "used")
10
+ //
11
+ // Huddle never sees a credential: sign-in is always the agent's own command.
12
+ import readline from "node:readline/promises";
13
+ import { spawnSync } from "node:child_process";
14
+ import { ADAPTERS, LEGACY, installCommand, loginLine } from "../adapters/index.js";
15
+ import { MIN_NODE } from "../preflight.js";
16
+ import { gitFix, nodeFix } from "../platform.js";
17
+ import { connectAgents, installAgent, installLine, runInteractive, setAgentChoice, signIn, skippedAgents } from "./app.js";
18
+ import { C, dot } from "./term.js";
19
+ function realDeps(rl) {
20
+ return {
21
+ ask: async (q, def = true) => {
22
+ if (!rl)
23
+ return def;
24
+ const a = (await rl.question(`${q} ${C.faint(def ? "[Y/n]" : "[y/N]")} `)).trim().toLowerCase();
25
+ return a === "" ? def : a.startsWith("y");
26
+ },
27
+ run: (cmd) => runInteractive(rl, cmd),
28
+ print: (l = "") => console.log(l),
29
+ probe: () => connectAgents(),
30
+ git: () => {
31
+ const r = spawnSync("git", ["--version"], { encoding: "utf8" });
32
+ return r.status === 0 ? (r.stdout.match(/\d+\.\d+(\.\d+)?/)?.[0] ?? "?") : null;
33
+ },
34
+ node: process.versions.node,
35
+ interactive: !!process.stdin.isTTY,
36
+ };
37
+ }
38
+ export async function runSetup(args, deps) {
39
+ const rl = deps ? null : process.stdin.isTTY ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
40
+ try {
41
+ return await setup(args, deps ?? realDeps(rl));
42
+ }
43
+ finally {
44
+ rl?.close();
45
+ }
46
+ }
47
+ /** The flow, with every side effect in `d` (tests pass fakes). Exit code: 0 = at least one agent ready. */
48
+ export async function setup(args, d) {
49
+ const yesAll = args.includes("--yes") || args.includes("-y");
50
+ const pick = args.find((a) => a.startsWith("--agents="))?.split("=")[1];
51
+ const only = pick?.split(",").map((s) => s.trim()).filter(Boolean);
52
+ const ask = (q, def = true) => (yesAll || !d.interactive ? Promise.resolve(def) : d.ask(q, def));
53
+ // 1. Huddle's own needs. Node is checked by preflight before we get here, but say it.
54
+ d.print(`\n${C.bold("Huddle setup")}\n`);
55
+ d.print(` ${C.green("✓")} node ${d.node}${Number(d.node.split(".")[0]) < MIN_NODE ? C.red(` — needs ${MIN_NODE}+: ${nodeFix()}`) : ""}`);
56
+ const git = d.git();
57
+ if (git)
58
+ d.print(` ${C.green("✓")} git ${git}`);
59
+ else
60
+ d.print(` ${C.red("✗")} git is missing — Huddle gives every agent its own git worktree. Install it:\n ${gitFix()}`);
61
+ if (only) {
62
+ const known = new Set(ADAPTERS.map((a) => a.id));
63
+ const unknown = only.filter((id) => !known.has(id));
64
+ if (unknown.length) {
65
+ d.print(C.red(`✗ --agents: no agent called ${unknown.join(", ")}.`) + ` Known: ${[...known].join(", ")}`);
66
+ return 1;
67
+ }
68
+ setAgentChoice(only, true);
69
+ }
70
+ // 2. Which agents you use. Asked once per agent; --yes keeps your earlier choices.
71
+ let results = await d.probe();
72
+ const inScope = (r) => (only ? only.includes(r.adapter.id) : true);
73
+ if (!only && d.interactive && !yesAll) {
74
+ d.print(C.faint("\n Pick the agents you want Huddle to use. Skipped ones are never installed, asked about, or put to work."));
75
+ const skipped = skippedAgents();
76
+ const use = [];
77
+ const skip = [];
78
+ for (const r of results) {
79
+ const id = r.adapter.id;
80
+ const was = r.state === "skipped" ? r.was : r.state;
81
+ const note = was === "ready" ? "ready" : was === "not_authed" ? "installed, not signed in" : "not installed";
82
+ (await d.ask(` Use ${dot(id)} ${C.bold(id)} ${C.faint(`(${note})`)}?`, !skipped.has(id)) ? use : skip).push(id);
83
+ }
84
+ setAgentChoice(use, true);
85
+ setAgentChoice(skip, false);
86
+ if (skip.some((id) => !skipped.has(id)) || use.some((id) => skipped.has(id)))
87
+ results = await d.probe();
88
+ }
89
+ // 3. Install what's missing, with this platform's command.
90
+ const missing = results.filter((r) => r.state === "not_installed" && inScope(r) && (only || !LEGACY.has(r.adapter.id)));
91
+ if (missing.length) {
92
+ d.print(`\n ${C.bold("Not installed:")}`);
93
+ for (const r of missing)
94
+ d.print(` ${dot(r.adapter.id)} ${C.bold(r.adapter.id.padEnd(12))} ${C.faint(installCommand(r.adapter) ? "$ " : "")}${installLine(r.adapter)}`);
95
+ const installable = missing.filter((r) => installCommand(r.adapter));
96
+ let chosen = [];
97
+ if (installable.length) {
98
+ if (!d.interactive && !yesAll) {
99
+ d.print(C.faint("\n Run those yourself, or `huddle setup --yes` to have Huddle run them."));
100
+ }
101
+ else if (await ask(`\n Install all missing (${installable.map((r) => r.adapter.id).join(", ")})?`)) {
102
+ chosen = installable.map((r) => r.adapter);
103
+ }
104
+ else {
105
+ for (const r of installable)
106
+ if (await d.ask(` Install ${r.adapter.id}?`, false))
107
+ chosen.push(r.adapter);
108
+ }
109
+ }
110
+ let installed = false;
111
+ for (const a of chosen)
112
+ installed = installAgent(d, a) || installed;
113
+ if (installed)
114
+ results = await d.probe();
115
+ }
116
+ // 4. Sign in, one agent at a time: its own login, with the terminal handed over.
117
+ const signedOut = results.filter((r) => r.state === "not_authed" && inScope(r));
118
+ let signedIn = false;
119
+ for (const r of signedOut) {
120
+ const a = r.adapter;
121
+ const line = loginLine(a);
122
+ if (!line) {
123
+ d.print(`\n ${dot(a.id)} ${C.bold(a.id)} isn't signed in — ${r.detail}`);
124
+ continue;
125
+ }
126
+ if (!d.interactive) {
127
+ d.print(`\n ${dot(a.id)} ${C.bold(a.id)} isn't signed in — run: ${C.bold(line)}`);
128
+ continue;
129
+ }
130
+ // Even with --yes: a sign-in opens a browser, so the human is needed anyway — ask.
131
+ if (await d.ask(`\n ${dot(a.id)} ${C.bold(a.id)} isn't signed in. Sign in now? ${C.faint(`($ ${line})`)}`)) {
132
+ signIn(d, a);
133
+ signedIn = true;
134
+ }
135
+ }
136
+ if (signedIn)
137
+ results = await d.probe();
138
+ // 5. Where that leaves you.
139
+ const ready = results.filter((r) => r.state === "ready");
140
+ const rest = results.filter((r) => inScope(r) && (r.state === "not_authed" || (r.state === "not_installed" && !LEGACY.has(r.adapter.id))));
141
+ d.print("");
142
+ if (ready.length)
143
+ d.print(`${C.green("✓")} ${ready.length} agent${ready.length === 1 ? "" : "s"} ready (${ready.map((r) => r.adapter.id).join(", ")}) — cd into a repo and run ${C.bold("huddle")}`);
144
+ else
145
+ d.print(`${C.yellow("No agents ready yet.")} Install or sign in to one, then run ${C.bold("huddle setup")} again.`);
146
+ if (rest.length)
147
+ d.print(C.faint(` not ready: ${rest.map((r) => r.adapter.id).join(", ")} — run huddle setup again any time`));
148
+ if (!git)
149
+ d.print(C.red(` git is still needed: ${gitFix()}`));
150
+ return ready.length && git ? 0 : 1;
151
+ }
152
+ //# sourceMappingURL=setup.js.map