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.
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Provider catalog — the 2026 "menu" the guided launcher shows for AI mode
3
+ * option (3): every kind of AI API provider, its setup format, docs link, and
4
+ * a known model list. Listing is FREE — it consumes zero tokens and makes zero
5
+ * network calls. A provider only becomes "active" the moment a task actually
6
+ * uses it.
7
+ *
8
+ * This is deliberately static + dependency-free: it is the vocabulary the
9
+ * launcher and Headroom knowledge doc are built from. Model lists are the
10
+ * publicly-known families as of 2026-07; the live `/models`-style discovery for
11
+ * a specific key is a separate, opt-in step (see capabilities.ts).
12
+ */
13
+ /**
14
+ * The registry. Ordered roughly by how commonly it is the user's daily driver.
15
+ * Model ids are the well-known 2026 families; keep them representative rather
16
+ * than exhaustive — the point is a browsable, $0 menu.
17
+ */
18
+ export const PROVIDER_CATALOG = [
19
+ {
20
+ id: "openai",
21
+ label: "OpenAI",
22
+ format: "openai",
23
+ maqProvider: "openai",
24
+ envVar: "OPENAI_API_KEY",
25
+ baseUrl: "https://api.openai.com/v1",
26
+ docsUrl: "https://platform.openai.com/api-keys",
27
+ setup: "export OPENAI_API_KEY=sk-... (optional OPENAI_BASE_URL)",
28
+ models: [
29
+ { id: "gpt-4o-mini", tier: "light", vision: true },
30
+ { id: "gpt-4.1-mini", tier: "mid", vision: true, longContext: true },
31
+ { id: "gpt-4o", tier: "mid", vision: true },
32
+ { id: "gpt-4.1", tier: "heavy", vision: true, longContext: true },
33
+ { id: "o4-mini", tier: "mid" },
34
+ { id: "o3", tier: "heavy", longContext: true },
35
+ ],
36
+ },
37
+ {
38
+ id: "anthropic",
39
+ label: "Anthropic (Claude)",
40
+ format: "anthropic",
41
+ maqProvider: "anthropic",
42
+ envVar: "ANTHROPIC_API_KEY",
43
+ baseUrl: "https://api.anthropic.com/v1",
44
+ docsUrl: "https://console.anthropic.com/settings/keys",
45
+ setup: "export ANTHROPIC_API_KEY=sk-ant-...",
46
+ models: [
47
+ { id: "claude-3-5-haiku-latest", tier: "light" },
48
+ { id: "claude-3-7-sonnet-latest", tier: "mid", vision: true, longContext: true },
49
+ { id: "claude-sonnet-4-latest", tier: "mid", vision: true, longContext: true },
50
+ { id: "claude-opus-4-latest", tier: "heavy", vision: true, longContext: true },
51
+ ],
52
+ },
53
+ {
54
+ id: "gemini",
55
+ label: "Google Gemini",
56
+ format: "gemini",
57
+ // Reached via an OpenAI-compatible proxy in MAQ today.
58
+ maqProvider: "openai-compatible",
59
+ envVar: "GEMINI_API_KEY",
60
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
61
+ docsUrl: "https://aistudio.google.com/apikey",
62
+ setup: "export GEMINI_API_KEY=... (OpenAI-compatible endpoint; set MAQ_PROVIDER_BASE_URL)",
63
+ models: [
64
+ { id: "gemini-2.0-flash", tier: "light", vision: true, longContext: true },
65
+ { id: "gemini-2.5-flash", tier: "mid", vision: true, longContext: true },
66
+ { id: "gemini-2.5-pro", tier: "heavy", vision: true, longContext: true },
67
+ ],
68
+ },
69
+ {
70
+ id: "groq",
71
+ label: "Groq (fast inference)",
72
+ format: "openai",
73
+ maqProvider: "groq",
74
+ envVar: "GROQ_API_KEY",
75
+ baseUrl: "https://api.groq.com/openai/v1",
76
+ docsUrl: "https://console.groq.com/keys",
77
+ setup: "export GROQ_API_KEY=gsk_... (free tier available)",
78
+ models: [
79
+ { id: "llama-3.1-8b-instant", tier: "light" },
80
+ { id: "llama-3.3-70b-versatile", tier: "mid" },
81
+ { id: "deepseek-r1-distill-llama-70b", tier: "heavy" },
82
+ ],
83
+ },
84
+ {
85
+ id: "xai",
86
+ label: "xAI (Grok)",
87
+ format: "openai",
88
+ maqProvider: "openai-compatible",
89
+ envVar: "XAI_API_KEY",
90
+ baseUrl: "https://api.x.ai/v1",
91
+ docsUrl: "https://console.x.ai",
92
+ setup: "export XAI_API_KEY=... (set MAQ_PROVIDER_BASE_URL=https://api.x.ai/v1)",
93
+ models: [
94
+ { id: "grok-3-mini", tier: "mid" },
95
+ { id: "grok-4", tier: "heavy", vision: true, longContext: true },
96
+ ],
97
+ },
98
+ {
99
+ id: "deepseek",
100
+ label: "DeepSeek",
101
+ format: "openai",
102
+ maqProvider: "openai-compatible",
103
+ envVar: "DEEPSEEK_API_KEY",
104
+ baseUrl: "https://api.deepseek.com/v1",
105
+ docsUrl: "https://platform.deepseek.com/api_keys",
106
+ setup: "export DEEPSEEK_API_KEY=... (set MAQ_PROVIDER_BASE_URL=https://api.deepseek.com/v1)",
107
+ models: [
108
+ { id: "deepseek-chat", tier: "mid", longContext: true },
109
+ { id: "deepseek-reasoner", tier: "heavy", longContext: true },
110
+ ],
111
+ },
112
+ {
113
+ id: "mistral",
114
+ label: "Mistral",
115
+ format: "openai",
116
+ maqProvider: "openai-compatible",
117
+ envVar: "MISTRAL_API_KEY",
118
+ baseUrl: "https://api.mistral.ai/v1",
119
+ docsUrl: "https://console.mistral.ai/api-keys",
120
+ setup: "export MISTRAL_API_KEY=... (set MAQ_PROVIDER_BASE_URL=https://api.mistral.ai/v1)",
121
+ models: [
122
+ { id: "mistral-small-latest", tier: "light" },
123
+ { id: "mistral-medium-latest", tier: "mid", longContext: true },
124
+ { id: "mistral-large-latest", tier: "heavy", longContext: true },
125
+ ],
126
+ },
127
+ {
128
+ id: "ollama",
129
+ label: "Ollama (local, $0)",
130
+ format: "ollama",
131
+ maqProvider: "ollama",
132
+ envVar: "",
133
+ baseUrl: "http://127.0.0.1:11434",
134
+ docsUrl: "https://ollama.com/library",
135
+ local: true,
136
+ setup: "run Ollama locally; optional OLLAMA_HOST",
137
+ models: [
138
+ { id: "llama3.2", tier: "light" },
139
+ { id: "llama3.1", tier: "mid", longContext: true },
140
+ { id: "qwen2.5-coder", tier: "mid" },
141
+ { id: "deepseek-r1", tier: "heavy" },
142
+ ],
143
+ },
144
+ {
145
+ id: "litellm",
146
+ label: "LiteLLM / any OpenAI-compatible proxy",
147
+ format: "openai",
148
+ maqProvider: "openai-compatible",
149
+ envVar: "MAQ_PROVIDER_API_KEY",
150
+ baseUrl: "http://127.0.0.1:4000",
151
+ docsUrl: "https://docs.litellm.ai/docs/simple_proxy",
152
+ setup: "point MAQ_PROVIDER_BASE_URL at your proxy (+ MAQ_PROVIDER_API_KEY)",
153
+ models: [{ id: "proxy-routed", tier: "mid" }],
154
+ },
155
+ ];
156
+ export function getCatalogProvider(id) {
157
+ return PROVIDER_CATALOG.find((p) => p.id === id);
158
+ }
159
+ /**
160
+ * The role each provider tends to play in the pipeline (fed into the Headroom
161
+ * knowledge doc). Roles: plan | code | review | summarize | fan-out.
162
+ */
163
+ const PROVIDER_GOOD_FOR = {
164
+ openai: ["plan", "code", "review"],
165
+ anthropic: ["plan", "code", "review", "summarize"],
166
+ gemini: ["summarize", "code", "fan-out"],
167
+ groq: ["fan-out", "summarize"],
168
+ xai: ["plan", "code"],
169
+ deepseek: ["code", "review"],
170
+ mistral: ["code", "summarize"],
171
+ ollama: ["fan-out", "summarize"],
172
+ litellm: ["plan", "code", "review"],
173
+ };
174
+ export function providerGoodFor(id) {
175
+ return PROVIDER_GOOD_FOR[id] ?? ["code"];
176
+ }
177
+ /**
178
+ * Detect which catalog providers are usable RIGHT NOW from the environment
179
+ * only — no network, no tokens. Local providers (Ollama) are reported as
180
+ * "actionable" (installed/reachable is confirmed later, on first use).
181
+ */
182
+ export function detectAvailableProviders(env = process.env) {
183
+ return PROVIDER_CATALOG.map((provider) => {
184
+ if (provider.local) {
185
+ return { provider, active: Boolean(env.OLLAMA_HOST) || true, reason: "local runtime (verified on first use)" };
186
+ }
187
+ const hasKey = provider.envVar ? Boolean(env[provider.envVar]) : false;
188
+ // openai-compatible providers also need a base URL configured in MAQ.
189
+ const needsBaseUrl = provider.maqProvider === "openai-compatible";
190
+ const hasBaseUrl = Boolean(env.MAQ_PROVIDER_BASE_URL);
191
+ const active = hasKey && (!needsBaseUrl || hasBaseUrl);
192
+ return {
193
+ provider,
194
+ active,
195
+ reason: hasKey
196
+ ? needsBaseUrl && !hasBaseUrl
197
+ ? `${provider.envVar} set; also set MAQ_PROVIDER_BASE_URL=${provider.baseUrl}`
198
+ : `${provider.envVar} present`
199
+ : `set ${provider.envVar} to activate`,
200
+ };
201
+ });
202
+ }
203
+ /** Flat list of every catalog model tagged with its provider (for tiering). */
204
+ export function allCatalogModels() {
205
+ return PROVIDER_CATALOG.flatMap((p) => p.models.map((m) => ({ ...m, provider: p.id, maqProvider: p.maqProvider })));
206
+ }
@@ -15,6 +15,7 @@
15
15
  * events, never a raw shared transcript.
