@staix/agent-hub 0.12.32 → 0.12.34
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 +6 -5
- package/docs/agent-notes/adapters.md +7 -3
- package/docs/agent-notes/budget.md +5 -3
- package/docs/agent-notes/bus.md +2 -0
- package/docs/agent-notes/daemon.md +7 -5
- package/docs/agent-notes/tasks.md +3 -2
- package/docs/agent-notes/tests.md +2 -1
- package/docs/operations.md +48 -14
- package/docs/quickstart.md +5 -3
- package/docs/security.md +10 -2
- package/docs/smoke.md +1 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +246 -4
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +20 -0
- package/docs/verification/2026-10-18-0.12.33.md +42 -0
- package/docs/verification/2026-10-18-0.12.34.md +48 -0
- package/docs/verified.json +33 -19
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +22882 -16496
- package/src/adapters/codex-appserver.ts +81 -11
- package/src/adapters/definitions/builtin-metadata.ts +101 -7
- package/src/adapters/definitions/native-cli-claude-launch.ts +90 -0
- package/src/adapters/definitions/native-cli-claude-setup.ts +80 -0
- package/src/adapters/definitions/native-cli-codex-launch.ts +14 -0
- package/src/adapters/definitions/native-cli-doctor.ts +46 -0
- package/src/adapters/definitions/native-cli-entry-claude.ts +52 -0
- package/src/adapters/definitions/native-cli-entry-codex.ts +26 -0
- package/src/adapters/definitions/native-cli-entry-jcode.ts +24 -0
- package/src/adapters/definitions/native-cli-entry-kimi.ts +52 -0
- package/src/adapters/definitions/native-cli-entry-local.ts +14 -0
- package/src/adapters/definitions/native-cli-entry-pi.ts +51 -0
- package/src/adapters/definitions/native-cli-entry-qwen.ts +20 -0
- package/src/adapters/definitions/native-cli-entry-types.ts +40 -0
- package/src/adapters/definitions/native-cli-entry.ts +19 -0
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +58 -0
- package/src/adapters/definitions/native-cli-launch-common.ts +17 -0
- package/src/adapters/definitions/native-cli-pi.ts +33 -0
- package/src/adapters/definitions/native-cli-preview-claude.ts +37 -0
- package/src/adapters/definitions/native-cli-preview-codex.ts +20 -0
- package/src/adapters/definitions/native-cli-preview-common.ts +57 -0
- package/src/adapters/definitions/native-cli-preview-jcode.ts +20 -0
- package/src/adapters/definitions/native-cli-preview-kimi.ts +28 -0
- package/src/adapters/definitions/native-cli-preview-pi.ts +36 -0
- package/src/adapters/definitions/native-cli-preview-qwen.ts +22 -0
- package/src/adapters/definitions/native-cli-preview.ts +16 -0
- package/src/adapters/definitions/native-config-defaults.ts +19 -0
- package/src/adapters/definitions/native-config-policy.ts +35 -0
- package/src/adapters/definitions/native-context.ts +35 -0
- package/src/adapters/definitions/native-daemon-kimi.ts +5 -0
- package/src/adapters/definitions/native-daemon-policy.ts +18 -0
- package/src/adapters/definitions/native-display.ts +18 -0
- package/src/adapters/definitions/native-factories.ts +48 -0
- package/src/adapters/definitions/native-policy-aux.ts +130 -0
- package/src/adapters/definitions/native-quota.ts +29 -0
- package/src/adapters/definitions/native-recovery-continuity.ts +12 -0
- package/src/adapters/definitions/native-recovery.ts +88 -0
- package/src/adapters/definitions/qwen.ts +8 -4
- package/src/adapters/registry-types.ts +97 -10
- package/src/adapters/registry.ts +103 -3
- package/src/cli/console-state.ts +5 -5
- package/src/cli/identity.ts +6 -5
- package/src/cli/jcode-mcp.ts +2 -58
- package/src/cli/launch.ts +14 -100
- package/src/cli/main.ts +35 -272
- package/src/cli/output.ts +3 -2
- package/src/cli/preview.ts +14 -125
- package/src/cli/recovery-guidance.ts +17 -13
- package/src/cli/reset.ts +13 -1
- package/src/cli/setup.ts +2 -80
- package/src/cli/shell-quote.ts +3 -0
- package/src/cli/terminal-recovery.ts +17 -10
- package/src/cli/upgrade-interactive.ts +4 -2
- package/src/cli/upgrade-presentation.ts +11 -1
- package/src/cli/upgrade-restore-state.ts +2 -1
- package/src/cli/upgrade-runtime.ts +107 -146
- package/src/cli/upgrade.ts +10 -7
- package/src/hub/budget.ts +6 -30
- package/src/hub/bus.ts +133 -14
- package/src/hub/conductor.ts +8 -4
- package/src/hub/config-apply.ts +9 -13
- package/src/hub/config-reload.ts +7 -2
- package/src/hub/context-window.ts +3 -27
- package/src/hub/control-client.ts +1 -1
- package/src/hub/crash.ts +2 -35
- package/src/hub/daemon.ts +244 -241
- package/src/hub/deferred-hub.ts +60 -0
- package/src/hub/delivery-journal.ts +6 -0
- package/src/hub/execution-budget.ts +2 -1
- package/src/hub/hub-tools.ts +3 -3
- package/src/hub/peer-catalog.ts +4 -13
- package/src/hub/permission-mode.ts +9 -12
- package/src/hub/recovery-continuity.ts +6 -5
- package/src/hub/report.ts +9 -8
- package/src/hub/restart.ts +12 -1
- package/src/hub/roster.ts +6 -7
- package/src/hub/routing.ts +21 -10
- package/src/hub/settings.ts +6 -5
- package/src/hub/settled-roles.ts +1 -1
- package/src/hub/start-mode.ts +8 -6
- package/src/hub/tasks.ts +14 -11
- package/src/hub/tooling.ts +4 -60
- package/src/hub/ui.ts +3 -1
- package/src/memory/recall.ts +4 -5
- package/src/models/route/signals.ts +3 -1
- package/src/pi/launch.ts +2 -1
- package/src/ui/index.html +3 -2
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +16 -6
- package/templates/routing.toml +2 -2
package/src/cli/launch.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { resolve, join, basename, dirname, isAbsolute } from "node:path";
|
|
|
6
6
|
import { processLiveness, processSignature } from "../pi/process-signature.ts";
|
|
7
7
|
import { realPath } from "../hub/project.ts";
|
|
8
8
|
import { peerChildEnv, processTable } from "../hub/child-process.ts";
|
|
9
|
+
import { definitionOf } from "../adapters/registry.ts";
|
|
10
|
+
import { kimiAcpCommand } from "../adapters/definitions/native-daemon-kimi.ts";
|
|
9
11
|
export const CLAUDE_CHANNEL = "plugin:agent-hub@agent-hub";
|
|
10
12
|
|
|
11
13
|
/** Launch identity belongs to the native child, never to the agent that invoked the wrapper. */
|
|
@@ -13,31 +15,12 @@ export function nativeLaunchEnv(tool: "claude" | "codex" | "pi" | "kimi", source
|
|
|
13
15
|
const env = peerChildEnv(tool, source);
|
|
14
16
|
delete env.AGENTHUB_UNATTENDED;
|
|
15
17
|
// Channel evidence for the plugin (issue #205): set beside the development-channel flag `buildLaunch` always passes.
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
+
const definition = definitionOf(tool);
|
|
19
|
+
if (definition?.identity.mcp === "channel-plugin") env.AGENTHUB_CHANNEL = "1";
|
|
20
|
+
if (definition?.attach?.pullOnlyForms?.includes("tui")) delete env.AGENTHUB_MODE; // its MCP child must attach pull-only, never as a tools socket
|
|
18
21
|
return env;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
/** A scoped candidate MCP server can be selected without promoting the installed plugin. */
|
|
22
|
-
function claudeChannel(userArgs: string[]): string {
|
|
23
|
-
const bundle = resolve(import.meta.dir, "../../plugins/agent-hub/server.js");
|
|
24
|
-
for (let i = 0; i < userArgs.length; i++) {
|
|
25
|
-
const arg = userArgs[i]!;
|
|
26
|
-
const path = arg === "--mcp-config" ? userArgs[i + 1] : arg.startsWith("--mcp-config=") ? arg.slice("--mcp-config=".length) : undefined;
|
|
27
|
-
if (!path) continue;
|
|
28
|
-
try {
|
|
29
|
-
const server = JSON.parse(readFileSync(resolve(path), "utf8")).mcpServers?.["agent-hub"];
|
|
30
|
-
if (server?.command === "bun" && Array.isArray(server.args) && server.args.some((value: unknown) => typeof value === "string" && resolve(value) === bundle)) return "server:agent-hub";
|
|
31
|
-
} catch { /* The native CLI reports invalid user-supplied MCP configuration. */ }
|
|
32
|
-
}
|
|
33
|
-
return CLAUDE_CHANNEL;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const OWNED: Record<"claude" | "codex", string[]> = {
|
|
37
|
-
claude: ["--dangerously-load-development-channels", "--dangerously-skip-permissions"],
|
|
38
|
-
codex: ["--remote", "--yolo", "--dangerously-bypass-approvals-and-sandbox"],
|
|
39
|
-
};
|
|
40
|
-
|
|
41
24
|
export const UNATTENDED_WARNING =
|
|
42
25
|
"WARNING: --unattended disables permission prompts. Other agents' messages are untrusted input and this agent will act on its own judgment without asking you.";
|
|
43
26
|
|
|
@@ -308,89 +291,20 @@ export function buildLaunch(
|
|
|
308
291
|
userArgs: string[],
|
|
309
292
|
ctx: { unattended: boolean; proxyUrl?: string; codexBin?: string; statusLine?: StatusLineTee; facts?: FactsHook; preview?: boolean },
|
|
310
293
|
): Launch {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (tool === "claude") {
|
|
319
|
-
// `--settings` takes one value, so a user-supplied one wins and Claude has no quota source for that session.
|
|
320
|
-
const own = passthrough.some((a) => a === "--settings" || a.startsWith("--settings="));
|
|
321
|
-
let settingsArgs = ctx.statusLine && !own ? ["--settings", sessionSettings(ctx.statusLine, ctx.facts)] : [];
|
|
322
|
-
let nativeArgs = passthrough;
|
|
323
|
-
let settingsFile: string | undefined;
|
|
324
|
-
if (own) {
|
|
325
|
-
if (passthrough.filter(a => a === "--settings" || a.startsWith("--settings=")).length > 1) throw new Error("pass --settings only once so the hub permission hook stays installed");
|
|
326
|
-
const index = passthrough.findIndex(a => a === "--settings" || a.startsWith("--settings="));
|
|
327
|
-
const arg = passthrough[index]!;
|
|
328
|
-
const value = arg === "--settings" ? passthrough[index + 1] : arg.slice("--settings=".length);
|
|
329
|
-
if (!value) throw new Error("--settings requires a JSON object or readable file so the hub permission hook can be installed");
|
|
330
|
-
let caller: Record<string, unknown>;
|
|
331
|
-
try {
|
|
332
|
-
caller = JSON.parse(value.trim().startsWith("{") ? value : readFileSync(resolve(value), "utf8"));
|
|
333
|
-
if (!caller || typeof caller !== "object" || Array.isArray(caller)) throw new Error("not an object");
|
|
334
|
-
} catch { throw new Error("cannot read --settings as a JSON object; the hub permission hook must be installed"); }
|
|
335
|
-
if (ctx.facts && caller.disableAllHooks === true) throw new Error("--settings disableAllHooks prevents the required hub permission hook; enable hooks to launch");
|
|
336
|
-
const injected = JSON.parse(sessionSettings(undefined, ctx.facts));
|
|
337
|
-
const callerHooks = caller.hooks;
|
|
338
|
-
if (callerHooks !== undefined && (!callerHooks || typeof callerHooks !== "object" || Array.isArray(callerHooks))) throw new Error("--settings hooks must be an object");
|
|
339
|
-
const hooks = { ...(callerHooks as Record<string, unknown> | undefined) };
|
|
340
|
-
for (const [event, entries] of Object.entries(injected.hooks ?? {})) {
|
|
341
|
-
const existing = hooks[event];
|
|
342
|
-
if (existing !== undefined && !Array.isArray(existing)) throw new Error(`--settings ${event} hooks must be an array`);
|
|
343
|
-
hooks[event] = [...(existing as unknown[] | undefined ?? []), ...(entries as unknown[])];
|
|
344
|
-
}
|
|
345
|
-
settingsArgs = ["--settings", JSON.stringify({ ...caller, ...(ctx.facts ? { hooks } : {}) })];
|
|
346
|
-
nativeArgs = passthrough.filter((_, i) => i !== index && (arg !== "--settings" || i !== index + 1));
|
|
347
|
-
} else if (ctx.facts && !ctx.statusLine) {
|
|
348
|
-
const injected = JSON.parse(sessionSettings(undefined, ctx.facts));
|
|
349
|
-
settingsArgs = ["--settings", JSON.stringify({ hooks: injected.hooks })];
|
|
350
|
-
}
|
|
351
|
-
if (settingsArgs.length && !ctx.preview) {
|
|
352
|
-
const stateDir = ctx.facts?.stateDir ?? ctx.statusLine?.stateDir;
|
|
353
|
-
if (!stateDir) throw new Error("Claude session settings require the hub state directory");
|
|
354
|
-
const file = join(stateDir, `claude-settings-${randomUUID()}.json`);
|
|
355
|
-
try {
|
|
356
|
-
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
357
|
-
writeFileSync(`${file}.tmp`, settingsArgs[1]!, { mode: 0o600, flag: "wx" });
|
|
358
|
-
renameSync(`${file}.tmp`, file);
|
|
359
|
-
} catch {
|
|
360
|
-
try { unlinkSync(`${file}.tmp`); } catch { /* no temp file was written */ }
|
|
361
|
-
throw new Error("cannot write private Claude session settings in the hub state directory");
|
|
362
|
-
}
|
|
363
|
-
settingsArgs[1] = file;
|
|
364
|
-
settingsFile = file;
|
|
365
|
-
}
|
|
366
|
-
const notes = [
|
|
367
|
-
unattended ? UNATTENDED_WARNING : "",
|
|
368
|
-
ctx.statusLine && own ? "note: you passed --settings, so the hub's status line tee is off and the budget coordinator cannot see Claude's quota (ahub budget set claude <0..1> still works)." : "",
|
|
369
|
-
].filter(Boolean);
|
|
370
|
-
return {
|
|
371
|
-
cmd: "claude",
|
|
372
|
-
args: ["--dangerously-load-development-channels", claudeChannel(passthrough), ...(unattended ? ["--dangerously-skip-permissions"] : []), ...settingsArgs, ...nativeArgs],
|
|
373
|
-
...(settingsFile ? { settingsFile } : {}),
|
|
374
|
-
permissionHook: !!ctx.facts,
|
|
375
|
-
...(ctx.facts ? { hookPurpose: ctx.facts.purpose ?? "facts" } : {}),
|
|
376
|
-
unattended,
|
|
377
|
-
...(notes.length ? { warning: notes.join("\n") } : {}),
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
if (!ctx.proxyUrl) throw new Error("codex proxy url is missing");
|
|
381
|
-
return {
|
|
382
|
-
cmd: ctx.codexBin ?? "codex",
|
|
383
|
-
args: ["--enable", "tui_app_server", "--remote", ctx.proxyUrl, ...(unattended ? ["--dangerously-bypass-approvals-and-sandbox"] : []), ...passthrough],
|
|
384
|
-
unattended,
|
|
385
|
-
...warning,
|
|
386
|
-
};
|
|
294
|
+
const adapter = definitionOf(tool);
|
|
295
|
+
if (!adapter) throw new Error("native launch definition unavailable");
|
|
296
|
+
const { metadata: _metadata, ...launch } = adapter.start.launch({
|
|
297
|
+
instance: tool, form: "tui", projectRoot: process.cwd(), stateDir: ctx.facts?.stateDir ?? ctx.statusLine?.stateDir ?? "",
|
|
298
|
+
entrypoint: resolve(import.meta.dir, "main.js"), userArgs, env: {}, ...ctx,
|
|
299
|
+
});
|
|
300
|
+
return launch;
|
|
387
301
|
}
|
|
388
302
|
|
|
389
303
|
|
|
390
304
|
/** The daemon and preview use exactly the same ACP command insertion. */
|
|
391
305
|
export function buildKimiLaunch(command: string[], model?: string): Launch {
|
|
392
|
-
const [cmd, ...args] = command;
|
|
393
|
-
return { cmd: cmd!, args
|
|
306
|
+
const [cmd, ...args] = kimiAcpCommand(command, model);
|
|
307
|
+
return { cmd: cmd!, args };
|
|
394
308
|
}
|
|
395
309
|
|
|
396
310
|
export { buildQwenLaunch } from "../adapters/definitions/qwen-native.ts";
|
package/src/cli/main.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { definitionOf } from "../adapters/registry.ts";
|
|
2
|
+
import { BUILTIN_CLI_ENTRIES, definitionOf, DOCTOR_TOOL_PROBES, DOCTOR_MEMORY_CAPTURE_PEERS, DOCTOR_PLUGIN_PROBE, nativeDoctorCheck, nativeDoctorRecoveryIdentity, nativeDoctorOptionalProbes } from "../adapters/registry.ts";
|
|
3
|
+
import { parsePiFlags } from "../adapters/definitions/native-cli-pi.ts";
|
|
3
4
|
import { supportsBoardReleaseFilter, supportsBoardColumnMetadata } from "./board-read.ts";
|
|
4
5
|
import { matchesPolicy } from "../hub/path-policy.ts";
|
|
5
6
|
import { projectedDiff } from "../hub/projected-diff.ts";
|
|
@@ -16,7 +17,6 @@ import { configureJcodeMcp } from "./jcode-mcp.ts";
|
|
|
16
17
|
import { qwenSessionId } from "../hub/crash.ts";
|
|
17
18
|
import { isPermissionMode } from "../hub/permission-mode.ts";
|
|
18
19
|
import { parseSettingText, settingDef, settingText, type SettingRow } from "../hub/settings.ts";
|
|
19
|
-
import { isSupportedPeer } from "../hub/peer-catalog.ts";
|
|
20
20
|
import { startModeOf } from "../hub/start-mode.ts";
|
|
21
21
|
import { currentRouting } from "../hub/routing.ts";
|
|
22
22
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -104,7 +104,7 @@ const [given = "help", ...rest] = argv;
|
|
|
104
104
|
// `ahub <command> -h` alone asks for that command's help instead of running it. Elsewhere `-h` may be message text
|
|
105
105
|
// (`ahub say try ls -h`), and claude and codex pass their arguments on to the agent.
|
|
106
106
|
const asksHelp = (a: string | undefined) => a === "--help" || a === "-h";
|
|
107
|
-
const cmd = asksHelp(given) || (given !==
|
|
107
|
+
const cmd = asksHelp(given) || (definitionOf(given)?.start.passthrough !== true && rest.length === 1 && asksHelp(rest[0])) ? "help" : given;
|
|
108
108
|
const args = cmd === "help" && given !== "help" && !asksHelp(given) ? [given] : rest;
|
|
109
109
|
let selected: { root: string; stateDir: string };
|
|
110
110
|
try {
|
|
@@ -233,32 +233,7 @@ function execWithEnv(bin: string, argv: string[], extra: NodeJS.ProcessEnv): nev
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/** `mode` is absent when no flag chose one: the caller takes the configured start mode (#269). `--headless` is `--mode headless`. */
|
|
236
|
-
|
|
237
|
-
const usage = "usage: ahub pi [--mode headless|tui | --headless] [--backend auto|dgx|mlx] [--model <alias>] [--session-id <id> | --session-file <path>] [--replace-session --session-file <path>]";
|
|
238
|
-
if (args.includes("--headless")) {
|
|
239
|
-
if (args.includes("--mode")) fail(usage);
|
|
240
|
-
args.splice(args.indexOf("--headless"), 1, "--mode", "headless");
|
|
241
|
-
}
|
|
242
|
-
for (const flag of ["--mode", "--backend", "--model", "--session-id", "--session-file"]) {
|
|
243
|
-
const index = args.indexOf(flag);
|
|
244
|
-
if (index >= 0 && (!args[index + 1] || args[index + 1]!.startsWith("--"))) fail(`${flag} needs a value`);
|
|
245
|
-
}
|
|
246
|
-
const mode = (args.includes("--mode") ? args[args.indexOf("--mode") + 1] : undefined) as string | undefined;
|
|
247
|
-
const backend = (args.includes("--backend") ? args[args.indexOf("--backend") + 1] : undefined) as string | undefined;
|
|
248
|
-
const model = args.includes("--model") ? args[args.indexOf("--model") + 1] : undefined;
|
|
249
|
-
const sessionId = args.includes("--session-id") ? args[args.indexOf("--session-id") + 1] : undefined;
|
|
250
|
-
const sessionFile = args.includes("--session-file") ? args[args.indexOf("--session-file") + 1] : undefined;
|
|
251
|
-
const replaceSession = args.includes("--replace-session");
|
|
252
|
-
if (replaceSession && (!sessionFile || sessionId)) fail("--replace-session requires --session-file, without --session-id");
|
|
253
|
-
const valueFlags = new Set(["--mode", "--backend", "--model", "--session-id", "--session-file"]);
|
|
254
|
-
for (let i = 0; i < args.length; i++) {
|
|
255
|
-
if (args[i] === "--replace-session") continue;
|
|
256
|
-
if (!valueFlags.has(args[i]!)) fail(`unknown Pi option: ${args[i]}`);
|
|
257
|
-
i++;
|
|
258
|
-
}
|
|
259
|
-
if ((mode !== undefined && !["headless", "tui"].includes(mode)) || (backend !== undefined && !["auto", "dgx", "mlx"].includes(backend)) || (sessionId && sessionFile)) fail(usage);
|
|
260
|
-
return { ...(replaceSession ? { replaceSession: true } : {}), ...(mode ? { mode: mode as "headless" | "tui" } : {}), ...(backend ? { backend: backend as "auto" | "dgx" | "mlx" } : {}), ...(model ? { model } : {}), ...(sessionId ? { sessionId } : {}), ...(sessionFile ? { sessionFile } : {}) };
|
|
261
|
-
}
|
|
236
|
+
const piFlags = () => parsePiFlags(args, fail);
|
|
262
237
|
|
|
263
238
|
async function projectRows() {
|
|
264
239
|
const registry = new Registry();
|
|
@@ -577,7 +552,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
577
552
|
|
|
578
553
|
/** Strict person command: recorded identity only, no session or terminal overrides. */
|
|
579
554
|
function parseRecoveryReattach(argv: readonly string[]): { id: string; peer: TerminalPeer } | undefined {
|
|
580
|
-
if (argv.length !== 3 || argv[0] !== "reattach" || !operationIdPattern.test(argv[1]!) ||
|
|
555
|
+
if (argv.length !== 3 || argv[0] !== "reattach" || !operationIdPattern.test(argv[1]!) || definitionOf(argv[2]!)?.session.launchRecord !== true) return undefined;
|
|
581
556
|
return { id: argv[1]!, peer: argv[2] as TerminalPeer };
|
|
582
557
|
}
|
|
583
558
|
|
|
@@ -827,220 +802,17 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
827
802
|
if (process.stdin.isTTY && process.stdout.isTTY && !args.includes("--no-console")) await runConsole({ client: await connect(), cwd, stateDir, liveTest });
|
|
828
803
|
},
|
|
829
804
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
// `--settings` outranks project and user settings, so the tee has to wrap whichever status line would have won:
|
|
843
|
-
// project local, then project, then user.
|
|
844
|
-
let original: { command?: string; refreshInterval?: number; padding?: number } | undefined;
|
|
845
|
-
for (const file of [join(cwd, ".claude", "settings.local.json"), join(cwd, ".claude", "settings.json"), join(process.env.HOME ?? "", ".claude", "settings.json")]) {
|
|
846
|
-
try {
|
|
847
|
-
original ??= JSON.parse(readFileSync(file, "utf8")).statusLine;
|
|
848
|
-
} catch {
|
|
849
|
-
// no such file, or no status line in it
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
// Turn-free facts and opted-in task sweeps share native PreToolUse/PostToolUse/Stop observations.
|
|
853
|
-
const facts = claudeObservationHooks(projectConfig(), { script: join(import.meta.dir, "facts-hook.ts"), stateDir });
|
|
854
|
-
let previousLaunch: unknown;
|
|
855
|
-
try { previousLaunch = JSON.parse(readFileSync(join(stateDir, "claude-launch.json"), "utf8")); } catch { /* no previous managed launch */ }
|
|
856
|
-
const launch = buildLaunch("claude", args, { unattended: unattendedEnv, statusLine: { script: join(import.meta.dir, "statusline-tee.ts"), stateDir, ...(original ? { original } : {}) }, ...(facts ? { facts } : {}) });
|
|
857
|
-
let result: Awaited<ReturnType<typeof runClaudeLaunch>>;
|
|
858
|
-
try {
|
|
859
|
-
let launchRecord: ClaudeLaunchRecord | undefined;
|
|
860
|
-
if (control?.instanceId) {
|
|
861
|
-
// Publish only the final launch's installed hook and effective permission flags.
|
|
862
|
-
launchRecord = { instanceId: control.instanceId, launchId: process.env.AGENTHUB_LAUNCH_ID!, permissionHook: launch.permissionHook === true, hookPurpose: launch.hookPurpose, settingsFile: launch.settingsFile, launcherPid: process.pid, launcherSignature: processSignature(process.pid), unattended: launch.unattended === true };
|
|
863
|
-
recordClaudeLaunch(stateDir, launchRecord);
|
|
864
|
-
cleanupStaleClaudeSettings(stateDir, previousLaunch);
|
|
865
|
-
}
|
|
866
|
-
if (launch.warning) console.error(launch.warning);
|
|
867
|
-
const env = nativeLaunchEnv("claude", childEnv());
|
|
868
|
-
result = await runClaudeLaunch(launch, { cwd, stateDir, env: { ...env, AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd }, ...(launchRecord ? { record: launchRecord } : {}) });
|
|
869
|
-
} finally {
|
|
870
|
-
cleanupClaudeSettings(stateDir, launch.settingsFile);
|
|
871
|
-
}
|
|
872
|
-
if (result.error) fail(`cannot run ${launch.cmd}: ${result.error.message}`);
|
|
873
|
-
process.exit(result.status ?? 1);
|
|
874
|
-
},
|
|
875
|
-
|
|
876
|
-
codex: async () => {
|
|
877
|
-
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
878
|
-
try { return console.log(JSON.stringify(launcherPreview("codex", args, cwd, stateDir, unattendedEnv), null, 2)); }
|
|
879
|
-
catch { fail("cannot preview launch: invalid arguments or configuration (details withheld to protect credentials)"); }
|
|
880
|
-
}
|
|
881
|
-
assertLifecycleAvailable();
|
|
882
|
-
const launch0 = buildLaunch("codex", args, { unattended: unattendedEnv, proxyUrl: "pending" }); // refuse bad flags before starting anything
|
|
883
|
-
// #215: a launch by a recovery operation records itself before the hub round trip, so the coordinator never takes
|
|
884
|
-
// a launcher still starting for one that never ran. An ordinary launch records only once the hub accepted it: a
|
|
885
|
-
// refused start must not replace the record of a Codex already running here (one row per peer and instance).
|
|
886
|
-
const recovering = !!process.env.AGENTHUB_RECOVERY_OPERATION && process.env.AGENTHUB_RECOVERY_OPERATION === recoveryLock(); // not an id a restored session inherited
|
|
887
|
-
const before = readControl(stateDir);
|
|
888
|
-
if (recovering && before?.instanceId) await recordTerminalLaunch("codex", cwd, stateDir, before.instanceId);
|
|
889
|
-
const hub = await connect();
|
|
890
|
-
const res = await hub.request({ t: "start", peer: "codex", args: { unattended: launch0.unattended === true }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
891
|
-
hub.close();
|
|
892
|
-
if (!res.ok) fail(res.error);
|
|
893
|
-
const control = readControl(stateDir);
|
|
894
|
-
if (!recovering && control?.instanceId) await recordTerminalLaunch("codex", cwd, stateDir, control.instanceId);
|
|
895
|
-
const launch = buildLaunch("codex", args, { unattended: unattendedEnv, proxyUrl: res.proxyUrl, codexBin: projectConfig().codex_bin });
|
|
896
|
-
if (launch0.warning) console.error(launch0.warning);
|
|
897
|
-
exec(launch.cmd, launch.args, "codex");
|
|
898
|
-
},
|
|
899
|
-
|
|
900
|
-
jcode: async () => {
|
|
901
|
-
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
902
|
-
try { return console.log(JSON.stringify(launcherPreview("jcode", args, cwd, stateDir, unattendedEnv), null, 2)); }
|
|
903
|
-
catch { fail("cannot preview Jcode launch: invalid arguments or configuration (details withheld)"); }
|
|
904
|
-
}
|
|
905
|
-
assertLifecycleAvailable();
|
|
906
|
-
const { one, rest } = takeFlags(args, ["--mode", "--model", "--provider-profile", "--session-id"], []);
|
|
907
|
-
if (rest.length || (one["--mode"] !== undefined && one["--mode"] !== "headless")) fail("usage: ahub jcode [--mode headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>]");
|
|
908
|
-
if (one["--session-id"] !== undefined && !qwenSessionId(one["--session-id"])) fail("invalid Jcode session identity");
|
|
909
|
-
const hub = await connect();
|
|
910
|
-
try {
|
|
911
|
-
const snapshot = (await hub.request({ t: "status" })).status;
|
|
912
|
-
if (snapshot?.roster?.find((row: any) => row.peer === "jcode")?.on !== true) fail("jcode is off roster; add it with ahub roster add jcode");
|
|
913
|
-
const written = configureJcodeMcp(cwd, stateDir, resolve(import.meta.dir, "../../plugins/agent-hub/server.js"));
|
|
914
|
-
if (written.tracked) console.error(".jcode/mcp.json is tracked; review this local machine-path change before committing it");
|
|
915
|
-
const reply = await hub.request({ t: "start", peer: "jcode", args: { mode: "headless", model: one["--model"], providerProfile: one["--provider-profile"], sessionId: one["--session-id"] }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 65_000);
|
|
916
|
-
if (!reply.ok) fail(reply.error);
|
|
917
|
-
console.log(reply.already ? "jcode is already attached" : "jcode attached (headless); native policy unmanaged, NO hub sandbox");
|
|
918
|
-
} finally { hub.close(); }
|
|
919
|
-
},
|
|
920
|
-
|
|
921
|
-
qwen: async () => {
|
|
922
|
-
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
923
|
-
try { return console.log(JSON.stringify(launcherPreview("qwen", args, cwd, stateDir, unattendedEnv), null, 2)); }
|
|
924
|
-
catch { fail("cannot preview Qwen launch: invalid arguments or configuration (details withheld)"); }
|
|
925
|
-
}
|
|
926
|
-
assertLifecycleAvailable();
|
|
927
|
-
const { one, rest } = takeFlags(args, ["--mode", "--model", "--session-id"], []);
|
|
928
|
-
const adapter = definitionOf("qwen")!;
|
|
929
|
-
if (one["--session-id"] !== undefined && !adapter.session.crashIdentity?.({ sessionId: one["--session-id"] }).sessionId) fail("invalid Qwen session identity");
|
|
930
|
-
if (rest.length || (one["--mode"] !== undefined && !(adapter.start.forms as readonly string[]).includes(one["--mode"]))) fail("Qwen TUI is not offered before verification; usage: ahub qwen [--mode headless] [--model <alias>] [--session-id <id>]");
|
|
931
|
-
const hub = await connect();
|
|
932
|
-
try {
|
|
933
|
-
const reply = await hub.request({ t: "start", peer: "qwen", args: { mode: "headless", ...(one["--model"] ? { model: one["--model"] } : {}), ...(one["--session-id"] ? { sessionId: one["--session-id"] } : {}) }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 35_000);
|
|
934
|
-
if (!reply.ok) fail(reply.error);
|
|
935
|
-
console.log(reply.already ? "qwen is already attached" : "qwen attached (headless); use ahub say @qwen and the console for approvals");
|
|
936
|
-
} finally { hub.close(); }
|
|
937
|
-
},
|
|
938
|
-
kimi: async () => {
|
|
939
|
-
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
940
|
-
try { return console.log(JSON.stringify(launcherPreview("kimi", args, cwd, stateDir, unattendedEnv), null, 2)); }
|
|
941
|
-
catch { fail("cannot preview launch: invalid arguments or configuration (details withheld to protect credentials)"); }
|
|
942
|
-
}
|
|
943
|
-
assertLifecycleAvailable();
|
|
944
|
-
const { one, rest } = takeFlags(args, ["--mode", "--model"], []);
|
|
945
|
-
const mode = one["--mode"] ?? startModeOf(projectConfig().peers, "kimi"), model = one["--model"];
|
|
946
|
-
if (rest.length || !["tui", "headless"].includes(mode)) fail("usage: ahub kimi [--mode tui|headless] [--model <alias>]");
|
|
947
|
-
if (mode === "tui" && one["--mode"] === undefined && !(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
948
|
-
if (model) fail("Kimi's TUI needs a terminal for these options; run in a terminal or add --mode headless");
|
|
949
|
-
const hub = await connect();
|
|
950
|
-
try {
|
|
951
|
-
const reply = await hub.request({ t: "peer_start", peer: "kimi" }, 35_000);
|
|
952
|
-
if (!reply.ok) fail(reply.error);
|
|
953
|
-
return console.log(reply.already ? "kimi is already attached" : `kimi: opened a terminal running ${reply.command}; it attaches pull-only when its TUI is ready`);
|
|
954
|
-
} finally { hub.close(); }
|
|
955
|
-
}
|
|
956
|
-
const hub = await connect();
|
|
957
|
-
try {
|
|
958
|
-
if (mode === "tui") {
|
|
959
|
-
const launch = buildKimiTuiLaunch(projectConfig().kimi_cmd, model);
|
|
960
|
-
const snapshot = (await hub.request({ t: "status" })).status;
|
|
961
|
-
if (snapshot?.roster?.find((row: any) => row.peer === "kimi")?.on === false) fail("kimi is off roster; add it with ahub roster add kimi");
|
|
962
|
-
const peer = snapshot?.peers?.kimi;
|
|
963
|
-
if (peer && !["offline"].includes(peer.state) && !peer.pullOnly) fail("Kimi is running headless; run ahub stop kimi before starting its TUI");
|
|
964
|
-
const admission = await hub.request({ t: "start", peer: "kimi", args: { mode: "tui" }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
965
|
-
if (!admission.ok) fail(admission.error);
|
|
966
|
-
let result: ReturnType<typeof spawnSync>;
|
|
967
|
-
try {
|
|
968
|
-
const written = configureKimiMcp(cwd, stateDir, resolve(import.meta.dir, "../../plugins/agent-hub/server.js"), "tui");
|
|
969
|
-
if (written.tracked) console.error(".kimi-code/mcp.json is tracked; the agent-hub entry contains machine paths. Review this local change before committing it.");
|
|
970
|
-
result = spawnSync(launch.cmd, launch.args, { cwd, stdio: "inherit", env: { ...nativeLaunchEnv("kimi", childEnv()), AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } });
|
|
971
|
-
} finally {
|
|
972
|
-
if (admission.admissionId) await hub.request({ t: "kimi_launch_end", admissionId: admission.admissionId }).catch(() => {});
|
|
973
|
-
}
|
|
974
|
-
if (result.error) fail(`cannot run ${launch.cmd}: ${result.error.message}`);
|
|
975
|
-
process.exit(result.status ?? 1);
|
|
976
|
-
}
|
|
977
|
-
const peer = (await hub.request({ t: "status" })).status?.peers?.kimi;
|
|
978
|
-
if (peer?.pullOnly && peer.state !== "offline") fail("Kimi runs in a terminal; end it there before starting a headless one");
|
|
979
|
-
configureKimiMcp(cwd, stateDir, resolve(import.meta.dir, "../../plugins/agent-hub/server.js"), "headless");
|
|
980
|
-
const res = await hub.request({ t: "start", peer: "kimi", args: { mode: "headless", model }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
981
|
-
if (!res.ok) fail(res.error);
|
|
982
|
-
console.log(res.already ? "kimi is already attached" : 'kimi attached (headless). Talk to it with: ahub say @kimi "..."');
|
|
983
|
-
} finally { hub.close(); }
|
|
984
|
-
},
|
|
985
|
-
|
|
986
|
-
pi: async () => {
|
|
987
|
-
// --headless is --mode headless everywhere this command goes: the preview, the hub and the conductor path.
|
|
988
|
-
if (args.includes("--headless") && !args.includes("--mode")) args.splice(args.indexOf("--headless"), 1, "--mode", "headless");
|
|
989
|
-
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
990
|
-
try { return console.log(JSON.stringify(launcherPreview("pi", args, cwd, stateDir, unattendedEnv), null, 2)); }
|
|
991
|
-
catch { fail("cannot preview launch: invalid arguments or configuration (details withheld to protect credentials)"); }
|
|
992
|
-
}
|
|
993
|
-
const flags = piFlags();
|
|
994
|
-
// #269: with no flag, Pi's start mode decides, and that is its TUI unless a person set headless.
|
|
995
|
-
const options = { ...flags, mode: flags.mode ?? startModeOf(projectConfig().peers, "pi") };
|
|
996
|
-
if (flags.mode === undefined && options.mode === "tui" && !(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
997
|
-
// The default asked for the TUI and there is no terminal here (the console's command line, a script): the hub
|
|
998
|
-
// opens one, or says how to. It is never downgraded to headless. The fixed command carries no option, so one
|
|
999
|
-
// that was given needs a terminal of its own. An explicit --mode tui is taken at its word, as before.
|
|
1000
|
-
if (Object.keys(flags).length) fail("Pi's TUI needs a terminal, and these options are not passed to one the hub opens: run this in a terminal, or add --mode headless");
|
|
1001
|
-
const hub = await connect();
|
|
1002
|
-
try {
|
|
1003
|
-
const reply = await hub.request({ t: "peer_start", peer: "pi" }, 35_000);
|
|
1004
|
-
if (reply.ok !== true) {
|
|
1005
|
-
const error = String(reply.error ?? "pi start refused");
|
|
1006
|
-
fail(/unknown (?:control )?(?:message|request|command)(?:\b|:)|this hub does not know "peer_start"/i.test(error) ? `${error}; upgrade the running hub, or run ahub pi --mode headless` : error);
|
|
1007
|
-
}
|
|
1008
|
-
return console.log(reply.already ? "pi is already attached" : reply.opened ? `pi: opened a terminal running ${reply.command}; it attaches when its TUI is ready` : 'pi attached (headless). Talk to it with: ahub say @pi "..."');
|
|
1009
|
-
} finally { hub.close(); }
|
|
1010
|
-
}
|
|
1011
|
-
// #215: as for Codex, a TUI launch by the recovery operation holding the lock records itself before the hub round
|
|
1012
|
-
// trip, so a resume in that window never reads it as gone; an ordinary launch records only after the hub accepted it.
|
|
1013
|
-
const recovering = options.mode === "tui" && !!process.env.AGENTHUB_RECOVERY_OPERATION && process.env.AGENTHUB_RECOVERY_OPERATION === recoveryLock();
|
|
1014
|
-
const before = readControl(stateDir);
|
|
1015
|
-
if (recovering && before?.instanceId) await recordTerminalLaunch("pi", cwd, stateDir, before.instanceId);
|
|
1016
|
-
const hub = await connect();
|
|
1017
|
-
const res = await hub.request({ t: "start", peer: "pi", args: options, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
1018
|
-
hub.close();
|
|
1019
|
-
if (!res.ok) fail(res.error);
|
|
1020
|
-
if (options.mode === "tui") {
|
|
1021
|
-
const launch = res.launch;
|
|
1022
|
-
if (!launch || typeof launch.cmd !== "string" || !Array.isArray(launch.args) || !launch.args.every((a: unknown) => typeof a === "string") || !launch.env || typeof launch.env !== "object") fail("Pi TUI launch metadata was not verified");
|
|
1023
|
-
const launchEnv = { ...(launch.env as NodeJS.ProcessEnv) };
|
|
1024
|
-
delete launchEnv.AGENTHUB_RECOVERY_OPERATION;
|
|
1025
|
-
delete launchEnv.AGENTHUB_UNATTENDED;
|
|
1026
|
-
const control = readControl(stateDir);
|
|
1027
|
-
if (!recovering && control?.instanceId) await recordTerminalLaunch("pi", cwd, stateDir, control.instanceId);
|
|
1028
|
-
return execWithEnv(launch.cmd, launch.args, launchEnv);
|
|
1029
|
-
}
|
|
1030
|
-
console.log(res.already ? "pi is already attached" : 'pi attached (headless). Talk to it with: ahub say @pi "..."');
|
|
1031
|
-
},
|
|
1032
|
-
|
|
1033
|
-
local: async () => {
|
|
1034
|
-
const opt = (flag: string) => (args.includes(flag) ? args[args.indexOf(flag) + 1] ?? fail(`${flag} needs a value`) : undefined);
|
|
1035
|
-
for (const flag of ["--route", "--model"]) if (args.includes(flag) && (!opt(flag) || opt(flag)!.startsWith("--"))) fail(`${flag} needs a value`);
|
|
1036
|
-
if (args.includes("--route") && args.includes("--model")) fail("use --route or --model, not both");
|
|
1037
|
-
if (args.some((a, i) => i % 2 === 0 && !["--route", "--model"].includes(a))) fail("usage: ahub local [--route <id> | --model <id>]");
|
|
1038
|
-
const hub = await connect();
|
|
1039
|
-
const res = await hub.request({ t: "start", peer: "local", args: { route: opt("--route"), model: opt("--model") }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
1040
|
-
hub.close();
|
|
1041
|
-
if (!res.ok) fail(res.error);
|
|
1042
|
-
console.log(res.already ? "local is already attached" : `local attached on ${res.model}. Give it work with: ahub say @local "..."`);
|
|
1043
|
-
},
|
|
805
|
+
...Object.fromEntries(BUILTIN_CLI_ENTRIES.map(entry => [entry.command, async () => {
|
|
806
|
+
// Preserve the original guard before native effects; previews remain read-only and precede this guard.
|
|
807
|
+
if (entry.lifecycleAdmission && !args.includes("--print-command") && !args.includes("--dry-run")) assertLifecycleAvailable();
|
|
808
|
+
return entry.execute({ args, cwd, stateDir, cliDir: import.meta.dir, unattendedEnv, definition: entry.definition,
|
|
809
|
+
projectConfig, connect, fail, exec, execWithEnv, takeFlags,
|
|
810
|
+
helpers: { launcherPreview, readControl, recordTerminalLaunch, recoveryLock, processSignature,
|
|
811
|
+
buildLaunch, claudeObservationHooks, recordClaudeLaunch, cleanupStaleClaudeSettings, cleanupClaudeSettings,
|
|
812
|
+
nativeLaunchEnv, runClaudeLaunch, childEnv, qwenSessionId, configureJcodeMcp, configureKimiMcp,
|
|
813
|
+
buildKimiTuiLaunch, startModeOf },
|
|
814
|
+
});
|
|
815
|
+
}])),
|
|
1044
816
|
|
|
1045
817
|
models: async () => {
|
|
1046
818
|
const action = outputArguments()[0] ?? "status";
|
|
@@ -1430,7 +1202,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1430
1202
|
roster: async () => {
|
|
1431
1203
|
const options = outputOptions(), flags = args.filter(arg => arg.startsWith("--")), positional = outputArguments().filter(arg => arg !== "--yes" && arg !== "--now");
|
|
1432
1204
|
const [sub = "list", peer, ...extra] = positional;
|
|
1433
|
-
if (!["list", "add", "remove"].includes(sub) || extra.length || (sub === "list" ? peer !== undefined : !
|
|
1205
|
+
if (!["list", "add", "remove"].includes(sub) || extra.length || (sub === "list" ? peer !== undefined : !definitionOf(peer ?? "")) || flags.some(flag => !["--json", "--full", "--yes", "--now"].includes(flag) && !flag.startsWith("--color=")) || (sub !== "list" && options.json) || (sub !== "remove" && flags.some(flag => ["--yes", "--now"].includes(flag)))) fail("usage: ahub roster [--json] | add <peer> | remove <peer> [--yes] [--now]");
|
|
1434
1206
|
const hub = await connect();
|
|
1435
1207
|
try {
|
|
1436
1208
|
const request = async (message: Record<string, unknown>) => { const reply = await hub.request(message, 35_000); if (reply.ok !== true) fail(String(reply.error ?? "roster request refused")); return reply; };
|
|
@@ -1934,7 +1706,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1934
1706
|
}
|
|
1935
1707
|
if (["assign", "clear"].includes(args[0] ?? "")) {
|
|
1936
1708
|
const [op, peer, id] = args;
|
|
1937
|
-
if (!peer || !
|
|
1709
|
+
if (!peer || !definitionOf(peer ?? "") || !id && op === "assign" || args.length > 3) fail("usage: ahub role assign <peer> <id> | clear <peer> [<id>]");
|
|
1938
1710
|
const hub = await connect();
|
|
1939
1711
|
try {
|
|
1940
1712
|
const view = await hub.request({ t: "settings_get" }); if (!view.ok) fail(view.error);
|
|
@@ -1955,15 +1727,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1955
1727
|
return res.status === 0 ? res.stdout.trim().split("\n")[0]! : undefined;
|
|
1956
1728
|
};
|
|
1957
1729
|
const checks: DoctorCheck[] = [];
|
|
1958
|
-
const row = (ok: boolean | undefined, name: string, detail: string) => {
|
|
1959
|
-
const section: DoctorCheck["section"] = ["bun", "claude", "codex", "kimi", "qwen", "jcode"].includes(name) ? "Tools"
|
|
1960
|
-
: name === "config" || name === "retired setting" ? "Config"
|
|
1961
|
-
: name.startsWith("memory") ? "Memory"
|
|
1962
|
-
: ["omniroute", "omniroute key", "local fixed_model", "switchyard", "pi mlx"].includes(name) ? "Models" : "Hub";
|
|
1963
|
-
const optional = ["claude", "codex", "kimi", "qwen", "jcode", "ahub daemon", "switchyard", "pi mlx"].includes(name) || section === "Memory";
|
|
1964
|
-
const level: DoctorCheck["level"] = ok === undefined ? (["switchyard", "omniroute"].includes(name) ? "warn" : "unknown") : ok ? "ok" : optional ? "warn" : "fail";
|
|
1965
|
-
checks.push({ section, level, name, detail });
|
|
1966
|
-
};
|
|
1730
|
+
const row = (ok: boolean | undefined, name: string, detail: string) => { checks.push(nativeDoctorCheck(ok, name, detail)); };
|
|
1967
1731
|
|
|
1968
1732
|
try {
|
|
1969
1733
|
const catalog = loadRoleCatalog(cwd);
|
|
@@ -1972,7 +1736,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1972
1736
|
const config = loadConfig(cwd), assigned = catalogAssignments(config.catalog_roles);
|
|
1973
1737
|
for (const peer of Object.keys(assigned)) selectedRoles(peer, config.roles, assigned, catalog);
|
|
1974
1738
|
} catch { row(false, "catalog roles", "invalid assignment id, shape or flow base"); }
|
|
1975
|
-
for (const bin of
|
|
1739
|
+
for (const bin of DOCTOR_TOOL_PROBES) {
|
|
1976
1740
|
const v = version(bin);
|
|
1977
1741
|
row(!!v, bin, v ?? "not found on PATH");
|
|
1978
1742
|
}
|
|
@@ -1986,26 +1750,24 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1986
1750
|
row(!status.deliveryError && pending === 0, "delivery recovery", status.deliveryError ?? (pending ? `${pending} deliveries need review; run ahub queue list` : "journal healthy; no deliveries need review"));
|
|
1987
1751
|
const observed = await hub.request({ t: "recovery", op: "inspect", expectedInstanceId: status.instanceId });
|
|
1988
1752
|
for (const peer of Object.values(observed.recovery?.peers ?? {}) as { id: string; state: string; sessionId?: string; threadId?: string }[]) {
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
row(!!identity, `${peer.id} recovery ID`, identity ? "recorded" : `missing; verify the current conversation and reconnect with ahub ${peer.id} before upgrading`);
|
|
1753
|
+
const check = nativeDoctorRecoveryIdentity(peer);
|
|
1754
|
+
if (check) row(check.ok, check.name, check.detail);
|
|
1992
1755
|
}
|
|
1993
1756
|
} finally { hub.close(); }
|
|
1994
1757
|
}
|
|
1995
|
-
const plugin = pluginState(parseList<InstalledPlugin>(spawnSync(
|
|
1996
|
-
row(plugin.state === "current",
|
|
1758
|
+
const plugin = pluginState(parseList<InstalledPlugin>(spawnSync(DOCTOR_PLUGIN_PROBE.command, [...DOCTOR_PLUGIN_PROBE.argv], { encoding: "utf8" }).stdout ?? ""), join(import.meta.dir, "..", ".."));
|
|
1759
|
+
row(plugin.state === "current", DOCTOR_PLUGIN_PROBE.name, plugin.state === "missing" ? "missing: run ahub setup" : plugin.state === "current" ? `agent-hub@agent-hub ${plugin.version}` : `agent-hub@agent-hub is stale (${plugin.why}): run ahub setup`);
|
|
1997
1760
|
|
|
1998
1761
|
const config = loadConfig(cwd);
|
|
1999
1762
|
row(!config.ignored, "config", config.ignored ? `${config.ignored.join("; ")} (move them to .agenthub/config.local.json)` : "no machine-local field ignored");
|
|
2000
1763
|
for (const line of config.retired ?? []) row(false, "retired setting", line);
|
|
2001
|
-
|
|
2002
|
-
row(verifiedJcodeCommand(
|
|
1764
|
+
for (const probe of nativeDoctorOptionalProbes(config)) {
|
|
1765
|
+
if (probe.kind === "source-identity") row(verifiedJcodeCommand(probe.command, cwd), probe.name, probe.detail);
|
|
1766
|
+
else {
|
|
1767
|
+
const v = version(probe.command[0]!);
|
|
1768
|
+
row(v === probe.expected, probe.name, v === probe.expected ? probe.confirmed : probe.unconfirmed);
|
|
1769
|
+
}
|
|
2003
1770
|
}
|
|
2004
|
-
if (config.roster.qwen === true) {
|
|
2005
|
-
// ponytail: keep this bounded CLI-version probe and 0.25.0 pin in the A1 guard baseline; A2 moves every version probe together when VersionContext gains its observed input.
|
|
2006
|
-
const v = version(config.qwen_cmd[0]!);
|
|
2007
|
-
row(v === "0.25.0", "qwen", v === "0.25.0" ? "CLI reports 0.25.0; native ACP identity/modes and provider access require a separate live check" : "Qwen Code 0.25.0 executable not confirmed; provider and ACP readiness unknown");
|
|
2008
|
-
} // issue #83
|
|
2009
1771
|
const omni = new OmniRoute(config.omniroute);
|
|
2010
1772
|
const gateway = await omni.base();
|
|
2011
1773
|
row(gateway ? true : config.omniroute.urls.length || process.env.AGENTHUB_OMNIROUTE_URL ? false : undefined, "omniroute", gateway ? `${new URL(gateway).host} healthy` : config.omniroute.urls.length || process.env.AGENTHUB_OMNIROUTE_URL ? "no candidate reachable (VPN off?); ahub local cannot run" : "not configured: set omniroute.urls in .agenthub/config.local.json (any OpenAI-compatible gateway); ahub local cannot run");
|
|
@@ -2031,7 +1793,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2031
1793
|
row(mem.ok, "memory worker", mem.ok ? `claude-mem ${mem.version ?? ""} at ${memory.url}` : `unavailable at ${memory.url} (the hub works without it)`);
|
|
2032
1794
|
const bridge = spawnSync("dot", ["ai", "memory", "status"], { encoding: "utf8" });
|
|
2033
1795
|
const out = bridge.error ? "" : bridge.stdout.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2034
|
-
for (const tool of
|
|
1796
|
+
for (const tool of DOCTOR_MEMORY_CAPTURE_PEERS) {
|
|
2035
1797
|
const ready = new RegExp(`✓\\s+${tool} ready`).test(out);
|
|
2036
1798
|
row(bridge.error ? undefined : ready, `memory capture: ${tool}`, bridge.error ? "unknown (`dot ai memory status` not available)" : `${ready ? "ready" : "not ready"}, per ` + "`dot ai memory status`");
|
|
2037
1799
|
}
|
|
@@ -2058,11 +1820,12 @@ async function runConductorCommand(): Promise<void> {
|
|
|
2058
1820
|
input = { peer: args[0] };
|
|
2059
1821
|
} else {
|
|
2060
1822
|
// Native TUIs return a human launch command through the daemon; an agent shell never executes it.
|
|
2061
|
-
|
|
2062
|
-
|
|
1823
|
+
const parseStart = definitionOf(cmd)?.start.conductorStartArgs;
|
|
1824
|
+
const parsed = parseStart ? parseStart(args) : args.length ? new Error(`conductor starts accept no launch overrides; use ahub ${cmd}`) : {};
|
|
1825
|
+
if (parsed instanceof Error) fail(parsed.message);
|
|
2063
1826
|
op = "hub_peer_start";
|
|
2064
1827
|
// #269: a mode may only repeat the peer's start mode, which a person sets; without one the hub takes that mode.
|
|
2065
|
-
input = { peer: cmd, ...
|
|
1828
|
+
input = { peer: cmd, ...parsed };
|
|
2066
1829
|
}
|
|
2067
1830
|
console.log(await taskOp(op, input));
|
|
2068
1831
|
}
|
package/src/cli/output.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { definitionOf } from "../adapters/registry.ts";
|
|
1
2
|
import { boardColumns, type BoardPeerInput } from "../ui/task-board.ts";
|
|
2
3
|
import { peerStyle } from "../ui/peer-palette.ts";
|
|
3
4
|
import { cells, TABLES, table, fitLine, wrap, duration, relative, plural, terminalText, initialConsoleState, stateTone, type Span, type Tone } from "./console-state.ts";
|
|
@@ -234,8 +235,8 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
234
235
|
: flat(p.paused).replace(/^(budget: .*), resets .+$/, "$1; reset unknown");
|
|
235
236
|
out.push(...detail("paused", pauseText, columns, "attention"));
|
|
236
237
|
}
|
|
237
|
-
if (p.toolsOnly) out.push(...detail(id
|
|
238
|
-
if (p.pullOnly && id
|
|
238
|
+
if (p.toolsOnly) out.push(...detail(definitionOf(id)?.attach?.pullOnlyForms?.includes("tui") && p.pullOnly ? "pull-only" : "tools-only", flat(p.toolsOnly).replace(/^(?:tools-only|pull-only):\s*/, ""), columns, "attention"));
|
|
239
|
+
if (p.pullOnly && definitionOf(id)?.attach?.pullOnlyForms?.includes("tui")) out.push(...detail(`${id} last read`, typeof p.lastInboxAt === "number" ? relative(p.lastInboxAt, now) : "never", columns, "muted"));
|
|
239
240
|
if (p.permissionNote) out.push(...detail("permission", p.permissionNote, columns, "muted"));
|
|
240
241
|
if (p.requestedModel && p.servedBy && p.requestedModel !== p.servedBy) out.push(...detail("requested model", p.requestedModel, columns));
|
|
241
242
|
if (typeof p.oldestQueuedAt === "number") out.push(...detail(`${id} oldest queued`, relative(p.oldestQueuedAt, now), columns, "muted"));
|