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,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
|
+
}
|
package/dist/core/session.d.ts
CHANGED
|
@@ -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. */
|
package/dist/core/session.js
CHANGED
|
@@ -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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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)",
|
package/dist/server/daemon.js
CHANGED
|
@@ -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.
|
|
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;
|