huddle-cli 0.1.0 → 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/QUICKSTART.md +102 -0
- package/README.md +23 -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 +6 -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/attach/connect.js +4 -1
- package/dist/attach/guard.js +21 -2
- package/dist/attach/live.js +5 -1
- package/dist/attach/plug.js +14 -3
- package/dist/attach/screen.js +150 -0
- package/dist/attach/session.js +7 -2
- package/dist/cli.js +108 -40
- package/dist/mcp/context-tools.js +79 -0
- package/dist/mcp/server.js +38 -11
- package/dist/orchestrator/ask.js +13 -10
- package/dist/orchestrator/brain.js +13 -3
- package/dist/orchestrator/council.js +427 -86
- package/dist/orchestrator/goal.js +196 -0
- package/dist/orchestrator/handoff.js +2 -2
- package/dist/orchestrator/launcher.js +29 -13
- package/dist/orchestrator/learn.js +105 -0
- package/dist/orchestrator/planner.js +25 -7
- package/dist/orchestrator/qa.js +62 -14
- package/dist/orchestrator/radar.js +719 -0
- package/dist/orchestrator/scheduler.js +498 -39
- package/dist/orchestrator/takeover.js +155 -0
- package/dist/platform.js +273 -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 +212 -0
- package/dist/room/progress.js +40 -1
- package/dist/room/server.js +169 -39
- package/dist/room/tunnel.js +6 -5
- package/dist/sandbox/sync.js +4 -4
- package/dist/sandbox/worktree.js +145 -14
- package/dist/ui/app.js +554 -133
- package/dist/ui/catalog.js +62 -24
- package/dist/ui/dashboard.js +194 -51
- package/dist/ui/doctor.js +15 -11
- package/dist/ui/lessons.js +81 -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 +666 -69
- package/dist/ui/term.js +151 -16
- package/dist/ui/watch.js +145 -48
- package/package.json +6 -3
package/dist/attach/plug.js
CHANGED
|
@@ -7,6 +7,8 @@ import { execFileSync } from "node:child_process";
|
|
|
7
7
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import { contextTools, toContent } from "../mcp/context-tools.js";
|
|
11
|
+
import { packageInfo } from "../preflight.js";
|
|
10
12
|
import { checkoutRoot, ensureLive, liveCall, mainRoot, roomCall } from "./live.js";
|
|
11
13
|
/** What every attached agent is told, via the MCP `instructions` field. */
|
|
12
14
|
export const LIVE_INSTRUCTIONS = `
|
|
@@ -16,6 +18,7 @@ Before you edit ANY file:
|
|
|
16
18
|
2. huddle_announce_plan(intent, touches) with every file (or "dir/") you'll change. If it returns conflicts, DO NOT edit those files: huddle_note the holder or pick other work, and tell your user.
|
|
17
19
|
3. When you finish a change, huddle_release so the others can proceed.
|
|
18
20
|
Notes from other sessions arrive in "notes_for_you" on any huddle_* reply — read them and answer with huddle_note. Record lasting decisions with huddle_remember.
|
|
21
|
+
The shared screen holds what your user put up for everyone (logs, docs, screenshots): huddle_context lists it, huddle_context_read / huddle_context_search open it, huddle_context_note pins what you found.
|
|
19
22
|
`.trim();
|
|
20
23
|
const tools = [
|
|
21
24
|
{
|
|
@@ -119,7 +122,12 @@ export async function runLivePlug() {
|
|
|
119
122
|
await sayHello();
|
|
120
123
|
return roomCall(st, method, p, body, sid);
|
|
121
124
|
};
|
|
125
|
+
// The shared screen: the same tools (and Room routes) as a `huddle join` agent's Plug.
|
|
126
|
+
const screen = contextTools((p, method, body) => plain(method, p, body ?? {}));
|
|
122
127
|
const call = (name, a) => {
|
|
128
|
+
const ctx = screen.find((t) => t.name === name);
|
|
129
|
+
if (ctx)
|
|
130
|
+
return ctx.handler(a);
|
|
123
131
|
switch (name) {
|
|
124
132
|
case "huddle_peers":
|
|
125
133
|
return live("peers", {});
|
|
@@ -139,12 +147,15 @@ export async function runLivePlug() {
|
|
|
139
147
|
}
|
|
140
148
|
throw new Error(`unknown tool: ${name}`);
|
|
141
149
|
};
|
|
142
|
-
const server = new Server({ name: "huddle", version:
|
|
143
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
150
|
+
const server = new Server({ name: "huddle", version: packageInfo().version }, { capabilities: { tools: {} }, instructions: LIVE_INSTRUCTIONS });
|
|
151
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
152
|
+
tools: [...tools.map((t) => ({ ...t })), ...screen.map(({ handler: _h, ...t }) => t)],
|
|
153
|
+
}));
|
|
144
154
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
145
155
|
try {
|
|
146
156
|
const result = await call(req.params.name, req.params.arguments ?? {});
|
|
147
|
-
|
|
157
|
+
// toContent: JSON text, plus an MCP image block for a screenshot.
|
|
158
|
+
return { content: toContent(result) };
|
|
148
159
|
}
|
|
149
160
|
catch (e) {
|
|
150
161
|
// Never wedge the agent: say what's wrong and let it carry on.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Attach mode, the human's side of the shared screen: `huddle add` and
|
|
2
|
+
// `huddle ctx`, typed in any terminal while your own claude / codex / cursor
|
|
3
|
+
// sessions run in theirs.
|
|
4
|
+
//
|
|
5
|
+
// huddle add err.log src/auth/ "*.png" files, folders (one bundle), globs
|
|
6
|
+
// cat err.log | huddle add - stdin (pbpaste | huddle add - too)
|
|
7
|
+
// huddle ctx [show <id>|rm <id>|note <id> <text>|clear]
|
|
8
|
+
//
|
|
9
|
+
// Same store, same commands as the `huddle` prompt (src/ui/paste.ts on
|
|
10
|
+
// src/room/context.ts) — only the db differs: the live room's .huddle/live.db.
|
|
11
|
+
// The room reads that db on every call, so writing it here is the same whether
|
|
12
|
+
// the room is up or not; when it's up, each session hears about a new item (or
|
|
13
|
+
// a pinned note) in notes_for_you on its next huddle_* call — the path
|
|
14
|
+
// huddle_note takes (src/room/live.ts, inbox).
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { getAdapter } from "../adapters/index.js";
|
|
18
|
+
import { ContextStore, normalizePathToken } from "../room/context.js";
|
|
19
|
+
import { RoomDB } from "../room/db.js";
|
|
20
|
+
import { addPaths, addPiped, ctxCommand } from "../ui/paste.js";
|
|
21
|
+
import { C } from "../ui/term.js";
|
|
22
|
+
import { confirm } from "./connect.js";
|
|
23
|
+
import { ensureHuddleDir, liveCall, liveFiles, mainRoot, pingLive, readLive } from "./live.js";
|
|
24
|
+
/** The sessions in the live room right now (agent names), or null when it's down. */
|
|
25
|
+
async function liveAgents(repo) {
|
|
26
|
+
const st = readLive(repo);
|
|
27
|
+
if (!st || !(await pingLive(st)))
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
const r = await liveCall(st, "peers", {}, "", 2000);
|
|
31
|
+
return (r.body.sessions ?? []).map((s) => String(s.agent));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** A ScreenUI on the live room's store. `interactive` = a person can answer the secrets question. */
|
|
38
|
+
function screenUI(repo, agents, interactive) {
|
|
39
|
+
let db = null;
|
|
40
|
+
const ui = {
|
|
41
|
+
print: (l = "") => console.log(l),
|
|
42
|
+
yes: (q, def = true) => confirm(q, def),
|
|
43
|
+
db: async () => {
|
|
44
|
+
if (!db) {
|
|
45
|
+
ensureHuddleDir(repo); // .huddle/ with its own .gitignore
|
|
46
|
+
db = new RoomDB(liveFiles(repo).db);
|
|
47
|
+
}
|
|
48
|
+
return db;
|
|
49
|
+
},
|
|
50
|
+
audience: () => {
|
|
51
|
+
const list = [...new Set(agents ?? [])];
|
|
52
|
+
return { agents: list, companies: new Set(list.map((a) => getAdapter(a)?.vendor ?? a)).size };
|
|
53
|
+
},
|
|
54
|
+
root: () => repo,
|
|
55
|
+
interactive,
|
|
56
|
+
announce: false, // the live room delivers it with each session's notes
|
|
57
|
+
};
|
|
58
|
+
return { ui, close: () => db?.close() };
|
|
59
|
+
}
|
|
60
|
+
function needRepo() {
|
|
61
|
+
const repo = mainRoot();
|
|
62
|
+
if (!repo)
|
|
63
|
+
console.log(C.red("✗ Not a git repo.") + " The shared screen belongs to a repo — cd into the one your agents work in.");
|
|
64
|
+
return repo;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A path as typed: file:// URLs and ~ resolved (cmd.exe expands neither), and
|
|
68
|
+
* Windows separators read as "/" where "\" isn't one (logs\err.log on macOS).
|
|
69
|
+
*/
|
|
70
|
+
export function cliPath(arg, cwd = process.cwd()) {
|
|
71
|
+
const p = normalizePathToken(arg);
|
|
72
|
+
if (path.sep === "\\" || !p.includes("\\") || fs.existsSync(path.resolve(cwd, p)))
|
|
73
|
+
return p;
|
|
74
|
+
return p.replace(/\\/g, "/");
|
|
75
|
+
}
|
|
76
|
+
async function readAllStdin(stdin = process.stdin) {
|
|
77
|
+
let s = "";
|
|
78
|
+
for await (const c of stdin)
|
|
79
|
+
s += c.toString();
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
/** `huddle add <file|folder|glob|->…` — `--title <t>` names a stdin item. */
|
|
83
|
+
export async function runAdd(args) {
|
|
84
|
+
const repo = needRepo();
|
|
85
|
+
if (!repo)
|
|
86
|
+
return void (process.exitCode = 1);
|
|
87
|
+
let title;
|
|
88
|
+
const paths = [];
|
|
89
|
+
let piped = false;
|
|
90
|
+
for (let i = 0; i < args.length; i++) {
|
|
91
|
+
const a = args[i];
|
|
92
|
+
if (a === "--title" && i + 1 < args.length)
|
|
93
|
+
title = args[++i];
|
|
94
|
+
else if (a.startsWith("--title="))
|
|
95
|
+
title = a.slice(8);
|
|
96
|
+
else if (a === "-")
|
|
97
|
+
piped = true;
|
|
98
|
+
else
|
|
99
|
+
paths.push(cliPath(a));
|
|
100
|
+
}
|
|
101
|
+
if (!paths.length && !piped) {
|
|
102
|
+
console.log(`usage: ${C.bold("huddle add <file|folder|glob>")} … ${C.faint("· or pipe it:")} ${C.bold("cat err.log | huddle add -")}`);
|
|
103
|
+
return void (process.exitCode = 1);
|
|
104
|
+
}
|
|
105
|
+
if (piped && process.stdin.isTTY) {
|
|
106
|
+
console.log(C.red(" ✗ nothing piped in") + C.faint(" — e.g. pbpaste | huddle add - · cat err.log | huddle add -"));
|
|
107
|
+
return void (process.exitCode = 1);
|
|
108
|
+
}
|
|
109
|
+
const agents = await liveAgents(repo);
|
|
110
|
+
// Stdin is the pipe with `-`, so nobody can answer: secrets are redacted.
|
|
111
|
+
const { ui, close } = screenUI(repo, agents, !piped && !!process.stdin.isTTY && !!process.stdout.isTTY);
|
|
112
|
+
let failed = 0;
|
|
113
|
+
let before = 0;
|
|
114
|
+
try {
|
|
115
|
+
const store = ContextStore.open((await ui.db()));
|
|
116
|
+
before = store.list().length;
|
|
117
|
+
if (piped && !(await addPiped(store, await readAllStdin(), ui, { title })))
|
|
118
|
+
failed++;
|
|
119
|
+
if (paths.length)
|
|
120
|
+
failed += await addPaths(paths, ui);
|
|
121
|
+
const added = store.list().length - before;
|
|
122
|
+
if (added > 0)
|
|
123
|
+
console.log(C.faint(agents === null
|
|
124
|
+
? " no live room right now — sessions find it with huddle_context when they're up"
|
|
125
|
+
: agents.length
|
|
126
|
+
? ` ${[...new Set(agents)].join(", ")} see it on their next huddle_* call`
|
|
127
|
+
: " live room is up; sessions that join find it with huddle_context"));
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
close();
|
|
131
|
+
}
|
|
132
|
+
if (failed)
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
}
|
|
135
|
+
/** `huddle ctx [list|show <id>|rm <id…>|note <id> <text>|clear|add <path…>]` */
|
|
136
|
+
export async function runCtx(args) {
|
|
137
|
+
const repo = needRepo();
|
|
138
|
+
if (!repo)
|
|
139
|
+
return void (process.exitCode = 1);
|
|
140
|
+
const { ui, close } = screenUI(repo, await liveAgents(repo), !!process.stdin.isTTY && !!process.stdout.isTTY);
|
|
141
|
+
try {
|
|
142
|
+
if (args[0] === "add")
|
|
143
|
+
return void (await addPaths(args.slice(1).map((a) => cliPath(a)), ui));
|
|
144
|
+
await ctxCommand(args.join(" "), ui);
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=screen.js.map
|
package/dist/attach/session.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
// other's files mid-edit; `huddle land` brings a session's branch home through
|
|
5
5
|
// the merge engine (commit → tests → rebase → fast-forward), after you say so.
|
|
6
6
|
//
|
|
7
|
-
// Plus `huddle up` / `huddle down` / `huddle peers`, the human's view of the room
|
|
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).
|
|
8
9
|
import { spawnSync } from "node:child_process";
|
|
9
10
|
import fs from "node:fs";
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import { getAdapter } from "../adapters/index.js";
|
|
13
|
+
import { spawnToolSync } from "../platform.js";
|
|
12
14
|
import { createWorktree, landMerge, prepareMerge, removeWorktree } from "../sandbox/worktree.js";
|
|
13
15
|
import { C, dot } from "../ui/term.js";
|
|
14
16
|
import { detectTestCmd } from "../ui/app.js";
|
|
@@ -78,7 +80,7 @@ export async function runAttach(args) {
|
|
|
78
80
|
const { wt, cmd } = prepareAttach(repo, agentId);
|
|
79
81
|
console.log(`${dot(agentId)} ${C.bold(agentId)} in its own worktree ${C.faint(path.relative(repo, wt.path))} on ${C.bold(wt.branch)}`);
|
|
80
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`));
|
|
81
|
-
const r =
|
|
83
|
+
const r = spawnToolSync([...cmd, ...passthrough], { cwd: wt.path, stdio: "inherit" });
|
|
82
84
|
if (r.error)
|
|
83
85
|
console.log(C.red(`✗ couldn't start ${cmd[0]}: ${r.error.message}`));
|
|
84
86
|
const changed = spawnSync("git", ["status", "--porcelain"], { cwd: wt.path, encoding: "utf8" }).stdout.trim();
|
|
@@ -162,6 +164,9 @@ export async function runPeers() {
|
|
|
162
164
|
console.log(` ${dot(s.agent)} ${C.bold(s.id.padEnd(12))} ${s.doing}`);
|
|
163
165
|
console.log(C.faint(` ${s.branch || "?"} · ${s.worktree} · holds ${s.holds.length ? s.holds.join(", ") : "nothing"} · seen ${s.seen_s_ago}s ago`));
|
|
164
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")}`);
|
|
165
170
|
const notes = r.body.notes ?? [];
|
|
166
171
|
if (notes.length) {
|
|
167
172
|
console.log(`\n ${C.bold("notes")}`);
|
package/dist/cli.js
CHANGED
|
@@ -8,96 +8,151 @@
|
|
|
8
8
|
// --agents=claude,codex only these agents work on this run
|
|
9
9
|
// --council / --no-council every agent brainstorms the design first (default: big goals)
|
|
10
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)
|
|
11
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)
|
|
12
16
|
// huddle --version print the version
|
|
13
17
|
// huddle agents --all every connector (built in + agents.json) and how to get it
|
|
14
18
|
// huddle agents install <id> install one connector's CLI, after asking
|
|
15
19
|
// huddle board print the board + shared memory
|
|
16
20
|
// huddle view [repo] the live view (ctrl-t) from another terminal, read-only
|
|
17
|
-
// huddle story [--html f] replay the latest run: who said what to whom,
|
|
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)
|
|
18
23
|
// huddle init set up .huddle/ in this repo (automatic on first run)
|
|
19
24
|
// huddle up | down attach mode: start/stop this repo's live room
|
|
20
25
|
// huddle connect | disconnect wire your own claude/codex/cursor sessions into it
|
|
21
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
|
|
22
29
|
// huddle attach <agent> open an agent in its own worktree, connected
|
|
23
30
|
// huddle land [branch] tests -> rebase -> merge an attach session's branch
|
|
24
31
|
// huddle plug (internal) the MCP server each agent talks to
|
|
25
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
|
|
26
34
|
import { packageInfo } from "./preflight.js"; // first: refuses old Node before anything else loads
|
|
27
|
-
import {
|
|
28
|
-
import { C } from "./ui/term.js";
|
|
29
|
-
import { connectAgents, ensureInit, huddleDir, printBoard, runApp, runGoal } from "./ui/app.js";
|
|
30
|
-
import { INVITE_CODE, joinRoom } from "./ui/remote.js";
|
|
31
|
-
import { startRelay } from "./relay/server.js";
|
|
32
|
-
import { runDoctor } from "./ui/doctor.js";
|
|
35
|
+
import { C, useAsciiOutput } from "./ui/term.js";
|
|
33
36
|
import { SqliteUnavailableError } from "./room/sqlite.js";
|
|
34
|
-
|
|
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.
|
|
35
43
|
const USAGE = `${C.violet("huddle")} — your agents, in the same room
|
|
36
44
|
|
|
37
|
-
${
|
|
38
|
-
${
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
${
|
|
47
|
-
${C.bold("huddle agents install <id>")} install one (asks first)
|
|
48
|
-
${C.bold("huddle relay")} ${C.faint("[--port 8080]")} run a relay so rooms work across networks
|
|
49
|
-
${C.bold("huddle board")} print the board
|
|
50
|
-
${C.bold("huddle view")} ${C.faint("[repo]")} watch a run live from another terminal ${C.faint("(read-only)")}
|
|
51
|
-
${C.bold("huddle story")} ${C.faint("[--html f]")} replay the last run: messages, reviews, merges ${C.faint("(--list, --run n)")}
|
|
52
|
-
${C.bold("huddle --version")} print the version
|
|
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
|
|
53
55
|
|
|
54
|
-
${
|
|
55
|
-
${
|
|
56
|
-
${
|
|
57
|
-
|
|
58
|
-
${
|
|
59
|
-
${
|
|
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
|
|
60
82
|
`;
|
|
61
83
|
async function main() {
|
|
62
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();
|
|
63
88
|
switch (cmd) {
|
|
64
89
|
case undefined:
|
|
65
90
|
if (!process.stdin.isTTY)
|
|
66
91
|
return void console.log(USAGE);
|
|
67
|
-
return runApp();
|
|
92
|
+
return (await app()).runApp();
|
|
68
93
|
case "plug":
|
|
69
|
-
return runPlug();
|
|
70
|
-
case "init":
|
|
94
|
+
return (await import("./mcp/server.js")).runPlug();
|
|
95
|
+
case "init": {
|
|
96
|
+
const { ensureInit, huddleDir } = await app();
|
|
71
97
|
if (!ensureInit()) {
|
|
72
98
|
console.log(C.red("✗ Not a git repo.") + " Huddle sandboxes are git worktrees — run `git init` first.");
|
|
73
99
|
process.exit(1);
|
|
74
100
|
}
|
|
75
101
|
return void console.log(`${C.green("✓")} Huddle initialized in ${C.faint(huddleDir())}`);
|
|
76
|
-
|
|
77
|
-
case "
|
|
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");
|
|
78
108
|
if (rest[0] === "install" && rest[1])
|
|
79
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);
|
|
80
112
|
if (rest.includes("--all"))
|
|
81
113
|
return printCatalog();
|
|
82
|
-
|
|
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();
|
|
83
121
|
return;
|
|
84
122
|
case "join": {
|
|
85
123
|
// `huddle join <address> <CODE>` joins someone's room; anything else is a goal.
|
|
124
|
+
const { INVITE_CODE, joinRoom } = await import("./ui/remote.js");
|
|
86
125
|
if (rest.length === 2 && INVITE_CODE.test(rest[1]))
|
|
87
126
|
return joinRoom(rest[0], rest[1]);
|
|
88
|
-
const
|
|
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(" ");
|
|
89
131
|
if (!goal) {
|
|
90
132
|
console.log(C.red("✗ Give a goal:") + ' huddle join "build the login page"');
|
|
91
133
|
process.exit(1);
|
|
92
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
|
+
}
|
|
93
147
|
const flag = rest.find((a) => a.startsWith("--approvals="))?.split("=")[1];
|
|
94
148
|
const approvals = (["safe", "auto", "yolo"].includes(flag ?? "") ? flag : "auto");
|
|
95
|
-
|
|
149
|
+
let probed = await connectAgents();
|
|
96
150
|
let results = probed;
|
|
97
|
-
// --agents=claude,codex: run with just these (the rest sit this one out).
|
|
151
|
+
// --agents=claude,codex: run with just these (the rest sit this one out) — even ones you skipped.
|
|
98
152
|
const pick = rest.find((a) => a.startsWith("--agents="))?.split("=")[1];
|
|
99
153
|
if (pick !== undefined) {
|
|
100
154
|
const ids = pick.split(",").map((s) => s.trim()).filter(Boolean);
|
|
155
|
+
results = probed = (await import("./adapters/index.js")).unskip(probed, ids);
|
|
101
156
|
const unknown = ids.filter((id) => !results.some((r) => r.adapter.id === id));
|
|
102
157
|
if (!ids.length || unknown.length) {
|
|
103
158
|
console.log(C.red(`✗ --agents: no agent called ${unknown.join(", ") || "(empty)"}.`) + ` Known: ${results.map((r) => r.adapter.id).join(", ")}`);
|
|
@@ -115,18 +170,25 @@ async function main() {
|
|
|
115
170
|
return runGoal(goal, { results, probed, mode: rest.includes("--plan") ? "plan" : "auto", approvals, council, qa });
|
|
116
171
|
}
|
|
117
172
|
case "board":
|
|
118
|
-
return printBoard();
|
|
173
|
+
return (await app()).printBoard();
|
|
119
174
|
case "view":
|
|
120
175
|
case "watch":
|
|
121
176
|
return (await import("./ui/watch.js")).runView(rest);
|
|
122
177
|
case "story":
|
|
123
178
|
return (await import("./ui/story.js")).runStory(rest);
|
|
179
|
+
case "lessons":
|
|
180
|
+
return (await import("./ui/lessons.js")).runLessons(rest);
|
|
124
181
|
case "up":
|
|
125
182
|
return (await import("./attach/session.js")).runUp(rest);
|
|
126
183
|
case "down":
|
|
127
184
|
return (await import("./attach/session.js")).runDown();
|
|
128
185
|
case "peers":
|
|
129
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);
|
|
130
192
|
case "attach":
|
|
131
193
|
return (await import("./attach/session.js")).runAttach(rest);
|
|
132
194
|
case "land":
|
|
@@ -137,6 +199,11 @@ async function main() {
|
|
|
137
199
|
return (await import("./attach/connect.js")).runDisconnect();
|
|
138
200
|
case "guard":
|
|
139
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);
|
|
140
207
|
case "relay": {
|
|
141
208
|
// Run a relay so rooms work across networks. Host it anywhere Node runs.
|
|
142
209
|
const i = rest.indexOf("--port");
|
|
@@ -145,6 +212,7 @@ async function main() {
|
|
|
145
212
|
console.log(C.red("✗ --port needs a number from 0 to 65535,") + " e.g. huddle relay --port 8080");
|
|
146
213
|
process.exit(1);
|
|
147
214
|
}
|
|
215
|
+
const { startRelay } = await import("./relay/server.js");
|
|
148
216
|
const r = await startRelay(port, (l) => console.log(C.faint(new Date().toISOString().slice(11, 19) + " " + l)));
|
|
149
217
|
console.log(`${C.violet("huddle relay")} listening on :${r.port} ${C.faint("· people use it with: relay ws://<this-host>:" + r.port)}`);
|
|
150
218
|
return;
|
|
@@ -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
|