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,228 @@
1
+ /**
2
+ * Orchestrator — the three god-level execution engines that sit ABOVE the
3
+ * single Scout→Plan→Execute→Verify pipeline. The master (Headroom) never reads
4
+ * the full codebase; it decomposes a goal, dispatches work, and reasons only
5
+ * over the condensed sub-results — then decides the next move until the goal is
6
+ * met.
7
+ *
8
+ * parallel — independent sub-tasks assigned per model, run at once; the
9
+ * master joins the replies, thinks the next batch, repeats until
10
+ * the goal is satisfied (or maxRounds).
11
+ * loop — one hard deliverable; run, and if it doesn't verify, refine the
12
+ * instruction with the failure and retry until it passes (or
13
+ * maxIterations). Best for long single tasks.
14
+ * safe — split into mini-parts on LIGHT models in isolation (no
15
+ * interference), MERGE the parts via a loop, then a final parallel
16
+ * validation pass.
17
+ *
18
+ * Every collaborator (decompose / runTask / evaluate / merge) is injectable, so
19
+ * the engines are fully unit-testable offline with deterministic fakes. The
20
+ * default collaborators are backed by the real provider + runPipeline.
21
+ */
22
+ import { makeEvent } from "./types.js";
23
+ import { runPipeline } from "./pipeline.js";
24
+ import { loadConfig } from "./config-store.js";
25
+ import { getProvider } from "./model.js";
26
+ /* --------------------------- pure helpers ------------------------------ */
27
+ /**
28
+ * Deterministic, offline goal splitter used as a fallback when a real model
29
+ * can't (or shouldn't) be asked to decompose. Splits on natural connectors.
30
+ */
31
+ export function heuristicSplit(goal) {
32
+ const parts = goal
33
+ .split(/\s*(?:,?\s+and\s+then\s+|;|\n|\bthen\b|,\s+and\s+|\band also\b)\s*/i)
34
+ .map((p) => p.trim())
35
+ .filter((p) => p.length > 2);
36
+ const uniq = [...new Set(parts)];
37
+ return uniq.length >= 2 ? uniq : [goal.trim()];
38
+ }
39
+ /** Refine a failed task with the failure context for the next loop iteration. */
40
+ export function refineTask(goal, last) {
41
+ return `${goal}\n\nThe previous attempt did not pass verification (status=${last.status}: ${last.summary}). Diagnose the root cause and fix it; do not repeat the same approach.`;
42
+ }
43
+ /** Bounded-concurrency map (a tiny worker pool). Preserves input order. */
44
+ export async function pool(items, limit, fn) {
45
+ const results = new Array(items.length);
46
+ let next = 0;
47
+ const n = Math.max(1, Math.min(limit, items.length || 1));
48
+ async function worker() {
49
+ for (;;) {
50
+ const i = next++;
51
+ if (i >= items.length)
52
+ return;
53
+ results[i] = await fn(items[i], i);
54
+ }
55
+ }
56
+ await Promise.all(Array.from({ length: n }, () => worker()));
57
+ return results;
58
+ }
59
+ /* ------------------------- default collaborators ----------------------- */
60
+ function makeDefaultDeps(opts) {
61
+ const cwd = opts.cwd ?? process.cwd();
62
+ const cfg = loadConfig();
63
+ const providerName = opts.provider ?? cfg.provider;
64
+ const runTask = async (task, ctx) => {
65
+ const model = opts.model ?? (ctx.tier === "strong" ? cfg.strongModel : cfg.cheapModel);
66
+ const res = await runPipeline(task, {
67
+ cwd,
68
+ target: opts.target,
69
+ provider: providerName,
70
+ model,
71
+ dryRun: opts.dryRun,
72
+ onEvent: ctx.onEvent,
73
+ signal: ctx.signal,
74
+ });
75
+ return {
76
+ task,
77
+ verified: res.verify.verified,
78
+ status: res.execute.status,
79
+ summary: res.plan?.winner.summary ?? res.verify.details ?? task,
80
+ tier: ctx.tier,
81
+ };
82
+ };
83
+ const decompose = async (goal, ctx) => {
84
+ // Follow-up rounds re-run only the unfinished work.
85
+ if (ctx.round > 1 && ctx.prior.some((p) => !p.verified)) {
86
+ return ctx.prior.filter((p) => !p.verified).map((p) => refineTask(goal, p));
87
+ }
88
+ // Ask the model for a JSON array of subtasks; fall back to the splitter.
89
+ try {
90
+ const provider = getProvider(providerName);
91
+ const resp = await provider.complete({
92
+ model: opts.model ?? cfg.cheapModel,
93
+ messages: [
94
+ { role: "system", content: "Decompose the user's goal into 2-5 independent sub-tasks. Reply with ONLY a JSON array of short strings, no prose." },
95
+ { role: "user", content: goal },
96
+ ],
97
+ maxTokens: 300,
98
+ });
99
+ const arr = JSON.parse(resp.text.trim().replace(/^```(json)?/i, "").replace(/```$/, "").trim());
100
+ if (Array.isArray(arr) && arr.length >= 2)
101
+ return arr.map(String).slice(0, 6);
102
+ }
103
+ catch {
104
+ /* fall through */
105
+ }
106
+ return heuristicSplit(goal);
107
+ };
108
+ const evaluate = async (goal, results) => {
109
+ const failed = results.filter((r) => !r.verified);
110
+ return {
111
+ done: failed.length === 0,
112
+ reason: failed.length ? `${failed.length} of ${results.length} sub-task(s) unverified` : "all sub-tasks verified against the goal",
113
+ followups: failed.map((r) => refineTask(goal, r)),
114
+ };
115
+ };
116
+ const merge = async (goal, results, ctx) => {
117
+ const task = `Integrate the following completed sub-results into one coherent solution for the goal "${goal}". Resolve overlaps and ensure the whole passes verification.\n` +
118
+ results.map((r) => `- ${r.task} → ${r.status} (${r.summary})`).join("\n");
119
+ return runTask(task, { ...ctx, tier: "strong" });
120
+ };
121
+ return { decompose, runTask, evaluate, merge };
122
+ }
123
+ /* ------------------------------- engines ------------------------------- */
124
+ export async function runOrchestration(goal, mode, opts = {}) {
125
+ const deps = { ...makeDefaultDeps(opts), ...opts.deps };
126
+ const emit = (e) => opts.onEvent?.(e);
127
+ emit(makeEvent("task.started", { goal, mode, engine: "orchestrator" }));
128
+ let result;
129
+ try {
130
+ if (mode === "loop")
131
+ result = await engineLoop(goal, deps, opts, emit);
132
+ else if (mode === "safe")
133
+ result = await engineSafe(goal, deps, opts, emit);
134
+ else
135
+ result = await engineParallel(goal, deps, opts, emit);
136
+ }
137
+ catch (err) {
138
+ emit(makeEvent("task.error", { message: err instanceof Error ? err.message : String(err), mode }));
139
+ throw err;
140
+ }
141
+ emit(makeEvent("task.done", { verified: result.verified, mode, rounds: result.rounds, subtasks: result.subtasks.length, summary: result.summary }));
142
+ return result;
143
+ }
144
+ async function engineParallel(goal, deps, opts, emit) {
145
+ const maxRounds = Math.max(1, opts.maxRounds ?? 3);
146
+ const concurrency = opts.maxConcurrency ?? 4;
147
+ const all = []; // full history (what actually ran)
148
+ const completed = []; // verified results carried forward
149
+ let outstanding = []; // last round's failures (fed to the next decompose)
150
+ let round = 0;
151
+ let evaluated = { done: false, reason: "", followups: [] };
152
+ while (round < maxRounds) {
153
+ await opts.checkpoint?.();
154
+ round++;
155
+ const phase = `parallel-round-${round}`;
156
+ emit(makeEvent("phase.started", { phase, mode: "parallel" }));
157
+ const tasks = await deps.decompose(goal, { round, prior: outstanding });
158
+ emit(makeEvent("agent.event", { note: "decomposed", round, subtasks: tasks }));
159
+ const results = await pool(tasks, concurrency, (task) => deps.runTask(task, { tier: "cheap", onEvent: emit, signal: opts.signal }));
160
+ all.push(...results);
161
+ // A follow-up round addresses the previous round's failures, so only THIS
162
+ // round's failures count as outstanding — old ones are superseded.
163
+ completed.push(...results.filter((r) => r.verified));
164
+ outstanding = results.filter((r) => !r.verified);
165
+ evaluated = await deps.evaluate(goal, [...completed, ...outstanding]);
166
+ emit(makeEvent("phase.done", { phase, done: evaluated.done, reason: evaluated.reason, verifiedThisRound: results.filter((r) => r.verified).length }));
167
+ if (evaluated.done || outstanding.length === 0)
168
+ break;
169
+ }
170
+ return { goal, mode: "parallel", rounds: round, subtasks: all, verified: evaluated.done, summary: evaluated.reason };
171
+ }
172
+ async function engineLoop(goal, deps, opts, emit) {
173
+ const maxIterations = Math.max(1, opts.maxIterations ?? 4);
174
+ const attempts = [];
175
+ let task = goal;
176
+ let iter = 0;
177
+ let last = null;
178
+ while (iter < maxIterations) {
179
+ await opts.checkpoint?.();
180
+ iter++;
181
+ const phase = `loop-iteration-${iter}`;
182
+ emit(makeEvent("phase.started", { phase, mode: "loop" }));
183
+ last = await deps.runTask(task, { tier: "strong", onEvent: emit, signal: opts.signal });
184
+ attempts.push(last);
185
+ emit(makeEvent("phase.done", { phase, verified: last.verified, status: last.status }));
186
+ if (last.verified)
187
+ break;
188
+ task = refineTask(goal, last);
189
+ emit(makeEvent("agent.event", { note: "refining after failed verification", iteration: iter }));
190
+ }
191
+ return {
192
+ goal,
193
+ mode: "loop",
194
+ rounds: iter,
195
+ subtasks: attempts,
196
+ verified: last?.verified ?? false,
197
+ summary: last?.verified ? `verified after ${iter} iteration(s)` : `not verified after ${iter} iteration(s)`,
198
+ };
199
+ }
200
+ async function engineSafe(goal, deps, opts, emit) {
201
+ const concurrency = opts.maxConcurrency ?? 4;
202
+ // 1. Split into mini-parts and run them ISOLATED on light models.
203
+ await opts.checkpoint?.();
204
+ emit(makeEvent("phase.started", { phase: "safe-split", mode: "safe" }));
205
+ const parts = await deps.decompose(goal, { round: 1, prior: [] });
206
+ emit(makeEvent("agent.event", { note: "split into isolated mini-parts", parts }));
207
+ const partResults = await pool(parts, concurrency, (task) => deps.runTask(task, { tier: "cheap", onEvent: emit, signal: opts.signal }));
208
+ emit(makeEvent("phase.done", { phase: "safe-split", parts: partResults.length, verified: partResults.filter((r) => r.verified).length }));
209
+ // 2. MERGE the parts via a (single) integration step on a strong model.
210
+ await opts.checkpoint?.();
211
+ emit(makeEvent("phase.started", { phase: "safe-merge", mode: "safe" }));
212
+ // Merging integrates everyone's work — a MAJOR action, so it passes through
213
+ // the permission gate (moderate mode may hold it for approval).
214
+ const allowMerge = opts.requestPermission ? await opts.requestPermission("merge", "integrate all sub-results into the final solution", "major") : true;
215
+ if (!allowMerge) {
216
+ emit(makeEvent("agent.event", { note: "merge held by permission policy (request-box)", phase: "safe-merge" }));
217
+ emit(makeEvent("phase.done", { phase: "safe-merge", held: true }));
218
+ return { goal, mode: "safe", rounds: 1, subtasks: partResults, verified: false, summary: "merge held pending approval" };
219
+ }
220
+ const merged = await deps.merge(goal, partResults, { onEvent: emit, signal: opts.signal });
221
+ emit(makeEvent("phase.done", { phase: "safe-merge", verified: merged.verified, status: merged.status }));
222
+ // 3. Final validation pass over everything.
223
+ await opts.checkpoint?.();
224
+ const all = [...partResults, merged];
225
+ const evaluated = await deps.evaluate(goal, all);
226
+ emit(makeEvent("phase.done", { phase: "safe-validate", done: evaluated.done, reason: evaluated.reason }));
227
+ return { goal, mode: "safe", rounds: 1, subtasks: all, verified: evaluated.done && merged.verified, summary: evaluated.reason };
228
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * permissions — the "request box" the spec describes.
3
+ *
4
+ * A single Headroom master controls everything, but it never performs work
5
+ * itself; its ants (workers) do. So permission is about gating the ants' major
6
+ * actions, not the master. Two postures:
7
+ *
8
+ * full — everything is allowed; the box stays empty.
9
+ * moderate — every MAJOR or DESTRUCTIVE action stops and is filed as a
10
+ * request. A goal-aware policy (the Headroom check) auto-approves
11
+ * actions that clearly serve the stated goal and holds the rest for
12
+ * an explicit approve/deny (by the master or a human via the UI).
13
+ *
14
+ * Requests are held in an in-memory box; each pending request exposes a promise
15
+ * (`await`) that resolves when it is decided. Low-risk actions never queue.
16
+ */
17
+ export type PermissionMode = "full" | "moderate";
18
+ export type Risk = "low" | "major" | "destructive";
19
+ export type RequestStatus = "pending" | "approved" | "denied";
20
+ export interface PermissionRequest {
21
+ id: string;
22
+ action: string;
23
+ detail: string;
24
+ risk: Risk;
25
+ goal?: string;
26
+ status: RequestStatus;
27
+ reason: string;
28
+ ts: string;
29
+ decidedBy?: string;
30
+ }
31
+ /** Classify an action string into a risk level (deterministic, pattern-based). */
32
+ export declare function classifyRisk(action: string, detail?: string): Risk;
33
+ export interface Policy {
34
+ (req: Pick<PermissionRequest, "action" | "detail" | "risk" | "goal">): {
35
+ allow: boolean;
36
+ reason: string;
37
+ };
38
+ }
39
+ /**
40
+ * The Headroom check: allow when the action plainly serves the goal.
41
+ * - low risk → always allow
42
+ * - destructive → never auto-allow (must be approved explicitly)
43
+ * - major → allow only if it aligns with the goal (keyword overlap)
44
+ */
45
+ export declare function goalAwarePolicy(req: Pick<PermissionRequest, "action" | "detail" | "risk" | "goal">): {
46
+ allow: boolean;
47
+ reason: string;
48
+ };
49
+ export declare class PermissionBroker {
50
+ private mode;
51
+ private policy;
52
+ private box;
53
+ private waiters;
54
+ constructor(mode?: PermissionMode, opts?: {
55
+ policy?: Policy;
56
+ });
57
+ getMode(): PermissionMode;
58
+ /**
59
+ * File a request. Returns the (possibly already-decided) request. In `full`
60
+ * mode everything is approved immediately; in `moderate` mode the policy runs
61
+ * and only holds what it cannot justify.
62
+ */
63
+ request(action: string, detail: string, ctx?: {
64
+ risk?: Risk;
65
+ goal?: string;
66
+ }): PermissionRequest;
67
+ /** Resolve once the request is decided. Already-decided requests resolve now. */
68
+ await(id: string): Promise<boolean>;
69
+ /** Convenience: file + await in one call. */
70
+ gate(action: string, detail: string, ctx?: {
71
+ risk?: Risk;
72
+ goal?: string;
73
+ }): Promise<boolean>;
74
+ approve(id: string, by?: string): boolean;
75
+ deny(id: string, by?: string): boolean;
76
+ private decide;
77
+ pending(): PermissionRequest[];
78
+ list(): PermissionRequest[];
79
+ get(id: string): PermissionRequest | undefined;
80
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * permissions — the "request box" the spec describes.
3
+ *
4
+ * A single Headroom master controls everything, but it never performs work
5
+ * itself; its ants (workers) do. So permission is about gating the ants' major
6
+ * actions, not the master. Two postures:
7
+ *
8
+ * full — everything is allowed; the box stays empty.
9
+ * moderate — every MAJOR or DESTRUCTIVE action stops and is filed as a
10
+ * request. A goal-aware policy (the Headroom check) auto-approves
11
+ * actions that clearly serve the stated goal and holds the rest for
12
+ * an explicit approve/deny (by the master or a human via the UI).
13
+ *
14
+ * Requests are held in an in-memory box; each pending request exposes a promise
15
+ * (`await`) that resolves when it is decided. Low-risk actions never queue.
16
+ */
17
+ import { randomUUID } from "node:crypto";
18
+ /** Classify an action string into a risk level (deterministic, pattern-based). */
19
+ export function classifyRisk(action, detail = "") {
20
+ const s = `${action} ${detail}`.toLowerCase();
21
+ if (/(rm\s+-rf|rmdir\s+\/s|drop\s+(table|database)|truncate|del\s+\/|format\s|mkfs|:\s*>\s|force[-\s]?push|--force\b|\bpush\b.*\s-f\b|reset\s+--hard|git\s+clean\s+-|delete\s+from|shutdown|reboot)/.test(s)) {
22
+ return "destructive";
23
+ }
24
+ if (/(write|create|modify|edit|install|npm\s+i|pip\s+install|deploy|publish|push|commit|merge|migrate|chmod|chown|mv\s|move|rename|apply|patch)/.test(s)) {
25
+ return "major";
26
+ }
27
+ return "low";
28
+ }
29
+ /**
30
+ * The Headroom check: allow when the action plainly serves the goal.
31
+ * - low risk → always allow
32
+ * - destructive → never auto-allow (must be approved explicitly)
33
+ * - major → allow only if it aligns with the goal (keyword overlap)
34
+ */
35
+ export function goalAwarePolicy(req) {
36
+ if (req.risk === "low")
37
+ return { allow: true, reason: "low-risk action" };
38
+ if (req.risk === "destructive")
39
+ return { allow: false, reason: "destructive action requires explicit approval" };
40
+ // major: align against the goal.
41
+ const goal = (req.goal ?? "").toLowerCase();
42
+ if (!goal)
43
+ return { allow: false, reason: "no goal context to justify a major action" };
44
+ const words = new Set((req.action + " " + req.detail).toLowerCase().split(/\W+/).filter((w) => w.length >= 4));
45
+ const goalWords = new Set(goal.split(/\W+/).filter((w) => w.length >= 4));
46
+ let overlap = 0;
47
+ for (const w of words)
48
+ if (goalWords.has(w))
49
+ overlap++;
50
+ return overlap > 0
51
+ ? { allow: true, reason: `major action aligns with the goal (${overlap} matching term(s))` }
52
+ : { allow: false, reason: "major action does not clearly serve the stated goal" };
53
+ }
54
+ export class PermissionBroker {
55
+ mode;
56
+ policy;
57
+ box = new Map();
58
+ waiters = new Map();
59
+ constructor(mode = "moderate", opts = {}) {
60
+ this.mode = mode;
61
+ this.policy = opts.policy ?? goalAwarePolicy;
62
+ }
63
+ getMode() {
64
+ return this.mode;
65
+ }
66
+ /**
67
+ * File a request. Returns the (possibly already-decided) request. In `full`
68
+ * mode everything is approved immediately; in `moderate` mode the policy runs
69
+ * and only holds what it cannot justify.
70
+ */
71
+ request(action, detail, ctx = {}) {
72
+ const risk = ctx.risk ?? classifyRisk(action, detail);
73
+ const req = {
74
+ id: randomUUID(),
75
+ action,
76
+ detail,
77
+ risk,
78
+ goal: ctx.goal,
79
+ status: "pending",
80
+ reason: "",
81
+ ts: new Date().toISOString(),
82
+ };
83
+ if (this.mode === "full") {
84
+ req.status = "approved";
85
+ req.reason = "full-permission mode";
86
+ this.box.set(req.id, req);
87
+ return req;
88
+ }
89
+ const verdict = this.policy({ action, detail, risk, goal: ctx.goal });
90
+ if (verdict.allow) {
91
+ req.status = "approved";
92
+ req.reason = verdict.reason;
93
+ }
94
+ else {
95
+ req.status = "pending";
96
+ req.reason = verdict.reason;
97
+ }
98
+ this.box.set(req.id, req);
99
+ return req;
100
+ }
101
+ /** Resolve once the request is decided. Already-decided requests resolve now. */
102
+ await(id) {
103
+ const req = this.box.get(id);
104
+ if (!req)
105
+ return Promise.resolve(false);
106
+ if (req.status !== "pending")
107
+ return Promise.resolve(req.status === "approved");
108
+ return new Promise((resolve) => {
109
+ const list = this.waiters.get(id) ?? [];
110
+ list.push({ resolve });
111
+ this.waiters.set(id, list);
112
+ });
113
+ }
114
+ /** Convenience: file + await in one call. */
115
+ async gate(action, detail, ctx = {}) {
116
+ const req = this.request(action, detail, ctx);
117
+ return this.await(req.id);
118
+ }
119
+ approve(id, by = "user") {
120
+ return this.decide(id, "approved", by);
121
+ }
122
+ deny(id, by = "user") {
123
+ return this.decide(id, "denied", by);
124
+ }
125
+ decide(id, status, by) {
126
+ const req = this.box.get(id);
127
+ if (!req || req.status !== "pending")
128
+ return false;
129
+ req.status = status;
130
+ req.decidedBy = by;
131
+ req.reason = `${status} by ${by}`;
132
+ const waiters = this.waiters.get(id) ?? [];
133
+ for (const w of waiters)
134
+ w.resolve(status === "approved");
135
+ this.waiters.delete(id);
136
+ return true;
137
+ }
138
+ pending() {
139
+ return [...this.box.values()].filter((r) => r.status === "pending");
140
+ }
141
+ list() {
142
+ return [...this.box.values()];
143
+ }
144
+ get(id) {
145
+ return this.box.get(id);
146
+ }
147
+ }
@@ -0,0 +1,68 @@
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
+ import type { CapabilityTier } from "./capabilities.js";
14
+ /** How a provider's HTTP API is shaped (drives which ModelProvider we use). */
15
+ export type ApiFormat = "openai" | "anthropic" | "gemini" | "ollama";
16
+ export interface CatalogModel {
17
+ id: string;
18
+ /** Rough capability class used for the efficient-"mid" auto pick. */
19
+ tier: CapabilityTier;
20
+ /** Multimodal / vision capable. */
21
+ vision?: boolean;
22
+ /** Very large context (>= 200k tokens). */
23
+ longContext?: boolean;
24
+ }
25
+ export interface CatalogProvider {
26
+ /** Stable id used in config (`provider`). */
27
+ id: string;
28
+ label: string;
29
+ /** OpenAI-compatible? anthropic-native? etc. */
30
+ format: ApiFormat;
31
+ /** The MAQ provider name getProvider() understands. */
32
+ maqProvider: string;
33
+ /** Env var that holds the API key (empty for keyless/local). */
34
+ envVar: string;
35
+ /** Default REST base URL. */
36
+ baseUrl: string;
37
+ /** Where the user gets a key / reads setup docs. */
38
+ docsUrl: string;
39
+ /** True for local/self-hosted providers (no key, no cost). */
40
+ local?: boolean;
41
+ /** Known model families (list-only; not fetched). */
42
+ models: CatalogModel[];
43
+ /** One-line setup hint shown in the launcher. */
44
+ setup: string;
45
+ }
46
+ /**
47
+ * The registry. Ordered roughly by how commonly it is the user's daily driver.
48
+ * Model ids are the well-known 2026 families; keep them representative rather
49
+ * than exhaustive — the point is a browsable, $0 menu.
50
+ */
51
+ export declare const PROVIDER_CATALOG: CatalogProvider[];
52
+ export declare function getCatalogProvider(id: string): CatalogProvider | undefined;
53
+ export declare function providerGoodFor(id: string): string[];
54
+ /**
55
+ * Detect which catalog providers are usable RIGHT NOW from the environment
56
+ * only — no network, no tokens. Local providers (Ollama) are reported as
57
+ * "actionable" (installed/reachable is confirmed later, on first use).
58
+ */
59
+ export declare function detectAvailableProviders(env?: NodeJS.ProcessEnv): Array<{
60
+ provider: CatalogProvider;
61
+ active: boolean;
62
+ reason: string;
63
+ }>;
64
+ /** Flat list of every catalog model tagged with its provider (for tiering). */
65
+ export declare function allCatalogModels(): Array<CatalogModel & {
66
+ provider: string;
67
+ maqProvider: string;
68
+ }>;