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,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe command execution.
|
|
3
|
+
*
|
|
4
|
+
* Uses spawn with an explicit argument array and shell:false so user/model
|
|
5
|
+
* provided values can never be interpreted by a shell. This is the single
|
|
6
|
+
* chokepoint for running worker CLIs and raw commands.
|
|
7
|
+
*/
|
|
8
|
+
export interface ExecOptions {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
/** Milliseconds before the child is killed. */
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
/** Hard cap on captured output per stream to avoid unbounded memory. */
|
|
14
|
+
maxBuffer?: number;
|
|
15
|
+
/** Abort signal; kills the child when aborted. */
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
export interface ExecOutcome {
|
|
19
|
+
code: number | null;
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
timedOut: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Run a command safely. `cmd` is the binary, `args` are passed verbatim.
|
|
26
|
+
* Never pass a full command string here — that is what enables injection.
|
|
27
|
+
*/
|
|
28
|
+
export declare function execSafe(cmd: string, args?: string[], opts?: ExecOptions): Promise<ExecOutcome>;
|
|
29
|
+
/** A single line of worker output, tagged by stream. */
|
|
30
|
+
export interface StreamLine {
|
|
31
|
+
stream: "stdout" | "stderr";
|
|
32
|
+
line: string;
|
|
33
|
+
}
|
|
34
|
+
export interface StreamOptions extends ExecOptions {
|
|
35
|
+
/** Called for every complete line the worker emits (real-time). */
|
|
36
|
+
onLine?: (l: StreamLine) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run a command and stream its output line-by-line via `onLine`, in real time,
|
|
40
|
+
* while still capturing the full transcript for the returned outcome. Same
|
|
41
|
+
* safety guarantees as execSafe: shell:false, explicit argument array.
|
|
42
|
+
*/
|
|
43
|
+
export declare function execStream(cmd: string, args?: string[], opts?: StreamOptions): Promise<ExecOutcome>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe command execution.
|
|
3
|
+
*
|
|
4
|
+
* Uses spawn with an explicit argument array and shell:false so user/model
|
|
5
|
+
* provided values can never be interpreted by a shell. This is the single
|
|
6
|
+
* chokepoint for running worker CLIs and raw commands.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
/**
|
|
10
|
+
* Run a command safely. `cmd` is the binary, `args` are passed verbatim.
|
|
11
|
+
* Never pass a full command string here — that is what enables injection.
|
|
12
|
+
*/
|
|
13
|
+
export function execSafe(cmd, args = [], opts = {}) {
|
|
14
|
+
const maxBuffer = opts.maxBuffer ?? 10 * 1024 * 1024;
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const child = spawn(cmd, args, {
|
|
17
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
18
|
+
env: opts.env ?? process.env,
|
|
19
|
+
shell: false,
|
|
20
|
+
windowsHide: true,
|
|
21
|
+
});
|
|
22
|
+
let stdout = "";
|
|
23
|
+
let stderr = "";
|
|
24
|
+
let timedOut = false;
|
|
25
|
+
let killed = false;
|
|
26
|
+
const timer = opts.timeoutMs
|
|
27
|
+
? setTimeout(() => {
|
|
28
|
+
timedOut = true;
|
|
29
|
+
killed = true;
|
|
30
|
+
child.kill("SIGKILL");
|
|
31
|
+
}, opts.timeoutMs)
|
|
32
|
+
: null;
|
|
33
|
+
// Abort support: kill the child when the caller's signal fires (parity with
|
|
34
|
+
// execStream). Without this, a cancelled session could not stop a
|
|
35
|
+
// non-streaming worker (verify test-runner, the /v1/exec runner, …).
|
|
36
|
+
const onAbort = () => {
|
|
37
|
+
killed = true;
|
|
38
|
+
try {
|
|
39
|
+
child.kill("SIGKILL");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* ignore */
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
if (opts.signal) {
|
|
46
|
+
if (opts.signal.aborted)
|
|
47
|
+
onAbort();
|
|
48
|
+
else
|
|
49
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
50
|
+
}
|
|
51
|
+
child.stdout?.on("data", (d) => {
|
|
52
|
+
if (stdout.length < maxBuffer)
|
|
53
|
+
stdout += d.toString();
|
|
54
|
+
});
|
|
55
|
+
child.stderr?.on("data", (d) => {
|
|
56
|
+
if (stderr.length < maxBuffer)
|
|
57
|
+
stderr += d.toString();
|
|
58
|
+
});
|
|
59
|
+
child.on("error", (err) => {
|
|
60
|
+
if (timer)
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
63
|
+
resolve({ code: null, stdout, stderr: stderr + String(err), timedOut });
|
|
64
|
+
});
|
|
65
|
+
child.on("close", (code) => {
|
|
66
|
+
if (timer)
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
69
|
+
resolve({ code: killed ? null : code, stdout, stderr, timedOut });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Run a command and stream its output line-by-line via `onLine`, in real time,
|
|
75
|
+
* while still capturing the full transcript for the returned outcome. Same
|
|
76
|
+
* safety guarantees as execSafe: shell:false, explicit argument array.
|
|
77
|
+
*/
|
|
78
|
+
export function execStream(cmd, args = [], opts = {}) {
|
|
79
|
+
const maxBuffer = opts.maxBuffer ?? 20 * 1024 * 1024;
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const child = spawn(cmd, args, {
|
|
82
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
83
|
+
env: opts.env ?? process.env,
|
|
84
|
+
shell: false,
|
|
85
|
+
windowsHide: true,
|
|
86
|
+
});
|
|
87
|
+
let stdout = "";
|
|
88
|
+
let stderr = "";
|
|
89
|
+
let outBuf = "";
|
|
90
|
+
let errBuf = "";
|
|
91
|
+
let timedOut = false;
|
|
92
|
+
let killed = false;
|
|
93
|
+
const timer = opts.timeoutMs
|
|
94
|
+
? setTimeout(() => {
|
|
95
|
+
timedOut = true;
|
|
96
|
+
killed = true;
|
|
97
|
+
child.kill("SIGKILL");
|
|
98
|
+
}, opts.timeoutMs)
|
|
99
|
+
: null;
|
|
100
|
+
const onAbort = () => {
|
|
101
|
+
killed = true;
|
|
102
|
+
try {
|
|
103
|
+
child.kill("SIGKILL");
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
if (opts.signal) {
|
|
110
|
+
if (opts.signal.aborted)
|
|
111
|
+
onAbort();
|
|
112
|
+
else
|
|
113
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
114
|
+
}
|
|
115
|
+
const pump = (chunk, which) => {
|
|
116
|
+
let buf = (which === "stdout" ? outBuf : errBuf) + chunk;
|
|
117
|
+
const parts = buf.split(/\r?\n/);
|
|
118
|
+
buf = parts.pop() ?? "";
|
|
119
|
+
if (which === "stdout")
|
|
120
|
+
outBuf = buf;
|
|
121
|
+
else
|
|
122
|
+
errBuf = buf;
|
|
123
|
+
for (const line of parts)
|
|
124
|
+
opts.onLine?.({ stream: which, line });
|
|
125
|
+
};
|
|
126
|
+
child.stdout?.on("data", (d) => {
|
|
127
|
+
const s = d.toString();
|
|
128
|
+
if (stdout.length < maxBuffer)
|
|
129
|
+
stdout += s;
|
|
130
|
+
pump(s, "stdout");
|
|
131
|
+
});
|
|
132
|
+
child.stderr?.on("data", (d) => {
|
|
133
|
+
const s = d.toString();
|
|
134
|
+
if (stderr.length < maxBuffer)
|
|
135
|
+
stderr += s;
|
|
136
|
+
pump(s, "stderr");
|
|
137
|
+
});
|
|
138
|
+
child.on("error", (err) => {
|
|
139
|
+
if (timer)
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
resolve({ code: null, stdout, stderr: stderr + String(err), timedOut });
|
|
142
|
+
});
|
|
143
|
+
child.on("close", (code) => {
|
|
144
|
+
if (timer)
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
147
|
+
if (outBuf)
|
|
148
|
+
opts.onLine?.({ stream: "stdout", line: outBuf });
|
|
149
|
+
if (errBuf)
|
|
150
|
+
opts.onLine?.({ stream: "stderr", line: errBuf });
|
|
151
|
+
resolve({ code: killed ? null : code, stdout, stderr, timedOut });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flows — scheduled agent sessions (CAO-style, dependency-free). A flow pairs a
|
|
3
|
+
* task with a simple schedule and (optionally) a target/profile. Flows persist
|
|
4
|
+
* to `~/.maqcli/flows.json`; a Scheduler ticks them on the daemon.
|
|
5
|
+
*
|
|
6
|
+
* Schedule grammar (kept intentionally small, not full cron):
|
|
7
|
+
* "@hourly" | "@daily" | "@weekly"
|
|
8
|
+
* "every 30m" | "every 2h" | "every 45s"
|
|
9
|
+
* `nextRun(schedule, from)` returns the next Date; a real cron parser can slot
|
|
10
|
+
* in behind it later.
|
|
11
|
+
*/
|
|
12
|
+
export interface Flow {
|
|
13
|
+
name: string;
|
|
14
|
+
schedule: string;
|
|
15
|
+
task: string;
|
|
16
|
+
target?: string;
|
|
17
|
+
profile?: string;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function loadFlows(): Flow[];
|
|
22
|
+
export declare function saveFlows(flows: Flow[]): void;
|
|
23
|
+
export declare function addFlow(flow: Flow): Flow[];
|
|
24
|
+
export declare function removeFlow(name: string): Flow[];
|
|
25
|
+
/** Parse a schedule to a millisecond interval, or null if invalid. */
|
|
26
|
+
export declare function scheduleMs(schedule: string): number | null;
|
|
27
|
+
export declare function nextRun(schedule: string, from?: Date): Date | null;
|
|
28
|
+
/** Ticks enabled flows on their interval, invoking `run(flow)` each time. */
|
|
29
|
+
export declare class Scheduler {
|
|
30
|
+
private run;
|
|
31
|
+
private log?;
|
|
32
|
+
private timers;
|
|
33
|
+
constructor(run: (flow: Flow) => void, log?: ((msg: string) => void) | undefined);
|
|
34
|
+
start(flows?: Flow[]): void;
|
|
35
|
+
stop(): void;
|
|
36
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flows — scheduled agent sessions (CAO-style, dependency-free). A flow pairs a
|
|
3
|
+
* task with a simple schedule and (optionally) a target/profile. Flows persist
|
|
4
|
+
* to `~/.maqcli/flows.json`; a Scheduler ticks them on the daemon.
|
|
5
|
+
*
|
|
6
|
+
* Schedule grammar (kept intentionally small, not full cron):
|
|
7
|
+
* "@hourly" | "@daily" | "@weekly"
|
|
8
|
+
* "every 30m" | "every 2h" | "every 45s"
|
|
9
|
+
* `nextRun(schedule, from)` returns the next Date; a real cron parser can slot
|
|
10
|
+
* in behind it later.
|
|
11
|
+
*/
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
function flowsPath() {
|
|
16
|
+
const dir = process.env.MAQ_CONFIG_DIR ?? join(homedir(), ".maqcli");
|
|
17
|
+
return join(dir, "flows.json");
|
|
18
|
+
}
|
|
19
|
+
export function loadFlows() {
|
|
20
|
+
const p = flowsPath();
|
|
21
|
+
if (!existsSync(p))
|
|
22
|
+
return [];
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function saveFlows(flows) {
|
|
31
|
+
const p = flowsPath();
|
|
32
|
+
mkdirSync(join(p, ".."), { recursive: true });
|
|
33
|
+
writeFileSync(p, JSON.stringify(flows, null, 2), "utf8");
|
|
34
|
+
}
|
|
35
|
+
export function addFlow(flow) {
|
|
36
|
+
const flows = loadFlows().filter((f) => f.name !== flow.name);
|
|
37
|
+
flows.push(flow);
|
|
38
|
+
saveFlows(flows);
|
|
39
|
+
return flows;
|
|
40
|
+
}
|
|
41
|
+
export function removeFlow(name) {
|
|
42
|
+
const flows = loadFlows().filter((f) => f.name !== name);
|
|
43
|
+
saveFlows(flows);
|
|
44
|
+
return flows;
|
|
45
|
+
}
|
|
46
|
+
/** Parse a schedule to a millisecond interval, or null if invalid. */
|
|
47
|
+
export function scheduleMs(schedule) {
|
|
48
|
+
const s = schedule.trim().toLowerCase();
|
|
49
|
+
if (s === "@hourly")
|
|
50
|
+
return 3600_000;
|
|
51
|
+
if (s === "@daily")
|
|
52
|
+
return 86_400_000;
|
|
53
|
+
if (s === "@weekly")
|
|
54
|
+
return 604_800_000;
|
|
55
|
+
const m = /^every\s+(\d+)\s*([smhd])$/.exec(s);
|
|
56
|
+
if (m) {
|
|
57
|
+
const n = Number(m[1]);
|
|
58
|
+
const unit = { s: 1000, m: 60_000, h: 3600_000, d: 86_400_000 }[m[2]];
|
|
59
|
+
return n * unit;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
export function nextRun(schedule, from = new Date()) {
|
|
64
|
+
const ms = scheduleMs(schedule);
|
|
65
|
+
return ms === null ? null : new Date(from.getTime() + ms);
|
|
66
|
+
}
|
|
67
|
+
/** Ticks enabled flows on their interval, invoking `run(flow)` each time. */
|
|
68
|
+
export class Scheduler {
|
|
69
|
+
run;
|
|
70
|
+
log;
|
|
71
|
+
timers = [];
|
|
72
|
+
constructor(run, log) {
|
|
73
|
+
this.run = run;
|
|
74
|
+
this.log = log;
|
|
75
|
+
}
|
|
76
|
+
start(flows = loadFlows()) {
|
|
77
|
+
this.stop();
|
|
78
|
+
for (const flow of flows) {
|
|
79
|
+
if (!flow.enabled)
|
|
80
|
+
continue;
|
|
81
|
+
const ms = scheduleMs(flow.schedule);
|
|
82
|
+
if (ms === null) {
|
|
83
|
+
this.log?.(`flow '${flow.name}': invalid schedule '${flow.schedule}', skipped`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const timer = setInterval(() => this.run(flow), ms);
|
|
87
|
+
this.timers.push(timer);
|
|
88
|
+
this.log?.(`flow '${flow.name}': scheduled ${flow.schedule} (every ${ms}ms)`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
stop() {
|
|
92
|
+
for (const t of this.timers)
|
|
93
|
+
clearInterval(t);
|
|
94
|
+
this.timers = [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headroom-style context compression (local, reversible).
|
|
3
|
+
*
|
|
4
|
+
* This is a lightweight, dependency-free implementation of the pattern used by
|
|
5
|
+
* Headroom (github.com/headroomlabs-ai/headroom): compress verbose tool output
|
|
6
|
+
* / logs / file reads before they reach a model, while keeping the original
|
|
7
|
+
* retrievable on demand (Compress-Cache-Retrieve). It is intentionally
|
|
8
|
+
* conservative — lossless-ish transforms only — so it is safe to enable by
|
|
9
|
+
* default. The real product can swap this for the upstream engine.
|
|
10
|
+
*/
|
|
11
|
+
export interface CompressionResult {
|
|
12
|
+
compressed: string;
|
|
13
|
+
originalTokensEst: number;
|
|
14
|
+
compressedTokensEst: number;
|
|
15
|
+
ratio: number;
|
|
16
|
+
/** Retrieval handle for the original content. */
|
|
17
|
+
ref: string;
|
|
18
|
+
}
|
|
19
|
+
/** Rough token estimate: ~4 chars/token, good enough for budgeting. */
|
|
20
|
+
export declare function estimateTokens(text: string): number;
|
|
21
|
+
export declare class Headroom {
|
|
22
|
+
private store;
|
|
23
|
+
/** Compress a blob of text and cache the original for retrieval. */
|
|
24
|
+
compress(content: string): CompressionResult;
|
|
25
|
+
/** Retrieve the original content by reference (the escape hatch). */
|
|
26
|
+
retrieve(ref: string): string | undefined;
|
|
27
|
+
size(): number;
|
|
28
|
+
/**
|
|
29
|
+
* Conservative, reversible-in-spirit transforms:
|
|
30
|
+
* - collapse runs of blank lines
|
|
31
|
+
* - trim trailing whitespace
|
|
32
|
+
* - collapse long runs of repeated lines into a "(xN)" marker
|
|
33
|
+
* - re-serialize JSON compactly when the whole blob parses as JSON
|
|
34
|
+
*/
|
|
35
|
+
private transform;
|
|
36
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headroom-style context compression (local, reversible).
|
|
3
|
+
*
|
|
4
|
+
* This is a lightweight, dependency-free implementation of the pattern used by
|
|
5
|
+
* Headroom (github.com/headroomlabs-ai/headroom): compress verbose tool output
|
|
6
|
+
* / logs / file reads before they reach a model, while keeping the original
|
|
7
|
+
* retrievable on demand (Compress-Cache-Retrieve). It is intentionally
|
|
8
|
+
* conservative — lossless-ish transforms only — so it is safe to enable by
|
|
9
|
+
* default. The real product can swap this for the upstream engine.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
/** Rough token estimate: ~4 chars/token, good enough for budgeting. */
|
|
13
|
+
export function estimateTokens(text) {
|
|
14
|
+
return Math.ceil(text.length / 4);
|
|
15
|
+
}
|
|
16
|
+
export class Headroom {
|
|
17
|
+
store = new Map();
|
|
18
|
+
/** Compress a blob of text and cache the original for retrieval. */
|
|
19
|
+
compress(content) {
|
|
20
|
+
const ref = "hr_" + createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
21
|
+
this.store.set(ref, content);
|
|
22
|
+
const compressed = this.transform(content);
|
|
23
|
+
const originalTokensEst = estimateTokens(content);
|
|
24
|
+
const compressedTokensEst = estimateTokens(compressed);
|
|
25
|
+
return {
|
|
26
|
+
compressed,
|
|
27
|
+
originalTokensEst,
|
|
28
|
+
compressedTokensEst,
|
|
29
|
+
ratio: originalTokensEst === 0 ? 1 : compressedTokensEst / originalTokensEst,
|
|
30
|
+
ref,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Retrieve the original content by reference (the escape hatch). */
|
|
34
|
+
retrieve(ref) {
|
|
35
|
+
return this.store.get(ref);
|
|
36
|
+
}
|
|
37
|
+
size() {
|
|
38
|
+
return this.store.size;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Conservative, reversible-in-spirit transforms:
|
|
42
|
+
* - collapse runs of blank lines
|
|
43
|
+
* - trim trailing whitespace
|
|
44
|
+
* - collapse long runs of repeated lines into a "(xN)" marker
|
|
45
|
+
* - re-serialize JSON compactly when the whole blob parses as JSON
|
|
46
|
+
*/
|
|
47
|
+
transform(content) {
|
|
48
|
+
const trimmed = content.trim();
|
|
49
|
+
// JSON: compact it (drops insignificant whitespace only).
|
|
50
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
51
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.stringify(JSON.parse(trimmed));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* fall through to text handling */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const lines = content.split(/\r?\n/).map((l) => l.replace(/[ \t]+$/g, ""));
|
|
60
|
+
const out = [];
|
|
61
|
+
let blankRun = 0;
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < lines.length) {
|
|
64
|
+
const line = lines[i];
|
|
65
|
+
if (line === "") {
|
|
66
|
+
blankRun++;
|
|
67
|
+
if (blankRun <= 1)
|
|
68
|
+
out.push("");
|
|
69
|
+
i++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
blankRun = 0;
|
|
73
|
+
// Collapse consecutive identical non-blank lines.
|
|
74
|
+
let repeat = 1;
|
|
75
|
+
while (i + repeat < lines.length && lines[i + repeat] === line)
|
|
76
|
+
repeat++;
|
|
77
|
+
if (repeat >= 3) {
|
|
78
|
+
out.push(`${line} (x${repeat})`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
for (let k = 0; k < repeat; k++)
|
|
82
|
+
out.push(line);
|
|
83
|
+
}
|
|
84
|
+
i += repeat;
|
|
85
|
+
}
|
|
86
|
+
return out.join("\n");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module help-topics
|
|
3
|
+
*
|
|
4
|
+
* Conceptual help system for `maq help <topic>`.
|
|
5
|
+
* Provides detailed reference content for core maq concepts
|
|
6
|
+
* including the pipeline, permissions, routing, board, skills,
|
|
7
|
+
* providers, safety, and audit subsystems.
|
|
8
|
+
*
|
|
9
|
+
* Zero npm dependencies. ESM module.
|
|
10
|
+
*/
|
|
11
|
+
/** A single help topic with its name, display title, and prose content. */
|
|
12
|
+
export interface HelpTopic {
|
|
13
|
+
name: string;
|
|
14
|
+
title: string;
|
|
15
|
+
content: string;
|
|
16
|
+
}
|
|
17
|
+
/** Returns a shallow copy of all available help topics. */
|
|
18
|
+
export declare function listTopics(): HelpTopic[];
|
|
19
|
+
/** Looks up a topic by name (case-insensitive). Returns `null` if not found. */
|
|
20
|
+
export declare function getTopic(name: string): HelpTopic | null;
|
|
21
|
+
/**
|
|
22
|
+
* Renders a topic as a formatted string suitable for terminal output.
|
|
23
|
+
* The title is displayed with an underline, followed by word-wrapped
|
|
24
|
+
* paragraphs of the content body.
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderTopic(topic: HelpTopic): string;
|