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.
- package/CHANGELOG.md +69 -0
- package/QUICKSTART.md +135 -0
- package/README.md +26 -4
- package/dist/adapters/amp.js +1 -1
- package/dist/adapters/antigravity.js +2 -0
- package/dist/adapters/claude.js +19 -3
- package/dist/adapters/codex.js +9 -1
- package/dist/adapters/cursor.js +12 -5
- package/dist/adapters/custom.js +4 -2
- package/dist/adapters/droid.js +1 -0
- package/dist/adapters/goose.js +2 -0
- package/dist/adapters/index.js +44 -18
- package/dist/adapters/kiro.js +1 -1
- package/dist/adapters/opencode.js +1 -1
- package/dist/app/live.js +142 -0
- package/dist/app/page.js +701 -0
- package/dist/app/server.js +262 -0
- package/dist/args.js +18 -0
- package/dist/attach/connect.js +7 -4
- package/dist/attach/guard.js +21 -2
- package/dist/attach/live.js +5 -1
- package/dist/attach/plug.js +21 -3
- package/dist/attach/screen.js +150 -0
- package/dist/attach/session.js +7 -2
- package/dist/cli.js +134 -42
- package/dist/mcp/context-tools.js +79 -0
- package/dist/mcp/server.js +57 -11
- package/dist/orchestrator/ask.js +13 -10
- package/dist/orchestrator/brain.js +16 -4
- package/dist/orchestrator/budget.js +283 -0
- package/dist/orchestrator/busy.js +106 -0
- package/dist/orchestrator/council.js +434 -87
- package/dist/orchestrator/goal.js +196 -0
- package/dist/orchestrator/handoff.js +2 -2
- package/dist/orchestrator/launcher.js +31 -13
- package/dist/orchestrator/learn.js +101 -0
- package/dist/orchestrator/planner.js +54 -9
- package/dist/orchestrator/qa.js +71 -15
- package/dist/orchestrator/radar.js +719 -0
- package/dist/orchestrator/scheduler.js +575 -50
- package/dist/orchestrator/takeover.js +155 -0
- package/dist/platform.js +377 -0
- package/dist/room/context.js +1285 -0
- package/dist/room/contract.js +132 -0
- package/dist/room/db.js +79 -16
- package/dist/room/followup.js +233 -0
- package/dist/room/identity.js +2 -2
- package/dist/room/lessons.js +275 -0
- package/dist/room/live.js +75 -20
- package/dist/room/mail.js +225 -0
- package/dist/room/progress.js +40 -1
- package/dist/room/server.js +220 -39
- package/dist/room/tunnel.js +6 -5
- package/dist/room/user.js +54 -0
- package/dist/sandbox/sync.js +4 -4
- package/dist/sandbox/worktree.js +145 -14
- package/dist/ui/app.js +963 -159
- package/dist/ui/brief.js +494 -0
- package/dist/ui/catalog.js +62 -24
- package/dist/ui/dashboard.js +269 -55
- package/dist/ui/doctor.js +15 -11
- package/dist/ui/export.js +1194 -0
- package/dist/ui/lessons.js +81 -0
- package/dist/ui/narrator.js +56 -0
- package/dist/ui/paste.js +515 -0
- package/dist/ui/remote.js +11 -5
- package/dist/ui/setup.js +152 -0
- package/dist/ui/story.js +716 -94
- package/dist/ui/talk.js +176 -0
- package/dist/ui/term.js +182 -16
- package/dist/ui/transcript.js +434 -0
- package/dist/ui/watch.js +149 -48
- 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(
|
|
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(
|
|
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
|
|
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
|
|
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"}`));
|
package/dist/ui/setup.js
ADDED
|
@@ -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
|