maqcli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/capabilities.d.ts +2 -0
- package/dist/core/cli-probe.d.ts +44 -0
- package/dist/core/cli-probe.js +85 -0
- package/dist/core/launcher.d.ts +6 -0
- package/dist/core/launcher.js +61 -20
- package/dist/core/orchestrator.d.ts +5 -0
- package/dist/core/orchestrator.js +8 -0
- package/dist/core/permissions.d.ts +80 -0
- package/dist/core/permissions.js +147 -0
- package/dist/core/providers-catalog.d.ts +1 -0
- package/dist/core/providers-catalog.js +18 -0
- package/dist/core/session.d.ts +2 -0
- package/dist/core/session.js +1 -0
- package/dist/index.js +1 -1
- package/dist/server/daemon.js +34 -1
- package/dist/server/webui.js +77 -1
- package/package.json +1 -1
|
@@ -21,6 +21,8 @@ export interface TieredModel {
|
|
|
21
21
|
tier: CapabilityTier;
|
|
22
22
|
vision?: boolean;
|
|
23
23
|
longContext?: boolean;
|
|
24
|
+
/** Roles this model is suited for (plan|code|review|summarize|fan-out). */
|
|
25
|
+
goodFor?: string[];
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* Heuristic classifier for a raw model id when we have no catalog tag (e.g. a
|
|
@@ -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
|
+
}
|
package/dist/core/launcher.d.ts
CHANGED
|
@@ -31,6 +31,12 @@ export declare function browserOpenCommand(url: string, platform?: NodeJS.Platfo
|
|
|
31
31
|
export declare function openBrowser(url: string): void;
|
|
32
32
|
/** The Megalodon splash. `color=false` yields a plain-text version. */
|
|
33
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[];
|
|
34
40
|
export interface OnboardingChoices {
|
|
35
41
|
/** Registered worker/master models (already tiered). */
|
|
36
42
|
models: TieredModel[];
|
package/dist/core/launcher.js
CHANGED
|
@@ -21,18 +21,10 @@ import { spawn } from "node:child_process";
|
|
|
21
21
|
import { randomInt } from "node:crypto";
|
|
22
22
|
import { loadConfig, saveConfig } from "./config-store.js";
|
|
23
23
|
import { detectAgents } from "./registry.js";
|
|
24
|
-
import { PROVIDER_CATALOG, detectAvailableProviders, getCatalogProvider, } from "./providers-catalog.js";
|
|
24
|
+
import { PROVIDER_CATALOG, detectAvailableProviders, getCatalogProvider, providerGoodFor, } from "./providers-catalog.js";
|
|
25
25
|
import { classifyModel, pickEfficient } from "./capabilities.js";
|
|
26
26
|
import { buildKnowledge, saveKnowledge, roleForModel } from "./onboarding.js";
|
|
27
|
-
|
|
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
|
-
};
|
|
27
|
+
import { probeCliCapability } from "./cli-probe.js";
|
|
36
28
|
/* --------------------------- pure, testable ---------------------------- */
|
|
37
29
|
/** A user-facing 9-digit pairing/auth key (100000000–999999999). */
|
|
38
30
|
export function generateAuthKey() {
|
|
@@ -85,6 +77,45 @@ export function megalodonSplash(color = true) {
|
|
|
85
77
|
"",
|
|
86
78
|
].join("\n");
|
|
87
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Frames of the megalodon swimming in from the left (red fin, white body). The
|
|
82
|
+
* last frame is the settled splash. Used only on a TTY; tests use the pure
|
|
83
|
+
* megalodonSplash above.
|
|
84
|
+
*/
|
|
85
|
+
export function megalodonFrames(color = true) {
|
|
86
|
+
const r = color ? RED : "";
|
|
87
|
+
const w = color ? WHITE : "";
|
|
88
|
+
const x = color ? RST : "";
|
|
89
|
+
const shark = (pad) => {
|
|
90
|
+
const p = " ".repeat(pad);
|
|
91
|
+
return [
|
|
92
|
+
"",
|
|
93
|
+
"",
|
|
94
|
+
`${p}${w} ,${x}`,
|
|
95
|
+
`${p}${w} ,'| ${r}▄▄${x}`,
|
|
96
|
+
`${p}${r}≈≈≈≈≈≈≈${w},'__|${r}████████▀${x}`,
|
|
97
|
+
`${p}${w} \`.|${r}████▀${x}`,
|
|
98
|
+
`${p}${w} \`.${r}▀${x}`,
|
|
99
|
+
"",
|
|
100
|
+
"",
|
|
101
|
+
].join("\n");
|
|
102
|
+
};
|
|
103
|
+
return [shark(0), shark(6), shark(14), shark(24), megalodonSplash(color)];
|
|
104
|
+
}
|
|
105
|
+
async function animateSplash(color) {
|
|
106
|
+
if (!process.stdout.isTTY) {
|
|
107
|
+
line(megalodonSplash(color));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const frames = megalodonFrames(color);
|
|
111
|
+
for (let i = 0; i < frames.length; i++) {
|
|
112
|
+
process.stdout.write("\x1b[2J\x1b[H"); // clear + home
|
|
113
|
+
process.stdout.write(frames[i]);
|
|
114
|
+
if (i < frames.length - 1)
|
|
115
|
+
await new Promise((res) => setTimeout(res, 110));
|
|
116
|
+
}
|
|
117
|
+
process.stdout.write("\n");
|
|
118
|
+
}
|
|
88
119
|
/**
|
|
89
120
|
* Persist an onboarding outcome: pick the efficient model if none was chosen,
|
|
90
121
|
* write config (provider/model/tiers/permission/onboarded) and the Headroom
|
|
@@ -121,7 +152,7 @@ export function applyOnboarding(choices) {
|
|
|
121
152
|
: m.tier === "heavy"
|
|
122
153
|
? "reviewer"
|
|
123
154
|
: "worker";
|
|
124
|
-
return roleForModel(m, role, choices.source);
|
|
155
|
+
return roleForModel(m, role, choices.source, m.goodFor ?? []);
|
|
125
156
|
});
|
|
126
157
|
const knowledge = buildKnowledge({
|
|
127
158
|
providers,
|
|
@@ -144,7 +175,7 @@ function line(s = "") {
|
|
|
144
175
|
* (piped stdin), it prints guidance and returns without blocking.
|
|
145
176
|
*/
|
|
146
177
|
export async function runLauncher(cwd) {
|
|
147
|
-
|
|
178
|
+
await animateSplash(useColor());
|
|
148
179
|
if (!process.stdin.isTTY) {
|
|
149
180
|
line("maq: guided setup needs an interactive terminal.");
|
|
150
181
|
line(" • run `maq start` in a real terminal, or");
|
|
@@ -242,15 +273,22 @@ async function registerClis(rl) {
|
|
|
242
273
|
line("or pick option 3 (API providers). Continuing with none for now.");
|
|
243
274
|
return { models: [], source: "cli" };
|
|
244
275
|
}
|
|
245
|
-
line("\nFound these authenticated CLIs (
|
|
276
|
+
line("\nFound these authenticated CLIs. Asking each one to self-report its capabilities (uses your own subscription, $0)…");
|
|
246
277
|
const models = [];
|
|
247
278
|
for (const a of ready) {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
279
|
+
const cap = await probeCliCapability(a.name, { timeoutMs: 45000 });
|
|
280
|
+
models.push({
|
|
281
|
+
id: cap.maqProvider,
|
|
282
|
+
provider: a.name,
|
|
283
|
+
maqProvider: cap.maqProvider,
|
|
284
|
+
tier: cap.tier,
|
|
285
|
+
vision: cap.vision,
|
|
286
|
+
goodFor: cap.goodFor,
|
|
287
|
+
});
|
|
288
|
+
const how = cap.probed ? `self-reported ${cap.tier}` : `${cap.tier} (probe unavailable)`;
|
|
289
|
+
const gf = cap.goodFor.length ? ` good for: ${cap.goodFor.join(", ")}` : "";
|
|
290
|
+
line(` • ${a.name.padEnd(12)} → ${cap.maqProvider} [${how}]${gf}`);
|
|
252
291
|
}
|
|
253
|
-
line(`\n(To register each CLI's own model list, MAQ will ask it "/models" on first use.)`);
|
|
254
292
|
return { models, source: "cli" };
|
|
255
293
|
}
|
|
256
294
|
async function registerSingleApi(rl) {
|
|
@@ -269,7 +307,7 @@ async function registerSingleApi(rl) {
|
|
|
269
307
|
const model = chosen.models[mpick - 1] ?? chosen.models[0];
|
|
270
308
|
line("\nNote: a single model can't fan out — parallel/safe modes are disabled until you add more.");
|
|
271
309
|
return {
|
|
272
|
-
models: [{ id: model.id, provider: chosen.id, maqProvider: chosen.maqProvider, tier: model.tier }],
|
|
310
|
+
models: [{ id: model.id, provider: chosen.id, maqProvider: chosen.maqProvider, tier: model.tier, goodFor: providerGoodFor(chosen.id) }],
|
|
273
311
|
source: "api",
|
|
274
312
|
};
|
|
275
313
|
}
|
|
@@ -292,6 +330,9 @@ async function registerMultiApi(rl) {
|
|
|
292
330
|
provider: d.provider.id,
|
|
293
331
|
maqProvider: d.provider.maqProvider,
|
|
294
332
|
tier: m.tier ?? classifyModel(m.id),
|
|
333
|
+
vision: m.vision,
|
|
334
|
+
longContext: m.longContext,
|
|
335
|
+
goodFor: providerGoodFor(d.provider.id),
|
|
295
336
|
})));
|
|
296
337
|
line(`\nRegistered ${models.length} models across ${active.length} active provider(s).`);
|
|
297
338
|
return { models, source: "api" };
|
|
@@ -312,7 +353,7 @@ async function connectMobile(rl) {
|
|
|
312
353
|
async function launchUi(authKey) {
|
|
313
354
|
// Reuse the daemon; open its landing page. Import lazily to avoid a cycle.
|
|
314
355
|
const { createDaemon } = await import("../server/daemon.js");
|
|
315
|
-
const daemon = createDaemon({ token: authKey, version: "0.
|
|
356
|
+
const daemon = createDaemon({ token: authKey, version: "0.5.0" });
|
|
316
357
|
try {
|
|
317
358
|
const { host, port } = await daemon.listen();
|
|
318
359
|
const url = `http://${host}:${port}/`;
|
|
@@ -70,6 +70,11 @@ export interface OrchestrationOptions {
|
|
|
70
70
|
onEvent?: (e: MaqEvent) => void;
|
|
71
71
|
signal?: AbortSignal;
|
|
72
72
|
checkpoint?: () => Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Permission gate consulted before MAJOR steps (moderate mode). Resolves
|
|
75
|
+
* true to proceed, false to hold. When absent, everything proceeds.
|
|
76
|
+
*/
|
|
77
|
+
requestPermission?: (action: string, detail: string, risk: "low" | "major" | "destructive") => Promise<boolean>;
|
|
73
78
|
/** Override any collaborator (tests inject deterministic ones). */
|
|
74
79
|
deps?: Partial<OrchestratorDeps>;
|
|
75
80
|
}
|
|
@@ -209,6 +209,14 @@ async function engineSafe(goal, deps, opts, emit) {
|
|
|
209
209
|
// 2. MERGE the parts via a (single) integration step on a strong model.
|
|
210
210
|
await opts.checkpoint?.();
|
|
211
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
|
+
}
|
|
212
220
|
const merged = await deps.merge(goal, partResults, { onEvent: emit, signal: opts.signal });
|
|
213
221
|
emit(makeEvent("phase.done", { phase: "safe-merge", verified: merged.verified, status: merged.status }));
|
|
214
222
|
// 3. Final validation pass over everything.
|
|
@@ -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
|
+
}
|
|
@@ -50,6 +50,7 @@ export interface CatalogProvider {
|
|
|
50
50
|
*/
|
|
51
51
|
export declare const PROVIDER_CATALOG: CatalogProvider[];
|
|
52
52
|
export declare function getCatalogProvider(id: string): CatalogProvider | undefined;
|
|
53
|
+
export declare function providerGoodFor(id: string): string[];
|
|
53
54
|
/**
|
|
54
55
|
* Detect which catalog providers are usable RIGHT NOW from the environment
|
|
55
56
|
* only — no network, no tokens. Local providers (Ollama) are reported as
|
|
@@ -156,6 +156,24 @@ export const PROVIDER_CATALOG = [
|
|
|
156
156
|
export function getCatalogProvider(id) {
|
|
157
157
|
return PROVIDER_CATALOG.find((p) => p.id === id);
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* The role each provider tends to play in the pipeline (fed into the Headroom
|
|
161
|
+
* knowledge doc). Roles: plan | code | review | summarize | fan-out.
|
|
162
|
+
*/
|
|
163
|
+
const PROVIDER_GOOD_FOR = {
|
|
164
|
+
openai: ["plan", "code", "review"],
|
|
165
|
+
anthropic: ["plan", "code", "review", "summarize"],
|
|
166
|
+
gemini: ["summarize", "code", "fan-out"],
|
|
167
|
+
groq: ["fan-out", "summarize"],
|
|
168
|
+
xai: ["plan", "code"],
|
|
169
|
+
deepseek: ["code", "review"],
|
|
170
|
+
mistral: ["code", "summarize"],
|
|
171
|
+
ollama: ["fan-out", "summarize"],
|
|
172
|
+
litellm: ["plan", "code", "review"],
|
|
173
|
+
};
|
|
174
|
+
export function providerGoodFor(id) {
|
|
175
|
+
return PROVIDER_GOOD_FOR[id] ?? ["code"];
|
|
176
|
+
}
|
|
159
177
|
/**
|
|
160
178
|
* Detect which catalog providers are usable RIGHT NOW from the environment
|
|
161
179
|
* only — no network, no tokens. Local providers (Ollama) are reported as
|
package/dist/core/session.d.ts
CHANGED
|
@@ -66,6 +66,8 @@ export interface CreateOptions {
|
|
|
66
66
|
maxRounds?: number;
|
|
67
67
|
maxIterations?: number;
|
|
68
68
|
maxConcurrency?: number;
|
|
69
|
+
/** Permission gate for major orchestration steps (moderate mode). */
|
|
70
|
+
requestPermission?: (action: string, detail: string, risk: "low" | "major" | "destructive") => Promise<boolean>;
|
|
69
71
|
}
|
|
70
72
|
type Listener = (e: MaqEvent) => void;
|
|
71
73
|
/** Thrown through the pipeline checkpoint when a session is cancelled. */
|
package/dist/core/session.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -43,7 +43,7 @@ import { runInit } from "./core/init-wizard.js";
|
|
|
43
43
|
import { CostTracker } from "./core/cost-tracker.js";
|
|
44
44
|
import { runLauncher } from "./core/launcher.js";
|
|
45
45
|
import { runOrchestration } from "./core/orchestrator.js";
|
|
46
|
-
const VERSION = "0.
|
|
46
|
+
const VERSION = "0.5.0";
|
|
47
47
|
async function main(argv) {
|
|
48
48
|
const [command, ...rest] = argv;
|
|
49
49
|
switch (command) {
|
package/dist/server/daemon.js
CHANGED
|
@@ -34,6 +34,8 @@ import { execSafe } from "../core/exec.js";
|
|
|
34
34
|
import { commandCatalog, maqCommands } from "../core/command-catalog.js";
|
|
35
35
|
import { InteractiveRegistry } from "../core/interactive-registry.js";
|
|
36
36
|
import { webuiHtml } from "./webui.js";
|
|
37
|
+
import { PermissionBroker } from "../core/permissions.js";
|
|
38
|
+
import { loadConfig } from "../core/config-store.js";
|
|
37
39
|
/** Generate a URL-safe token. */
|
|
38
40
|
export function generateToken() {
|
|
39
41
|
return randomBytes(24).toString("base64url");
|
|
@@ -49,11 +51,17 @@ export function createDaemon(opts = {}) {
|
|
|
49
51
|
const host = opts.host ?? process.env.MAQ_HOST ?? "127.0.0.1";
|
|
50
52
|
const port = opts.port ?? Number(process.env.MAQ_PORT ?? 7717);
|
|
51
53
|
const token = opts.token ?? process.env.MAQ_TOKEN ?? generateToken();
|
|
52
|
-
const version = opts.version ?? "0.
|
|
54
|
+
const version = opts.version ?? "0.5.0";
|
|
53
55
|
const corsOrigin = opts.corsOrigin ?? process.env.MAQ_CORS_ORIGIN;
|
|
54
56
|
const registry = opts.registry ?? new SessionRegistry();
|
|
55
57
|
const interactive = new InteractiveRegistry();
|
|
56
58
|
const startedAt = Date.now();
|
|
59
|
+
// The request-box. Posture comes from config (set by the guided launcher).
|
|
60
|
+
const permissionMode = (() => {
|
|
61
|
+
const m = loadConfig().permissionMode;
|
|
62
|
+
return m === "full" ? "full" : "moderate";
|
|
63
|
+
})();
|
|
64
|
+
const broker = new PermissionBroker(permissionMode);
|
|
57
65
|
// Track live SSE responses so shutdown can end them deterministically.
|
|
58
66
|
// Without this, server.close() blocks forever on the long-lived event
|
|
59
67
|
// streams (they only emit a keep-alive ping every 15s and never end on their
|
|
@@ -123,6 +131,28 @@ export function createDaemon(opts = {}) {
|
|
|
123
131
|
sendJson(res, 200, commandCatalog());
|
|
124
132
|
return;
|
|
125
133
|
}
|
|
134
|
+
// The permission request-box.
|
|
135
|
+
if (path === "/v1/requests" && method === "GET") {
|
|
136
|
+
sendJson(res, 200, { mode: broker.getMode(), pending: broker.pending(), requests: broker.list() });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const reqMatch = /^\/v1\/requests\/([^/]+)$/.exec(path);
|
|
140
|
+
if (reqMatch && method === "POST") {
|
|
141
|
+
const id = decodeURIComponent(reqMatch[1]);
|
|
142
|
+
const body = await readJson(req);
|
|
143
|
+
const action = String(body.action ?? "");
|
|
144
|
+
let ok = false;
|
|
145
|
+
if (action === "approve")
|
|
146
|
+
ok = broker.approve(id, "web");
|
|
147
|
+
else if (action === "deny")
|
|
148
|
+
ok = broker.deny(id, "web");
|
|
149
|
+
else {
|
|
150
|
+
sendJson(res, 400, { error: "action must be approve|deny" });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
sendJson(res, ok ? 202 : 409, { id, action, ok, request: broker.get(id) });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
126
156
|
// Whitelisted CLI runner — powers the app's Master (terminal) edition.
|
|
127
157
|
// Only known `maq` subcommands run; never arbitrary shell.
|
|
128
158
|
if (path === "/v1/exec" && method === "POST") {
|
|
@@ -209,6 +239,9 @@ export function createDaemon(opts = {}) {
|
|
|
209
239
|
maxRounds: typeof body.maxRounds === "number" ? body.maxRounds : undefined,
|
|
210
240
|
maxIterations: typeof body.maxIterations === "number" ? body.maxIterations : undefined,
|
|
211
241
|
maxConcurrency: typeof body.maxConcurrency === "number" ? body.maxConcurrency : undefined,
|
|
242
|
+
// Major orchestration steps pass through the request-box, judged
|
|
243
|
+
// against this session's goal.
|
|
244
|
+
requestPermission: (action, detail, risk) => broker.gate(action, detail, { risk, goal: task }),
|
|
212
245
|
});
|
|
213
246
|
sendJson(res, 201, registry.summarize(s));
|
|
214
247
|
return;
|
package/dist/server/webui.js
CHANGED
|
@@ -90,6 +90,20 @@ export function webuiHtml(version) {
|
|
|
90
90
|
pre{white-space:pre-wrap;word-break:break-word;font-size:12px;color:var(--ink)}
|
|
91
91
|
.rhead{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--edge)}
|
|
92
92
|
a{color:var(--blue2)}
|
|
93
|
+
.overlay{position:fixed;inset:0;background:var(--bg);z-index:60;display:none;flex-direction:column}
|
|
94
|
+
.overlay.show{display:flex}
|
|
95
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px;padding:16px;overflow:auto;align-content:start}
|
|
96
|
+
.fcard{background:var(--panel);border:1px solid var(--edge);border-radius:12px;padding:14px;cursor:pointer}
|
|
97
|
+
.fcard:hover{border-color:var(--blue);box-shadow:0 0 0 1px var(--blue) inset}
|
|
98
|
+
.fcard h4{margin:0 0 4px;color:var(--ink);font-size:14px}
|
|
99
|
+
.fcard .cat{font-size:10px;color:var(--blue2);text-transform:uppercase;letter-spacing:1px}
|
|
100
|
+
.fcard p{margin:6px 0 0;font-size:12px;color:var(--mut)}
|
|
101
|
+
.req{padding:9px 12px;border-bottom:1px solid #0f1626}
|
|
102
|
+
.req .a{font-size:12.5px;color:var(--ink)} .req .r{font-size:11px;color:var(--mut);margin:2px 0 6px}
|
|
103
|
+
.req.destructive{box-shadow:inset 3px 0 0 var(--red)} .req.major{box-shadow:inset 3px 0 0 var(--warn)}
|
|
104
|
+
.req .btns{display:flex;gap:6px}
|
|
105
|
+
.req button{border:none;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:11px}
|
|
106
|
+
.ok-btn{background:var(--ok);color:#00231a} .no-btn{background:var(--red);color:#2a0007}
|
|
93
107
|
</style></head>
|
|
94
108
|
<body>
|
|
95
109
|
<header>
|
|
@@ -98,6 +112,7 @@ export function webuiHtml(version) {
|
|
|
98
112
|
<span class="e-mut" style="font-size:12px">megalodon · v${version}</span>
|
|
99
113
|
<span class="grow"></span>
|
|
100
114
|
<div class="switch">
|
|
115
|
+
<button onclick="openFeatures()">☰ Features</button>
|
|
101
116
|
<button onclick="feat('agents')">Agents</button>
|
|
102
117
|
<button onclick="feat('connectivity')">Connectivity</button>
|
|
103
118
|
<button onclick="feat('doctor')">Doctor</button>
|
|
@@ -107,6 +122,8 @@ export function webuiHtml(version) {
|
|
|
107
122
|
</header>
|
|
108
123
|
<main id="main">
|
|
109
124
|
<div class="col left">
|
|
125
|
+
<div class="sec">Request box <span id="reqCount" class="badge"></span></div>
|
|
126
|
+
<div id="requests"><div class="empty">No pending requests.</div></div>
|
|
110
127
|
<div class="sec">Sessions & history</div>
|
|
111
128
|
<div id="sessions"><div class="empty">No sessions yet.</div></div>
|
|
112
129
|
<div class="sec">Agents</div>
|
|
@@ -139,6 +156,15 @@ export function webuiHtml(version) {
|
|
|
139
156
|
<div id="preview" style="padding:12px"><div class="empty">Select a session to inspect its result.</div></div>
|
|
140
157
|
</div>
|
|
141
158
|
</main>
|
|
159
|
+
<div class="overlay" id="features">
|
|
160
|
+
<div class="rhead" style="padding:14px 16px">
|
|
161
|
+
<button class="ghost" onclick="closeFeatures()">← Back to home</button>
|
|
162
|
+
<b style="margin-left:12px">All features</b>
|
|
163
|
+
<span class="grow"></span>
|
|
164
|
+
<span class="e-mut" style="font-size:12px">everything MAQ can do — click to run</span>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="grid" id="featureGrid"><div class="empty">loading…</div></div>
|
|
167
|
+
</div>
|
|
142
168
|
<script>
|
|
143
169
|
"use strict";
|
|
144
170
|
var base = location.origin;
|
|
@@ -312,7 +338,57 @@ function renderAgents(agents){
|
|
|
312
338
|
});
|
|
313
339
|
}
|
|
314
340
|
|
|
315
|
-
|
|
341
|
+
async function openFeatures(){
|
|
342
|
+
document.getElementById('features').classList.add('show');
|
|
343
|
+
var grid=document.getElementById('featureGrid'); grid.innerHTML='loading…';
|
|
344
|
+
try{
|
|
345
|
+
var cat=await (await api('/v1/commands')).json();
|
|
346
|
+
var cmds=(cat.maq||[]); grid.innerHTML='';
|
|
347
|
+
cmds.forEach(function(c){
|
|
348
|
+
var card=h('div','fcard');
|
|
349
|
+
card.innerHTML='<div class="cat">'+c.category+'</div><h4>'+c.name+'</h4><p>'+c.summary+'</p>';
|
|
350
|
+
card.onclick=function(){ runFeature(c); };
|
|
351
|
+
grid.appendChild(card);
|
|
352
|
+
});
|
|
353
|
+
}catch(e){ grid.innerHTML='<div class="empty">'+e+'</div>'; }
|
|
354
|
+
}
|
|
355
|
+
function closeFeatures(){ document.getElementById('features').classList.remove('show'); }
|
|
356
|
+
function runFeature(c){
|
|
357
|
+
closeFeatures();
|
|
358
|
+
if(c.needsInput==='task'||c.needsInput==='query'){
|
|
359
|
+
var g=document.getElementById('goal'); g.focus();
|
|
360
|
+
if(c.name==='orchestrate'){ mode='parallel'; renderModes(); }
|
|
361
|
+
g.placeholder='['+c.name+'] '+c.summary;
|
|
362
|
+
} else {
|
|
363
|
+
feat(c.name);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function loadRequests(){
|
|
368
|
+
try{
|
|
369
|
+
var d=await (await api('/v1/requests')).json();
|
|
370
|
+
var box=document.getElementById('requests'); var pend=(d.pending||[]);
|
|
371
|
+
document.getElementById('reqCount').textContent=pend.length?String(pend.length):'';
|
|
372
|
+
if(!pend.length){ box.innerHTML='<div class="empty">No pending requests.</div>'; return; }
|
|
373
|
+
box.innerHTML='';
|
|
374
|
+
pend.forEach(function(r){
|
|
375
|
+
var it=h('div','req '+r.risk);
|
|
376
|
+
var a=h('div','a'); a.textContent=r.action+' — '+(r.detail||'');
|
|
377
|
+
var reason=h('div','r'); reason.textContent=r.risk+' · '+(r.reason||'');
|
|
378
|
+
var btns=h('div','btns');
|
|
379
|
+
var ok=h('button','ok-btn','Approve'); ok.onclick=function(){ decideReq(r.id,'approve'); };
|
|
380
|
+
var no=h('button','no-btn','Deny'); no.onclick=function(){ decideReq(r.id,'deny'); };
|
|
381
|
+
btns.appendChild(ok); btns.appendChild(no);
|
|
382
|
+
it.appendChild(a); it.appendChild(reason); it.appendChild(btns); box.appendChild(it);
|
|
383
|
+
});
|
|
384
|
+
}catch(e){}
|
|
385
|
+
}
|
|
386
|
+
async function decideReq(id,action){
|
|
387
|
+
try{ await api('/v1/requests/'+id,{method:'POST',body:JSON.stringify({action:action})}); loadRequests(); }catch(e){}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
renderModes(); refreshSessions(); feat('agents'); loadRequests();
|
|
391
|
+
setInterval(refreshSessions, 5000); setInterval(loadRequests, 3000);
|
|
316
392
|
document.getElementById('goal').addEventListener('keydown', function(e){ if((e.metaKey||e.ctrlKey)&&e.key==='Enter')startTask(); });
|
|
317
393
|
</script>
|
|
318
394
|
</body></html>`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maqcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MAQ master orchestrator — a token-efficient, agent-agnostic supervisor CLI that sits on top of any worker CLI (AI or not) via a Scout -> Plan -> Execute -> Verify pipeline.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|