maqcli 0.2.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/README.md +223 -0
- package/dist/core/audit.d.ts +43 -0
- package/dist/core/audit.js +77 -0
- package/dist/core/board.d.ts +78 -0
- package/dist/core/board.js +256 -0
- package/dist/core/catalog.d.ts +50 -0
- package/dist/core/catalog.js +103 -0
- package/dist/core/command-catalog.d.ts +44 -0
- package/dist/core/command-catalog.js +86 -0
- package/dist/core/completion.d.ts +24 -0
- package/dist/core/completion.js +309 -0
- package/dist/core/complexity.d.ts +17 -0
- package/dist/core/complexity.js +87 -0
- package/dist/core/config-store.d.ts +33 -0
- package/dist/core/config-store.js +61 -0
- package/dist/core/connectivity.d.ts +34 -0
- package/dist/core/connectivity.js +49 -0
- package/dist/core/cost-tracker.d.ts +89 -0
- package/dist/core/cost-tracker.js +189 -0
- package/dist/core/cost.d.ts +35 -0
- package/dist/core/cost.js +89 -0
- package/dist/core/exec.d.ts +43 -0
- package/dist/core/exec.js +154 -0
- package/dist/core/flows.d.ts +36 -0
- package/dist/core/flows.js +96 -0
- package/dist/core/headroom.d.ts +36 -0
- package/dist/core/headroom.js +88 -0
- package/dist/core/help-topics.d.ts +26 -0
- package/dist/core/help-topics.js +294 -0
- package/dist/core/init-wizard.d.ts +26 -0
- package/dist/core/init-wizard.js +168 -0
- package/dist/core/interactive-registry.d.ts +50 -0
- package/dist/core/interactive-registry.js +86 -0
- package/dist/core/interactive.d.ts +48 -0
- package/dist/core/interactive.js +137 -0
- package/dist/core/logger.d.ts +16 -0
- package/dist/core/logger.js +46 -0
- package/dist/core/memory.d.ts +28 -0
- package/dist/core/memory.js +70 -0
- package/dist/core/metered.d.ts +9 -0
- package/dist/core/metered.js +16 -0
- package/dist/core/model.d.ts +74 -0
- package/dist/core/model.js +199 -0
- package/dist/core/pipeline.d.ts +33 -0
- package/dist/core/pipeline.js +223 -0
- package/dist/core/plugins.d.ts +21 -0
- package/dist/core/plugins.js +38 -0
- package/dist/core/probe.d.ts +48 -0
- package/dist/core/probe.js +156 -0
- package/dist/core/profiles.d.ts +42 -0
- package/dist/core/profiles.js +153 -0
- package/dist/core/providers.d.ts +84 -0
- package/dist/core/providers.js +275 -0
- package/dist/core/recall.d.ts +29 -0
- package/dist/core/recall.js +83 -0
- package/dist/core/registry.d.ts +41 -0
- package/dist/core/registry.js +162 -0
- package/dist/core/router.d.ts +33 -0
- package/dist/core/router.js +40 -0
- package/dist/core/sandbox.d.ts +78 -0
- package/dist/core/sandbox.js +268 -0
- package/dist/core/session.d.ts +105 -0
- package/dist/core/session.js +252 -0
- package/dist/core/skills.d.ts +56 -0
- package/dist/core/skills.js +289 -0
- package/dist/core/subagent.d.ts +40 -0
- package/dist/core/subagent.js +55 -0
- package/dist/core/supervisor.d.ts +37 -0
- package/dist/core/supervisor.js +40 -0
- package/dist/core/tools.d.ts +39 -0
- package/dist/core/tools.js +159 -0
- package/dist/core/types.d.ts +87 -0
- package/dist/core/types.js +10 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1032 -0
- package/dist/phases/execute.d.ts +39 -0
- package/dist/phases/execute.js +166 -0
- package/dist/phases/plan.d.ts +11 -0
- package/dist/phases/plan.js +118 -0
- package/dist/phases/scout.d.ts +10 -0
- package/dist/phases/scout.js +113 -0
- package/dist/phases/verify.d.ts +22 -0
- package/dist/phases/verify.js +81 -0
- package/dist/server/daemon.d.ts +50 -0
- package/dist/server/daemon.js +377 -0
- package/dist/server/relay-bridge.d.ts +44 -0
- package/dist/server/relay-bridge.js +175 -0
- package/package.json +39 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real model providers (LiteLLM-style single interface, dependency-free).
|
|
3
|
+
*
|
|
4
|
+
* Uses Node's global `fetch` (Node >= 18/20) — no SDKs, no runtime deps. Every
|
|
5
|
+
* provider implements the same `ModelProvider.complete()` surface used by the
|
|
6
|
+
* offline `HeuristicProvider`, so Scout/Plan/Verify never care which backend
|
|
7
|
+
* runs. API keys come from environment variables only (never config files),
|
|
8
|
+
* calls have hard timeouts (AbortController) and bounded retries with backoff,
|
|
9
|
+
* and real usage is used for token/cost accounting when the API returns it.
|
|
10
|
+
*
|
|
11
|
+
* Providers:
|
|
12
|
+
* - OpenAICompatibleProvider — OpenAI, LiteLLM proxy, Groq, Together, Ollama's
|
|
13
|
+
* OpenAI-compatible endpoint, or any `/v1/chat/completions` server.
|
|
14
|
+
* - AnthropicProvider — Anthropic Messages API (`/v1/messages`).
|
|
15
|
+
* - OllamaProvider — Ollama native `/api/chat` (no key, localhost by default).
|
|
16
|
+
*/
|
|
17
|
+
import type { CompletionRequest, CompletionResponse, ModelProvider } from "./model.js";
|
|
18
|
+
export interface HttpProviderOptions {
|
|
19
|
+
/** Base URL, e.g. https://api.openai.com/v1 (no trailing slash needed). */
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
/** API key (already resolved from env by the factory). */
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
/** Per-call timeout in ms. */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
/** Max attempts (>=1). Retries apply to network errors and 429/5xx. */
|
|
26
|
+
maxRetries?: number;
|
|
27
|
+
/** Extra headers. */
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
export declare class ProviderError extends Error {
|
|
31
|
+
readonly status?: number | undefined;
|
|
32
|
+
readonly retryable: boolean;
|
|
33
|
+
constructor(message: string, status?: number | undefined, retryable?: boolean);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* OpenAI-compatible chat completions. Works with OpenAI, a LiteLLM proxy, and
|
|
37
|
+
* any server exposing POST {baseUrl}/chat/completions.
|
|
38
|
+
*/
|
|
39
|
+
export declare class OpenAICompatibleProvider implements ModelProvider {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
private opts;
|
|
42
|
+
constructor(name: string, options: HttpProviderOptions);
|
|
43
|
+
complete(req: CompletionRequest): Promise<CompletionResponse>;
|
|
44
|
+
}
|
|
45
|
+
/** Anthropic Messages API provider (`/v1/messages`). */
|
|
46
|
+
export declare class AnthropicProvider implements ModelProvider {
|
|
47
|
+
readonly name = "anthropic";
|
|
48
|
+
private opts;
|
|
49
|
+
constructor(options: HttpProviderOptions);
|
|
50
|
+
complete(req: CompletionRequest): Promise<CompletionResponse>;
|
|
51
|
+
}
|
|
52
|
+
/** Ollama native `/api/chat` provider (local, no API key). */
|
|
53
|
+
export declare class OllamaProvider implements ModelProvider {
|
|
54
|
+
readonly name = "ollama";
|
|
55
|
+
private baseUrl;
|
|
56
|
+
private timeoutMs;
|
|
57
|
+
private maxRetries;
|
|
58
|
+
constructor(options?: Partial<HttpProviderOptions>);
|
|
59
|
+
complete(req: CompletionRequest): Promise<CompletionResponse>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* CLI-as-provider — reuse an already-authenticated worker CLI (Claude Code,
|
|
63
|
+
* Gemini CLI, Codex, ...) as the master's OWN thinking model, at $0 marginal
|
|
64
|
+
* cost (the user's existing subscription pays for it). This is the "prefer the
|
|
65
|
+
* detected AI as Headroom's light model" strategy: the master's Scout/Plan/
|
|
66
|
+
* Verify prompts are answered by piping them to the CLI's headless mode.
|
|
67
|
+
*
|
|
68
|
+
* The command runner is injectable for testing; by default it uses execSafe
|
|
69
|
+
* (shell:false, argument array — the prompt is never shell-interpreted).
|
|
70
|
+
*/
|
|
71
|
+
export type CliRunner = (bin: string, args: string[], input: string) => Promise<{
|
|
72
|
+
code: number | null;
|
|
73
|
+
stdout: string;
|
|
74
|
+
stderr: string;
|
|
75
|
+
}>;
|
|
76
|
+
export declare class CliProvider implements ModelProvider {
|
|
77
|
+
private binPath;
|
|
78
|
+
private headless;
|
|
79
|
+
private runner;
|
|
80
|
+
private timeoutMs;
|
|
81
|
+
readonly name: string;
|
|
82
|
+
constructor(agentName: string, binPath: string, headless: string[], runner: CliRunner, timeoutMs?: number);
|
|
83
|
+
complete(req: CompletionRequest): Promise<CompletionResponse>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real model providers (LiteLLM-style single interface, dependency-free).
|
|
3
|
+
*
|
|
4
|
+
* Uses Node's global `fetch` (Node >= 18/20) — no SDKs, no runtime deps. Every
|
|
5
|
+
* provider implements the same `ModelProvider.complete()` surface used by the
|
|
6
|
+
* offline `HeuristicProvider`, so Scout/Plan/Verify never care which backend
|
|
7
|
+
* runs. API keys come from environment variables only (never config files),
|
|
8
|
+
* calls have hard timeouts (AbortController) and bounded retries with backoff,
|
|
9
|
+
* and real usage is used for token/cost accounting when the API returns it.
|
|
10
|
+
*
|
|
11
|
+
* Providers:
|
|
12
|
+
* - OpenAICompatibleProvider — OpenAI, LiteLLM proxy, Groq, Together, Ollama's
|
|
13
|
+
* OpenAI-compatible endpoint, or any `/v1/chat/completions` server.
|
|
14
|
+
* - AnthropicProvider — Anthropic Messages API (`/v1/messages`).
|
|
15
|
+
* - OllamaProvider — Ollama native `/api/chat` (no key, localhost by default).
|
|
16
|
+
*/
|
|
17
|
+
import { costUsd } from "./cost.js";
|
|
18
|
+
function estTokens(s) {
|
|
19
|
+
return Math.ceil(s.length / 4);
|
|
20
|
+
}
|
|
21
|
+
export class ProviderError extends Error {
|
|
22
|
+
status;
|
|
23
|
+
retryable;
|
|
24
|
+
constructor(message, status, retryable = false) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.status = status;
|
|
27
|
+
this.retryable = retryable;
|
|
28
|
+
this.name = "ProviderError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function fetchWithRetry(url, init, timeoutMs, maxRetries) {
|
|
32
|
+
let lastErr;
|
|
33
|
+
for (let attempt = 0; attempt < Math.max(1, maxRetries); attempt++) {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
if (res.status === 429 || (res.status >= 500 && res.status <= 599)) {
|
|
40
|
+
lastErr = new ProviderError(`upstream ${res.status}`, res.status, true);
|
|
41
|
+
if (attempt < maxRetries - 1) {
|
|
42
|
+
await sleep(backoffMs(attempt, res.headers.get("retry-after")));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
return res;
|
|
46
|
+
}
|
|
47
|
+
return res;
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
lastErr = e;
|
|
52
|
+
const aborted = e instanceof Error && e.name === "AbortError";
|
|
53
|
+
if (attempt < maxRetries - 1) {
|
|
54
|
+
await sleep(backoffMs(attempt, null));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
throw new ProviderError(aborted ? `request timed out after ${timeoutMs}ms` : `network error: ${e.message}`, undefined, true);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw lastErr instanceof Error ? lastErr : new ProviderError(String(lastErr));
|
|
61
|
+
}
|
|
62
|
+
function backoffMs(attempt, retryAfter) {
|
|
63
|
+
if (retryAfter) {
|
|
64
|
+
const secs = Number(retryAfter);
|
|
65
|
+
if (!Number.isNaN(secs))
|
|
66
|
+
return Math.min(secs * 1000, 20000);
|
|
67
|
+
}
|
|
68
|
+
const base = 300 * 2 ** attempt;
|
|
69
|
+
return Math.min(base + Math.random() * 250, 10000);
|
|
70
|
+
}
|
|
71
|
+
function sleep(ms) {
|
|
72
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
73
|
+
}
|
|
74
|
+
/** Split a message list into an Anthropic-style {system, messages}. */
|
|
75
|
+
function splitSystem(messages) {
|
|
76
|
+
const system = messages
|
|
77
|
+
.filter((m) => m.role === "system")
|
|
78
|
+
.map((m) => m.content)
|
|
79
|
+
.join("\n\n");
|
|
80
|
+
const rest = messages.filter((m) => m.role !== "system");
|
|
81
|
+
return { system, rest };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* OpenAI-compatible chat completions. Works with OpenAI, a LiteLLM proxy, and
|
|
85
|
+
* any server exposing POST {baseUrl}/chat/completions.
|
|
86
|
+
*/
|
|
87
|
+
export class OpenAICompatibleProvider {
|
|
88
|
+
name;
|
|
89
|
+
opts;
|
|
90
|
+
constructor(name, options) {
|
|
91
|
+
this.name = name;
|
|
92
|
+
this.opts = {
|
|
93
|
+
baseUrl: options.baseUrl.replace(/\/$/, ""),
|
|
94
|
+
apiKey: options.apiKey,
|
|
95
|
+
timeoutMs: options.timeoutMs ?? 60000,
|
|
96
|
+
maxRetries: options.maxRetries ?? 3,
|
|
97
|
+
headers: options.headers ?? {},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async complete(req) {
|
|
101
|
+
const url = `${this.opts.baseUrl}/chat/completions`;
|
|
102
|
+
const body = {
|
|
103
|
+
model: req.model,
|
|
104
|
+
messages: req.messages,
|
|
105
|
+
...(req.maxTokens ? { max_tokens: req.maxTokens } : {}),
|
|
106
|
+
};
|
|
107
|
+
const headers = {
|
|
108
|
+
"content-type": "application/json",
|
|
109
|
+
...this.opts.headers,
|
|
110
|
+
};
|
|
111
|
+
if (this.opts.apiKey)
|
|
112
|
+
headers.authorization = `Bearer ${this.opts.apiKey}`;
|
|
113
|
+
const res = await fetchWithRetry(url, { method: "POST", headers, body: JSON.stringify(body) }, this.opts.timeoutMs, this.opts.maxRetries);
|
|
114
|
+
const raw = await res.text();
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
throw new ProviderError(`${this.name} ${res.status}: ${raw.slice(0, 300)}`, res.status, res.status >= 500);
|
|
117
|
+
}
|
|
118
|
+
const data = safeJson(raw);
|
|
119
|
+
const text = data?.choices?.[0]?.message?.content ?? "";
|
|
120
|
+
const promptTokens = data?.usage?.prompt_tokens ?? estTokens(JSON.stringify(req.messages));
|
|
121
|
+
const completionTokens = data?.usage?.completion_tokens ?? estTokens(text);
|
|
122
|
+
return {
|
|
123
|
+
text,
|
|
124
|
+
model: data?.model ?? req.model,
|
|
125
|
+
provider: this.name,
|
|
126
|
+
promptTokensEst: promptTokens,
|
|
127
|
+
completionTokensEst: completionTokens,
|
|
128
|
+
costUsd: costUsd(req.model, promptTokens, completionTokens),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Anthropic Messages API provider (`/v1/messages`). */
|
|
133
|
+
export class AnthropicProvider {
|
|
134
|
+
name = "anthropic";
|
|
135
|
+
opts;
|
|
136
|
+
constructor(options) {
|
|
137
|
+
this.opts = {
|
|
138
|
+
baseUrl: (options.baseUrl || "https://api.anthropic.com/v1").replace(/\/$/, ""),
|
|
139
|
+
apiKey: options.apiKey ?? "",
|
|
140
|
+
timeoutMs: options.timeoutMs ?? 60000,
|
|
141
|
+
maxRetries: options.maxRetries ?? 3,
|
|
142
|
+
headers: options.headers ?? {},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async complete(req) {
|
|
146
|
+
const url = `${this.opts.baseUrl}/messages`;
|
|
147
|
+
const { system, rest } = splitSystem(req.messages);
|
|
148
|
+
const body = {
|
|
149
|
+
model: req.model,
|
|
150
|
+
max_tokens: req.maxTokens ?? 1024,
|
|
151
|
+
...(system ? { system } : {}),
|
|
152
|
+
messages: rest.map((m) => ({ role: m.role === "assistant" ? "assistant" : "user", content: m.content })),
|
|
153
|
+
};
|
|
154
|
+
const headers = {
|
|
155
|
+
"content-type": "application/json",
|
|
156
|
+
"x-api-key": this.opts.apiKey,
|
|
157
|
+
"anthropic-version": "2023-06-01",
|
|
158
|
+
...this.opts.headers,
|
|
159
|
+
};
|
|
160
|
+
const res = await fetchWithRetry(url, { method: "POST", headers, body: JSON.stringify(body) }, this.opts.timeoutMs, this.opts.maxRetries);
|
|
161
|
+
const raw = await res.text();
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
throw new ProviderError(`anthropic ${res.status}: ${raw.slice(0, 300)}`, res.status, res.status >= 500);
|
|
164
|
+
}
|
|
165
|
+
const data = safeJson(raw);
|
|
166
|
+
const text = Array.isArray(data?.content)
|
|
167
|
+
? data.content.filter((b) => b?.type === "text").map((b) => b.text ?? "").join("")
|
|
168
|
+
: "";
|
|
169
|
+
const promptTokens = data?.usage?.input_tokens ?? estTokens(JSON.stringify(req.messages));
|
|
170
|
+
const completionTokens = data?.usage?.output_tokens ?? estTokens(text);
|
|
171
|
+
return {
|
|
172
|
+
text,
|
|
173
|
+
model: data?.model ?? req.model,
|
|
174
|
+
provider: this.name,
|
|
175
|
+
promptTokensEst: promptTokens,
|
|
176
|
+
completionTokensEst: completionTokens,
|
|
177
|
+
costUsd: costUsd(req.model, promptTokens, completionTokens),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Ollama native `/api/chat` provider (local, no API key). */
|
|
182
|
+
export class OllamaProvider {
|
|
183
|
+
name = "ollama";
|
|
184
|
+
baseUrl;
|
|
185
|
+
timeoutMs;
|
|
186
|
+
maxRetries;
|
|
187
|
+
constructor(options = {}) {
|
|
188
|
+
this.baseUrl = (options.baseUrl || process.env.OLLAMA_HOST || "http://127.0.0.1:11434").replace(/\/$/, "");
|
|
189
|
+
this.timeoutMs = options.timeoutMs ?? 120000;
|
|
190
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
191
|
+
}
|
|
192
|
+
async complete(req) {
|
|
193
|
+
const url = `${this.baseUrl}/api/chat`;
|
|
194
|
+
const body = {
|
|
195
|
+
model: req.model,
|
|
196
|
+
messages: req.messages,
|
|
197
|
+
stream: false,
|
|
198
|
+
...(req.maxTokens ? { options: { num_predict: req.maxTokens } } : {}),
|
|
199
|
+
};
|
|
200
|
+
const res = await fetchWithRetry(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }, this.timeoutMs, this.maxRetries);
|
|
201
|
+
const raw = await res.text();
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
throw new ProviderError(`ollama ${res.status}: ${raw.slice(0, 300)}`, res.status, res.status >= 500);
|
|
204
|
+
}
|
|
205
|
+
const data = safeJson(raw);
|
|
206
|
+
const text = data?.message?.content ?? "";
|
|
207
|
+
const promptTokens = data?.prompt_eval_count ?? estTokens(JSON.stringify(req.messages));
|
|
208
|
+
const completionTokens = data?.eval_count ?? estTokens(text);
|
|
209
|
+
return {
|
|
210
|
+
text,
|
|
211
|
+
model: data?.model ?? req.model,
|
|
212
|
+
provider: this.name,
|
|
213
|
+
promptTokensEst: promptTokens,
|
|
214
|
+
completionTokensEst: completionTokens,
|
|
215
|
+
costUsd: 0, // local inference is free
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function safeJson(s) {
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(s);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export class CliProvider {
|
|
228
|
+
binPath;
|
|
229
|
+
headless;
|
|
230
|
+
runner;
|
|
231
|
+
timeoutMs;
|
|
232
|
+
name;
|
|
233
|
+
constructor(agentName, binPath, headless, runner, timeoutMs = 120000) {
|
|
234
|
+
this.binPath = binPath;
|
|
235
|
+
this.headless = headless;
|
|
236
|
+
this.runner = runner;
|
|
237
|
+
this.timeoutMs = timeoutMs;
|
|
238
|
+
this.name = `cli:${agentName}`;
|
|
239
|
+
}
|
|
240
|
+
async complete(req) {
|
|
241
|
+
const prompt = req.messages.map((m) => `${m.role.toUpperCase()}: ${m.content}`).join("\n\n");
|
|
242
|
+
const args = this.headless.map((a) => (a === "{task}" ? prompt : a));
|
|
243
|
+
const outcome = await this.runner(this.binPath, args, prompt);
|
|
244
|
+
if (outcome.code !== 0 && !outcome.stdout) {
|
|
245
|
+
throw new ProviderError(`${this.name} exited ${outcome.code}: ${outcome.stderr.slice(0, 300)}`, undefined, false);
|
|
246
|
+
}
|
|
247
|
+
const text = extractCliText(outcome.stdout);
|
|
248
|
+
return {
|
|
249
|
+
text,
|
|
250
|
+
model: this.name,
|
|
251
|
+
provider: this.name,
|
|
252
|
+
promptTokensEst: estTokens(prompt),
|
|
253
|
+
completionTokensEst: estTokens(text),
|
|
254
|
+
costUsd: 0, // covered by the user's existing CLI subscription
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** Some CLIs emit JSON ({result|text|content}); fall back to raw stdout. */
|
|
259
|
+
function extractCliText(stdout) {
|
|
260
|
+
const trimmed = stdout.trim();
|
|
261
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
262
|
+
const j = safeJson(trimmed);
|
|
263
|
+
if (j) {
|
|
264
|
+
if (typeof j.result === "string")
|
|
265
|
+
return j.result;
|
|
266
|
+
if (typeof j.text === "string")
|
|
267
|
+
return j.text;
|
|
268
|
+
if (typeof j.content === "string")
|
|
269
|
+
return j.content;
|
|
270
|
+
if (Array.isArray(j.content))
|
|
271
|
+
return j.content.map((b) => b?.text ?? "").join("");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return trimmed;
|
|
275
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall memory (CAO memory_store / memory_recall analogue).
|
|
3
|
+
*
|
|
4
|
+
* A durable, per-project note store the master can write to and read from, so
|
|
5
|
+
* knowledge survives across sessions and is injected as context at the start of
|
|
6
|
+
* a run. Distinct from `memory.ts` (which only appends failure lessons to
|
|
7
|
+
* AGENTS.md): this is a general, queryable key/text store.
|
|
8
|
+
*
|
|
9
|
+
* Storage: append-only JSONL at `<cwd>/.maq/memory/notes.jsonl` (dependency-free,
|
|
10
|
+
* human-inspectable). Recall is a cheap keyword/overlap score — good enough to
|
|
11
|
+
* surface relevant notes; a vector index can slot in behind `recall()` later.
|
|
12
|
+
*/
|
|
13
|
+
export interface MemoryNote {
|
|
14
|
+
ts: string;
|
|
15
|
+
key: string;
|
|
16
|
+
text: string;
|
|
17
|
+
tags: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare class RecallMemory {
|
|
20
|
+
private dir;
|
|
21
|
+
private path;
|
|
22
|
+
constructor(cwd: string);
|
|
23
|
+
store(key: string, text: string, tags?: string[]): MemoryNote;
|
|
24
|
+
all(): MemoryNote[];
|
|
25
|
+
/** Rank notes by keyword overlap with the query; newest breaks ties. */
|
|
26
|
+
recall(query: string, limit?: number): MemoryNote[];
|
|
27
|
+
/** A compact context block of the most relevant notes for a task. */
|
|
28
|
+
contextBlock(query: string, limit?: number): string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall memory (CAO memory_store / memory_recall analogue).
|
|
3
|
+
*
|
|
4
|
+
* A durable, per-project note store the master can write to and read from, so
|
|
5
|
+
* knowledge survives across sessions and is injected as context at the start of
|
|
6
|
+
* a run. Distinct from `memory.ts` (which only appends failure lessons to
|
|
7
|
+
* AGENTS.md): this is a general, queryable key/text store.
|
|
8
|
+
*
|
|
9
|
+
* Storage: append-only JSONL at `<cwd>/.maq/memory/notes.jsonl` (dependency-free,
|
|
10
|
+
* human-inspectable). Recall is a cheap keyword/overlap score — good enough to
|
|
11
|
+
* surface relevant notes; a vector index can slot in behind `recall()` later.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, appendFileSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
export class RecallMemory {
|
|
16
|
+
dir;
|
|
17
|
+
path;
|
|
18
|
+
constructor(cwd) {
|
|
19
|
+
this.dir = join(cwd, ".maq", "memory");
|
|
20
|
+
this.path = join(this.dir, "notes.jsonl");
|
|
21
|
+
}
|
|
22
|
+
store(key, text, tags = []) {
|
|
23
|
+
mkdirSync(this.dir, { recursive: true });
|
|
24
|
+
const note = { ts: new Date().toISOString(), key: key.trim(), text: text.trim(), tags };
|
|
25
|
+
appendFileSync(this.path, JSON.stringify(note) + "\n", "utf8");
|
|
26
|
+
return note;
|
|
27
|
+
}
|
|
28
|
+
all() {
|
|
29
|
+
if (!existsSync(this.path))
|
|
30
|
+
return [];
|
|
31
|
+
return readFileSync(this.path, "utf8")
|
|
32
|
+
.split(/\r?\n/)
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.map((l) => {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(l);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
.filter((n) => n !== null);
|
|
43
|
+
}
|
|
44
|
+
/** Rank notes by keyword overlap with the query; newest breaks ties. */
|
|
45
|
+
recall(query, limit = 5) {
|
|
46
|
+
const terms = [...tokenize(query)];
|
|
47
|
+
if (terms.length === 0)
|
|
48
|
+
return this.all().slice(-limit).reverse();
|
|
49
|
+
const scored = this.all().map((n) => {
|
|
50
|
+
const hay = tokenize(`${n.key} ${n.text} ${n.tags.join(" ")}`);
|
|
51
|
+
const prefixes = new Set([...hay].map((w) => w.slice(0, 4)));
|
|
52
|
+
let score = 0;
|
|
53
|
+
for (const t of terms) {
|
|
54
|
+
// Exact token, or shared 4-char prefix (so token≈tokens, validate≈validation).
|
|
55
|
+
if (hay.has(t) || prefixes.has(t.slice(0, 4)))
|
|
56
|
+
score++;
|
|
57
|
+
}
|
|
58
|
+
return { n, score };
|
|
59
|
+
});
|
|
60
|
+
return scored
|
|
61
|
+
.filter((s) => s.score > 0)
|
|
62
|
+
.sort((a, b) => b.score - a.score || (a.n.ts < b.n.ts ? 1 : -1))
|
|
63
|
+
.slice(0, limit)
|
|
64
|
+
.map((s) => s.n);
|
|
65
|
+
}
|
|
66
|
+
/** A compact context block of the most relevant notes for a task. */
|
|
67
|
+
contextBlock(query, limit = 5) {
|
|
68
|
+
const notes = this.recall(query, limit);
|
|
69
|
+
if (notes.length === 0)
|
|
70
|
+
return "";
|
|
71
|
+
return ["Relevant recalled notes:", ...notes.map((n) => `- (${n.key}) ${clip(n.text, 200)}`)].join("\n");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function tokenize(s) {
|
|
75
|
+
return new Set((s || "")
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.split(/[^a-z0-9]+/)
|
|
78
|
+
.filter((w) => w.length > 2));
|
|
79
|
+
}
|
|
80
|
+
function clip(s, n) {
|
|
81
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
82
|
+
return t.length > n ? t.slice(0, n) + "…" : t;
|
|
83
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent registry + detection.
|
|
3
|
+
*
|
|
4
|
+
* Scans PATH for known worker-CLI binaries and inspects each tool's own
|
|
5
|
+
* config/auth directory to report installed/authenticated status. Detection is
|
|
6
|
+
* pure filesystem work — no network, no token cost.
|
|
7
|
+
*/
|
|
8
|
+
import type { DetectedAgent } from "./types.js";
|
|
9
|
+
interface AgentSpec {
|
|
10
|
+
name: string;
|
|
11
|
+
/** Binary base names to search for on PATH. */
|
|
12
|
+
bins: string[];
|
|
13
|
+
/** Files/dirs whose existence implies auth/config for this tool. */
|
|
14
|
+
authPaths: string[];
|
|
15
|
+
stableJsonStream: boolean;
|
|
16
|
+
/** Headless invocation template; "{task}" is replaced at run time. */
|
|
17
|
+
headless: string[] | null;
|
|
18
|
+
}
|
|
19
|
+
export declare const KNOWN_AGENTS: AgentSpec[];
|
|
20
|
+
/** Resolve a binary base name against PATH; returns absolute path or null. */
|
|
21
|
+
export declare function whichBin(base: string, pathEnv?: string): string | null;
|
|
22
|
+
/** Detect all known agents on the host. */
|
|
23
|
+
export declare function detectAgents(pathEnv?: string): DetectedAgent[];
|
|
24
|
+
/** Look up the headless template for a given agent name. */
|
|
25
|
+
export declare function agentSpec(name: string): AgentSpec | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Advisory when no AI worker CLI is installed+authenticated. AI CLIs manage
|
|
28
|
+
* their own login/logout on the device, so this is re-evaluated on every scan;
|
|
29
|
+
* a user logging out is reflected on the next detect. Returns null when at
|
|
30
|
+
* least one agent is ready.
|
|
31
|
+
*/
|
|
32
|
+
export declare function authAdvisory(agents?: DetectedAgent[]): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve which target to use given a requested target and detection results.
|
|
35
|
+
* Returns the chosen agent name, "none", or throws if ambiguous.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveTarget(requested: string, agents: DetectedAgent[]): {
|
|
38
|
+
target: string;
|
|
39
|
+
ambiguous: string[];
|
|
40
|
+
};
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent registry + detection.
|
|
3
|
+
*
|
|
4
|
+
* Scans PATH for known worker-CLI binaries and inspects each tool's own
|
|
5
|
+
* config/auth directory to report installed/authenticated status. Detection is
|
|
6
|
+
* pure filesystem work — no network, no token cost.
|
|
7
|
+
*/
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join, delimiter } from "node:path";
|
|
10
|
+
import { existsSync, statSync } from "node:fs";
|
|
11
|
+
const HOME = homedir();
|
|
12
|
+
export const KNOWN_AGENTS = [
|
|
13
|
+
{
|
|
14
|
+
name: "claude-code",
|
|
15
|
+
bins: ["claude"],
|
|
16
|
+
authPaths: [join(HOME, ".claude"), join(HOME, ".claude.json")],
|
|
17
|
+
stableJsonStream: true,
|
|
18
|
+
headless: ["-p", "{task}", "--output-format", "json"],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "codex",
|
|
22
|
+
bins: ["codex"],
|
|
23
|
+
authPaths: [join(HOME, ".codex", "auth.json"), join(HOME, ".codex")],
|
|
24
|
+
// Codex lacks a stable JSON event stream for orchestration -> needs stdout parsing.
|
|
25
|
+
stableJsonStream: false,
|
|
26
|
+
headless: ["exec", "--json", "{task}"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "gemini",
|
|
30
|
+
bins: ["gemini"],
|
|
31
|
+
authPaths: [join(HOME, ".gemini")],
|
|
32
|
+
stableJsonStream: false,
|
|
33
|
+
headless: ["-p", "{task}"],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "opencode",
|
|
37
|
+
bins: ["opencode"],
|
|
38
|
+
authPaths: [join(HOME, ".config", "opencode"), join(HOME, ".opencode")],
|
|
39
|
+
stableJsonStream: false,
|
|
40
|
+
headless: ["run", "{task}"],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "aider",
|
|
44
|
+
bins: ["aider"],
|
|
45
|
+
authPaths: [join(HOME, ".aider.conf.yml")],
|
|
46
|
+
stableJsonStream: false,
|
|
47
|
+
headless: ["--message", "{task}"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "amazon-q",
|
|
51
|
+
bins: ["q"],
|
|
52
|
+
authPaths: [join(HOME, ".aws", "amazonq"), join(HOME, ".local", "share", "amazon-q")],
|
|
53
|
+
stableJsonStream: false,
|
|
54
|
+
headless: ["chat", "{task}"],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const IS_WIN = process.platform === "win32";
|
|
58
|
+
const WIN_EXTS = ((process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";"));
|
|
59
|
+
/** Resolve a binary base name against PATH; returns absolute path or null. */
|
|
60
|
+
export function whichBin(base, pathEnv = process.env.PATH ?? "") {
|
|
61
|
+
const dirs = pathEnv.split(delimiter).filter(Boolean);
|
|
62
|
+
for (const dir of dirs) {
|
|
63
|
+
if (IS_WIN) {
|
|
64
|
+
for (const ext of WIN_EXTS) {
|
|
65
|
+
const cand = join(dir, base + ext);
|
|
66
|
+
if (isExecutableFile(cand))
|
|
67
|
+
return cand;
|
|
68
|
+
}
|
|
69
|
+
const bare = join(dir, base);
|
|
70
|
+
if (isExecutableFile(bare))
|
|
71
|
+
return bare;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const cand = join(dir, base);
|
|
75
|
+
if (isExecutableFile(cand))
|
|
76
|
+
return cand;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function isExecutableFile(p) {
|
|
82
|
+
try {
|
|
83
|
+
return existsSync(p) && statSync(p).isFile();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function anyExists(paths) {
|
|
90
|
+
return paths.some((p) => {
|
|
91
|
+
try {
|
|
92
|
+
return existsSync(p);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** Detect all known agents on the host. */
|
|
100
|
+
export function detectAgents(pathEnv = process.env.PATH ?? "") {
|
|
101
|
+
return KNOWN_AGENTS.map((spec) => {
|
|
102
|
+
let binPath = null;
|
|
103
|
+
for (const b of spec.bins) {
|
|
104
|
+
binPath = whichBin(b, pathEnv);
|
|
105
|
+
if (binPath)
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
name: spec.name,
|
|
110
|
+
binPath,
|
|
111
|
+
installed: binPath !== null,
|
|
112
|
+
authenticated: anyExists(spec.authPaths),
|
|
113
|
+
stableJsonStream: spec.stableJsonStream,
|
|
114
|
+
headless: spec.headless,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** Look up the headless template for a given agent name. */
|
|
119
|
+
export function agentSpec(name) {
|
|
120
|
+
return KNOWN_AGENTS.find((a) => a.name === name || a.bins.includes(name));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Advisory when no AI worker CLI is installed+authenticated. AI CLIs manage
|
|
124
|
+
* their own login/logout on the device, so this is re-evaluated on every scan;
|
|
125
|
+
* a user logging out is reflected on the next detect. Returns null when at
|
|
126
|
+
* least one agent is ready.
|
|
127
|
+
*/
|
|
128
|
+
export function authAdvisory(agents = detectAgents()) {
|
|
129
|
+
const ready = agents.filter((a) => a.installed && a.authenticated);
|
|
130
|
+
if (ready.length > 0)
|
|
131
|
+
return null;
|
|
132
|
+
const installedButLoggedOut = agents.filter((a) => a.installed && !a.authenticated).map((a) => a.name);
|
|
133
|
+
if (installedButLoggedOut.length > 0) {
|
|
134
|
+
return `No authenticated AI CLI detected (installed but logged out: ${installedButLoggedOut.join(", ")}). Log in to one in a terminal to enable AI mode; until then MAQ uses the offline master and --target none.`;
|
|
135
|
+
}
|
|
136
|
+
return "No AI CLI detected. Install/log in to one (claude, codex, gemini, …) to enable AI mode; MAQ works without AI via --target none.";
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Resolve which target to use given a requested target and detection results.
|
|
140
|
+
* Returns the chosen agent name, "none", or throws if ambiguous.
|
|
141
|
+
*/
|
|
142
|
+
export function resolveTarget(requested, agents) {
|
|
143
|
+
if (requested === "none")
|
|
144
|
+
return { target: "none", ambiguous: [] };
|
|
145
|
+
if (requested !== "auto") {
|
|
146
|
+
const found = agents.find((a) => a.name === requested || agentSpec(requested)?.name === a.name);
|
|
147
|
+
if (found && found.installed)
|
|
148
|
+
return { target: found.name, ambiguous: [] };
|
|
149
|
+
return { target: requested, ambiguous: [] };
|
|
150
|
+
}
|
|
151
|
+
// auto: pick the single authenticated+installed agent, else surface ambiguity.
|
|
152
|
+
const ready = agents.filter((a) => a.installed && a.authenticated);
|
|
153
|
+
if (ready.length === 1)
|
|
154
|
+
return { target: ready[0].name, ambiguous: [] };
|
|
155
|
+
if (ready.length === 0) {
|
|
156
|
+
const installed = agents.filter((a) => a.installed);
|
|
157
|
+
if (installed.length === 1)
|
|
158
|
+
return { target: installed[0].name, ambiguous: [] };
|
|
159
|
+
return { target: "none", ambiguous: [] };
|
|
160
|
+
}
|
|
161
|
+
return { target: ready[0].name, ambiguous: ready.map((a) => a.name) };
|
|
162
|
+
}
|