huddle-cli 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +357 -2
- package/dist/adapters/amp.js +40 -0
- package/dist/adapters/antigravity.js +52 -0
- package/dist/adapters/claude.js +70 -0
- package/dist/adapters/codex.js +59 -0
- package/dist/adapters/copilot.js +69 -0
- package/dist/adapters/cursor.js +85 -0
- package/dist/adapters/custom.js +340 -0
- package/dist/adapters/droid.js +57 -0
- package/dist/adapters/gemini.js +61 -0
- package/dist/adapters/goose.js +55 -0
- package/dist/adapters/index.js +135 -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 +409 -0
- package/dist/attach/guard.js +64 -0
- package/dist/attach/live.js +222 -0
- package/dist/attach/plug.js +174 -0
- package/dist/attach/session.js +172 -0
- package/dist/cli.js +176 -0
- package/dist/mcp/server.js +240 -0
- package/dist/orchestrator/ask.js +51 -0
- package/dist/orchestrator/brain.js +164 -0
- package/dist/orchestrator/council.js +274 -0
- package/dist/orchestrator/handoff.js +117 -0
- package/dist/orchestrator/launcher.js +155 -0
- package/dist/orchestrator/planner.js +194 -0
- package/dist/orchestrator/qa.js +350 -0
- package/dist/orchestrator/scheduler.js +748 -0
- package/dist/orchestrator/takeover.js +85 -0
- package/dist/preflight.js +31 -0
- package/dist/relay/server.js +113 -0
- package/dist/room/db.js +617 -0
- package/dist/room/identity.js +122 -0
- package/dist/room/live.js +293 -0
- package/dist/room/progress.js +110 -0
- package/dist/room/relaylink.js +85 -0
- package/dist/room/server.js +752 -0
- package/dist/room/sqlite.js +156 -0
- package/dist/room/transport.js +107 -0
- package/dist/room/tunnel.js +85 -0
- package/dist/sandbox/sync.js +104 -0
- package/dist/sandbox/worktree.js +261 -0
- package/dist/types.js +4 -0
- package/dist/ui/app.js +1534 -0
- package/dist/ui/catalog.js +75 -0
- package/dist/ui/dashboard.js +192 -0
- package/dist/ui/doctor.js +54 -0
- package/dist/ui/logo.js +40 -0
- package/dist/ui/remote.js +332 -0
- package/dist/ui/story.js +932 -0
- package/dist/ui/term.js +114 -0
- package/dist/ui/watch.js +174 -0
- package/install.sh +44 -0
- package/package.json +60 -4
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// GitHub Copilot CLI — headless via `copilot -p`. The Plug goes in a file in
|
|
5
|
+
// .huddle/run/ handed over with --additional-mcp-config=@<file>, which adds a
|
|
6
|
+
// server "for this session only": ~/.copilot/mcp-config.json is never touched.
|
|
7
|
+
// Auth is Copilot's own (`copilot login`, or COPILOT_GITHUB_TOKEN / GH_TOKEN).
|
|
8
|
+
//
|
|
9
|
+
// Sources (checked 2026-10):
|
|
10
|
+
// https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli
|
|
11
|
+
// npm install -g @github/copilot · brew install --cask copilot-cli · curl -fsSL https://gh.io/copilot-install | bash
|
|
12
|
+
// https://docs.github.com/en/copilot/reference/cli-command-reference
|
|
13
|
+
// -p/--prompt, --model=MODEL, --allow-tool=TOOL (comma list), --deny-tool, --allow-all-tools,
|
|
14
|
+
// --allow-all, --no-ask-user, --disable-builtin-mcps, -s/--silent,
|
|
15
|
+
// --additional-mcp-config=JSON or @file, local server fields { type, command, args, env, tools },
|
|
16
|
+
// permission patterns: SERVER-NAME (all of that server's tools), write, read, shell(git push)
|
|
17
|
+
// Not yet run live on this machine. The reference says --allow-all-tools is
|
|
18
|
+
// "required when using the CLI programmatically"; safe mode instead pre-allows
|
|
19
|
+
// just the Plug + file edits, so if Copilot refuses it, use auto.
|
|
20
|
+
const PERMISSIONS = {
|
|
21
|
+
safe: ["--allow-tool=huddle,write,read"],
|
|
22
|
+
auto: ["--allow-all-tools", "--deny-tool=shell(git push)"],
|
|
23
|
+
yolo: ["--allow-all"],
|
|
24
|
+
};
|
|
25
|
+
const authed = () => ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"].some((k) => !!process.env[k]) ||
|
|
26
|
+
fs.existsSync(process.env.COPILOT_HOME ?? path.join(os.homedir(), ".copilot"));
|
|
27
|
+
export const copilotAdapter = {
|
|
28
|
+
id: "copilot",
|
|
29
|
+
vendor: "github",
|
|
30
|
+
bin: "copilot",
|
|
31
|
+
roles: ["worker", "reviewer"],
|
|
32
|
+
prefers: ["frontend", "backend", "quickfix"],
|
|
33
|
+
models: { orchestrator: "", worker: "", fast: "", reviewer: "" },
|
|
34
|
+
accountNote: "your GitHub Copilot plan",
|
|
35
|
+
homepage: "https://docs.github.com/en/copilot/how-tos/copilot-cli",
|
|
36
|
+
optional: true,
|
|
37
|
+
install: { cmd: "npm install -g @github/copilot", login: ["copilot", "login"] },
|
|
38
|
+
authProbe: {
|
|
39
|
+
cmd: ["copilot", "--version"],
|
|
40
|
+
ok: (r) => r.code === 0,
|
|
41
|
+
// Copilot has no "login status" command; a token or its config dir is the best cheap signal.
|
|
42
|
+
check: authed,
|
|
43
|
+
fixHint: "run `copilot login` (or set COPILOT_GITHUB_TOKEN)",
|
|
44
|
+
},
|
|
45
|
+
plug: {
|
|
46
|
+
file: { path: "copilot-mcp.json", in: "run-dir" },
|
|
47
|
+
entry: (plug) => ({ mcpServers: { huddle: { type: "local", ...plug, tools: ["*"] } } }),
|
|
48
|
+
},
|
|
49
|
+
ask: ({ prompt, cwd, model }) => ({
|
|
50
|
+
cmd: ["copilot", "-p", prompt, "-s", "--no-ask-user", "--deny-tool=write", "--deny-tool=shell", ...(model ? [`--model=${model}`] : [])],
|
|
51
|
+
cwd,
|
|
52
|
+
}),
|
|
53
|
+
launch: ({ worktree, prompt, model, mcpConfigPath, approvals }) => ({
|
|
54
|
+
cmd: [
|
|
55
|
+
"copilot",
|
|
56
|
+
"-p",
|
|
57
|
+
prompt,
|
|
58
|
+
...(model ? [`--model=${model}`] : []),
|
|
59
|
+
`--additional-mcp-config=@${mcpConfigPath}`,
|
|
60
|
+
// Only Huddle's Plug on top of yours — not the built-in GitHub server,
|
|
61
|
+
// so a headless agent can't open issues/PRs on your account.
|
|
62
|
+
"--disable-builtin-mcps",
|
|
63
|
+
"--no-ask-user", // nobody is there to answer
|
|
64
|
+
...PERMISSIONS[approvals],
|
|
65
|
+
],
|
|
66
|
+
cwd: worktree,
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=copilot.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { cursorUsage } from "./usage.js";
|
|
3
|
+
/**
|
|
4
|
+
* The model for quick prompt-only answers (council turns, cross-reviews), or
|
|
5
|
+
* undefined = cursor-agent's default (Auto). Found live (capstone, both
|
|
6
|
+
* rounds): Cursor's council critique timed out at 2 min. Probed with the same
|
|
7
|
+
* prompt and no tools used: Auto spent 126s thinking (12k chars) before a
|
|
8
|
+
* 120-word answer; composer-2.5 answered as well in 21s. Model ids depend on
|
|
9
|
+
* the account, so this picks only from what `cursor-agent --list-models`
|
|
10
|
+
* lists here (a "composer-<version>" id, Cursor's own fast model), once per
|
|
11
|
+
* process. HUDDLE_CURSOR_FAST_MODEL=<id> chooses one; =auto keeps the default.
|
|
12
|
+
*/
|
|
13
|
+
let fastPick;
|
|
14
|
+
export function cursorFastModel(list = listCursorModels) {
|
|
15
|
+
const env = process.env.HUDDLE_CURSOR_FAST_MODEL?.trim();
|
|
16
|
+
if (env)
|
|
17
|
+
return env === "auto" ? undefined : env;
|
|
18
|
+
if (fastPick)
|
|
19
|
+
return fastPick.model;
|
|
20
|
+
let out = "";
|
|
21
|
+
try {
|
|
22
|
+
out = list();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* not installed / signed out: keep the default */
|
|
26
|
+
}
|
|
27
|
+
const ids = out.split("\n").map((l) => /^\s*([\w.-]+)\s+-\s/.exec(l)?.[1]).filter((x) => !!x);
|
|
28
|
+
fastPick = { model: ids.find((id) => /^composer-[\d.]+$/.test(id)) };
|
|
29
|
+
return fastPick.model;
|
|
30
|
+
}
|
|
31
|
+
/** For tests: forget the cached pick. */
|
|
32
|
+
export function resetCursorFastModel() {
|
|
33
|
+
fastPick = undefined;
|
|
34
|
+
}
|
|
35
|
+
function listCursorModels() {
|
|
36
|
+
const r = spawnSync("cursor-agent", ["--list-models"], { encoding: "utf8", timeout: 15_000 });
|
|
37
|
+
return r.status === 0 ? r.stdout : "";
|
|
38
|
+
}
|
|
39
|
+
// Cursor CLI (cursor-agent) — headless coding agent. Personal, team or SSO
|
|
40
|
+
// account: Huddle doesn't care. `cursor-agent` owns its own login (SSO
|
|
41
|
+
// included); Huddle only checks that the login succeeded, then shells out to it.
|
|
42
|
+
export const cursorAdapter = {
|
|
43
|
+
id: "cursor",
|
|
44
|
+
vendor: "cursor",
|
|
45
|
+
bin: "cursor-agent",
|
|
46
|
+
roles: ["worker"],
|
|
47
|
+
prefers: ["in-editor-edits", "frontend", "quickfix"],
|
|
48
|
+
models: { orchestrator: "", worker: "", fast: "", reviewer: "" },
|
|
49
|
+
accountNote: "your Cursor account (SSO handled by cursor-agent itself)",
|
|
50
|
+
install: { cmd: "curl https://cursor.com/install -fsS | bash", login: ["cursor-agent", "login"] },
|
|
51
|
+
authProbe: {
|
|
52
|
+
cmd: ["cursor-agent", "status"],
|
|
53
|
+
ok: (r) => r.code === 0 && !/not logged in/i.test(r.stdout + r.stderr),
|
|
54
|
+
fixHint: "run `cursor-agent login` (SSO works too — Huddle never sees it)",
|
|
55
|
+
},
|
|
56
|
+
plug: {
|
|
57
|
+
// cursor-agent reads project MCP from .cursor/mcp.json in its cwd.
|
|
58
|
+
file: { path: ".cursor/mcp.json", in: "worktree" },
|
|
59
|
+
entry: (plug) => ({ mcpServers: { huddle: plug } }),
|
|
60
|
+
},
|
|
61
|
+
ask: ({ prompt, cwd, model, quick }) => {
|
|
62
|
+
// Quick asks (council turns, reviews) get the fast model when the account has one; the brain keeps its own.
|
|
63
|
+
const m = model || (quick ? cursorFastModel() : undefined);
|
|
64
|
+
return { cmd: ["cursor-agent", "-p", prompt, "--mode", "ask", "--trust", ...(m ? ["--model", m] : [])], cwd };
|
|
65
|
+
},
|
|
66
|
+
usage: cursorUsage,
|
|
67
|
+
launch: ({ worktree, prompt, model, approvals }) => ({
|
|
68
|
+
cmd: [
|
|
69
|
+
"cursor-agent",
|
|
70
|
+
"-p",
|
|
71
|
+
prompt,
|
|
72
|
+
...(model ? ["--model", model] : []),
|
|
73
|
+
// headless: trust the worktree and the Plug without prompting
|
|
74
|
+
"--trust",
|
|
75
|
+
"--approve-mcps",
|
|
76
|
+
"--output-format",
|
|
77
|
+
"stream-json", // so Huddle can count tokens
|
|
78
|
+
...(approvals === "safe" ? ["--sandbox", "enabled"] : []),
|
|
79
|
+
...(approvals === "auto" ? ["--force", "--sandbox", "enabled"] : []),
|
|
80
|
+
...(approvals === "yolo" ? ["--force", "--sandbox", "disabled"] : []),
|
|
81
|
+
],
|
|
82
|
+
cwd: worktree,
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// User-defined connectors: any agent CLI that can run headless and speak MCP
|
|
2
|
+
// can join Huddle by being DECLARED, no code and no release needed.
|
|
3
|
+
//
|
|
4
|
+
// ~/.huddle/agents.json yours, on this machine (HUDDLE_HOME moves it)
|
|
5
|
+
// <repo>/.huddle/agents.json this repo only (must not be committed — see below)
|
|
6
|
+
//
|
|
7
|
+
// A file is { "agents": [ <spec>, … ] } (a bare array works too). Each spec
|
|
8
|
+
// becomes a normal Adapter; the same id as a built-in replaces it, which is
|
|
9
|
+
// how you work around a vendor renaming a flag before Huddle ships a fix.
|
|
10
|
+
// Templates use {placeholders}; see docs/ADAPTERS.md for the full reference
|
|
11
|
+
// and a worked example.
|
|
12
|
+
//
|
|
13
|
+
// Safety: a spec names a program Huddle will run, so a repo's own file is
|
|
14
|
+
// only honored while it is untracked. A cloned repo can't smuggle in an agent.
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { agyUsage, ampUsage, claudeUsage, codexUsage, cursorUsage, qwenUsage } from "./usage.js";
|
|
20
|
+
/** Token parsers a spec can reuse by name (`"usage": "claude"`). */
|
|
21
|
+
export const USAGE_PARSERS = {
|
|
22
|
+
claude: claudeUsage, // also any CLI that emits Claude-style stream-json
|
|
23
|
+
codex: codexUsage,
|
|
24
|
+
cursor: cursorUsage,
|
|
25
|
+
agy: agyUsage,
|
|
26
|
+
gemini: agyUsage,
|
|
27
|
+
amp: ampUsage,
|
|
28
|
+
qwen: qwenUsage,
|
|
29
|
+
};
|
|
30
|
+
const SPEC_KEYS = new Set([
|
|
31
|
+
"id", "bin", "launch", "vendor", "approvals", "ask", "askOutFile", "plug", "env", "install", "login", "loginNote",
|
|
32
|
+
"auth", "usage", "roles", "prefers", "models", "accountNote", "homepage", "optional", "$comment",
|
|
33
|
+
]);
|
|
34
|
+
const ROLES = new Set(["orchestrator", "worker", "reviewer", "fast"]);
|
|
35
|
+
/** Placeholders that expand to several tokens when they are a whole token. */
|
|
36
|
+
const SPREAD = new Set(["approvals", "plug.args", "plug.argv"]);
|
|
37
|
+
const LAUNCH_VARS = new Set(["prompt", "worktree", "model", "mcpConfigPath", "plug.command", "plug.json", "plug.server", ...SPREAD]);
|
|
38
|
+
const ASK_VARS = new Set(["prompt", "cwd", "model", "outFile"]);
|
|
39
|
+
/** Placeholders that hand over the Plug (so the agent actually gets huddle_* tools). */
|
|
40
|
+
const PLUG_VARS = ["mcpConfigPath", "plug.command", "plug.json", "plug.server", "plug.args", "plug.argv"];
|
|
41
|
+
// {name}; {{name}} is left alone, so another tool's own templates pass through.
|
|
42
|
+
const PH = /(?<!\{)\{([a-zA-Z][\w.]*)\}(?!\})/g;
|
|
43
|
+
const placeholders = (s) => [...s.matchAll(PH)].map((m) => m[1]);
|
|
44
|
+
const flatStrings = (t) => t.flatMap((a) => (Array.isArray(a) ? a : [a]));
|
|
45
|
+
const isStrArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
46
|
+
/** Check one spec; returns problems (empty = valid). */
|
|
47
|
+
export function validateSpec(raw) {
|
|
48
|
+
const errs = [];
|
|
49
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
50
|
+
return ["must be an object"];
|
|
51
|
+
const s = raw;
|
|
52
|
+
for (const k of Object.keys(s))
|
|
53
|
+
if (!SPEC_KEYS.has(k))
|
|
54
|
+
errs.push(`unknown field "${k}"`);
|
|
55
|
+
if (typeof s.id !== "string" || !/^[a-z0-9][a-z0-9_-]*$/.test(s.id))
|
|
56
|
+
errs.push(`"id" must be lowercase letters, digits, - or _`);
|
|
57
|
+
if (typeof s.bin !== "string" || !s.bin.trim())
|
|
58
|
+
errs.push(`"bin" must name the executable`);
|
|
59
|
+
const checkTemplate = (field, t, allowed) => {
|
|
60
|
+
if (!Array.isArray(t) || !t.length || !t.every((a) => typeof a === "string" || isStrArray(a))) {
|
|
61
|
+
errs.push(`"${field}" must be a non-empty array of strings (or [string, …] groups)`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const strs = flatStrings(t);
|
|
65
|
+
for (const v of strs.flatMap(placeholders))
|
|
66
|
+
if (!allowed.has(v))
|
|
67
|
+
errs.push(`"${field}" uses unknown placeholder {${v}}`);
|
|
68
|
+
for (const str of strs)
|
|
69
|
+
for (const v of placeholders(str))
|
|
70
|
+
if (SPREAD.has(v) && str !== `{${v}}`)
|
|
71
|
+
errs.push(`{${v}} must be a whole token in "${field}"`);
|
|
72
|
+
if (!strs.some((x) => placeholders(x).includes("prompt")))
|
|
73
|
+
errs.push(`"${field}" must include {prompt}`);
|
|
74
|
+
};
|
|
75
|
+
checkTemplate("launch", s.launch, LAUNCH_VARS);
|
|
76
|
+
if (s.ask !== undefined)
|
|
77
|
+
checkTemplate("ask", s.ask, ASK_VARS);
|
|
78
|
+
if (s.approvals !== undefined) {
|
|
79
|
+
const a = s.approvals;
|
|
80
|
+
if (!a || typeof a !== "object" || Array.isArray(a))
|
|
81
|
+
errs.push(`"approvals" must be {safe?, auto?, yolo?}`);
|
|
82
|
+
else
|
|
83
|
+
for (const [k, v] of Object.entries(a)) {
|
|
84
|
+
if (!["safe", "auto", "yolo"].includes(k))
|
|
85
|
+
errs.push(`"approvals.${k}": modes are safe, auto, yolo`);
|
|
86
|
+
else if (!isStrArray(v))
|
|
87
|
+
errs.push(`"approvals.${k}" must be an array of strings`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let hasFile = false;
|
|
91
|
+
if (s.plug !== undefined) {
|
|
92
|
+
const p = s.plug;
|
|
93
|
+
if (!p || typeof p !== "object" || typeof p.file !== "string" || !p.file)
|
|
94
|
+
errs.push(`"plug.file" must be a path`);
|
|
95
|
+
else {
|
|
96
|
+
hasFile = true;
|
|
97
|
+
if (path.isAbsolute(p.file) || p.file.split(/[\\/]/).includes(".."))
|
|
98
|
+
errs.push(`"plug.file" must be relative, inside the worktree or run dir`);
|
|
99
|
+
if (p.in !== undefined && p.in !== "worktree" && p.in !== "run-dir")
|
|
100
|
+
errs.push(`"plug.in" is "worktree" or "run-dir"`);
|
|
101
|
+
for (const k of Object.keys(p))
|
|
102
|
+
if (!["file", "in", "entry"].includes(k))
|
|
103
|
+
errs.push(`unknown field "plug.${k}"`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (s.env !== undefined) {
|
|
107
|
+
const e = s.env;
|
|
108
|
+
if (!e || typeof e !== "object" || Array.isArray(e) || !Object.values(e).every((v) => typeof v === "string"))
|
|
109
|
+
errs.push(`"env" must map names to strings`);
|
|
110
|
+
else
|
|
111
|
+
for (const v of Object.values(e).flatMap(placeholders))
|
|
112
|
+
if (!LAUNCH_VARS.has(v) || SPREAD.has(v))
|
|
113
|
+
errs.push(`"env" uses unknown placeholder {${v}}`);
|
|
114
|
+
}
|
|
115
|
+
// The whole point is the Plug: refuse a spec that would join without huddle_* tools.
|
|
116
|
+
const used = [...(Array.isArray(s.launch) ? flatStrings(s.launch) : []), ...Object.values(s.env ?? {})]
|
|
117
|
+
.filter((x) => typeof x === "string")
|
|
118
|
+
.flatMap(placeholders);
|
|
119
|
+
if (!hasFile && used.includes("mcpConfigPath"))
|
|
120
|
+
errs.push(`{mcpConfigPath} needs "plug.file"`);
|
|
121
|
+
if (!hasFile && !used.some((v) => PLUG_VARS.includes(v)))
|
|
122
|
+
errs.push(`no way to hand it the Plug: set "plug.file", or use {plug.json} / {plug.command} {plug.args} in "launch"`);
|
|
123
|
+
if (s.login !== undefined && !isStrArray(s.login))
|
|
124
|
+
errs.push(`"login" must be an array of strings`);
|
|
125
|
+
if (s.install !== undefined && typeof s.install !== "string")
|
|
126
|
+
errs.push(`"install" must be a shell command string`);
|
|
127
|
+
if (s.auth !== undefined) {
|
|
128
|
+
const a = s.auth;
|
|
129
|
+
if (!a || typeof a !== "object")
|
|
130
|
+
errs.push(`"auth" must be an object`);
|
|
131
|
+
else {
|
|
132
|
+
if (a.cmd !== undefined && (!isStrArray(a.cmd) || !a.cmd.length))
|
|
133
|
+
errs.push(`"auth.cmd" must be a non-empty array of strings`);
|
|
134
|
+
if (a.failPattern !== undefined) {
|
|
135
|
+
try {
|
|
136
|
+
new RegExp(String(a.failPattern), "i");
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
errs.push(`"auth.failPattern" is not a valid regex`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (s.usage !== undefined && !(typeof s.usage === "string" && s.usage in USAGE_PARSERS))
|
|
145
|
+
errs.push(`"usage" is one of: ${Object.keys(USAGE_PARSERS).join(", ")}`);
|
|
146
|
+
if (s.roles !== undefined && !(isStrArray(s.roles) && s.roles.every((r) => ROLES.has(r))))
|
|
147
|
+
errs.push(`"roles" are orchestrator, worker, reviewer, fast`);
|
|
148
|
+
if (s.prefers !== undefined && !isStrArray(s.prefers))
|
|
149
|
+
errs.push(`"prefers" must be an array of strings`);
|
|
150
|
+
if (s.models !== undefined) {
|
|
151
|
+
const m = s.models;
|
|
152
|
+
if (!m || typeof m !== "object" || !Object.entries(m).every(([k, v]) => ROLES.has(k) && typeof v === "string"))
|
|
153
|
+
errs.push(`"models" maps orchestrator/worker/fast/reviewer to model names`);
|
|
154
|
+
}
|
|
155
|
+
return errs;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Fill a command template. A whole-token {approvals}/{plug.args}/{plug.argv}
|
|
159
|
+
* spreads into several tokens; a token or [group] whose placeholders are all
|
|
160
|
+
* empty (e.g. no model configured) is dropped instead of passing "".
|
|
161
|
+
*/
|
|
162
|
+
export function renderTemplate(t, vars) {
|
|
163
|
+
// `empty`: some placeholder in the token had no value — "--model={model}" with no model.
|
|
164
|
+
const one = (s) => {
|
|
165
|
+
const whole = s.match(/^\{([\w.]+)\}$/);
|
|
166
|
+
if (whole && Array.isArray(vars[whole[1]])) {
|
|
167
|
+
const arr = vars[whole[1]];
|
|
168
|
+
return { out: arr, empty: !arr.length };
|
|
169
|
+
}
|
|
170
|
+
let empty = false;
|
|
171
|
+
const out = s.replace(PH, (_, k) => {
|
|
172
|
+
const v = vars[k];
|
|
173
|
+
const str = Array.isArray(v) ? v.join(" ") : (v ?? "");
|
|
174
|
+
if (!str)
|
|
175
|
+
empty = true;
|
|
176
|
+
return str;
|
|
177
|
+
});
|
|
178
|
+
return { out: [out], empty };
|
|
179
|
+
};
|
|
180
|
+
const cmd = [];
|
|
181
|
+
for (const a of t) {
|
|
182
|
+
const parts = (Array.isArray(a) ? a : [a]).map(one);
|
|
183
|
+
if (parts.some((p) => p.empty))
|
|
184
|
+
continue;
|
|
185
|
+
cmd.push(...parts.flatMap((p) => p.out));
|
|
186
|
+
}
|
|
187
|
+
return cmd;
|
|
188
|
+
}
|
|
189
|
+
/** Fill a JSON template: "{plug.args}"/"{plug.argv}"/"{plug.env}" as whole strings become real arrays/objects. */
|
|
190
|
+
function renderJson(v, plug) {
|
|
191
|
+
if (typeof v === "string") {
|
|
192
|
+
if (v === "{plug.args}")
|
|
193
|
+
return plug.args;
|
|
194
|
+
if (v === "{plug.argv}")
|
|
195
|
+
return [plug.command, ...plug.args];
|
|
196
|
+
if (v === "{plug.env}")
|
|
197
|
+
return plug.env;
|
|
198
|
+
return v.replace(/\{plug\.command\}/g, plug.command);
|
|
199
|
+
}
|
|
200
|
+
if (Array.isArray(v))
|
|
201
|
+
return v.map((x) => renderJson(x, plug));
|
|
202
|
+
if (v && typeof v === "object")
|
|
203
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, renderJson(x, plug)]));
|
|
204
|
+
return v;
|
|
205
|
+
}
|
|
206
|
+
/** Turn a validated spec into an Adapter. */
|
|
207
|
+
export function specToAdapter(spec, source) {
|
|
208
|
+
const authCmd = spec.auth?.cmd ?? [spec.bin, "--version"];
|
|
209
|
+
const fail = spec.auth?.failPattern ? new RegExp(spec.auth.failPattern, "i") : null;
|
|
210
|
+
const plugFile = spec.plug?.file;
|
|
211
|
+
const server = (plug) => ({ command: plug.command, args: plug.args, env: plug.env });
|
|
212
|
+
return {
|
|
213
|
+
id: spec.id,
|
|
214
|
+
vendor: spec.vendor ?? spec.id,
|
|
215
|
+
bin: spec.bin,
|
|
216
|
+
roles: spec.roles ?? (spec.ask ? ["worker", "reviewer"] : ["worker"]),
|
|
217
|
+
prefers: spec.prefers ?? [],
|
|
218
|
+
models: { orchestrator: "", worker: "", fast: "", reviewer: "", ...spec.models },
|
|
219
|
+
install: { cmd: spec.install ?? "", login: spec.login, loginNote: spec.loginNote },
|
|
220
|
+
authProbe: {
|
|
221
|
+
cmd: authCmd,
|
|
222
|
+
ok: (r) => r.code === 0 && !(fail && fail.test(r.stdout + r.stderr)),
|
|
223
|
+
fixHint: spec.auth?.fixHint ?? `sign in to ${spec.bin} the way its docs say`,
|
|
224
|
+
},
|
|
225
|
+
plug: plugFile
|
|
226
|
+
? {
|
|
227
|
+
file: { path: plugFile, in: spec.plug.in ?? "run-dir" },
|
|
228
|
+
entry: (plug) => (spec.plug.entry !== undefined ? renderJson(spec.plug.entry, plug) : { mcpServers: { huddle: server(plug) } }),
|
|
229
|
+
}
|
|
230
|
+
: {},
|
|
231
|
+
launch: (ctx) => {
|
|
232
|
+
const vars = {
|
|
233
|
+
prompt: ctx.prompt,
|
|
234
|
+
worktree: ctx.worktree,
|
|
235
|
+
model: ctx.model,
|
|
236
|
+
mcpConfigPath: ctx.mcpConfigPath ?? "",
|
|
237
|
+
approvals: spec.approvals?.[ctx.approvals] ?? [],
|
|
238
|
+
"plug.command": ctx.plug.command,
|
|
239
|
+
"plug.args": ctx.plug.args,
|
|
240
|
+
"plug.argv": [ctx.plug.command, ...ctx.plug.args],
|
|
241
|
+
"plug.json": JSON.stringify({ huddle: server(ctx.plug) }),
|
|
242
|
+
"plug.server": JSON.stringify(server(ctx.plug)),
|
|
243
|
+
};
|
|
244
|
+
const env = spec.env
|
|
245
|
+
? Object.fromEntries(Object.entries(spec.env).map(([k, v]) => [k, renderTemplate([v], vars)[0] ?? ""]))
|
|
246
|
+
: undefined;
|
|
247
|
+
return { cmd: [spec.bin, ...renderTemplate(spec.launch, vars)], cwd: ctx.worktree, env };
|
|
248
|
+
},
|
|
249
|
+
ask: spec.ask
|
|
250
|
+
? ({ prompt, cwd, outFile, model }) => ({
|
|
251
|
+
cmd: [spec.bin, ...renderTemplate(spec.ask, { prompt, cwd, outFile, model: model ?? "" })],
|
|
252
|
+
cwd,
|
|
253
|
+
usesOutFile: spec.askOutFile,
|
|
254
|
+
})
|
|
255
|
+
: undefined,
|
|
256
|
+
usage: spec.usage ? USAGE_PARSERS[spec.usage] : undefined,
|
|
257
|
+
accountNote: spec.accountNote ?? (source ? `from ${tildify(source)}` : undefined),
|
|
258
|
+
homepage: spec.homepage,
|
|
259
|
+
optional: spec.optional,
|
|
260
|
+
source,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** Parse one agents.json's text. Invalid specs are skipped (and reported); valid ones still load. */
|
|
264
|
+
export function parseAgentsJson(text, source) {
|
|
265
|
+
const where = tildify(source);
|
|
266
|
+
let doc;
|
|
267
|
+
try {
|
|
268
|
+
doc = JSON.parse(text);
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
return { adapters: [], errors: [`${where}: not valid JSON (${e?.message ?? e})`] };
|
|
272
|
+
}
|
|
273
|
+
const list = Array.isArray(doc) ? doc : doc?.agents;
|
|
274
|
+
if (!Array.isArray(list))
|
|
275
|
+
return { adapters: [], errors: [`${where}: expected { "agents": [ … ] }`] };
|
|
276
|
+
const out = { adapters: [], errors: [] };
|
|
277
|
+
const seen = new Set();
|
|
278
|
+
list.forEach((raw, i) => {
|
|
279
|
+
const name = typeof raw?.id === "string" ? raw.id : `#${i + 1}`;
|
|
280
|
+
const errs = validateSpec(raw);
|
|
281
|
+
if (!errs.length && seen.has(name))
|
|
282
|
+
errs.push(`id "${name}" is declared twice`);
|
|
283
|
+
if (errs.length)
|
|
284
|
+
return void out.errors.push(...errs.map((e) => `${where} · ${name}: ${e}`));
|
|
285
|
+
seen.add(name);
|
|
286
|
+
out.adapters.push(specToAdapter(raw, source));
|
|
287
|
+
});
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
/** The two places agents.json lives, for this machine and (optionally) this repo. */
|
|
291
|
+
export function agentsFiles(repoRoot) {
|
|
292
|
+
const home = process.env.HUDDLE_HOME ?? path.join(os.homedir(), ".huddle");
|
|
293
|
+
return { user: path.join(home, "agents.json"), repo: repoRoot ? path.join(repoRoot, ".huddle", "agents.json") : null };
|
|
294
|
+
}
|
|
295
|
+
function tracked(repoRoot, file) {
|
|
296
|
+
try {
|
|
297
|
+
execFileSync("git", ["ls-files", "--error-unmatch", path.relative(repoRoot, file)], { cwd: repoRoot, stdio: "pipe" });
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function gitRoot(cwd) {
|
|
305
|
+
try {
|
|
306
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: "pipe" }).trim() || null;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** Load user then repo agents.json (repo wins on the same id). Missing files are fine. */
|
|
313
|
+
export function loadAgentsFiles(opts = {}) {
|
|
314
|
+
const repoRoot = opts.repoRoot !== undefined ? opts.repoRoot : gitRoot(opts.cwd ?? process.cwd());
|
|
315
|
+
const files = agentsFiles(repoRoot);
|
|
316
|
+
const out = { adapters: [], errors: [] };
|
|
317
|
+
for (const file of [files.user, files.repo]) {
|
|
318
|
+
if (!file || !fs.existsSync(file))
|
|
319
|
+
continue;
|
|
320
|
+
if (file === files.repo && repoRoot && tracked(repoRoot, file)) {
|
|
321
|
+
out.errors.push(`${tildify(file)}: ignored — it is committed to git, and a repo can't add agents to your machine. Move it to ${tildify(files.user)}.`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const r = parseAgentsJson(fs.readFileSync(file, "utf8"), file);
|
|
325
|
+
for (const a of r.adapters) {
|
|
326
|
+
const i = out.adapters.findIndex((x) => x.id === a.id);
|
|
327
|
+
if (i >= 0)
|
|
328
|
+
out.adapters[i] = a;
|
|
329
|
+
else
|
|
330
|
+
out.adapters.push(a);
|
|
331
|
+
}
|
|
332
|
+
out.errors.push(...r.errors);
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
function tildify(p) {
|
|
337
|
+
const home = os.homedir();
|
|
338
|
+
return p.startsWith(home + path.sep) ? "~" + p.slice(home.length) : p;
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=custom.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// Factory Droid — headless via `droid exec`. It reads project MCP servers
|
|
5
|
+
// from .factory/mcp.json in the project root, so the launcher writes the Plug
|
|
6
|
+
// into the worktree (kept out of commits). `droid exec` is read-only unless
|
|
7
|
+
// given an --auto level, which makes it a natural reviewer too. Auth is
|
|
8
|
+
// Factory's own (sign in inside `droid`, or FACTORY_API_KEY).
|
|
9
|
+
//
|
|
10
|
+
// Sources (checked 2026-10):
|
|
11
|
+
// https://docs.factory.ai/cli/getting-started/quickstart curl -fsSL https://app.factory.ai/cli | sh, brew, npm
|
|
12
|
+
// https://docs.factory.ai/cli/droid-exec/overview
|
|
13
|
+
// droid exec [prompt], -m/--model, --auto low|medium|high, --skip-permissions-unsafe,
|
|
14
|
+
// --cwd, default = read-only spec mode, FACTORY_API_KEY
|
|
15
|
+
// https://docs.factory.ai/cli/configuration/mcp
|
|
16
|
+
// project .factory/mcp.json, mcpServers { type: "stdio", command, args, env }
|
|
17
|
+
// UNVERIFIED: which --auto level is needed for project MCP tools in exec
|
|
18
|
+
// mode. Not yet run live on this machine.
|
|
19
|
+
const AUTONOMY = {
|
|
20
|
+
safe: ["--auto", "low"], // file edits
|
|
21
|
+
auto: ["--auto", "medium"], // + normal local dev commands (installs, tests)
|
|
22
|
+
yolo: ["--skip-permissions-unsafe"],
|
|
23
|
+
};
|
|
24
|
+
const authed = () => !!process.env.FACTORY_API_KEY || fs.existsSync(path.join(os.homedir(), ".factory"));
|
|
25
|
+
export const droidAdapter = {
|
|
26
|
+
id: "droid",
|
|
27
|
+
vendor: "factory",
|
|
28
|
+
bin: "droid",
|
|
29
|
+
roles: ["worker", "reviewer"],
|
|
30
|
+
prefers: ["backend", "refactor", "systems"],
|
|
31
|
+
models: { orchestrator: "", worker: "", fast: "", reviewer: "" },
|
|
32
|
+
accountNote: "your Factory account",
|
|
33
|
+
homepage: "https://docs.factory.ai/cli/getting-started/quickstart",
|
|
34
|
+
optional: true,
|
|
35
|
+
install: {
|
|
36
|
+
cmd: "curl -fsSL https://app.factory.ai/cli | sh",
|
|
37
|
+
login: ["droid"],
|
|
38
|
+
loginNote: "Droid signs you in from its own screen — once you're in, press ctrl-c to come back to Huddle",
|
|
39
|
+
},
|
|
40
|
+
authProbe: {
|
|
41
|
+
cmd: ["droid", "--version"],
|
|
42
|
+
ok: (r) => r.code === 0,
|
|
43
|
+
check: authed,
|
|
44
|
+
fixHint: "run `droid` once and sign in (or set FACTORY_API_KEY)",
|
|
45
|
+
},
|
|
46
|
+
plug: {
|
|
47
|
+
file: { path: ".factory/mcp.json", in: "worktree" },
|
|
48
|
+
entry: (plug) => ({ mcpServers: { huddle: { type: "stdio", ...plug } } }),
|
|
49
|
+
},
|
|
50
|
+
// No --auto = Droid's read-only spec mode.
|
|
51
|
+
ask: ({ prompt, cwd, model }) => ({ cmd: ["droid", "exec", ...(model ? ["-m", model] : []), prompt], cwd }),
|
|
52
|
+
launch: ({ worktree, prompt, model, approvals }) => ({
|
|
53
|
+
cmd: ["droid", "exec", ...(model ? ["-m", model] : []), ...AUTONOMY[approvals], "--cwd", worktree, prompt],
|
|
54
|
+
cwd: worktree,
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=droid.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// Gemini has no `login status` command. Since June 2026 Gemini CLI no longer
|
|
5
|
+
// serves personal Google accounts (sign-in fails with "migrate to Antigravity"),
|
|
6
|
+
// so personal OAuth doesn't count — an API key, Vertex, or a work account does.
|
|
7
|
+
// Personal accounts should use the Antigravity adapter instead.
|
|
8
|
+
function geminiAuthed() {
|
|
9
|
+
if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENAI_USE_VERTEXAI)
|
|
10
|
+
return true;
|
|
11
|
+
if (!fs.existsSync(path.join(os.homedir(), ".gemini", "oauth_creds.json")))
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
const settings = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".gemini", "settings.json"), "utf8"));
|
|
15
|
+
return settings?.security?.auth?.selectedType !== "oauth-personal";
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Gemini CLI — headless via `-p`. It reads MCP servers from .gemini/settings.json
|
|
22
|
+
// in its cwd, so the launcher writes that into the worktree (and the merge
|
|
23
|
+
// engine keeps it out of the commit). Auth is Google OAuth or GEMINI_API_KEY.
|
|
24
|
+
export const geminiAdapter = {
|
|
25
|
+
id: "gemini",
|
|
26
|
+
vendor: "google",
|
|
27
|
+
bin: "gemini",
|
|
28
|
+
roles: ["worker", "fast"],
|
|
29
|
+
prefers: ["research", "breadth", "docs", "data"],
|
|
30
|
+
models: { orchestrator: "", worker: "", fast: "", reviewer: "" },
|
|
31
|
+
accountNote: "personal Google",
|
|
32
|
+
install: { cmd: "npm install -g @google/gemini-cli" },
|
|
33
|
+
authProbe: {
|
|
34
|
+
cmd: ["gemini", "--version"],
|
|
35
|
+
ok: (r) => r.code === 0,
|
|
36
|
+
check: geminiAuthed,
|
|
37
|
+
fixHint: "personal Google accounts moved to Antigravity (June 2026) — use antigravity, or set GEMINI_API_KEY",
|
|
38
|
+
},
|
|
39
|
+
plug: {
|
|
40
|
+
file: { path: ".gemini/settings.json", in: "worktree" },
|
|
41
|
+
entry: (plug) => ({ mcpServers: { huddle: plug } }),
|
|
42
|
+
},
|
|
43
|
+
ask: ({ prompt, cwd, model }) => ({
|
|
44
|
+
cmd: ["gemini", "-p", prompt, "--approval-mode", "plan", "--skip-trust", ...(model ? ["-m", model] : [])],
|
|
45
|
+
cwd,
|
|
46
|
+
}),
|
|
47
|
+
launch: ({ worktree, prompt, model, approvals }) => ({
|
|
48
|
+
cmd: [
|
|
49
|
+
"gemini",
|
|
50
|
+
...(model ? ["-m", model] : []),
|
|
51
|
+
"-p",
|
|
52
|
+
prompt,
|
|
53
|
+
"--skip-trust", // worktrees are new folders; without trust the Plug config is ignored
|
|
54
|
+
...(approvals === "safe" ? ["--approval-mode", "auto_edit"] : []),
|
|
55
|
+
...(approvals === "auto" ? ["--approval-mode", "yolo", "--sandbox"] : []),
|
|
56
|
+
...(approvals === "yolo" ? ["--approval-mode", "yolo"] : []),
|
|
57
|
+
],
|
|
58
|
+
cwd: worktree,
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=gemini.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// goose (Block / AAIF) — headless via `goose run -t`. The Plug is added for
|
|
5
|
+
// this run only with --with-extension "huddle:ENV=… command args…", so the
|
|
6
|
+
// user's ~/.config/goose/config.yaml is never touched. Auth: goose uses the
|
|
7
|
+
// provider its owner picked in `goose configure` (keys live in goose's own
|
|
8
|
+
// config/keyring) — Huddle only checks that a config exists.
|
|
9
|
+
//
|
|
10
|
+
// Sources (checked 2026-10):
|
|
11
|
+
// https://goose-docs.ai/docs/getting-started/installation download_cli.sh (CONFIGURE=false), brew
|
|
12
|
+
// https://goose-docs.ai/docs/guides/goose-cli-commands
|
|
13
|
+
// run -t/--text, --with-extension "[name:]ENV1=val1 command args...", --model, --provider,
|
|
14
|
+
// --output-format text|json|stream-json, goose configure, goose info
|
|
15
|
+
// https://goose-docs.ai/docs/guides/goose-permissions Autonomous mode is the default
|
|
16
|
+
// Approvals: headless goose runs in its default (autonomous) mode in all three
|
|
17
|
+
// Huddle modes; the worktree is the fence. --model needs a matching provider,
|
|
18
|
+
// so Huddle passes neither. UNVERIFIED: how goose splits the extension string
|
|
19
|
+
// when a path contains spaces (tokens with spaces are double-quoted here).
|
|
20
|
+
// No `ask`: goose has no documented read-only flag. Not yet run live.
|
|
21
|
+
const quote = (s) => (/[\s"'\\]/.test(s) ? JSON.stringify(s) : s);
|
|
22
|
+
export function gooseExtension(plug) {
|
|
23
|
+
const env = Object.entries(plug.env).map(([k, v]) => `${k}=${quote(v)}`);
|
|
24
|
+
return `huddle:${[...env, quote(plug.command), ...plug.args.map(quote)].join(" ")}`;
|
|
25
|
+
}
|
|
26
|
+
const configured = () => !!process.env.GOOSE_PROVIDER ||
|
|
27
|
+
fs.existsSync(path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "goose", "config.yaml"));
|
|
28
|
+
export const gooseAdapter = {
|
|
29
|
+
id: "goose",
|
|
30
|
+
vendor: "block",
|
|
31
|
+
bin: "goose",
|
|
32
|
+
roles: ["worker"],
|
|
33
|
+
prefers: ["backend", "data", "docs"],
|
|
34
|
+
models: { orchestrator: "", worker: "", fast: "", reviewer: "" },
|
|
35
|
+
accountNote: "the provider in your goose config",
|
|
36
|
+
homepage: "https://goose-docs.ai/docs/getting-started/installation",
|
|
37
|
+
optional: true,
|
|
38
|
+
install: {
|
|
39
|
+
cmd: "curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash",
|
|
40
|
+
login: ["goose", "configure"],
|
|
41
|
+
loginNote: "pick a provider and paste its key in goose's own setup — Huddle never sees it",
|
|
42
|
+
},
|
|
43
|
+
authProbe: {
|
|
44
|
+
cmd: ["goose", "--version"],
|
|
45
|
+
ok: (r) => r.code === 0,
|
|
46
|
+
check: configured,
|
|
47
|
+
fixHint: "run `goose configure` and pick a provider",
|
|
48
|
+
},
|
|
49
|
+
plug: {},
|
|
50
|
+
launch: ({ worktree, prompt, plug }) => ({
|
|
51
|
+
cmd: ["goose", "run", "--with-extension", gooseExtension(plug), "-t", prompt],
|
|
52
|
+
cwd: worktree,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=goose.js.map
|