@staix/agent-hub 0.12.19 → 0.12.20
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/CHANGELOG.md +16 -0
- package/README.md +3 -3
- package/docs/agent-notes/adapters.md +1 -0
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +2 -1
- package/docs/agent-notes/models.md +3 -0
- package/docs/agent-notes/tasks.md +3 -2
- package/docs/events.md +4 -1
- package/docs/operations.md +413 -17
- package/docs/quickstart.md +1 -1
- package/docs/security.md +17 -10
- package/docs/smoke.md +58 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +399 -3
- package/docs/specs/2026-09-20-pi-local-models-design.md +2 -0
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +287 -0
- package/docs/specs/2026-10-04-switchyard-source-port-design.md +10 -0
- package/docs/verified.json +13 -13
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +63 -25
- package/src/adapters/claude-channel.ts +61 -20
- package/src/adapters/codex-appserver.ts +3 -0
- package/src/adapters/local-worker.ts +4 -0
- package/src/cli/console-state.ts +325 -102
- package/src/cli/console.ts +44 -23
- package/src/cli/help.ts +120 -0
- package/src/cli/identity.ts +3 -3
- package/src/cli/launch.ts +2 -0
- package/src/cli/main.ts +171 -75
- package/src/cli/preview.ts +1 -0
- package/src/cli/reset.ts +194 -0
- package/src/cli/status-lines.ts +9 -5
- package/src/cli/terminal-recovery.ts +140 -36
- package/src/cli/upgrade-runtime.ts +274 -56
- package/src/cli/upgrade.ts +366 -41
- package/src/hub/ask.ts +1 -1
- package/src/hub/board.ts +15 -7
- package/src/hub/bus.ts +60 -4
- package/src/hub/child-process.ts +1 -0
- package/src/hub/conductor.ts +38 -6
- package/src/hub/control-client.ts +5 -3
- package/src/hub/daemon.ts +56 -9
- package/src/hub/events.ts +7 -1
- package/src/hub/execution-budget.ts +15 -6
- package/src/hub/hub-tools.ts +4 -4
- package/src/hub/inference.ts +136 -1
- package/src/hub/lifecycle.ts +7 -1
- package/src/hub/peers.ts +2 -0
- package/src/hub/recovery-store.ts +51 -3
- package/src/hub/report.ts +43 -1
- package/src/hub/restart.ts +25 -0
- package/src/hub/routing.ts +48 -16
- package/src/hub/snapshots.ts +3 -2
- package/src/hub/tasks.ts +236 -83
- package/src/local/deny.ts +2 -1
- package/src/models/mlx.ts +11 -8
- package/src/models/ollama.ts +2 -2
- package/src/models/relay.ts +190 -42
- package/src/models/route/relay-selector.ts +43 -31
- package/src/models/route/runtime.ts +17 -7
- package/src/models/route/signals.ts +12 -1
- package/src/models/route/stage.ts +57 -3
- package/src/omniroute/client.ts +1 -1
- package/src/ui/index.html +1 -0
- package/templates/AGENTS.block.md +1 -1
- package/templates/routing.toml +8 -0
package/src/cli/console.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import { fileURLToPath } from "node:url";
|
|
2
3
|
import { StringDecoder } from "node:string_decoder";
|
|
3
4
|
import type { ControlClient } from "../hub/control-client.ts";
|
|
4
5
|
import { contextLine } from "./status-lines.ts";
|
|
5
6
|
import { renderTailEvent } from "./tail-render.ts";
|
|
6
|
-
import { initialConsoleState, paint, pruneApprovals, reduceConsole, renderConsoleLines, resolveColor, stateTone, terminalText, wrap } from "./console-state.ts";
|
|
7
|
-
import type { ConsoleEffect, ConsoleEvent, Tone } from "./console-state.ts";
|
|
7
|
+
import { approvalFrom, initialConsoleState, keyTable, notify, paint, panelRows, permissionText, plural, pruneApprovals, reduceConsole, renderConsoleLines, resolveColor, stateTone, terminalText, wrap } from "./console-state.ts";
|
|
8
|
+
import type { ConsoleEffect, ConsoleEvent, Detail, Tone } from "./console-state.ts";
|
|
8
9
|
import type { BusEvent } from "../hub/bus.ts";
|
|
9
10
|
|
|
10
11
|
export interface ConsoleTerminal {
|
|
@@ -56,11 +57,16 @@ function nativeTerminal(): ConsoleTerminal {
|
|
|
56
57
|
onSignal: listener => { process.on("SIGINT", listener); process.on("SIGTERM", listener); return () => { process.off("SIGINT", listener); process.off("SIGTERM", listener); }; },
|
|
57
58
|
};
|
|
58
59
|
}
|
|
60
|
+
/** `task_show` text as labeled fields; a message that is not a task stays text. */
|
|
61
|
+
function labeled(text: string): Detail {
|
|
62
|
+
try { const value = JSON.parse(text); return value && typeof value === "object" && !Array.isArray(value) ? value : String(value); }
|
|
63
|
+
catch { return text; }
|
|
64
|
+
}
|
|
59
65
|
/** One terminal client, no daemon ownership. All text crosses terminalText before writing. */
|
|
60
66
|
export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
61
67
|
const { client } = options; const terminal = options.terminal ?? nativeTerminal();
|
|
62
68
|
const color = options.color ?? resolveColor(undefined, { isTTY: terminal.isTTY, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR }) === true;
|
|
63
|
-
let state = initialConsoleState(!!options.panels);
|
|
69
|
+
let state = initialConsoleState(!!options.panels); state.project = basename(options.cwd);
|
|
64
70
|
let columns = options.columns ?? terminal.columns; let rows = options.rows ?? terminal.rows;
|
|
65
71
|
let active = true; let inAlternate = false; let plain = !terminal.isTTY || columns < 80 || rows < 24;
|
|
66
72
|
let polling = false; let childRunning = false; let cancelChild: (() => void) | undefined;
|
|
@@ -68,14 +74,16 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
68
74
|
const removers: (() => void)[] = []; const timers: ReturnType<typeof setInterval>[] = [];
|
|
69
75
|
let resolveDone: () => void = () => {}; const done = new Promise<void>(resolve => { resolveDone = resolve; });
|
|
70
76
|
let pendingStream: ConsoleEvent[] = []; let droppedStream = 0;
|
|
77
|
+
const notice = (text: string) => notify(state, text, Date.now());
|
|
71
78
|
const safeWrite = (text: string) => terminal.write(paint([{ text }], color));
|
|
72
79
|
const streamLines = (event: ConsoleEvent) => terminalText(event.text).split("\n").flatMap((text, index) => wrap(text, columns).map(line => [{ text: line, ...(index === 0 && event.tone ? { tone: event.tone } : {}) }]));
|
|
73
80
|
const writeStream = (event: ConsoleEvent) => {
|
|
74
|
-
terminal.write(`\x1b[${rows -
|
|
81
|
+
terminal.write(`\x1b[${rows - 4};1H`); // the scroll region ends above the rule and the three footer lines
|
|
75
82
|
for (const line of streamLines(event)) { terminal.write(paint(line, color)); terminal.write("\r\n"); }
|
|
76
83
|
};
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
/** `record: false` is for the console's own prints (the key table, a request viewed again): not events, so not in Events. */
|
|
85
|
+
const stream = (event: ConsoleEvent, record = true) => {
|
|
86
|
+
if (record) state.events = [...state.events, { ...event, text: terminalText(event.text) }].slice(-1000);
|
|
79
87
|
if (plain) {
|
|
80
88
|
const [header, ...body] = terminalText(event.text).split("\n");
|
|
81
89
|
terminal.write(paint([{ text: header ?? "", tone: event.tone }, ...body.map(text => ({ text: "\n" + text }))], color) + "\n");
|
|
@@ -87,14 +95,14 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
87
95
|
if (!active || plain) return;
|
|
88
96
|
if (state.mode === "panels" && !inAlternate) { terminal.write("\x1b[r\x1b[?1049h\x1b[2J"); inAlternate = true; }
|
|
89
97
|
else if (state.mode === "stream" && inAlternate) {
|
|
90
|
-
terminal.write(`\x1b[?1049l\x1b[1;${rows -
|
|
91
|
-
if (droppedStream) writeStream({ text: `${droppedStream
|
|
98
|
+
terminal.write(`\x1b[?1049l\x1b[1;${rows - 4}r`); inAlternate = false;
|
|
99
|
+
if (droppedStream) writeStream({ text: `${plural(droppedStream, "older panel-mode event")} omitted from console memory; inspect hub.log for the full stream.` });
|
|
92
100
|
for (const event of pendingStream) writeStream(event);
|
|
93
101
|
pendingStream = []; droppedStream = 0;
|
|
94
102
|
}
|
|
95
|
-
terminal.write(state.mode === "stream" ? `\x1b[1;${rows -
|
|
103
|
+
terminal.write(state.mode === "stream" ? `\x1b[1;${rows - 4}r` : "\x1b[r");
|
|
96
104
|
const lines = renderConsoleLines(state, columns, rows);
|
|
97
|
-
const start =
|
|
105
|
+
const start = rows - lines.length + 1;
|
|
98
106
|
for (const [index, line] of lines.entries()) { terminal.write(`\x1b[${start + index};1H\x1b[2K`); terminal.write(paint(line, color)); }
|
|
99
107
|
terminal.write(`\x1b[${rows};${Math.min(columns, Bun.stringWidth(paint(lines.at(-1) ?? [], false)) + 1)}H\x1b[?25h`);
|
|
100
108
|
};
|
|
@@ -129,9 +137,9 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
129
137
|
state.tasks = parsed.map(task => ({ ...task, ready: readyIds.has(task.id) }));
|
|
130
138
|
} }
|
|
131
139
|
if (queue?.ok && state.mode === "panels" && Array.isArray(queue.deliveries)) state.queue = queue.deliveries;
|
|
132
|
-
const error = replies.find(reply => reply.ok === false)?.error; if (error)
|
|
140
|
+
const error = replies.find(reply => reply.ok === false)?.error; if (error) notice(String(error));
|
|
133
141
|
draw();
|
|
134
|
-
} catch (error) { if (active) {
|
|
142
|
+
} catch (error) { if (active) { notice(String((error as Error).message)); draw(); } }
|
|
135
143
|
finally { polling = false; }
|
|
136
144
|
};
|
|
137
145
|
const run = options.runCommand ?? ((args, output, finished) => {
|
|
@@ -153,38 +161,49 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
153
161
|
if (!action.option) stream({ text: ` ! denial requested for permission ${action.id}`, kind: "permission", tone: "failure" });
|
|
154
162
|
return;
|
|
155
163
|
}
|
|
164
|
+
if (action.type === "keys") return stream({ text: keyTable(columns).map(line => paint(line, false)).join("\n"), kind: "console" }, false);
|
|
165
|
+
if (action.type === "print") return stream({ text: action.text, kind: action.kind, ...(action.tone ? { tone: action.tone } : {}) }, false);
|
|
156
166
|
if (action.type === "show") {
|
|
157
167
|
const result = await client.request(action.panel === 3 ? { t: "task", op: "task_show", args: { id: action.id } } : { t: "queue", op: "show", id: action.id }, 3000);
|
|
158
|
-
|
|
168
|
+
// A late reply is for the panel and row it was asked for: once the person has moved on, it would show one item
|
|
169
|
+
// while the keys act on another, so it is dropped.
|
|
170
|
+
if (!active || state.mode !== "panels" || state.panel !== action.panel || state.help || state.detail !== undefined || String(panelRows(state)[state.selection]?.id) !== action.id) return;
|
|
159
171
|
state.detailOffset = 0;
|
|
160
|
-
state.detail = result.ok === false ? String(result.error) : action.panel === 3 ? String(result.text) :
|
|
172
|
+
state.detail = result.ok === false ? String(result.error) : action.panel === 3 ? labeled(String(result.text)) : result.delivery ?? "delivery not found";
|
|
161
173
|
draw(); return;
|
|
162
174
|
}
|
|
163
|
-
if (childRunning) {
|
|
175
|
+
if (childRunning) { notice("a command is already running"); draw(); return; }
|
|
164
176
|
childRunning = true;
|
|
165
177
|
stream({ text: `> ${action.args.join(" ")}`, kind: "command" });
|
|
166
178
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
// Output lines sit at column 4, as message bodies do: what a command prints can carry peer text, and columns 0 and 2 are the hub's.
|
|
180
|
+
const output = (text: string) => { if (active) { stream({ text: text.replace(/\n$/, "").replace(/^/gm, " "), kind: "command" }); draw(); } };
|
|
181
|
+
cancelChild = run(action.args, output, () => { childRunning = false; cancelChild = undefined; if (active) void refresh(); });
|
|
182
|
+
} catch (error) { childRunning = false; notice(String((error as Error).message)); draw(); }
|
|
169
183
|
};
|
|
170
184
|
const handle = (key: string) => {
|
|
171
185
|
if (!active) return;
|
|
172
186
|
const result = reduceConsole(state, key); state = result.state;
|
|
173
|
-
if (state.mode === "panels" && (plain || columns < 80 || rows < 24)) { state.mode = "stream";
|
|
187
|
+
if (state.mode === "panels" && (plain || columns < 80 || rows < 24)) { state.mode = "stream"; notice("panels need at least 80x24"); }
|
|
174
188
|
for (const action of result.effects) void effect(action).catch(error => stop(`Console error: ${String(error.message)}`));
|
|
175
189
|
draw();
|
|
176
|
-
if (key === "v" && state.detail && state.mode === "stream") { stream({ text: state.detail, kind: "permission" }); state.detail = undefined; draw(); }
|
|
177
190
|
if (key === "\t") void refresh();
|
|
178
191
|
};
|
|
179
192
|
client.onPush = msg => {
|
|
180
193
|
if (!active) return;
|
|
181
194
|
try {
|
|
182
195
|
if (msg.t === "event") stream({ text: renderTailEvent(msg.e), peer: msg.e.peer ?? msg.e.env?.from, kind: msg.e.env?.kind ?? msg.e.t, tone: eventTone(msg.e) });
|
|
183
|
-
else if (msg.t === "context") stream({ text: ` ${msg.peer}: ${contextLine(msg.reading)}`, peer: msg.peer, kind: "context" });
|
|
196
|
+
else if (msg.t === "context") stream({ text: ` ${msg.peer}: ${contextLine(msg.reading, at => new Date(at).toLocaleTimeString())}`, peer: msg.peer, kind: "context" });
|
|
184
197
|
else if (msg.t === "notice") stream({ text: ` * ${msg.line}`, kind: "notice" });
|
|
185
198
|
else if (msg.t === "permission") {
|
|
186
|
-
|
|
187
|
-
|
|
199
|
+
const request = approvalFrom(msg);
|
|
200
|
+
stream({ text: permissionText(request), peer: request.peer, kind: "permission", tone: "attention" });
|
|
201
|
+
// Only the first request is selected on arrival: once a selection has ended, a request that arrives as the person
|
|
202
|
+
// reacts must not take its place, so from then on only [ ], j or k select.
|
|
203
|
+
if (request.expiresAt > Date.now() && !state.approvals.some(a => a.id === request.id)) {
|
|
204
|
+
state.approvals.push(request);
|
|
205
|
+
if (!state.autoSelected) { state.approvalId = request.id; state.autoSelected = true; }
|
|
206
|
+
}
|
|
188
207
|
} else if (msg.t === "permission_closed") {
|
|
189
208
|
state.approvals = state.approvals.filter(a => a.id !== msg.id); state = pruneApprovals(state, Date.now());
|
|
190
209
|
if (msg.reason === "expired" || msg.outcome === "cancelled" || msg.outcome?.startsWith("reject")) stream({ text: ` ! ${msg.peer} permission ${msg.id} ${msg.reason ?? msg.outcome}`, peer: msg.peer, kind: "permission", tone: "failure" });
|
|
@@ -199,7 +218,9 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
199
218
|
terminal.raw(true);
|
|
200
219
|
removers.push(terminal.onData(text => {
|
|
201
220
|
// A pasted chunk can only edit text, never approve, confirm or run commands. Bracketed-paste delimiters are stripped.
|
|
221
|
+
// Under the modal key table it does nothing at all.
|
|
202
222
|
if (Array.from(text).length > 1 && !["\x1b[A", "\x1b[B", "\x1b[C", "\x1b[D"].includes(text)) {
|
|
223
|
+
if (state.help && state.mode === "panels") return;
|
|
203
224
|
state.confirm = undefined; state.optionChoice = undefined; state.editing = true;
|
|
204
225
|
state.input += terminalText(text.replace(/\x1b\[20[01]~/g, "")).replace(/\n/g, " "); draw();
|
|
205
226
|
} else handle(text);
|
|
@@ -208,7 +229,7 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
208
229
|
columns = terminal.columns; rows = terminal.rows; plain = columns < 80 || rows < 24;
|
|
209
230
|
if (plain) {
|
|
210
231
|
if (inAlternate) { terminal.write("\x1b[?1049l"); inAlternate = false; }
|
|
211
|
-
terminal.write("\x1b[r"); state.mode = "stream";
|
|
232
|
+
terminal.write("\x1b[r"); state.mode = "stream"; notice("panels need at least 80x24");
|
|
212
233
|
safeWrite("\nTerminal below 80x24; showing the plain stream.\n");
|
|
213
234
|
}
|
|
214
235
|
draw();
|
package/src/cli/help.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { VERSION } from "../version.ts";
|
|
2
|
+
import { paint, wrap, type Span } from "./console-state.ts";
|
|
3
|
+
|
|
4
|
+
/** One usage form of `ahub help`: the usage starts with `ahub <command>`, alternatives joined by `|`. */
|
|
5
|
+
export interface HelpEntry { section: string; usage: string; description: string }
|
|
6
|
+
|
|
7
|
+
const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
8
|
+
["Projects", [
|
|
9
|
+
["ahub --project <path|id> <command>", "select a repository or worktree explicitly"],
|
|
10
|
+
["ahub projects [--json]", "list registered projects and live status"],
|
|
11
|
+
["ahub projects remove <id>", "forget a stopped registration (keeps project files)"],
|
|
12
|
+
["ahub setup [--yes]", "install or update the Claude Code channel plugin from this package, then run doctor"],
|
|
13
|
+
["ahub init [--dry-run --json]", "preview or write .agenthub/config.json and the AGENTS.md marker block (drops a legacy CLAUDE.md block)"],
|
|
14
|
+
["ahub help [command]", "this help, or one command's entries; also --help and -h"],
|
|
15
|
+
["ahub version | --version", "print the installed version"],
|
|
16
|
+
]],
|
|
17
|
+
["Daemon and runtime", [
|
|
18
|
+
["ahub up [--unattended] [--no-console]", "start the daemon; interactive terminals enter console"],
|
|
19
|
+
["ahub console [--panels] [--color=auto|always|never]", "enter the human console, leaving the daemon running on exit"],
|
|
20
|
+
["ahub status", "the hub, its peers, model backends and task counts"],
|
|
21
|
+
["ahub status --all", "show every registered project"],
|
|
22
|
+
["ahub logs [-f]", "the last 100 lines of hub.log; -f shows the last 10 and follows it"],
|
|
23
|
+
["ahub doctor", "check the tools, daemon, plugin, gateway, models and memory worker this project uses"],
|
|
24
|
+
["ahub doctor --orphans [--kill]", "list registrations whose project root is gone; --kill stops their daemons (SIGTERM, then SIGKILL) only after the process identity checks out"],
|
|
25
|
+
["ahub kill", "stop this project's hub"],
|
|
26
|
+
["ahub reset [--all] [--yes]", "list, then with --yes stop the hub and discard every queued and needs_review delivery, clear holds and pauses and drop session pointers; --all moves the state directory to .agenthub/archive instead"],
|
|
27
|
+
["ahub restart [--dry-run] [--yes]", "recover this project's runtime"],
|
|
28
|
+
["ahub upgrade --to <version> [--dry-run] [--yes]", "review and upgrade running projects"],
|
|
29
|
+
["ahub recovery status|resume|abort <operation-id>", "inspect, resume or cancel a preflight; status names the next actions"],
|
|
30
|
+
["ahub recovery dispose <operation-id> --fresh-session codex|claude --reason <text>", "end a partial operation by starting that peer as a new session, recording the lost conversation"],
|
|
31
|
+
["ahub recovery dispose <operation-id> --stop-and-archive --reason <text>", "end a partial operation: stop its own targets, archive its snapshot and release the lock; the upgrade is abandoned, not completed"],
|
|
32
|
+
["ahub daemon [--unattended]", "internal: the detached hub process that ahub up or the dashboard manager starts"],
|
|
33
|
+
["ahub recovery-run <operation-id>", "internal: the detached runner of an upgrade, restart or recovery resume"],
|
|
34
|
+
]],
|
|
35
|
+
["Agents", [
|
|
36
|
+
["ahub claude [--print-command] [--unattended] [args...]", "launch Claude Code with the hub channel"],
|
|
37
|
+
["ahub codex [--print-command] [--unattended] [args...]", "start the Codex adapter and attach the TUI"],
|
|
38
|
+
["ahub kimi [--print-command] [--model <alias>]", "start Kimi headless under ACP"],
|
|
39
|
+
["ahub pi [--print-command] [--mode headless|tui] [--backend auto|dgx|mlx] [--session-id <id>] [--session-file <path>]", "start Pi"],
|
|
40
|
+
["ahub local [--route <id> | --model <id>]", "start the hub-native worker on the self-hosted models (routing.toml)"],
|
|
41
|
+
["ahub models setup|status|start|stop", "prepare or inspect local Ollama MLX (legacy stop is explicit)"],
|
|
42
|
+
]],
|
|
43
|
+
["Messages and approvals", [
|
|
44
|
+
["ahub say [@peer ...] <text>", "send as the console user (no @peer = broadcast); delivered at once, start the text with [STATUS] to let it batch or [FYI] for the record only"],
|
|
45
|
+
["ahub tail", "live stream of messages, states and permission requests"],
|
|
46
|
+
["ahub permit <id> <option>", "answer a permission request shown by tail (\"deny\" cancels)"],
|
|
47
|
+
["ahub pause|resume <peer>", "hold a peer's deliveries in its queue / release them"],
|
|
48
|
+
["ahub queue list [--peer <id>] [--json]", "inspect durable deliveries"],
|
|
49
|
+
["ahub queue show <delivery-id>", "inspect one delivery and revision"],
|
|
50
|
+
["ahub queue resolve <delivery-id> --action completed|retry|discard --reason <text>", "release a delivery held as needs_review: mark it completed, retry it or discard it"],
|
|
51
|
+
]],
|
|
52
|
+
["Tasks and review", [
|
|
53
|
+
["ahub board [state | --ready]", "the task board; --ready: proposed tasks with nothing left to wait for"],
|
|
54
|
+
["ahub task propose [--class <c> | <class>] <title...> [--owner <peer>] [--path <p>]... [--after <id>]... [--urgent] [--detail <text>]", "put a task on the board; a first word that names a class is taken as the class; while a task named by --after is not approved yet, --owner reserves the task for that peer, offered to it first once it is ready; otherwise --owner assigns it now"],
|
|
55
|
+
["ahub task show|escalate <id>", "full task with history (PII text included) / hand it to the next peer in escalate_to"],
|
|
56
|
+
["ahub task assign <id> <peer>", "give a task to a peer yourself; a task that still waits is reserved for that peer instead"],
|
|
57
|
+
["ahub review <id> approved|changes_requested [note...] [--unmet <item>]...", "give a review verdict on a task"],
|
|
58
|
+
["ahub route explain <id>", "why a task went where it went, its reserved owner included"],
|
|
59
|
+
["ahub route explain --class <c> <title...>", "what would happen to such a task now"],
|
|
60
|
+
["ahub ask [--remember] <question...>", "answer from the task board, shared memory and this run's log, with the ids it rests on"],
|
|
61
|
+
["ahub remember <text...>", "save a note to the memory all agents share"],
|
|
62
|
+
]],
|
|
63
|
+
["Budget", [
|
|
64
|
+
["ahub budget", "quota windows per peer, and who is paused until when"],
|
|
65
|
+
["ahub budget set <peer> <0..1> [--resets-in 30m] [--window 5h|week]", "feed a reading by hand (also: test the relay)"],
|
|
66
|
+
["ahub budget resume <peer>", "override a budget pause; readings are ignored for that peer until the window resets"],
|
|
67
|
+
["ahub budget execution configure <config.json> | status [id] | disable <id>", "opt-in limits on model calls, tool calls, time and tokens for pi and local"],
|
|
68
|
+
]],
|
|
69
|
+
["History and reports", [
|
|
70
|
+
["ahub turns [peer] [--limit N]", "recent turns and the files each changed (a git work tree only)"],
|
|
71
|
+
["ahub undo <turn> [--yes] [--context]", "put back the files a turn changed; refuses files changed since. Without --yes it only lists them; --context also drops a Codex turn from its conversation"],
|
|
72
|
+
["ahub report [--since 7d|<iso>] [--by task] [--json]", "turns, tokens, messages, overlaps and task events per period"],
|
|
73
|
+
["ahub export [--since 7d|<iso>]", "structured events (events.jsonl) as JSON lines; never message bodies"],
|
|
74
|
+
["ahub check-path <file> [--peer <id>]", "other owners' open tasks that claim or changed a file"],
|
|
75
|
+
]],
|
|
76
|
+
["Hooks", [
|
|
77
|
+
["ahub check-path --hook", "check-path as a Claude Code PreToolUse hook (templates/claude-hooks.json); never blocks"],
|
|
78
|
+
["ahub facts --hook", "turn-free facts as a Claude Code PreToolUse, PostToolUse and Stop hook (issue #108); never blocks"],
|
|
79
|
+
]],
|
|
80
|
+
["Dashboard", [
|
|
81
|
+
["ahub ui [--no-open]", "open the local dashboard (or print its one-time link)"],
|
|
82
|
+
["ahub ui --all [--no-open]", "open the unified project dashboard"],
|
|
83
|
+
["ahub ui --all --stop", "stop only the dashboard manager"],
|
|
84
|
+
["ahub manager", "internal: the dashboard manager process that ahub ui --all starts"],
|
|
85
|
+
]],
|
|
86
|
+
];
|
|
87
|
+
export const HELP: readonly HelpEntry[] = SECTIONS.flatMap(([section, rows]) => rows.map(([usage, description]) => ({ section, usage, description })));
|
|
88
|
+
|
|
89
|
+
const COMMAND = /^ahub (-{0,2}[a-z][a-z-]*(?: ?\| ?-{0,2}[a-z][a-z-]*)*)/;
|
|
90
|
+
/** The command words a usage documents: `ahub pause|resume <peer>` documents pause and resume. */
|
|
91
|
+
export function helpCommands(entry: HelpEntry): string[] {
|
|
92
|
+
return COMMAND.exec(entry.usage)?.[1]!.split(/ ?\| ?/) ?? [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const DESCRIPTION = 34;
|
|
96
|
+
/**
|
|
97
|
+
* Usage at 2 spaces, description at one fixed column, wrapped at word boundaries to the terminal width
|
|
98
|
+
* (80 to 100 columns). A usage too long for that column puts its description on the next line.
|
|
99
|
+
* With a command, only its entries; an unknown command renders nothing.
|
|
100
|
+
*/
|
|
101
|
+
export function renderHelp(columns: number, color: boolean, command?: string): string {
|
|
102
|
+
const width = Math.min(100, Math.max(80, columns || 80));
|
|
103
|
+
const entries = command === undefined ? HELP : HELP.filter(entry => helpCommands(entry).includes(command));
|
|
104
|
+
const lines: Span[][] = command === undefined ? wrap(`agent-hub ${VERSION}: Claude Code, Codex and Kimi as peers in one project directory`, width, 0).map(text => [{ text }]) : [];
|
|
105
|
+
let section = "";
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (entry.section !== section) {
|
|
108
|
+
if (lines.length) lines.push([]);
|
|
109
|
+
lines.push([{ text: entry.section, tone: "strong" }]);
|
|
110
|
+
section = entry.section;
|
|
111
|
+
}
|
|
112
|
+
const prefix = COMMAND.exec(entry.usage)?.[0] ?? "";
|
|
113
|
+
const usage = wrap(entry.usage, width - 4, 0);
|
|
114
|
+
const description = wrap(entry.description, width - DESCRIPTION, 0);
|
|
115
|
+
const first: Span[] = [{ text: " " }, { text: prefix, tone: "info" }, { text: usage[0]!.slice(prefix.length) }];
|
|
116
|
+
if (usage.length === 1 && 2 + Bun.stringWidth(usage[0]!) + 2 <= DESCRIPTION) first.push({ text: " ".repeat(DESCRIPTION - 2 - Bun.stringWidth(usage[0]!)) + description.shift() });
|
|
117
|
+
lines.push(first, ...usage.slice(1).map(text => [{ text: ` ${text}` }]), ...description.map(text => [{ text: " ".repeat(DESCRIPTION) + text }]));
|
|
118
|
+
}
|
|
119
|
+
return lines.map(line => paint(line, color)).join("\n");
|
|
120
|
+
}
|
package/src/cli/identity.ts
CHANGED
|
@@ -35,16 +35,16 @@ export function classifyPeerCommand(command: string, args: readonly string[]): P
|
|
|
35
35
|
if (command === "route") return args[0] === "explain" ? "allowed" : "console";
|
|
36
36
|
if (command === "budget") return !args.length || args.every((arg) => arg === "--json") ? "allowed" : "console";
|
|
37
37
|
if (["claude", "codex", "kimi", "pi", "local", "pause", "resume"].includes(command)) return "conductor";
|
|
38
|
-
if (["help", "
|
|
38
|
+
if (["help", "version", "--version", "say", "status", "board", "report", "turns", "review", "remember", "facts", "check-path"].includes(command)) return "allowed";
|
|
39
39
|
return "console";
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/** No argument text is included: notices and refusal logs must not copy task text or credentials. */
|
|
43
43
|
export function cliCommandLabel(command: string, args: readonly string[]): string {
|
|
44
|
-
if (!["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "claude", "codex", "kimi", "pi", "local", "models", "say", "queue", "tail", "budget", "board", "task", "review", "remember", "ask", "route", "pause", "resume", "permit", "status", "logs", "export", "report", "facts", "check-path", "turns", "undo", "kill", "doctor"].includes(command)) return "unknown";
|
|
44
|
+
if (!["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "claude", "codex", "kimi", "pi", "local", "models", "say", "queue", "tail", "budget", "board", "task", "review", "remember", "ask", "route", "pause", "resume", "permit", "status", "logs", "export", "report", "facts", "check-path", "turns", "undo", "kill", "reset", "doctor"].includes(command)) return "unknown";
|
|
45
45
|
if (["task", "queue", "budget", "route", "recovery", "models", "projects"].includes(command)) {
|
|
46
46
|
const sub = args[0];
|
|
47
|
-
if (sub && ["propose", "show", "assign", "escalate", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "setup", "start", "stop", "remove"].includes(sub)) return `${command} ${sub}`;
|
|
47
|
+
if (sub && ["propose", "show", "assign", "escalate", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "setup", "start", "stop", "remove"].includes(sub)) return `${command} ${sub}`;
|
|
48
48
|
}
|
|
49
49
|
return (/^[a-z][a-z0-9-]*$/.test(command) && !/\s/.test(command)) || ["--help", "--version"].includes(command) ? command : "unknown";
|
|
50
50
|
}
|
package/src/cli/launch.ts
CHANGED
|
@@ -8,6 +8,8 @@ export const CLAUDE_CHANNEL = "plugin:agent-hub@agent-hub";
|
|
|
8
8
|
export function nativeLaunchEnv(tool: "claude" | "codex" | "pi", source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
9
9
|
const env = peerChildEnv(tool, source);
|
|
10
10
|
delete env.AGENTHUB_UNATTENDED;
|
|
11
|
+
// Channel evidence for the plugin (issue #205): set beside the development-channel flag `buildLaunch` always passes.
|
|
12
|
+
if (tool === "claude") env.AGENTHUB_CHANNEL = "1";
|
|
11
13
|
return env;
|
|
12
14
|
}
|
|
13
15
|
|