maqcli 0.2.0 → 0.5.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/dist/core/capabilities.d.ts +59 -0
- package/dist/core/capabilities.js +106 -0
- package/dist/core/cli-probe.d.ts +44 -0
- package/dist/core/cli-probe.js +85 -0
- package/dist/core/command-catalog.js +6 -0
- package/dist/core/config-store.d.ts +8 -0
- package/dist/core/config-store.js +4 -0
- package/dist/core/init-wizard.js +7 -5
- package/dist/core/launcher.d.ts +77 -0
- package/dist/core/launcher.js +381 -0
- package/dist/core/onboarding.d.ts +76 -0
- package/dist/core/onboarding.js +88 -0
- package/dist/core/orchestrator.d.ts +90 -0
- package/dist/core/orchestrator.js +228 -0
- package/dist/core/permissions.d.ts +80 -0
- package/dist/core/permissions.js +147 -0
- package/dist/core/providers-catalog.d.ts +68 -0
- package/dist/core/providers-catalog.js +206 -0
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +48 -23
- package/dist/index.js +62 -1
- package/dist/server/daemon.js +46 -1
- package/dist/server/relay-bridge.js +4 -0
- package/dist/server/webui.d.ts +19 -0
- package/dist/server/webui.js +395 -0
- package/package.json +1 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guided launcher — the zero-typing entry point. Running `maq` with no
|
|
3
|
+
* arguments (or `maq start`) lands here instead of a wall of command help.
|
|
4
|
+
*
|
|
5
|
+
* Flow (all keypress-driven, no command memorization):
|
|
6
|
+
* Megalodon splash
|
|
7
|
+
* → Path A: Connect to Mobile (start daemon, show pairing + 9-digit key)
|
|
8
|
+
* → Path B: AI Mode
|
|
9
|
+
* (1) Your installed CLIs → register as $0 workers/master
|
|
10
|
+
* (2) Single model API → allowed, flagged "limited"
|
|
11
|
+
* (3) Multi-provider APIs → 2026 catalog, list-only ($0)
|
|
12
|
+
* → auto-pick the EFFICIENT (mid) model → keep auto or set Headroom model
|
|
13
|
+
* → Permissions: Full | Moderate (request-box)
|
|
14
|
+
* → build Headroom knowledge, generate 9-digit key, open the browser UI
|
|
15
|
+
*
|
|
16
|
+
* Pure helpers (auth key, browser command, onboarding apply, splash) are
|
|
17
|
+
* exported for testing; the interactive shell is a thin wrapper over them.
|
|
18
|
+
*/
|
|
19
|
+
import { createInterface } from "node:readline";
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
21
|
+
import { randomInt } from "node:crypto";
|
|
22
|
+
import { loadConfig, saveConfig } from "./config-store.js";
|
|
23
|
+
import { detectAgents } from "./registry.js";
|
|
24
|
+
import { PROVIDER_CATALOG, detectAvailableProviders, getCatalogProvider, providerGoodFor, } from "./providers-catalog.js";
|
|
25
|
+
import { classifyModel, pickEfficient } from "./capabilities.js";
|
|
26
|
+
import { buildKnowledge, saveKnowledge, roleForModel } from "./onboarding.js";
|
|
27
|
+
import { probeCliCapability } from "./cli-probe.js";
|
|
28
|
+
/* --------------------------- pure, testable ---------------------------- */
|
|
29
|
+
/** A user-facing 9-digit pairing/auth key (100000000–999999999). */
|
|
30
|
+
export function generateAuthKey() {
|
|
31
|
+
return String(randomInt(100_000_000, 1_000_000_000));
|
|
32
|
+
}
|
|
33
|
+
/** OS-specific command to open a URL in the default browser. */
|
|
34
|
+
export function browserOpenCommand(url, platform = process.platform) {
|
|
35
|
+
if (platform === "win32")
|
|
36
|
+
return { cmd: "cmd", args: ["/c", "start", "", url] };
|
|
37
|
+
if (platform === "darwin")
|
|
38
|
+
return { cmd: "open", args: [url] };
|
|
39
|
+
return { cmd: "xdg-open", args: [url] };
|
|
40
|
+
}
|
|
41
|
+
/** Best-effort: open a URL in the default browser (never throws). */
|
|
42
|
+
export function openBrowser(url) {
|
|
43
|
+
try {
|
|
44
|
+
const { cmd, args } = browserOpenCommand(url);
|
|
45
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
46
|
+
child.on("error", () => { });
|
|
47
|
+
child.unref();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* ignore */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const RED = "\x1b[38;5;196m";
|
|
54
|
+
const WHITE = "\x1b[97m";
|
|
55
|
+
const DIM = "\x1b[2m";
|
|
56
|
+
const RST = "\x1b[0m";
|
|
57
|
+
/** The Megalodon splash. `color=false` yields a plain-text version. */
|
|
58
|
+
export function megalodonSplash(color = true) {
|
|
59
|
+
const r = color ? RED : "";
|
|
60
|
+
const w = color ? WHITE : "";
|
|
61
|
+
const d = color ? DIM : "";
|
|
62
|
+
const x = color ? RST : "";
|
|
63
|
+
return [
|
|
64
|
+
"",
|
|
65
|
+
`${w} ,`,
|
|
66
|
+
`${w} ,'| ${r}▄▄${x}`,
|
|
67
|
+
`${w} ,' | ${r}▄█████▄${x}`,
|
|
68
|
+
`${w} ${d}~~~~~${x}${w} ,' | ${r}▄█████████▄${x} ${w}M A Q${x}`,
|
|
69
|
+
`${w} ${d}~~${x}${w} ,'______|${r}████████████▀${x}`,
|
|
70
|
+
`${r} ≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈████████▀${x} ${d}megalodon${x}`,
|
|
71
|
+
`${w} \`. |${r}██████▀${x}`,
|
|
72
|
+
`${w} \`. |${r}████▀${x}`,
|
|
73
|
+
`${w} \`. |${r}▀▀${x}`,
|
|
74
|
+
`${w} \`.|${x}`,
|
|
75
|
+
"",
|
|
76
|
+
`${d} the apex orchestrator — it hunts, it doesn't type.${x}`,
|
|
77
|
+
"",
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Frames of the megalodon swimming in from the left (red fin, white body). The
|
|
82
|
+
* last frame is the settled splash. Used only on a TTY; tests use the pure
|
|
83
|
+
* megalodonSplash above.
|
|
84
|
+
*/
|
|
85
|
+
export function megalodonFrames(color = true) {
|
|
86
|
+
const r = color ? RED : "";
|
|
87
|
+
const w = color ? WHITE : "";
|
|
88
|
+
const x = color ? RST : "";
|
|
89
|
+
const shark = (pad) => {
|
|
90
|
+
const p = " ".repeat(pad);
|
|
91
|
+
return [
|
|
92
|
+
"",
|
|
93
|
+
"",
|
|
94
|
+
`${p}${w} ,${x}`,
|
|
95
|
+
`${p}${w} ,'| ${r}▄▄${x}`,
|
|
96
|
+
`${p}${r}≈≈≈≈≈≈≈${w},'__|${r}████████▀${x}`,
|
|
97
|
+
`${p}${w} \`.|${r}████▀${x}`,
|
|
98
|
+
`${p}${w} \`.${r}▀${x}`,
|
|
99
|
+
"",
|
|
100
|
+
"",
|
|
101
|
+
].join("\n");
|
|
102
|
+
};
|
|
103
|
+
return [shark(0), shark(6), shark(14), shark(24), megalodonSplash(color)];
|
|
104
|
+
}
|
|
105
|
+
async function animateSplash(color) {
|
|
106
|
+
if (!process.stdout.isTTY) {
|
|
107
|
+
line(megalodonSplash(color));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const frames = megalodonFrames(color);
|
|
111
|
+
for (let i = 0; i < frames.length; i++) {
|
|
112
|
+
process.stdout.write("\x1b[2J\x1b[H"); // clear + home
|
|
113
|
+
process.stdout.write(frames[i]);
|
|
114
|
+
if (i < frames.length - 1)
|
|
115
|
+
await new Promise((res) => setTimeout(res, 110));
|
|
116
|
+
}
|
|
117
|
+
process.stdout.write("\n");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Persist an onboarding outcome: pick the efficient model if none was chosen,
|
|
121
|
+
* write config (provider/model/tiers/permission/onboarded) and the Headroom
|
|
122
|
+
* knowledge doc, and mint a 9-digit key. Pure w.r.t. I/O beyond the config +
|
|
123
|
+
* knowledge files, so it is directly testable.
|
|
124
|
+
*/
|
|
125
|
+
export function applyOnboarding(choices) {
|
|
126
|
+
const cfg = loadConfig();
|
|
127
|
+
// Resolve the Headroom (efficient) model.
|
|
128
|
+
let headroom = choices.headroom ?? null;
|
|
129
|
+
if (!headroom) {
|
|
130
|
+
const picked = pickEfficient(choices.models);
|
|
131
|
+
headroom = picked ? { provider: picked.model.maqProvider, model: picked.model.id } : null;
|
|
132
|
+
}
|
|
133
|
+
if (headroom) {
|
|
134
|
+
cfg.provider = headroom.provider;
|
|
135
|
+
cfg.masterModel = headroom.model;
|
|
136
|
+
cfg.cheapModel = headroom.model;
|
|
137
|
+
cfg.headroomModel = choices.headroomAuto ? "" : headroom.model;
|
|
138
|
+
// Reserve the strongest available model for the hard "strong" tier.
|
|
139
|
+
const heavy = choices.models.filter((m) => m.tier === "heavy")[0];
|
|
140
|
+
const mid = choices.models.filter((m) => m.tier === "mid")[0];
|
|
141
|
+
cfg.strongModel = (heavy ?? mid)?.id ?? headroom.model;
|
|
142
|
+
}
|
|
143
|
+
cfg.permissionMode = choices.permissionMode;
|
|
144
|
+
cfg.onboarded = true;
|
|
145
|
+
saveConfig(cfg);
|
|
146
|
+
// Build + persist the Headroom knowledge doc.
|
|
147
|
+
const providers = choices.models.map((m) => {
|
|
148
|
+
const role = headroom && m.id === headroom.model
|
|
149
|
+
? "headroom-master"
|
|
150
|
+
: m.tier === "light"
|
|
151
|
+
? "fan-out"
|
|
152
|
+
: m.tier === "heavy"
|
|
153
|
+
? "reviewer"
|
|
154
|
+
: "worker";
|
|
155
|
+
return roleForModel(m, role, choices.source, m.goodFor ?? []);
|
|
156
|
+
});
|
|
157
|
+
const knowledge = buildKnowledge({
|
|
158
|
+
providers,
|
|
159
|
+
permissionMode: choices.permissionMode,
|
|
160
|
+
headroomModel: headroom ? { ...headroom, auto: choices.headroomAuto } : null,
|
|
161
|
+
});
|
|
162
|
+
const knowledgePath = saveKnowledge(knowledge);
|
|
163
|
+
return { config: cfg, knowledgePath, headroom, authKey: generateAuthKey() };
|
|
164
|
+
}
|
|
165
|
+
/* ----------------------------- interactive ----------------------------- */
|
|
166
|
+
function ask(rl, q) {
|
|
167
|
+
return new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
|
|
168
|
+
}
|
|
169
|
+
const useColor = () => Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
170
|
+
function line(s = "") {
|
|
171
|
+
process.stdout.write(s + "\n");
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Run the guided launcher. Returns 0 on success. In a non-interactive context
|
|
175
|
+
* (piped stdin), it prints guidance and returns without blocking.
|
|
176
|
+
*/
|
|
177
|
+
export async function runLauncher(cwd) {
|
|
178
|
+
await animateSplash(useColor());
|
|
179
|
+
if (!process.stdin.isTTY) {
|
|
180
|
+
line("maq: guided setup needs an interactive terminal.");
|
|
181
|
+
line(" • run `maq start` in a real terminal, or");
|
|
182
|
+
line(" • use commands directly, e.g. `maq run \"<task>\" --target none` (see `maq --help`).");
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
186
|
+
try {
|
|
187
|
+
return await drive(rl, cwd);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
rl.close();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function drive(rl, cwd) {
|
|
194
|
+
line("How do you want to start?");
|
|
195
|
+
line(" [1] Connect to Mobile — pair your phone as a control surface");
|
|
196
|
+
line(" [2] AI Mode — set up the intelligence layer");
|
|
197
|
+
line(" [0] Exit to command help");
|
|
198
|
+
const path = (await ask(rl, "\n> ")) || "2";
|
|
199
|
+
if (path === "0")
|
|
200
|
+
return 1;
|
|
201
|
+
if (path === "1")
|
|
202
|
+
return await connectMobile(rl);
|
|
203
|
+
return await aiMode(rl, cwd);
|
|
204
|
+
}
|
|
205
|
+
async function aiMode(rl, cwd) {
|
|
206
|
+
line("");
|
|
207
|
+
line("AI Mode — choose your intelligence source:");
|
|
208
|
+
line(" [1] Your installed AI CLIs — $0, reuses your existing subscriptions");
|
|
209
|
+
line(" [2] A single model API — one provider/model (limited: no fan-out)");
|
|
210
|
+
line(" [3] Multi-provider APIs — the 2026 catalog, pay only when used");
|
|
211
|
+
const choice = (await ask(rl, "\n> ")) || "1";
|
|
212
|
+
let models = [];
|
|
213
|
+
let source = "api";
|
|
214
|
+
if (choice === "1") {
|
|
215
|
+
({ models, source } = await registerClis(rl));
|
|
216
|
+
}
|
|
217
|
+
else if (choice === "2") {
|
|
218
|
+
({ models, source } = await registerSingleApi(rl));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
({ models, source } = await registerMultiApi(rl));
|
|
222
|
+
}
|
|
223
|
+
if (models.length === 0) {
|
|
224
|
+
line("\nNothing registered. Falling back to the offline heuristic master ($0).");
|
|
225
|
+
models = [{ id: "heuristic-local", provider: "heuristic", maqProvider: "heuristic", tier: "mid" }];
|
|
226
|
+
source = "local";
|
|
227
|
+
}
|
|
228
|
+
// Efficient-mid auto pick, then let the user confirm or override.
|
|
229
|
+
const picked = pickEfficient(models);
|
|
230
|
+
let headroom = null;
|
|
231
|
+
let headroomAuto = true;
|
|
232
|
+
if (picked) {
|
|
233
|
+
line(`\nAuto-picked the efficient model: ${picked.model.id} (${picked.note})`);
|
|
234
|
+
const keep = (await ask(rl, "Keep this as the Headroom model? [Y/n or number to choose] ")).toLowerCase();
|
|
235
|
+
if (keep === "" || keep === "y" || keep === "yes") {
|
|
236
|
+
headroom = { provider: picked.model.maqProvider, model: picked.model.id };
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
models.forEach((m, i) => line(` [${i + 1}] ${m.id} (${m.tier})`));
|
|
240
|
+
const pick = Number(await ask(rl, "Choose a model number: "));
|
|
241
|
+
const chosen = models[pick - 1] ?? picked.model;
|
|
242
|
+
headroom = { provider: chosen.maqProvider, model: chosen.id };
|
|
243
|
+
headroomAuto = false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Permissions.
|
|
247
|
+
line("");
|
|
248
|
+
line("Permissions:");
|
|
249
|
+
line(" [1] Full — the master (and its workers) may act without stopping");
|
|
250
|
+
line(" [2] Moderate — major/destructive actions queue to a request-box for approval");
|
|
251
|
+
const perm = (await ask(rl, "\n> ")) || "2";
|
|
252
|
+
const permissionMode = perm === "1" ? "full" : "moderate";
|
|
253
|
+
const result = applyOnboarding({ models, headroom, headroomAuto, permissionMode, source });
|
|
254
|
+
line("");
|
|
255
|
+
line("─────────────────────────────────────────────");
|
|
256
|
+
line(` Headroom model : ${result.headroom?.model ?? "heuristic-local"} (${headroomAuto ? "auto" : "manual"})`);
|
|
257
|
+
line(` Permissions : ${permissionMode}`);
|
|
258
|
+
line(` Knowledge doc : ${result.knowledgePath}`);
|
|
259
|
+
line(` Auth key : ${result.authKey}`);
|
|
260
|
+
line("─────────────────────────────────────────────");
|
|
261
|
+
const open = (await ask(rl, "\nLaunch the browser UI now? [Y/n] ")).toLowerCase();
|
|
262
|
+
if (open === "" || open === "y" || open === "yes") {
|
|
263
|
+
return await launchUi(result.authKey);
|
|
264
|
+
}
|
|
265
|
+
line("\nReady. Start anytime with: maq serve (then open the printed URL)");
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
async function registerClis(rl) {
|
|
269
|
+
const agents = detectAgents();
|
|
270
|
+
const ready = agents.filter((a) => a.installed && a.authenticated);
|
|
271
|
+
if (ready.length === 0) {
|
|
272
|
+
line("\nNo authenticated AI CLI found. Install/log in to one (claude, codex, gemini, …),");
|
|
273
|
+
line("or pick option 3 (API providers). Continuing with none for now.");
|
|
274
|
+
return { models: [], source: "cli" };
|
|
275
|
+
}
|
|
276
|
+
line("\nFound these authenticated CLIs. Asking each one to self-report its capabilities (uses your own subscription, $0)…");
|
|
277
|
+
const models = [];
|
|
278
|
+
for (const a of ready) {
|
|
279
|
+
const cap = await probeCliCapability(a.name, { timeoutMs: 45000 });
|
|
280
|
+
models.push({
|
|
281
|
+
id: cap.maqProvider,
|
|
282
|
+
provider: a.name,
|
|
283
|
+
maqProvider: cap.maqProvider,
|
|
284
|
+
tier: cap.tier,
|
|
285
|
+
vision: cap.vision,
|
|
286
|
+
goodFor: cap.goodFor,
|
|
287
|
+
});
|
|
288
|
+
const how = cap.probed ? `self-reported ${cap.tier}` : `${cap.tier} (probe unavailable)`;
|
|
289
|
+
const gf = cap.goodFor.length ? ` good for: ${cap.goodFor.join(", ")}` : "";
|
|
290
|
+
line(` • ${a.name.padEnd(12)} → ${cap.maqProvider} [${how}]${gf}`);
|
|
291
|
+
}
|
|
292
|
+
return { models, source: "cli" };
|
|
293
|
+
}
|
|
294
|
+
async function registerSingleApi(rl) {
|
|
295
|
+
const available = detectAvailableProviders().filter((p) => p.active && !p.provider.local);
|
|
296
|
+
if (available.length === 0) {
|
|
297
|
+
line("\nNo API keys detected in your environment. Set one (e.g. OPENAI_API_KEY) and re-run,");
|
|
298
|
+
line("or pick option 1 (your CLIs). Continuing with none for now.");
|
|
299
|
+
return { models: [], source: "api" };
|
|
300
|
+
}
|
|
301
|
+
line("\nActive API providers:");
|
|
302
|
+
available.forEach((p, i) => line(` [${i + 1}] ${p.provider.label}`));
|
|
303
|
+
const pick = Number(await ask(rl, "Choose a provider: ")) || 1;
|
|
304
|
+
const chosen = (available[pick - 1] ?? available[0]).provider;
|
|
305
|
+
chosen.models.forEach((m, i) => line(` [${i + 1}] ${m.id} (${m.tier})`));
|
|
306
|
+
const mpick = Number(await ask(rl, "Choose a model: ")) || 1;
|
|
307
|
+
const model = chosen.models[mpick - 1] ?? chosen.models[0];
|
|
308
|
+
line("\nNote: a single model can't fan out — parallel/safe modes are disabled until you add more.");
|
|
309
|
+
return {
|
|
310
|
+
models: [{ id: model.id, provider: chosen.id, maqProvider: chosen.maqProvider, tier: model.tier, goodFor: providerGoodFor(chosen.id) }],
|
|
311
|
+
source: "api",
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
async function registerMultiApi(rl) {
|
|
315
|
+
const detected = detectAvailableProviders();
|
|
316
|
+
line("\n2026 provider catalog (● active, ○ actionable — listing is free):");
|
|
317
|
+
for (const d of detected) {
|
|
318
|
+
const mark = d.active ? "●" : "○";
|
|
319
|
+
line(` ${mark} ${d.provider.label.padEnd(38)} ${d.reason}`);
|
|
320
|
+
line(` ${d.provider.setup}`);
|
|
321
|
+
}
|
|
322
|
+
const active = detected.filter((d) => d.active);
|
|
323
|
+
if (active.length === 0) {
|
|
324
|
+
line("\nNone active yet. Set an API key from the list above, then re-run `maq start`.");
|
|
325
|
+
return { models: [], source: "api" };
|
|
326
|
+
}
|
|
327
|
+
// Register every model of every active provider (available, not consuming tokens).
|
|
328
|
+
const models = active.flatMap((d) => d.provider.models.map((m) => ({
|
|
329
|
+
id: m.id,
|
|
330
|
+
provider: d.provider.id,
|
|
331
|
+
maqProvider: d.provider.maqProvider,
|
|
332
|
+
tier: m.tier ?? classifyModel(m.id),
|
|
333
|
+
vision: m.vision,
|
|
334
|
+
longContext: m.longContext,
|
|
335
|
+
goodFor: providerGoodFor(d.provider.id),
|
|
336
|
+
})));
|
|
337
|
+
line(`\nRegistered ${models.length} models across ${active.length} active provider(s).`);
|
|
338
|
+
return { models, source: "api" };
|
|
339
|
+
}
|
|
340
|
+
async function connectMobile(rl) {
|
|
341
|
+
const authKey = generateAuthKey();
|
|
342
|
+
line("\nConnect to Mobile");
|
|
343
|
+
line(" 1. On this machine, MAQ will run its daemon (loopback + a tunnel/tailnet for remote).");
|
|
344
|
+
line(" 2. In the MAQ phone app, pair using the details below.");
|
|
345
|
+
line("");
|
|
346
|
+
line(` Auth key (pairing PIN): ${authKey}`);
|
|
347
|
+
line("");
|
|
348
|
+
line("Starting the daemon now with: maq serve");
|
|
349
|
+
line(" (the app pairs with host/port + the token the daemon prints)");
|
|
350
|
+
await ask(rl, "\nPress Enter to continue…");
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
async function launchUi(authKey) {
|
|
354
|
+
// Reuse the daemon; open its landing page. Import lazily to avoid a cycle.
|
|
355
|
+
const { createDaemon } = await import("../server/daemon.js");
|
|
356
|
+
const daemon = createDaemon({ token: authKey, version: "0.5.0" });
|
|
357
|
+
try {
|
|
358
|
+
const { host, port } = await daemon.listen();
|
|
359
|
+
const url = `http://${host}:${port}/`;
|
|
360
|
+
line(`\nmaq daemon listening on ${url}`);
|
|
361
|
+
line(`auth key/token: ${authKey}`);
|
|
362
|
+
openBrowser(url);
|
|
363
|
+
line("Opened your browser. Press Ctrl-C here to stop the daemon.");
|
|
364
|
+
await new Promise((resolve) => {
|
|
365
|
+
const stop = () => {
|
|
366
|
+
line("\nshutting down…");
|
|
367
|
+
daemon.close().then(resolve);
|
|
368
|
+
};
|
|
369
|
+
process.on("SIGINT", stop);
|
|
370
|
+
process.on("SIGTERM", stop);
|
|
371
|
+
});
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
line(`could not start daemon: ${e.message}`);
|
|
376
|
+
return 1;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** For discoverability from `maq --help` / knowledge. */
|
|
380
|
+
export const LAUNCHER_PROVIDERS = PROVIDER_CATALOG.map((p) => p.id);
|
|
381
|
+
export { getCatalogProvider };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding knowledge — the JSON "brain dump" the guided launcher hands to
|
|
3
|
+
* Headroom so the master understands, in one place: what MAQ can do (feature
|
|
4
|
+
* catalog), what role each configured provider/model plays, the permission
|
|
5
|
+
* posture, and the available execution strategies.
|
|
6
|
+
*
|
|
7
|
+
* This is NOT injected wholesale into every prompt (that would waste tokens).
|
|
8
|
+
* It is stored at ~/.maqcli/headroom-knowledge.json and surfaced *by query* —
|
|
9
|
+
* the master pulls only the relevant slice for the user's current goal. Keeping
|
|
10
|
+
* it as structured JSON is what makes selective retrieval cheap.
|
|
11
|
+
*/
|
|
12
|
+
import type { TieredModel } from "./capabilities.js";
|
|
13
|
+
export interface ProviderRole {
|
|
14
|
+
provider: string;
|
|
15
|
+
model: string;
|
|
16
|
+
tier: string;
|
|
17
|
+
/** What this model is registered to do in the pipeline. */
|
|
18
|
+
role: "headroom-master" | "worker" | "fan-out" | "reviewer";
|
|
19
|
+
goodFor: string[];
|
|
20
|
+
source: "cli" | "api" | "local";
|
|
21
|
+
}
|
|
22
|
+
export interface HeadroomKnowledge {
|
|
23
|
+
version: number;
|
|
24
|
+
generatedAt: string;
|
|
25
|
+
/** The master's own management/dev skills, surfaced by query not by default. */
|
|
26
|
+
masterSkills: string[];
|
|
27
|
+
/** MAQ feature catalog (name + what it does) for capability-aware planning. */
|
|
28
|
+
features: Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
category: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
}>;
|
|
33
|
+
/** Every registered provider/model and the role it plays. */
|
|
34
|
+
providers: ProviderRole[];
|
|
35
|
+
permissionMode: "full" | "moderate";
|
|
36
|
+
executionModes: Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
when: string;
|
|
39
|
+
}>;
|
|
40
|
+
headroomModel: {
|
|
41
|
+
provider: string;
|
|
42
|
+
model: string;
|
|
43
|
+
auto: boolean;
|
|
44
|
+
} | null;
|
|
45
|
+
}
|
|
46
|
+
/** Management + latest-development skills the master can draw on, by query. */
|
|
47
|
+
export declare const MASTER_SKILLS: string[];
|
|
48
|
+
export declare const EXECUTION_MODES: {
|
|
49
|
+
id: string;
|
|
50
|
+
when: string;
|
|
51
|
+
}[];
|
|
52
|
+
export interface BuildKnowledgeInput {
|
|
53
|
+
providers: ProviderRole[];
|
|
54
|
+
permissionMode: "full" | "moderate";
|
|
55
|
+
headroomModel?: {
|
|
56
|
+
provider: string;
|
|
57
|
+
model: string;
|
|
58
|
+
auto: boolean;
|
|
59
|
+
} | null;
|
|
60
|
+
}
|
|
61
|
+
/** Build the knowledge document from the current configuration. */
|
|
62
|
+
export declare function buildKnowledge(input: BuildKnowledgeInput): HeadroomKnowledge;
|
|
63
|
+
/** Map a discovered tiered model into a provider-role entry. */
|
|
64
|
+
export declare function roleForModel(m: TieredModel, role: ProviderRole["role"], source: ProviderRole["source"], goodFor?: string[]): ProviderRole;
|
|
65
|
+
export declare function knowledgePath(): string;
|
|
66
|
+
export declare function saveKnowledge(k: HeadroomKnowledge): string;
|
|
67
|
+
export declare function loadKnowledge(): HeadroomKnowledge | null;
|
|
68
|
+
/**
|
|
69
|
+
* Selective retrieval: return only the knowledge slices relevant to a query,
|
|
70
|
+
* so the master pays for context proportional to the task, not the whole doc.
|
|
71
|
+
*/
|
|
72
|
+
export declare function queryKnowledge(k: HeadroomKnowledge, query: string): {
|
|
73
|
+
features: HeadroomKnowledge["features"];
|
|
74
|
+
skills: string[];
|
|
75
|
+
providers: ProviderRole[];
|
|
76
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding knowledge — the JSON "brain dump" the guided launcher hands to
|
|
3
|
+
* Headroom so the master understands, in one place: what MAQ can do (feature
|
|
4
|
+
* catalog), what role each configured provider/model plays, the permission
|
|
5
|
+
* posture, and the available execution strategies.
|
|
6
|
+
*
|
|
7
|
+
* This is NOT injected wholesale into every prompt (that would waste tokens).
|
|
8
|
+
* It is stored at ~/.maqcli/headroom-knowledge.json and surfaced *by query* —
|
|
9
|
+
* the master pulls only the relevant slice for the user's current goal. Keeping
|
|
10
|
+
* it as structured JSON is what makes selective retrieval cheap.
|
|
11
|
+
*/
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { configDir } from "./config-store.js";
|
|
15
|
+
import { maqCommands } from "./command-catalog.js";
|
|
16
|
+
/** Management + latest-development skills the master can draw on, by query. */
|
|
17
|
+
export const MASTER_SKILLS = [
|
|
18
|
+
"decompose a goal into independently-verifiable sub-tasks",
|
|
19
|
+
"route each sub-task to the cheapest model that can do it (RouteLLM-style)",
|
|
20
|
+
"compress verbose tool output before it reaches a model (Headroom compress-cache-retrieve)",
|
|
21
|
+
"run read-only Scout recon before proposing changes",
|
|
22
|
+
"gate plans behind a verifier; prefer the smallest change set",
|
|
23
|
+
"verify with the project's own tests, not self-assessment",
|
|
24
|
+
"record a lesson to AGENTS.md when verification fails (self-learning)",
|
|
25
|
+
"fan out independent work in parallel, then join results",
|
|
26
|
+
"keep worker context isolated; the master sees condensed events, not raw transcripts",
|
|
27
|
+
"ask for permission before destructive or out-of-scope actions when in moderate mode",
|
|
28
|
+
];
|
|
29
|
+
export const EXECUTION_MODES = [
|
|
30
|
+
{ id: "parallel", when: "independent sub-tasks; assign per model, run at once, join by goal" },
|
|
31
|
+
{ id: "loop", when: "one hard deliverable; iterate/refine in a loop until it passes" },
|
|
32
|
+
{ id: "safe", when: "split into mini-parts on light models (no interference), merge via a loop, then parallelize" },
|
|
33
|
+
];
|
|
34
|
+
/** Build the knowledge document from the current configuration. */
|
|
35
|
+
export function buildKnowledge(input) {
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
generatedAt: new Date().toISOString(),
|
|
39
|
+
masterSkills: MASTER_SKILLS,
|
|
40
|
+
features: maqCommands.map((c) => ({ name: c.name, category: c.category, summary: c.summary })),
|
|
41
|
+
providers: input.providers,
|
|
42
|
+
permissionMode: input.permissionMode,
|
|
43
|
+
executionModes: EXECUTION_MODES,
|
|
44
|
+
headroomModel: input.headroomModel ?? null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Map a discovered tiered model into a provider-role entry. */
|
|
48
|
+
export function roleForModel(m, role, source, goodFor = []) {
|
|
49
|
+
return { provider: m.maqProvider, model: m.id, tier: m.tier, role, goodFor, source };
|
|
50
|
+
}
|
|
51
|
+
export function knowledgePath() {
|
|
52
|
+
return join(configDir(), "headroom-knowledge.json");
|
|
53
|
+
}
|
|
54
|
+
export function saveKnowledge(k) {
|
|
55
|
+
const dir = configDir();
|
|
56
|
+
if (!existsSync(dir))
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
const p = knowledgePath();
|
|
59
|
+
writeFileSync(p, JSON.stringify(k, null, 2), "utf8");
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
export function loadKnowledge() {
|
|
63
|
+
const p = knowledgePath();
|
|
64
|
+
if (!existsSync(p))
|
|
65
|
+
return null;
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Selective retrieval: return only the knowledge slices relevant to a query,
|
|
75
|
+
* so the master pays for context proportional to the task, not the whole doc.
|
|
76
|
+
*/
|
|
77
|
+
export function queryKnowledge(k, query) {
|
|
78
|
+
const q = query.toLowerCase();
|
|
79
|
+
const words = q.split(/\W+/).filter((w) => w.length >= 3);
|
|
80
|
+
const match = (text) => words.some((w) => text.toLowerCase().includes(w));
|
|
81
|
+
const features = k.features.filter((f) => match(f.name) || match(f.summary));
|
|
82
|
+
const skills = k.masterSkills.filter((s) => match(s));
|
|
83
|
+
return {
|
|
84
|
+
features: features.length ? features : k.features.slice(0, 6),
|
|
85
|
+
skills: skills.length ? skills : k.masterSkills.slice(0, 4),
|
|
86
|
+
providers: k.providers,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator — the three god-level execution engines that sit ABOVE the
|
|
3
|
+
* single Scout→Plan→Execute→Verify pipeline. The master (Headroom) never reads
|
|
4
|
+
* the full codebase; it decomposes a goal, dispatches work, and reasons only
|
|
5
|
+
* over the condensed sub-results — then decides the next move until the goal is
|
|
6
|
+
* met.
|
|
7
|
+
*
|
|
8
|
+
* parallel — independent sub-tasks assigned per model, run at once; the
|
|
9
|
+
* master joins the replies, thinks the next batch, repeats until
|
|
10
|
+
* the goal is satisfied (or maxRounds).
|
|
11
|
+
* loop — one hard deliverable; run, and if it doesn't verify, refine the
|
|
12
|
+
* instruction with the failure and retry until it passes (or
|
|
13
|
+
* maxIterations). Best for long single tasks.
|
|
14
|
+
* safe — split into mini-parts on LIGHT models in isolation (no
|
|
15
|
+
* interference), MERGE the parts via a loop, then a final parallel
|
|
16
|
+
* validation pass.
|
|
17
|
+
*
|
|
18
|
+
* Every collaborator (decompose / runTask / evaluate / merge) is injectable, so
|
|
19
|
+
* the engines are fully unit-testable offline with deterministic fakes. The
|
|
20
|
+
* default collaborators are backed by the real provider + runPipeline.
|
|
21
|
+
*/
|
|
22
|
+
import type { MaqEvent } from "./types.js";
|
|
23
|
+
export type ExecutionMode = "parallel" | "loop" | "safe";
|
|
24
|
+
export interface SubTaskResult {
|
|
25
|
+
task: string;
|
|
26
|
+
verified: boolean;
|
|
27
|
+
status: string;
|
|
28
|
+
summary: string;
|
|
29
|
+
tier?: "cheap" | "strong";
|
|
30
|
+
}
|
|
31
|
+
export interface OrchestrationResult {
|
|
32
|
+
goal: string;
|
|
33
|
+
mode: ExecutionMode;
|
|
34
|
+
rounds: number;
|
|
35
|
+
subtasks: SubTaskResult[];
|
|
36
|
+
verified: boolean;
|
|
37
|
+
summary: string;
|
|
38
|
+
}
|
|
39
|
+
export interface EvaluateResult {
|
|
40
|
+
done: boolean;
|
|
41
|
+
reason: string;
|
|
42
|
+
followups: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface RunTaskCtx {
|
|
45
|
+
tier?: "cheap" | "strong";
|
|
46
|
+
onEvent?: (e: MaqEvent) => void;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}
|
|
49
|
+
/** Injectable collaborators; defaults are provider/pipeline-backed. */
|
|
50
|
+
export interface OrchestratorDeps {
|
|
51
|
+
decompose: (goal: string, ctx: {
|
|
52
|
+
round: number;
|
|
53
|
+
prior: SubTaskResult[];
|
|
54
|
+
}) => Promise<string[]>;
|
|
55
|
+
runTask: (task: string, ctx: RunTaskCtx) => Promise<SubTaskResult>;
|
|
56
|
+
evaluate: (goal: string, results: SubTaskResult[]) => Promise<EvaluateResult>;
|
|
57
|
+
merge: (goal: string, results: SubTaskResult[], ctx: RunTaskCtx) => Promise<SubTaskResult>;
|
|
58
|
+
}
|
|
59
|
+
export interface OrchestrationOptions {
|
|
60
|
+
cwd?: string;
|
|
61
|
+
target?: string;
|
|
62
|
+
provider?: string;
|
|
63
|
+
model?: string;
|
|
64
|
+
dryRun?: boolean;
|
|
65
|
+
maxConcurrency?: number;
|
|
66
|
+
/** parallel/safe: how many decompose→run→evaluate rounds before stopping. */
|
|
67
|
+
maxRounds?: number;
|
|
68
|
+
/** loop: how many refine→retry iterations before stopping. */
|
|
69
|
+
maxIterations?: number;
|
|
70
|
+
onEvent?: (e: MaqEvent) => void;
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
checkpoint?: () => Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Permission gate consulted before MAJOR steps (moderate mode). Resolves
|
|
75
|
+
* true to proceed, false to hold. When absent, everything proceeds.
|
|
76
|
+
*/
|
|
77
|
+
requestPermission?: (action: string, detail: string, risk: "low" | "major" | "destructive") => Promise<boolean>;
|
|
78
|
+
/** Override any collaborator (tests inject deterministic ones). */
|
|
79
|
+
deps?: Partial<OrchestratorDeps>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Deterministic, offline goal splitter used as a fallback when a real model
|
|
83
|
+
* can't (or shouldn't) be asked to decompose. Splits on natural connectors.
|
|
84
|
+
*/
|
|
85
|
+
export declare function heuristicSplit(goal: string): string[];
|
|
86
|
+
/** Refine a failed task with the failure context for the next loop iteration. */
|
|
87
|
+
export declare function refineTask(goal: string, last: SubTaskResult): string;
|
|
88
|
+
/** Bounded-concurrency map (a tiny worker pool). Preserves input order. */
|
|
89
|
+
export declare function pool<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
90
|
+
export declare function runOrchestration(goal: string, mode: ExecutionMode, opts?: OrchestrationOptions): Promise<OrchestrationResult>;
|