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,59 @@
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
+ /** Roles this model is suited for (plan|code|review|summarize|fan-out). */
25
+ goodFor?: string[];
26
+ }
27
+ /**
28
+ * Heuristic classifier for a raw model id when we have no catalog tag (e.g. a
29
+ * model name returned by a CLI's `/models`). Pattern-based, deterministic.
30
+ */
31
+ export declare function classifyModel(id: string): CapabilityTier;
32
+ export declare function tierRank(t: CapabilityTier): number;
33
+ /**
34
+ * Pick the efficient model from a set of available tiered models.
35
+ *
36
+ * Preference: a MID model first; if none, fall back to the strongest LIGHT
37
+ * (so we never silently jump to an expensive HEAVY as the "auto efficient"
38
+ * default). If only HEAVY exists, use it but flag it.
39
+ */
40
+ export declare function pickEfficient(models: TieredModel[]): {
41
+ model: TieredModel;
42
+ note: string;
43
+ } | null;
44
+ /** Split a set of models into their tier buckets (for display). */
45
+ export declare function groupByTier(models: TieredModel[]): Record<CapabilityTier, TieredModel[]>;
46
+ /**
47
+ * The instruction MAQ hands a worker CLI (option 1) so it self-reports its
48
+ * capabilities in a machine-parseable form. Kept tiny and format-strict so the
49
+ * reply can be stored directly into the Headroom knowledge doc.
50
+ */
51
+ export declare const CAPABILITY_PROBE_INSTRUCTION: string;
52
+ /** Parse a capability-probe reply into a partial TieredModel-ish record. */
53
+ export declare function parseCapabilityReply(reply: string): {
54
+ tier: CapabilityTier;
55
+ strengths: string[];
56
+ contextTokens?: number;
57
+ vision?: boolean;
58
+ goodFor: string[];
59
+ } | 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
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * cli-probe — actually USE the user's own AI CLIs to learn what they can do.
3
+ *
4
+ * The launcher's option (1) registers installed CLIs, but a name alone tells us
5
+ * nothing about capability. Here we hand each authenticated CLI the
6
+ * CAPABILITY_PROBE_INSTRUCTION (via its headless mode, at $0 marginal cost —
7
+ * the user's existing subscription pays) and parse its self-reported tier /
8
+ * strengths / context / good-for. That report is what we store into the
9
+ * Headroom knowledge doc so the master can route work to the right model.
10
+ *
11
+ * The model call is injectable (`complete`) so this is unit-testable offline
12
+ * without spawning a real CLI.
13
+ */
14
+ import { type CapabilityTier } from "./capabilities.js";
15
+ export interface CliCapability {
16
+ name: string;
17
+ maqProvider: string;
18
+ tier: CapabilityTier;
19
+ strengths: string[];
20
+ goodFor: string[];
21
+ contextTokens?: number;
22
+ vision?: boolean;
23
+ /** Whether the report came from the CLI itself vs a heuristic fallback. */
24
+ probed: boolean;
25
+ }
26
+ export type CompleteFn = (prompt: string) => Promise<string>;
27
+ /**
28
+ * Probe a single CLI. `complete` defaults to the cli:<name> provider; pass a
29
+ * stub in tests. Never throws — on any failure it returns a heuristic default
30
+ * so onboarding always proceeds.
31
+ */
32
+ export declare function probeCliCapability(name: string, opts?: {
33
+ complete?: CompleteFn;
34
+ timeoutMs?: number;
35
+ }): Promise<CliCapability>;
36
+ /**
37
+ * Probe every authenticated CLI (or a provided list). Runs in parallel; each
38
+ * probe is independently best-effort.
39
+ */
40
+ export declare function probeInstalledClis(opts?: {
41
+ names?: string[];
42
+ complete?: CompleteFn;
43
+ timeoutMs?: number;
44
+ }): Promise<CliCapability[]>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * cli-probe — actually USE the user's own AI CLIs to learn what they can do.
3
+ *
4
+ * The launcher's option (1) registers installed CLIs, but a name alone tells us
5
+ * nothing about capability. Here we hand each authenticated CLI the
6
+ * CAPABILITY_PROBE_INSTRUCTION (via its headless mode, at $0 marginal cost —
7
+ * the user's existing subscription pays) and parse its self-reported tier /
8
+ * strengths / context / good-for. That report is what we store into the
9
+ * Headroom knowledge doc so the master can route work to the right model.
10
+ *
11
+ * The model call is injectable (`complete`) so this is unit-testable offline
12
+ * without spawning a real CLI.
13
+ */
14
+ import { getProvider } from "./model.js";
15
+ import { detectAgents } from "./registry.js";
16
+ import { CAPABILITY_PROBE_INSTRUCTION, parseCapabilityReply, classifyModel, } from "./capabilities.js";
17
+ /**
18
+ * Probe a single CLI. `complete` defaults to the cli:<name> provider; pass a
19
+ * stub in tests. Never throws — on any failure it returns a heuristic default
20
+ * so onboarding always proceeds.
21
+ */
22
+ export async function probeCliCapability(name, opts = {}) {
23
+ const maqProvider = `cli:${name}`;
24
+ const fallback = {
25
+ name,
26
+ maqProvider,
27
+ tier: classifyModel(name),
28
+ strengths: [],
29
+ goodFor: ["code"],
30
+ probed: false,
31
+ };
32
+ const complete = opts.complete ??
33
+ (async (prompt) => {
34
+ const provider = getProvider(maqProvider, { strict: true });
35
+ const res = await provider.complete({
36
+ model: maqProvider,
37
+ messages: [{ role: "user", content: prompt }],
38
+ maxTokens: 300,
39
+ });
40
+ return res.text;
41
+ });
42
+ try {
43
+ const timeoutMs = opts.timeoutMs ?? 45000;
44
+ const reply = await withTimeout(complete(CAPABILITY_PROBE_INSTRUCTION), timeoutMs);
45
+ const parsed = parseCapabilityReply(reply);
46
+ if (!parsed)
47
+ return fallback;
48
+ return {
49
+ name,
50
+ maqProvider,
51
+ tier: parsed.tier,
52
+ strengths: parsed.strengths,
53
+ goodFor: parsed.goodFor.length ? parsed.goodFor : ["code"],
54
+ contextTokens: parsed.contextTokens,
55
+ vision: parsed.vision,
56
+ probed: true,
57
+ };
58
+ }
59
+ catch {
60
+ return fallback;
61
+ }
62
+ }
63
+ /**
64
+ * Probe every authenticated CLI (or a provided list). Runs in parallel; each
65
+ * probe is independently best-effort.
66
+ */
67
+ export async function probeInstalledClis(opts = {}) {
68
+ const names = opts.names ??
69
+ detectAgents()
70
+ .filter((a) => a.installed && a.authenticated)
71
+ .map((a) => a.name);
72
+ return Promise.all(names.map((n) => probeCliCapability(n, { complete: opts.complete, timeoutMs: opts.timeoutMs })));
73
+ }
74
+ function withTimeout(p, ms) {
75
+ return new Promise((resolve, reject) => {
76
+ const timer = setTimeout(() => reject(new Error("capability probe timed out")), ms);
77
+ p.then((v) => {
78
+ clearTimeout(timer);
79
+ resolve(v);
80
+ }).catch((e) => {
81
+ clearTimeout(timer);
82
+ reject(e);
83
+ });
84
+ });
85
+ }
@@ -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");
@@ -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 = await detectAgents();
96
- if (agents.length > 0) {
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 agents) {
99
- console.log(` • ${agent}`);
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 ${agents.length} agent(s): ${agents.join(", ")}`);
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,77 @@
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
+ /**
35
+ * Frames of the megalodon swimming in from the left (red fin, white body). The
36
+ * last frame is the settled splash. Used only on a TTY; tests use the pure
37
+ * megalodonSplash above.
38
+ */
39
+ export declare function megalodonFrames(color?: boolean): string[];
40
+ export interface OnboardingChoices {
41
+ /** Registered worker/master models (already tiered). */
42
+ models: TieredModel[];
43
+ /** Chosen Headroom (efficient) model; null lets applyOnboarding auto-pick. */
44
+ headroom?: {
45
+ provider: string;
46
+ model: string;
47
+ } | null;
48
+ /** Whether the Headroom model was auto-picked. */
49
+ headroomAuto: boolean;
50
+ permissionMode: "full" | "moderate";
51
+ /** How each model was sourced, for the knowledge doc. */
52
+ source: ProviderRole["source"];
53
+ }
54
+ export interface OnboardingResult {
55
+ config: MaqConfig;
56
+ knowledgePath: string;
57
+ headroom: {
58
+ provider: string;
59
+ model: string;
60
+ } | null;
61
+ authKey: string;
62
+ }
63
+ /**
64
+ * Persist an onboarding outcome: pick the efficient model if none was chosen,
65
+ * write config (provider/model/tiers/permission/onboarded) and the Headroom
66
+ * knowledge doc, and mint a 9-digit key. Pure w.r.t. I/O beyond the config +
67
+ * knowledge files, so it is directly testable.
68
+ */
69
+ export declare function applyOnboarding(choices: OnboardingChoices): OnboardingResult;
70
+ /**
71
+ * Run the guided launcher. Returns 0 on success. In a non-interactive context
72
+ * (piped stdin), it prints guidance and returns without blocking.
73
+ */
74
+ export declare function runLauncher(cwd: string): Promise<number>;
75
+ /** For discoverability from `maq --help` / knowledge. */
76
+ export declare const LAUNCHER_PROVIDERS: string[];
77
+ export { getCatalogProvider };