maqcli 0.2.0 → 0.4.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 +57 -0
- package/dist/core/capabilities.js +106 -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 +71 -0
- package/dist/core/launcher.js +340 -0
- package/dist/core/onboarding.d.ts +76 -0
- package/dist/core/onboarding.js +88 -0
- package/dist/core/orchestrator.d.ts +85 -0
- package/dist/core/orchestrator.js +220 -0
- package/dist/core/providers-catalog.d.ts +67 -0
- package/dist/core/providers-catalog.js +188 -0
- package/dist/core/session.d.ts +10 -0
- package/dist/core/session.js +47 -23
- package/dist/index.js +62 -1
- package/dist/server/daemon.js +13 -1
- package/dist/server/relay-bridge.js +4 -0
- package/dist/server/webui.d.ts +19 -0
- package/dist/server/webui.js +319 -0
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability tiering — the brain behind "pick an EFFICIENT model, not the
|
|
3
|
+
* cheapest and not the most expensive".
|
|
4
|
+
*
|
|
5
|
+
* Every model the launcher discovers (from the static catalog, from a worker
|
|
6
|
+
* CLI's /models list, or from a live provider) is classified into one of three
|
|
7
|
+
* tiers. The guided launcher's AUTO choice for the Headroom model is the best
|
|
8
|
+
* available MID model: enough capability to manage the whole god-level flow
|
|
9
|
+
* without paying for max power or crippling it with a toy model.
|
|
10
|
+
*
|
|
11
|
+
* light — triage/summaries/cheap fan-out workers
|
|
12
|
+
* mid — the efficient default: plans, routing, Headroom management
|
|
13
|
+
* heavy — reserved for the hardest single steps (opt-in / manual)
|
|
14
|
+
*/
|
|
15
|
+
export type CapabilityTier = "light" | "mid" | "heavy";
|
|
16
|
+
export interface TieredModel {
|
|
17
|
+
id: string;
|
|
18
|
+
provider: string;
|
|
19
|
+
/** MAQ provider name for getProvider(). */
|
|
20
|
+
maqProvider: string;
|
|
21
|
+
tier: CapabilityTier;
|
|
22
|
+
vision?: boolean;
|
|
23
|
+
longContext?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Heuristic classifier for a raw model id when we have no catalog tag (e.g. a
|
|
27
|
+
* model name returned by a CLI's `/models`). Pattern-based, deterministic.
|
|
28
|
+
*/
|
|
29
|
+
export declare function classifyModel(id: string): CapabilityTier;
|
|
30
|
+
export declare function tierRank(t: CapabilityTier): number;
|
|
31
|
+
/**
|
|
32
|
+
* Pick the efficient model from a set of available tiered models.
|
|
33
|
+
*
|
|
34
|
+
* Preference: a MID model first; if none, fall back to the strongest LIGHT
|
|
35
|
+
* (so we never silently jump to an expensive HEAVY as the "auto efficient"
|
|
36
|
+
* default). If only HEAVY exists, use it but flag it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pickEfficient(models: TieredModel[]): {
|
|
39
|
+
model: TieredModel;
|
|
40
|
+
note: string;
|
|
41
|
+
} | null;
|
|
42
|
+
/** Split a set of models into their tier buckets (for display). */
|
|
43
|
+
export declare function groupByTier(models: TieredModel[]): Record<CapabilityTier, TieredModel[]>;
|
|
44
|
+
/**
|
|
45
|
+
* The instruction MAQ hands a worker CLI (option 1) so it self-reports its
|
|
46
|
+
* capabilities in a machine-parseable form. Kept tiny and format-strict so the
|
|
47
|
+
* reply can be stored directly into the Headroom knowledge doc.
|
|
48
|
+
*/
|
|
49
|
+
export declare const CAPABILITY_PROBE_INSTRUCTION: string;
|
|
50
|
+
/** Parse a capability-probe reply into a partial TieredModel-ish record. */
|
|
51
|
+
export declare function parseCapabilityReply(reply: string): {
|
|
52
|
+
tier: CapabilityTier;
|
|
53
|
+
strengths: string[];
|
|
54
|
+
contextTokens?: number;
|
|
55
|
+
vision?: boolean;
|
|
56
|
+
goodFor: string[];
|
|
57
|
+
} | null;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability tiering — the brain behind "pick an EFFICIENT model, not the
|
|
3
|
+
* cheapest and not the most expensive".
|
|
4
|
+
*
|
|
5
|
+
* Every model the launcher discovers (from the static catalog, from a worker
|
|
6
|
+
* CLI's /models list, or from a live provider) is classified into one of three
|
|
7
|
+
* tiers. The guided launcher's AUTO choice for the Headroom model is the best
|
|
8
|
+
* available MID model: enough capability to manage the whole god-level flow
|
|
9
|
+
* without paying for max power or crippling it with a toy model.
|
|
10
|
+
*
|
|
11
|
+
* light — triage/summaries/cheap fan-out workers
|
|
12
|
+
* mid — the efficient default: plans, routing, Headroom management
|
|
13
|
+
* heavy — reserved for the hardest single steps (opt-in / manual)
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Heuristic classifier for a raw model id when we have no catalog tag (e.g. a
|
|
17
|
+
* model name returned by a CLI's `/models`). Pattern-based, deterministic.
|
|
18
|
+
*/
|
|
19
|
+
export function classifyModel(id) {
|
|
20
|
+
const s = id.toLowerCase();
|
|
21
|
+
// Heavy: flagship / reasoning / "pro"/"opus"/"large"/"ultra" families.
|
|
22
|
+
if (/(opus|-4\.1$|-4\.5|ultra|large|o3\b|reasoner|r1\b|pro\b|405b|grok-4)/.test(s)) {
|
|
23
|
+
return "heavy";
|
|
24
|
+
}
|
|
25
|
+
// Reasoning "mini" models (o1/o3/o4-mini) are efficient MID, not light.
|
|
26
|
+
if (/\bo[1345]-mini/.test(s)) {
|
|
27
|
+
return "mid";
|
|
28
|
+
}
|
|
29
|
+
// Light: explicitly small / fast / nano / haiku / mini / 8b / flash-lite.
|
|
30
|
+
// NB: \bmini avoids matching "geMINI".
|
|
31
|
+
if (/(nano|\bmini|instant|haiku|flash-lite|-8b|-7b|1\.5-flash|3\.2$|small)/.test(s)) {
|
|
32
|
+
return "light";
|
|
33
|
+
}
|
|
34
|
+
// Mid: the efficient middle — flash, sonnet, medium, 70b, coder.
|
|
35
|
+
if (/(flash|sonnet|medium|70b|coder|grok-3|deepseek-chat)/.test(s)) {
|
|
36
|
+
return "mid";
|
|
37
|
+
}
|
|
38
|
+
// Default unknown to mid so AUTO stays "efficient" rather than risky.
|
|
39
|
+
return "mid";
|
|
40
|
+
}
|
|
41
|
+
const TIER_ORDER = { light: 0, mid: 1, heavy: 2 };
|
|
42
|
+
export function tierRank(t) {
|
|
43
|
+
return TIER_ORDER[t];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Pick the efficient model from a set of available tiered models.
|
|
47
|
+
*
|
|
48
|
+
* Preference: a MID model first; if none, fall back to the strongest LIGHT
|
|
49
|
+
* (so we never silently jump to an expensive HEAVY as the "auto efficient"
|
|
50
|
+
* default). If only HEAVY exists, use it but flag it.
|
|
51
|
+
*/
|
|
52
|
+
export function pickEfficient(models) {
|
|
53
|
+
if (models.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
const mids = models.filter((m) => m.tier === "mid");
|
|
56
|
+
if (mids.length > 0) {
|
|
57
|
+
// Prefer a mid model with long context (better for Headroom management).
|
|
58
|
+
const best = mids.find((m) => m.longContext) ?? mids[0];
|
|
59
|
+
return { model: best, note: "efficient mid-tier: balanced capability vs cost" };
|
|
60
|
+
}
|
|
61
|
+
const lights = models.filter((m) => m.tier === "light");
|
|
62
|
+
if (lights.length > 0) {
|
|
63
|
+
return { model: lights[0], note: "no mid model available; using the strongest light model" };
|
|
64
|
+
}
|
|
65
|
+
return { model: models[0], note: "only heavy models available; consider a mid model to save cost" };
|
|
66
|
+
}
|
|
67
|
+
/** Split a set of models into their tier buckets (for display). */
|
|
68
|
+
export function groupByTier(models) {
|
|
69
|
+
const out = { light: [], mid: [], heavy: [] };
|
|
70
|
+
for (const m of models)
|
|
71
|
+
out[m.tier].push(m);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The instruction MAQ hands a worker CLI (option 1) so it self-reports its
|
|
76
|
+
* capabilities in a machine-parseable form. Kept tiny and format-strict so the
|
|
77
|
+
* reply can be stored directly into the Headroom knowledge doc.
|
|
78
|
+
*/
|
|
79
|
+
export const CAPABILITY_PROBE_INSTRUCTION = [
|
|
80
|
+
"You are being registered as a worker model inside the MAQ orchestrator.",
|
|
81
|
+
"Reply with ONLY a compact JSON object, no prose, in exactly this shape:",
|
|
82
|
+
'{"tier":"light|mid|heavy","strengths":["..."],"context_tokens":<int>,"vision":<bool>,"good_for":["plan"|"code"|"review"|"summarize"|"fan-out"]}',
|
|
83
|
+
"Base it on your own known capabilities. Do not include markdown fences.",
|
|
84
|
+
].join("\n");
|
|
85
|
+
/** Parse a capability-probe reply into a partial TieredModel-ish record. */
|
|
86
|
+
export function parseCapabilityReply(reply) {
|
|
87
|
+
const trimmed = reply.trim().replace(/^```(json)?/i, "").replace(/```$/, "").trim();
|
|
88
|
+
const start = trimmed.indexOf("{");
|
|
89
|
+
const end = trimmed.lastIndexOf("}");
|
|
90
|
+
if (start === -1 || end === -1 || end <= start)
|
|
91
|
+
return null;
|
|
92
|
+
try {
|
|
93
|
+
const j = JSON.parse(trimmed.slice(start, end + 1));
|
|
94
|
+
const tier = ["light", "mid", "heavy"].includes(j.tier) ? j.tier : "mid";
|
|
95
|
+
return {
|
|
96
|
+
tier,
|
|
97
|
+
strengths: Array.isArray(j.strengths) ? j.strengths.map(String) : [],
|
|
98
|
+
contextTokens: typeof j.context_tokens === "number" ? j.context_tokens : undefined,
|
|
99
|
+
vision: Boolean(j.vision),
|
|
100
|
+
goodFor: Array.isArray(j.good_for) ? j.good_for.map(String) : [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -18,6 +18,12 @@ export const maqCommands = [
|
|
|
18
18
|
{ name: "audit", type: "boolean", description: "write hash-chained audit log" },
|
|
19
19
|
] },
|
|
20
20
|
{ name: "scout", category: "pipeline", summary: "Read-only recon; structured findings (0 tokens).", usage: 'maq scout "<task>"', needsInput: "task", args: [] },
|
|
21
|
+
{ name: "orchestrate", category: "pipeline", summary: "Run a goal through the parallel/loop/safe execution engine.", usage: 'maq orchestrate "<goal>" -m parallel|loop|safe', needsInput: "task",
|
|
22
|
+
args: [
|
|
23
|
+
{ name: "mode", type: "enum", choices: ["parallel", "loop", "safe"], required: true, description: "execution engine" },
|
|
24
|
+
{ name: "target", type: "enum", choices: ["auto", "claude-code", "codex", "gemini", "none"], description: "worker CLI" },
|
|
25
|
+
{ name: "concurrency", type: "string", description: "max parallel sub-tasks" },
|
|
26
|
+
] },
|
|
21
27
|
{ name: "plan", category: "pipeline", summary: "Verifier-gated candidate plan.", usage: 'maq plan "<task>"', needsInput: "task", args: [] },
|
|
22
28
|
{ name: "verify", category: "pipeline", summary: "Run project tests / cross-model review.", usage: "maq verify [--cwd d]", needsInput: "none", args: [] },
|
|
23
29
|
{ name: "swarm", category: "pipeline", summary: "Run several tasks across parallel workers, then join.", usage: 'maq swarm "<t1>" "<t2>" … [--target t] [--concurrency N]', needsInput: "none", args: [{ name: "concurrency", type: "string", description: "max parallel workers" }] },
|
|
@@ -23,6 +23,14 @@ export interface MaqConfig {
|
|
|
23
23
|
projectTargets: Record<string, string>;
|
|
24
24
|
/** Default permission level (strict or standard) */
|
|
25
25
|
defaultPermission: string;
|
|
26
|
+
/** Guided-launcher posture: "full" (all allowed) | "moderate" (request-box). */
|
|
27
|
+
permissionMode: string;
|
|
28
|
+
/** Default execution strategy: "parallel" | "loop" | "safe". */
|
|
29
|
+
executionMode: string;
|
|
30
|
+
/** The efficient (mid) model the Headroom master runs on; "" = auto. */
|
|
31
|
+
headroomModel: string;
|
|
32
|
+
/** True once the guided launcher has completed first-run setup. */
|
|
33
|
+
onboarded: boolean;
|
|
26
34
|
}
|
|
27
35
|
export declare const DEFAULT_CONFIG: MaqConfig;
|
|
28
36
|
export declare function configDir(): string;
|
|
@@ -15,6 +15,10 @@ export const DEFAULT_CONFIG = {
|
|
|
15
15
|
compactionThreshold: 0.6,
|
|
16
16
|
projectTargets: {},
|
|
17
17
|
defaultPermission: "standard",
|
|
18
|
+
permissionMode: "moderate",
|
|
19
|
+
executionMode: "loop",
|
|
20
|
+
headroomModel: "",
|
|
21
|
+
onboarded: false,
|
|
18
22
|
};
|
|
19
23
|
export function configDir() {
|
|
20
24
|
return process.env.MAQ_CONFIG_DIR ?? join(homedir(), ".maqcli");
|
package/dist/core/init-wizard.js
CHANGED
|
@@ -92,13 +92,15 @@ async function _runWizard(rl, cwd, summary) {
|
|
|
92
92
|
console.log(" ─────────────────────────────");
|
|
93
93
|
console.log("");
|
|
94
94
|
/* ── 2. Auto-detect agents ──────────────────────────────────── */
|
|
95
|
-
const agents =
|
|
96
|
-
|
|
95
|
+
const agents = detectAgents();
|
|
96
|
+
const installed = agents.filter((a) => a.installed);
|
|
97
|
+
if (installed.length > 0) {
|
|
97
98
|
console.log(" Detected agents:");
|
|
98
|
-
for (const agent of
|
|
99
|
-
|
|
99
|
+
for (const agent of installed) {
|
|
100
|
+
const status = agent.authenticated ? "authenticated" : "installed (logged out)";
|
|
101
|
+
console.log(` • ${agent.name} — ${status}`);
|
|
100
102
|
}
|
|
101
|
-
summary.push(`Detected ${
|
|
103
|
+
summary.push(`Detected ${installed.length} agent(s): ${installed.map((a) => a.name).join(", ")}`);
|
|
102
104
|
}
|
|
103
105
|
else {
|
|
104
106
|
console.log(" No agents detected.");
|
|
@@ -0,0 +1,71 @@
|
|
|
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 { type MaqConfig } from "./config-store.js";
|
|
20
|
+
import { getCatalogProvider } from "./providers-catalog.js";
|
|
21
|
+
import { type TieredModel } from "./capabilities.js";
|
|
22
|
+
import { type ProviderRole } from "./onboarding.js";
|
|
23
|
+
/** A user-facing 9-digit pairing/auth key (100000000–999999999). */
|
|
24
|
+
export declare function generateAuthKey(): string;
|
|
25
|
+
/** OS-specific command to open a URL in the default browser. */
|
|
26
|
+
export declare function browserOpenCommand(url: string, platform?: NodeJS.Platform): {
|
|
27
|
+
cmd: string;
|
|
28
|
+
args: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Best-effort: open a URL in the default browser (never throws). */
|
|
31
|
+
export declare function openBrowser(url: string): void;
|
|
32
|
+
/** The Megalodon splash. `color=false` yields a plain-text version. */
|
|
33
|
+
export declare function megalodonSplash(color?: boolean): string;
|
|
34
|
+
export interface OnboardingChoices {
|
|
35
|
+
/** Registered worker/master models (already tiered). */
|
|
36
|
+
models: TieredModel[];
|
|
37
|
+
/** Chosen Headroom (efficient) model; null lets applyOnboarding auto-pick. */
|
|
38
|
+
headroom?: {
|
|
39
|
+
provider: string;
|
|
40
|
+
model: string;
|
|
41
|
+
} | null;
|
|
42
|
+
/** Whether the Headroom model was auto-picked. */
|
|
43
|
+
headroomAuto: boolean;
|
|
44
|
+
permissionMode: "full" | "moderate";
|
|
45
|
+
/** How each model was sourced, for the knowledge doc. */
|
|
46
|
+
source: ProviderRole["source"];
|
|
47
|
+
}
|
|
48
|
+
export interface OnboardingResult {
|
|
49
|
+
config: MaqConfig;
|
|
50
|
+
knowledgePath: string;
|
|
51
|
+
headroom: {
|
|
52
|
+
provider: string;
|
|
53
|
+
model: string;
|
|
54
|
+
} | null;
|
|
55
|
+
authKey: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Persist an onboarding outcome: pick the efficient model if none was chosen,
|
|
59
|
+
* write config (provider/model/tiers/permission/onboarded) and the Headroom
|
|
60
|
+
* knowledge doc, and mint a 9-digit key. Pure w.r.t. I/O beyond the config +
|
|
61
|
+
* knowledge files, so it is directly testable.
|
|
62
|
+
*/
|
|
63
|
+
export declare function applyOnboarding(choices: OnboardingChoices): OnboardingResult;
|
|
64
|
+
/**
|
|
65
|
+
* Run the guided launcher. Returns 0 on success. In a non-interactive context
|
|
66
|
+
* (piped stdin), it prints guidance and returns without blocking.
|
|
67
|
+
*/
|
|
68
|
+
export declare function runLauncher(cwd: string): Promise<number>;
|
|
69
|
+
/** For discoverability from `maq --help` / knowledge. */
|
|
70
|
+
export declare const LAUNCHER_PROVIDERS: string[];
|
|
71
|
+
export { getCatalogProvider };
|
|
@@ -0,0 +1,340 @@
|
|
|
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, } from "./providers-catalog.js";
|
|
25
|
+
import { classifyModel, pickEfficient } from "./capabilities.js";
|
|
26
|
+
import { buildKnowledge, saveKnowledge, roleForModel } from "./onboarding.js";
|
|
27
|
+
/** Preferred lightest master invocation per detected CLI agent ($0 marginal). */
|
|
28
|
+
const CLI_MASTER_HINT = {
|
|
29
|
+
gemini: "cli:gemini",
|
|
30
|
+
"claude-code": "cli:claude-code",
|
|
31
|
+
codex: "cli:codex",
|
|
32
|
+
opencode: "cli:opencode",
|
|
33
|
+
"amazon-q": "cli:amazon-q",
|
|
34
|
+
aider: "cli:aider",
|
|
35
|
+
};
|
|
36
|
+
/* --------------------------- pure, testable ---------------------------- */
|
|
37
|
+
/** A user-facing 9-digit pairing/auth key (100000000–999999999). */
|
|
38
|
+
export function generateAuthKey() {
|
|
39
|
+
return String(randomInt(100_000_000, 1_000_000_000));
|
|
40
|
+
}
|
|
41
|
+
/** OS-specific command to open a URL in the default browser. */
|
|
42
|
+
export function browserOpenCommand(url, platform = process.platform) {
|
|
43
|
+
if (platform === "win32")
|
|
44
|
+
return { cmd: "cmd", args: ["/c", "start", "", url] };
|
|
45
|
+
if (platform === "darwin")
|
|
46
|
+
return { cmd: "open", args: [url] };
|
|
47
|
+
return { cmd: "xdg-open", args: [url] };
|
|
48
|
+
}
|
|
49
|
+
/** Best-effort: open a URL in the default browser (never throws). */
|
|
50
|
+
export function openBrowser(url) {
|
|
51
|
+
try {
|
|
52
|
+
const { cmd, args } = browserOpenCommand(url);
|
|
53
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
54
|
+
child.on("error", () => { });
|
|
55
|
+
child.unref();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* ignore */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const RED = "\x1b[38;5;196m";
|
|
62
|
+
const WHITE = "\x1b[97m";
|
|
63
|
+
const DIM = "\x1b[2m";
|
|
64
|
+
const RST = "\x1b[0m";
|
|
65
|
+
/** The Megalodon splash. `color=false` yields a plain-text version. */
|
|
66
|
+
export function megalodonSplash(color = true) {
|
|
67
|
+
const r = color ? RED : "";
|
|
68
|
+
const w = color ? WHITE : "";
|
|
69
|
+
const d = color ? DIM : "";
|
|
70
|
+
const x = color ? RST : "";
|
|
71
|
+
return [
|
|
72
|
+
"",
|
|
73
|
+
`${w} ,`,
|
|
74
|
+
`${w} ,'| ${r}▄▄${x}`,
|
|
75
|
+
`${w} ,' | ${r}▄█████▄${x}`,
|
|
76
|
+
`${w} ${d}~~~~~${x}${w} ,' | ${r}▄█████████▄${x} ${w}M A Q${x}`,
|
|
77
|
+
`${w} ${d}~~${x}${w} ,'______|${r}████████████▀${x}`,
|
|
78
|
+
`${r} ≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈████████▀${x} ${d}megalodon${x}`,
|
|
79
|
+
`${w} \`. |${r}██████▀${x}`,
|
|
80
|
+
`${w} \`. |${r}████▀${x}`,
|
|
81
|
+
`${w} \`. |${r}▀▀${x}`,
|
|
82
|
+
`${w} \`.|${x}`,
|
|
83
|
+
"",
|
|
84
|
+
`${d} the apex orchestrator — it hunts, it doesn't type.${x}`,
|
|
85
|
+
"",
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Persist an onboarding outcome: pick the efficient model if none was chosen,
|
|
90
|
+
* write config (provider/model/tiers/permission/onboarded) and the Headroom
|
|
91
|
+
* knowledge doc, and mint a 9-digit key. Pure w.r.t. I/O beyond the config +
|
|
92
|
+
* knowledge files, so it is directly testable.
|
|
93
|
+
*/
|
|
94
|
+
export function applyOnboarding(choices) {
|
|
95
|
+
const cfg = loadConfig();
|
|
96
|
+
// Resolve the Headroom (efficient) model.
|
|
97
|
+
let headroom = choices.headroom ?? null;
|
|
98
|
+
if (!headroom) {
|
|
99
|
+
const picked = pickEfficient(choices.models);
|
|
100
|
+
headroom = picked ? { provider: picked.model.maqProvider, model: picked.model.id } : null;
|
|
101
|
+
}
|
|
102
|
+
if (headroom) {
|
|
103
|
+
cfg.provider = headroom.provider;
|
|
104
|
+
cfg.masterModel = headroom.model;
|
|
105
|
+
cfg.cheapModel = headroom.model;
|
|
106
|
+
cfg.headroomModel = choices.headroomAuto ? "" : headroom.model;
|
|
107
|
+
// Reserve the strongest available model for the hard "strong" tier.
|
|
108
|
+
const heavy = choices.models.filter((m) => m.tier === "heavy")[0];
|
|
109
|
+
const mid = choices.models.filter((m) => m.tier === "mid")[0];
|
|
110
|
+
cfg.strongModel = (heavy ?? mid)?.id ?? headroom.model;
|
|
111
|
+
}
|
|
112
|
+
cfg.permissionMode = choices.permissionMode;
|
|
113
|
+
cfg.onboarded = true;
|
|
114
|
+
saveConfig(cfg);
|
|
115
|
+
// Build + persist the Headroom knowledge doc.
|
|
116
|
+
const providers = choices.models.map((m) => {
|
|
117
|
+
const role = headroom && m.id === headroom.model
|
|
118
|
+
? "headroom-master"
|
|
119
|
+
: m.tier === "light"
|
|
120
|
+
? "fan-out"
|
|
121
|
+
: m.tier === "heavy"
|
|
122
|
+
? "reviewer"
|
|
123
|
+
: "worker";
|
|
124
|
+
return roleForModel(m, role, choices.source);
|
|
125
|
+
});
|
|
126
|
+
const knowledge = buildKnowledge({
|
|
127
|
+
providers,
|
|
128
|
+
permissionMode: choices.permissionMode,
|
|
129
|
+
headroomModel: headroom ? { ...headroom, auto: choices.headroomAuto } : null,
|
|
130
|
+
});
|
|
131
|
+
const knowledgePath = saveKnowledge(knowledge);
|
|
132
|
+
return { config: cfg, knowledgePath, headroom, authKey: generateAuthKey() };
|
|
133
|
+
}
|
|
134
|
+
/* ----------------------------- interactive ----------------------------- */
|
|
135
|
+
function ask(rl, q) {
|
|
136
|
+
return new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
|
|
137
|
+
}
|
|
138
|
+
const useColor = () => Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
139
|
+
function line(s = "") {
|
|
140
|
+
process.stdout.write(s + "\n");
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Run the guided launcher. Returns 0 on success. In a non-interactive context
|
|
144
|
+
* (piped stdin), it prints guidance and returns without blocking.
|
|
145
|
+
*/
|
|
146
|
+
export async function runLauncher(cwd) {
|
|
147
|
+
line(megalodonSplash(useColor()));
|
|
148
|
+
if (!process.stdin.isTTY) {
|
|
149
|
+
line("maq: guided setup needs an interactive terminal.");
|
|
150
|
+
line(" • run `maq start` in a real terminal, or");
|
|
151
|
+
line(" • use commands directly, e.g. `maq run \"<task>\" --target none` (see `maq --help`).");
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
155
|
+
try {
|
|
156
|
+
return await drive(rl, cwd);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
rl.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function drive(rl, cwd) {
|
|
163
|
+
line("How do you want to start?");
|
|
164
|
+
line(" [1] Connect to Mobile — pair your phone as a control surface");
|
|
165
|
+
line(" [2] AI Mode — set up the intelligence layer");
|
|
166
|
+
line(" [0] Exit to command help");
|
|
167
|
+
const path = (await ask(rl, "\n> ")) || "2";
|
|
168
|
+
if (path === "0")
|
|
169
|
+
return 1;
|
|
170
|
+
if (path === "1")
|
|
171
|
+
return await connectMobile(rl);
|
|
172
|
+
return await aiMode(rl, cwd);
|
|
173
|
+
}
|
|
174
|
+
async function aiMode(rl, cwd) {
|
|
175
|
+
line("");
|
|
176
|
+
line("AI Mode — choose your intelligence source:");
|
|
177
|
+
line(" [1] Your installed AI CLIs — $0, reuses your existing subscriptions");
|
|
178
|
+
line(" [2] A single model API — one provider/model (limited: no fan-out)");
|
|
179
|
+
line(" [3] Multi-provider APIs — the 2026 catalog, pay only when used");
|
|
180
|
+
const choice = (await ask(rl, "\n> ")) || "1";
|
|
181
|
+
let models = [];
|
|
182
|
+
let source = "api";
|
|
183
|
+
if (choice === "1") {
|
|
184
|
+
({ models, source } = await registerClis(rl));
|
|
185
|
+
}
|
|
186
|
+
else if (choice === "2") {
|
|
187
|
+
({ models, source } = await registerSingleApi(rl));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
({ models, source } = await registerMultiApi(rl));
|
|
191
|
+
}
|
|
192
|
+
if (models.length === 0) {
|
|
193
|
+
line("\nNothing registered. Falling back to the offline heuristic master ($0).");
|
|
194
|
+
models = [{ id: "heuristic-local", provider: "heuristic", maqProvider: "heuristic", tier: "mid" }];
|
|
195
|
+
source = "local";
|
|
196
|
+
}
|
|
197
|
+
// Efficient-mid auto pick, then let the user confirm or override.
|
|
198
|
+
const picked = pickEfficient(models);
|
|
199
|
+
let headroom = null;
|
|
200
|
+
let headroomAuto = true;
|
|
201
|
+
if (picked) {
|
|
202
|
+
line(`\nAuto-picked the efficient model: ${picked.model.id} (${picked.note})`);
|
|
203
|
+
const keep = (await ask(rl, "Keep this as the Headroom model? [Y/n or number to choose] ")).toLowerCase();
|
|
204
|
+
if (keep === "" || keep === "y" || keep === "yes") {
|
|
205
|
+
headroom = { provider: picked.model.maqProvider, model: picked.model.id };
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
models.forEach((m, i) => line(` [${i + 1}] ${m.id} (${m.tier})`));
|
|
209
|
+
const pick = Number(await ask(rl, "Choose a model number: "));
|
|
210
|
+
const chosen = models[pick - 1] ?? picked.model;
|
|
211
|
+
headroom = { provider: chosen.maqProvider, model: chosen.id };
|
|
212
|
+
headroomAuto = false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Permissions.
|
|
216
|
+
line("");
|
|
217
|
+
line("Permissions:");
|
|
218
|
+
line(" [1] Full — the master (and its workers) may act without stopping");
|
|
219
|
+
line(" [2] Moderate — major/destructive actions queue to a request-box for approval");
|
|
220
|
+
const perm = (await ask(rl, "\n> ")) || "2";
|
|
221
|
+
const permissionMode = perm === "1" ? "full" : "moderate";
|
|
222
|
+
const result = applyOnboarding({ models, headroom, headroomAuto, permissionMode, source });
|
|
223
|
+
line("");
|
|
224
|
+
line("─────────────────────────────────────────────");
|
|
225
|
+
line(` Headroom model : ${result.headroom?.model ?? "heuristic-local"} (${headroomAuto ? "auto" : "manual"})`);
|
|
226
|
+
line(` Permissions : ${permissionMode}`);
|
|
227
|
+
line(` Knowledge doc : ${result.knowledgePath}`);
|
|
228
|
+
line(` Auth key : ${result.authKey}`);
|
|
229
|
+
line("─────────────────────────────────────────────");
|
|
230
|
+
const open = (await ask(rl, "\nLaunch the browser UI now? [Y/n] ")).toLowerCase();
|
|
231
|
+
if (open === "" || open === "y" || open === "yes") {
|
|
232
|
+
return await launchUi(result.authKey);
|
|
233
|
+
}
|
|
234
|
+
line("\nReady. Start anytime with: maq serve (then open the printed URL)");
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
async function registerClis(rl) {
|
|
238
|
+
const agents = detectAgents();
|
|
239
|
+
const ready = agents.filter((a) => a.installed && a.authenticated);
|
|
240
|
+
if (ready.length === 0) {
|
|
241
|
+
line("\nNo authenticated AI CLI found. Install/log in to one (claude, codex, gemini, …),");
|
|
242
|
+
line("or pick option 3 (API providers). Continuing with none for now.");
|
|
243
|
+
return { models: [], source: "cli" };
|
|
244
|
+
}
|
|
245
|
+
line("\nFound these authenticated CLIs (registered as $0 workers):");
|
|
246
|
+
const models = [];
|
|
247
|
+
for (const a of ready) {
|
|
248
|
+
const masterModel = CLI_MASTER_HINT[a.name] ?? `cli:${a.name}`;
|
|
249
|
+
// A CLI reused as a master is treated as a mid-tier worker for tiering.
|
|
250
|
+
models.push({ id: masterModel, provider: a.name, maqProvider: masterModel, tier: "mid" });
|
|
251
|
+
line(` • ${a.name} → ${masterModel}`);
|
|
252
|
+
}
|
|
253
|
+
line(`\n(To register each CLI's own model list, MAQ will ask it "/models" on first use.)`);
|
|
254
|
+
return { models, source: "cli" };
|
|
255
|
+
}
|
|
256
|
+
async function registerSingleApi(rl) {
|
|
257
|
+
const available = detectAvailableProviders().filter((p) => p.active && !p.provider.local);
|
|
258
|
+
if (available.length === 0) {
|
|
259
|
+
line("\nNo API keys detected in your environment. Set one (e.g. OPENAI_API_KEY) and re-run,");
|
|
260
|
+
line("or pick option 1 (your CLIs). Continuing with none for now.");
|
|
261
|
+
return { models: [], source: "api" };
|
|
262
|
+
}
|
|
263
|
+
line("\nActive API providers:");
|
|
264
|
+
available.forEach((p, i) => line(` [${i + 1}] ${p.provider.label}`));
|
|
265
|
+
const pick = Number(await ask(rl, "Choose a provider: ")) || 1;
|
|
266
|
+
const chosen = (available[pick - 1] ?? available[0]).provider;
|
|
267
|
+
chosen.models.forEach((m, i) => line(` [${i + 1}] ${m.id} (${m.tier})`));
|
|
268
|
+
const mpick = Number(await ask(rl, "Choose a model: ")) || 1;
|
|
269
|
+
const model = chosen.models[mpick - 1] ?? chosen.models[0];
|
|
270
|
+
line("\nNote: a single model can't fan out — parallel/safe modes are disabled until you add more.");
|
|
271
|
+
return {
|
|
272
|
+
models: [{ id: model.id, provider: chosen.id, maqProvider: chosen.maqProvider, tier: model.tier }],
|
|
273
|
+
source: "api",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async function registerMultiApi(rl) {
|
|
277
|
+
const detected = detectAvailableProviders();
|
|
278
|
+
line("\n2026 provider catalog (● active, ○ actionable — listing is free):");
|
|
279
|
+
for (const d of detected) {
|
|
280
|
+
const mark = d.active ? "●" : "○";
|
|
281
|
+
line(` ${mark} ${d.provider.label.padEnd(38)} ${d.reason}`);
|
|
282
|
+
line(` ${d.provider.setup}`);
|
|
283
|
+
}
|
|
284
|
+
const active = detected.filter((d) => d.active);
|
|
285
|
+
if (active.length === 0) {
|
|
286
|
+
line("\nNone active yet. Set an API key from the list above, then re-run `maq start`.");
|
|
287
|
+
return { models: [], source: "api" };
|
|
288
|
+
}
|
|
289
|
+
// Register every model of every active provider (available, not consuming tokens).
|
|
290
|
+
const models = active.flatMap((d) => d.provider.models.map((m) => ({
|
|
291
|
+
id: m.id,
|
|
292
|
+
provider: d.provider.id,
|
|
293
|
+
maqProvider: d.provider.maqProvider,
|
|
294
|
+
tier: m.tier ?? classifyModel(m.id),
|
|
295
|
+
})));
|
|
296
|
+
line(`\nRegistered ${models.length} models across ${active.length} active provider(s).`);
|
|
297
|
+
return { models, source: "api" };
|
|
298
|
+
}
|
|
299
|
+
async function connectMobile(rl) {
|
|
300
|
+
const authKey = generateAuthKey();
|
|
301
|
+
line("\nConnect to Mobile");
|
|
302
|
+
line(" 1. On this machine, MAQ will run its daemon (loopback + a tunnel/tailnet for remote).");
|
|
303
|
+
line(" 2. In the MAQ phone app, pair using the details below.");
|
|
304
|
+
line("");
|
|
305
|
+
line(` Auth key (pairing PIN): ${authKey}`);
|
|
306
|
+
line("");
|
|
307
|
+
line("Starting the daemon now with: maq serve");
|
|
308
|
+
line(" (the app pairs with host/port + the token the daemon prints)");
|
|
309
|
+
await ask(rl, "\nPress Enter to continue…");
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
async function launchUi(authKey) {
|
|
313
|
+
// Reuse the daemon; open its landing page. Import lazily to avoid a cycle.
|
|
314
|
+
const { createDaemon } = await import("../server/daemon.js");
|
|
315
|
+
const daemon = createDaemon({ token: authKey, version: "0.4.0" });
|
|
316
|
+
try {
|
|
317
|
+
const { host, port } = await daemon.listen();
|
|
318
|
+
const url = `http://${host}:${port}/`;
|
|
319
|
+
line(`\nmaq daemon listening on ${url}`);
|
|
320
|
+
line(`auth key/token: ${authKey}`);
|
|
321
|
+
openBrowser(url);
|
|
322
|
+
line("Opened your browser. Press Ctrl-C here to stop the daemon.");
|
|
323
|
+
await new Promise((resolve) => {
|
|
324
|
+
const stop = () => {
|
|
325
|
+
line("\nshutting down…");
|
|
326
|
+
daemon.close().then(resolve);
|
|
327
|
+
};
|
|
328
|
+
process.on("SIGINT", stop);
|
|
329
|
+
process.on("SIGTERM", stop);
|
|
330
|
+
});
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
line(`could not start daemon: ${e.message}`);
|
|
335
|
+
return 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** For discoverability from `maq --help` / knowledge. */
|
|
339
|
+
export const LAUNCHER_PROVIDERS = PROVIDER_CATALOG.map((p) => p.id);
|
|
340
|
+
export { getCatalogProvider };
|