16
16
  */
17
17
  import type { MaqEvent, PipelineResult } from "./types.js";
18
+ import { type ExecutionMode, type OrchestrationResult } from "./orchestrator.js";
18
19
  export type SessionStatus = "running" | "paused" | "cancelled" | "done" | "error";
19
20
  export interface InboxMessage {
20
21
  ts: string;
@@ -29,9 +30,11 @@ export interface Session {
29
30
  cwd: string;
30
31
  createdAt: string;
31
32
  updatedAt: string;
33
+ mode?: ExecutionMode;
32
34
  events: MaqEvent[];
33
35
  inbox: InboxMessage[];
34
36
  result?: PipelineResult;
37
+ orchestration?: OrchestrationResult;
35
38
  error?: string;
36
39
  }
37
40
  export interface SessionSummary {
@@ -44,6 +47,7 @@ export interface SessionSummary {
44
47
  updatedAt: string;
45
48
  verified?: boolean;
46
49
  eventCount: number;
50
+ mode?: ExecutionMode;
47
51
  }
48
52
  export interface CreateOptions {
49
53
  cwd?: string;
@@ -56,6 +60,14 @@ export interface CreateOptions {
56
60
  provider?: string;
57
61
  /** Model override (else config tier models). */
58
62
  model?: string;
63
+ /** Execution engine: when set, run the orchestrator instead of one pipeline. */
64
+ mode?: ExecutionMode;
65
+ /** Orchestration tuning. */
66
+ maxRounds?: number;
67
+ maxIterations?: number;
68
+ maxConcurrency?: number;
69
+ /** Permission gate for major orchestration steps (moderate mode). */
70
+ requestPermission?: (action: string, detail: string, risk: "low" | "major" | "destructive") => Promise<boolean>;
59
71
  }
60
72
  type Listener = (e: MaqEvent) => void;
61
73
  /** Thrown through the pipeline checkpoint when a session is cancelled. */
@@ -18,6 +18,7 @@ import { EventEmitter } from "node:events";
18
18
  import { randomUUID } from "node:crypto";
19
19
  import { makeEvent } from "./types.js";
20
20
  import { runPipeline } from "./pipeline.js";
21
+ import { runOrchestration } from "./orchestrator.js";
21
22
  import { ProfileManager } from "./profiles.js";
22
23
  /** Thrown through the pipeline checkpoint when a session is cancelled. */
23
24
  export class CancelError extends Error {
@@ -73,8 +74,9 @@ export class SessionRegistry {
73
74
  cwd: s.cwd,
74
75
  createdAt: s.createdAt,
75
76
  updatedAt: s.updatedAt,
76
- verified: s.result?.verify.verified,
77
+ verified: s.orchestration ? s.orchestration.verified : s.result?.verify.verified,
77
78
  eventCount: s.events.length,
79
+ mode: s.mode,
78
80
  };
79
81
  }
80
82
  /** Subscribe to live events for a session. Returns an unsubscribe fn. */
@@ -126,6 +128,7 @@ export class SessionRegistry {
126
128
  cwd: opts.cwd ?? process.cwd(),
127
129
  createdAt: now,
128
130
  updatedAt: now,
131
+ mode: opts.mode,
129
132
  events: [],
130
133
  inbox: [],
131
134
  };
@@ -142,28 +145,50 @@ export class SessionRegistry {
142
145
  c.resumeWaiters.push(() => (this.controls.get(id)?.cancelled ? reject(new CancelError()) : resolve()));
143
146
  });
144
147
  };
145
- const pipelineOpts = {
146
- cwd: session.cwd,
147
- target,
148
- provider,
149
- model,
150
- dryRun: opts.dryRun,
151
- timeoutMs: opts.timeoutMs,
152
- onEvent: (e) => this.push(session, e),
153
- signal: control.abort.signal,
154
- checkpoint,
155
- };
156
- // Run asynchronously; never throw out of assign/handoff spawn.
157
- runPipeline(task, pipelineOpts)
158
- .then((result) => {
159
- if (session.status === "cancelled")
160
- return;
161
- session.result = result;
162
- session.status = "done";
163
- session.updatedAt = new Date().toISOString();
164
- this.bus.emit(`done:${id}`);
165
- })
166
- .catch((err) => {
148
+ const onEvent = (e) => this.push(session, e);
149
+ // Choose the runner: a single pipeline, or one of the orchestration engines.
150
+ const run = opts.mode
151
+ ? runOrchestration(task, opts.mode, {
152
+ cwd: session.cwd,
153
+ target,
154
+ provider,
155
+ model,
156
+ dryRun: opts.dryRun,
157
+ maxRounds: opts.maxRounds,
158
+ maxIterations: opts.maxIterations,
159
+ maxConcurrency: opts.maxConcurrency,
160
+ onEvent,
161
+ signal: control.abort.signal,
162
+ checkpoint,
163
+ requestPermission: opts.requestPermission,
164
+ }).then((orchestration) => {
165
+ if (session.status === "cancelled")
166
+ return;
167
+ session.orchestration = orchestration;
168
+ session.status = "done";
169
+ session.updatedAt = new Date().toISOString();
170
+ this.bus.emit(`done:${id}`);
171
+ })
172
+ : runPipeline(task, {
173
+ cwd: session.cwd,
174
+ target,
175
+ provider,
176
+ model,
177
+ dryRun: opts.dryRun,
178
+ timeoutMs: opts.timeoutMs,
179
+ onEvent,
180
+ signal: control.abort.signal,
181
+ checkpoint,
182
+ }).then((result) => {
183
+ if (session.status === "cancelled")
184
+ return;
185
+ session.result = result;
186
+ session.status = "done";
187
+ session.updatedAt = new Date().toISOString();
188
+ this.bus.emit(`done:${id}`);
189
+ });
190
+ // Never throw out of assign/handoff spawn.
191
+ run.catch((err) => {
167
192
  if (err instanceof CancelError || this.controls.get(id)?.cancelled) {
168
193
  session.status = "cancelled";
169
194
  session.updatedAt = new Date().toISOString();
package/dist/index.js CHANGED
@@ -41,11 +41,17 @@ import { getTopic, listTopics, renderTopic } from "./core/help-topics.js";
41
41
  import { generateCompletion, detectShell } from "./core/completion.js";
42
42
  import { runInit } from "./core/init-wizard.js";
43
43
  import { CostTracker } from "./core/cost-tracker.js";
44
- const VERSION = "0.2.0";
44
+ import { runLauncher } from "./core/launcher.js";
45
+ import { runOrchestration } from "./core/orchestrator.js";
46
+ const VERSION = "0.5.0";
45
47
  async function main(argv) {
46
48
  const [command, ...rest] = argv;
47
49
  switch (command) {
48
50
  case undefined:
51
+ // Zero typing: no args launches the guided experience.
52
+ return runLauncher(rest[0] ?? process.cwd());
53
+ case "start":
54
+ return runLauncher(process.cwd());
49
55
  case "-h":
50
56
  case "--help":
51
57
  printHelp();
@@ -67,6 +73,8 @@ async function main(argv) {
67
73
  return cmdPlan(rest);
68
74
  case "run":
69
75
  return cmdRun(rest);
76
+ case "orchestrate":
77
+ return cmdOrchestrate(rest);
70
78
  case "verify":
71
79
  return cmdVerify(rest);
72
80
  case "serve":
@@ -253,6 +261,55 @@ async function cmdRun(args) {
253
261
  }
254
262
  return result.verify.verified && result.execute.status !== "failed" ? 0 : 3;
255
263
  }
264
+ async function cmdOrchestrate(args) {
265
+ const { values, positionals } = parseArgs({
266
+ args,
267
+ options: {
268
+ ...commonFlags(),
269
+ mode: { type: "string", short: "m" },
270
+ target: { type: "string", short: "t" },
271
+ provider: { type: "string" },
272
+ model: { type: "string" },
273
+ "dry-run": { type: "boolean", default: false },
274
+ concurrency: { type: "string" },
275
+ "max-rounds": { type: "string" },
276
+ "max-iterations": { type: "string" },
277
+ },
278
+ allowPositionals: true,
279
+ });
280
+ const goal = positionals.join(" ").trim();
281
+ const mode = values.mode ?? "parallel";
282
+ if (!goal || !["parallel", "loop", "safe"].includes(mode)) {
283
+ logger.error('usage: maq orchestrate "<goal>" --mode parallel|loop|safe [--target t] [--concurrency N] [--max-rounds N] [--max-iterations N] [--dry-run] [--json]');
284
+ return 1;
285
+ }
286
+ const streamJson = values.json;
287
+ const result = await runOrchestration(goal, mode, {
288
+ cwd: values.cwd ?? process.cwd(),
289
+ target: values.target,
290
+ provider: values.provider,
291
+ model: values.model,
292
+ dryRun: values["dry-run"],
293
+ maxConcurrency: values.concurrency ? Number(values.concurrency) : undefined,
294
+ maxRounds: values["max-rounds"] ? Number(values["max-rounds"]) : undefined,
295
+ maxIterations: values["max-iterations"] ? Number(values["max-iterations"]) : undefined,
296
+ onEvent: (e) => {
297
+ if (streamJson)
298
+ logger.out(JSON.stringify(e));
299
+ else
300
+ renderEvent(e);
301
+ },
302
+ });
303
+ if (streamJson) {
304
+ logger.out(JSON.stringify(result));
305
+ }
306
+ else {
307
+ logger.out("");
308
+ logger.out(`orchestration [${result.mode}] ${result.verified ? "VERIFIED" : "unverified"} in ${result.rounds} round(s), ${result.subtasks.length} sub-task(s)`);
309
+ logger.out(` ${result.summary}`);
310
+ }
311
+ return result.verified ? 0 : 3;
312
+ }
256
313
  async function cmdVerify(args) {
257
314
  const { values } = parseArgs({ args, options: commonFlags(), allowPositionals: true });
258
315
  const cwd = values.cwd ?? process.cwd();
@@ -982,11 +1039,15 @@ function printHelp() {
982
1039
  "",
983
1040
  "Usage: maq <command> [options]",
984
1041
  "",
1042
+ " (run `maq` with no arguments for the guided, zero-typing launcher)",
1043
+ "",
985
1044
  "Commands:",
1045
+ " start Guided launcher: pick mobile/AI mode, models, permissions",
986
1046
  " detect Scan PATH + auth dirs for worker CLIs",
987
1047
  ' scout "<task>" Read-only recon; prints structured findings',
988
1048
  ' plan "<task>" Verifier-gated candidate plan (scout + plan)',
989
1049
  ' run "<task>" [opts] Full pipeline; dispatch to a worker or raw',
1050
+ ' orchestrate "<goal>" -m MODE Run the parallel|loop|safe execution engine over a goal',
990
1051
  " verify [--cwd <dir>] Run project tests / cross-model review",
991
1052
  " serve [--host --port --token] Start the HTTP+SSE daemon (app seam)",
992
1053
  " sessions [<id>] [pause|resume|cancel] List/inspect/control daemon sessions (needs MAQ_TOKEN)",
@@ -33,6 +33,9 @@ import { probeConnectivity } from "../core/probe.js";
33
33
  import { execSafe } from "../core/exec.js";
34
34
  import { commandCatalog, maqCommands } from "../core/command-catalog.js";
35
35
  import { InteractiveRegistry } from "../core/interactive-registry.js";
36
+ import { webuiHtml } from "./webui.js";
37
+ import { PermissionBroker } from "../core/permissions.js";
38
+ import { loadConfig } from "../core/config-store.js";
36
39
  /** Generate a URL-safe token. */
37
40
  export function generateToken() {
38
41
  return randomBytes(24).toString("base64url");
@@ -48,11 +51,17 @@ export function createDaemon(opts = {}) {
48
51
  const host = opts.host ?? process.env.MAQ_HOST ?? "127.0.0.1";
49
52
  const port = opts.port ?? Number(process.env.MAQ_PORT ?? 7717);
50
53
  const token = opts.token ?? process.env.MAQ_TOKEN ?? generateToken();
51
- const version = opts.version ?? "0.2.0";
54
+ const version = opts.version ?? "0.5.0";
52
55
  const corsOrigin = opts.corsOrigin ?? process.env.MAQ_CORS_ORIGIN;
53
56
  const registry = opts.registry ?? new SessionRegistry();
54
57
  const interactive = new InteractiveRegistry();
55
58
  const startedAt = Date.now();
59
+ // The request-box. Posture comes from config (set by the guided launcher).
60
+ const permissionMode = (() => {
61
+ const m = loadConfig().permissionMode;
62
+ return m === "full" ? "full" : "moderate";
63
+ })();
64
+ const broker = new PermissionBroker(permissionMode);
56
65
  // Track live SSE responses so shutdown can end them deterministically.
57
66
  // Without this, server.close() blocks forever on the long-lived event
58
67
  // streams (they only emit a keep-alive ping every 15s and never end on their
@@ -87,6 +96,13 @@ export function createDaemon(opts = {}) {
87
96
  res.end();
88
97
  return;
89
98
  }
99
+ // The god-level control UI (no auth for the HTML shell; API calls it makes
100
+ // carry the Bearer token). Served at / and /app.
101
+ if ((path === "/" || path === "/app") && method === "GET") {
102
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
103
+ res.end(webuiHtml(version));
104
+ return;
105
+ }
90
106
  // Liveness needs no auth.
91
107
  if (path === "/health" && method === "GET") {
92
108
  sendJson(res, 200, {
@@ -115,6 +131,28 @@ export function createDaemon(opts = {}) {
115
131
  sendJson(res, 200, commandCatalog());
116
132
  return;
117
133
  }
134
+ // The permission request-box.
135
+ if (path === "/v1/requests" && method === "GET") {
136
+ sendJson(res, 200, { mode: broker.getMode(), pending: broker.pending(), requests: broker.list() });
137
+ return;
138
+ }
139
+ const reqMatch = /^\/v1\/requests\/([^/]+)$/.exec(path);
140
+ if (reqMatch && method === "POST") {
141
+ const id = decodeURIComponent(reqMatch[1]);
142
+ const body = await readJson(req);
143
+ const action = String(body.action ?? "");
144
+ let ok = false;
145
+ if (action === "approve")
146
+ ok = broker.approve(id, "web");
147
+ else if (action === "deny")
148
+ ok = broker.deny(id, "web");
149
+ else {
150
+ sendJson(res, 400, { error: "action must be approve|deny" });
151
+ return;
152
+ }
153
+ sendJson(res, ok ? 202 : 409, { id, action, ok, request: broker.get(id) });
154
+ return;
155
+ }
118
156
  // Whitelisted CLI runner — powers the app's Master (terminal) edition.
119
157
  // Only known `maq` subcommands run; never arbitrary shell.
120
158
  if (path === "/v1/exec" && method === "POST") {
@@ -197,6 +235,13 @@ export function createDaemon(opts = {}) {
197
235
  cwd: typeof body.cwd === "string" ? body.cwd : undefined,
198
236
  dryRun: Boolean(body.dryRun),
199
237
  timeoutMs: typeof body.timeoutMs === "number" ? body.timeoutMs : undefined,
238
+ mode: body.mode === "parallel" || body.mode === "loop" || body.mode === "safe" ? body.mode : undefined,
239
+ maxRounds: typeof body.maxRounds === "number" ? body.maxRounds : undefined,
240
+ maxIterations: typeof body.maxIterations === "number" ? body.maxIterations : undefined,
241
+ maxConcurrency: typeof body.maxConcurrency === "number" ? body.maxConcurrency : undefined,
242
+ // Major orchestration steps pass through the request-box, judged
243
+ // against this session's goal.
244
+ requestPermission: (action, detail, risk) => broker.gate(action, detail, { risk, goal: task }),
200
245
  });
201
246
  sendJson(res, 201, registry.summarize(s));
202
247
  return;
@@ -111,6 +111,10 @@ export class RelayBridge {
111
111
  target: typeof op.target === "string" ? op.target : undefined,
112
112
  cwd: typeof op.cwd === "string" ? op.cwd : undefined,
113
113
  dryRun: Boolean(op.dryRun),
114
+ mode: op.mode === "parallel" || op.mode === "loop" || op.mode === "safe" ? op.mode : undefined,
115
+ maxRounds: typeof op.maxRounds === "number" ? op.maxRounds : undefined,
116
+ maxIterations: typeof op.maxIterations === "number" ? op.maxIterations : undefined,
117
+ maxConcurrency: typeof op.maxConcurrency === "number" ? op.maxConcurrency : undefined,
114
118
  });
115
119
  this.reply({ kind: "created", session: reg.summarize(s), reqId });
116
120
  this.subscribe(s.id);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * webui.ts — the self-contained "god-level" control UI the daemon serves at
3
+ * `/app` (and `/`). Zero build step, zero dependencies: one HTML document with
4
+ * inline CSS + vanilla JS. It renders the SAME normalized event stream the
5
+ * mobile app consumes, so there is no second source of truth.
6
+ *
7
+ * Layout (Cursor-style, megalodon theme — deep black + electric blue + white):
8
+ * ┌ left ────────────┬ center ───────────────────────┬ right (collapsible) ┐
9
+ * │ sessions/agents/ │ goal input + mode toggle │ preview: session │
10
+ * │ history │ (parallel|loop|safe) + send │ detail / result │
11
+ * │ │ phase chips + live SSE console │ JSON │
12
+ * └──────────────────┴────────────────────────────────┴──────────────────────┘
13
+ * A feature switcher (top bar) runs detect / doctor / connectivity / models.
14
+ *
15
+ * Auth: the daemon requires a Bearer token for /v1/* . The page reads it from
16
+ * ?token=… or a prompt, keeps it in memory only, and streams SSE via fetch +
17
+ * ReadableStream (so the token rides in the Authorization header, never the URL).
18
+ */
19
+ export declare function webuiHtml(version: string): string;