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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +102 -0
- package/README.md +376 -2
- package/dist/adapters/amp.js +40 -0
- package/dist/adapters/antigravity.js +54 -0
- package/dist/adapters/claude.js +86 -0
- package/dist/adapters/codex.js +64 -0
- package/dist/adapters/copilot.js +69 -0
- package/dist/adapters/cursor.js +92 -0
- package/dist/adapters/custom.js +342 -0
- package/dist/adapters/droid.js +58 -0
- package/dist/adapters/gemini.js +61 -0
- package/dist/adapters/goose.js +57 -0
- package/dist/adapters/index.js +161 -0
- package/dist/adapters/kiro.js +41 -0
- package/dist/adapters/opencode.js +63 -0
- package/dist/adapters/qwen.js +67 -0
- package/dist/adapters/types.js +6 -0
- package/dist/adapters/usage.js +141 -0
- package/dist/attach/connect.js +412 -0
- package/dist/attach/guard.js +83 -0
- package/dist/attach/live.js +226 -0
- package/dist/attach/plug.js +185 -0
- package/dist/attach/screen.js +150 -0
- package/dist/attach/session.js +177 -0
- package/dist/cli.js +244 -0
- package/dist/mcp/context-tools.js +79 -0
- package/dist/mcp/server.js +267 -0
- package/dist/orchestrator/ask.js +54 -0
- package/dist/orchestrator/brain.js +174 -0
- package/dist/orchestrator/council.js +615 -0
- package/dist/orchestrator/goal.js +196 -0
- package/dist/orchestrator/handoff.js +117 -0
- package/dist/orchestrator/launcher.js +171 -0
- package/dist/orchestrator/learn.js +105 -0
- package/dist/orchestrator/planner.js +212 -0
- package/dist/orchestrator/qa.js +398 -0
- package/dist/orchestrator/radar.js +719 -0
- package/dist/orchestrator/scheduler.js +1207 -0
- package/dist/orchestrator/takeover.js +240 -0
- package/dist/platform.js +273 -0
- package/dist/preflight.js +31 -0
- package/dist/relay/server.js +113 -0
- package/dist/room/context.js +1285 -0
- package/dist/room/contract.js +132 -0
- package/dist/room/db.js +680 -0
- package/dist/room/followup.js +233 -0
- package/dist/room/identity.js +122 -0
- package/dist/room/lessons.js +275 -0
- package/dist/room/live.js +348 -0
- package/dist/room/mail.js +212 -0
- package/dist/room/progress.js +149 -0
- package/dist/room/relaylink.js +85 -0
- package/dist/room/server.js +882 -0
- package/dist/room/sqlite.js +156 -0
- package/dist/room/transport.js +107 -0
- package/dist/room/tunnel.js +86 -0
- package/dist/sandbox/sync.js +104 -0
- package/dist/sandbox/worktree.js +392 -0
- package/dist/types.js +4 -0
- package/dist/ui/app.js +1955 -0
- package/dist/ui/catalog.js +113 -0
- package/dist/ui/dashboard.js +335 -0
- package/dist/ui/doctor.js +58 -0
- package/dist/ui/lessons.js +81 -0
- package/dist/ui/logo.js +40 -0
- package/dist/ui/paste.js +515 -0
- package/dist/ui/remote.js +338 -0
- package/dist/ui/setup.js +152 -0
- package/dist/ui/story.js +1529 -0
- package/dist/ui/term.js +249 -0
- package/dist/ui/watch.js +271 -0
- package/install.sh +44 -0
- 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
|