@staix/agent-hub 0.12.33 → 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 +9 -0
- package/README.md +3 -3
- package/docs/agent-notes/adapters.md +7 -3
- package/docs/agent-notes/budget.md +5 -3
- package/docs/agent-notes/daemon.md +3 -3
- package/docs/agent-notes/tasks.md +2 -1
- package/docs/agent-notes/tests.md +2 -1
- package/docs/operations.md +20 -10
- package/docs/quickstart.md +4 -2
- package/docs/security.md +4 -1
- package/docs/smoke.md +1 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +246 -4
- package/docs/verification/2026-10-18-0.12.34.md +48 -0
- package/docs/verified.json +29 -18
- 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/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-restore-state.ts +2 -1
- package/src/cli/upgrade-runtime.ts +92 -141
- package/src/cli/upgrade.ts +8 -7
- package/src/hub/budget.ts +6 -30
- 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/crash.ts +2 -35
- package/src/hub/daemon.ts +226 -232
- 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/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 +13 -10
- 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/preview.ts
CHANGED
|
@@ -1,134 +1,23 @@
|
|
|
1
|
-
import { definitionOf,
|
|
2
|
-
import { jcodeCommand } from "../adapters/jcode.ts";
|
|
3
|
-
import { qwenSessionId } from "../hub/crash.ts";
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
1
|
+
import { definitionOf, builtinPreviewOf } from "../adapters/registry.ts";
|
|
6
2
|
import { loadConfig } from "../hub/daemon.ts";
|
|
7
3
|
import { childEnv } from "../hub/child-process.ts";
|
|
8
|
-
import { relayModelIds } from "../models/relay.ts";
|
|
9
|
-
import { buildPiLaunch } from "../pi/launch.ts";
|
|
10
4
|
import { startModeOf } from "../hub/start-mode.ts";
|
|
11
|
-
import { buildLaunch, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, claudeObservationHooks
|
|
5
|
+
import { buildLaunch, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, claudeObservationHooks } from "./launch.ts";
|
|
12
6
|
import { kimiMcpPlan } from "./kimi-mcp.ts";
|
|
7
|
+
import { jcodeCommand } from "../adapters/jcode.ts";
|
|
8
|
+
import { qwenSessionId } from "../hub/crash.ts";
|
|
9
|
+
import { relayModelIds } from "../models/relay.ts";
|
|
10
|
+
import { buildPiLaunch } from "../pi/launch.ts";
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
const UNRESOLVED = "[unresolved]";
|
|
16
|
-
/** Arbitrary native arguments can contain secrets even without a credential-looking key. */
|
|
17
|
-
function publicArg(arg: string): string {
|
|
18
|
-
const flags = new Set(["--settings", "--model", "--mcp-config", "--session-id", "--session-file", "--resume", "--continue", "--print", "--safe", "--new"]);
|
|
19
|
-
const key = arg.split("=", 1)[0]!;
|
|
20
|
-
return flags.has(key) ? (arg.includes("=") ? key + "=" + REDACTED : key) : REDACTED;
|
|
21
|
-
}
|
|
22
|
-
function nativeCommand(command: string[], trusted: string[]): string[] {
|
|
23
|
-
return command.map((value, i) => value === trusted[i] ? value : REDACTED);
|
|
24
|
-
}
|
|
25
|
-
/** Read-only preview. Inputs passed to shared native builders are redacted before serialization. */
|
|
12
|
+
/** Read-only builtin preview. Native redaction uses the same launch builders as actual startup. */
|
|
26
13
|
export function launcherPreview(tool: "claude" | "codex" | "kimi" | "pi" | "qwen" | "jcode", raw: string[], cwd: string, stateDir: string, unattended: boolean) {
|
|
27
14
|
const args = raw.filter(arg => !["--print-command", "--dry-run", "--json"].includes(arg));
|
|
28
15
|
const config = loadConfig(cwd);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const next = args[at + 1];
|
|
37
|
-
if (!next || next.startsWith("--")) throw new Error("missing option value");
|
|
38
|
-
return next;
|
|
39
|
-
};
|
|
40
|
-
if (tool === "claude") {
|
|
41
|
-
let original: { command?: string; refreshInterval?: number; padding?: number } | undefined;
|
|
42
|
-
for (const file of [join(cwd, ".claude/settings.local.json"), join(cwd, ".claude/settings.json"), join(process.env.HOME ?? "", ".claude/settings.json")]) {
|
|
43
|
-
try { original ??= JSON.parse(readFileSync(file, "utf8")).statusLine; } catch { /* optional */ }
|
|
44
|
-
}
|
|
45
|
-
// Build with original inputs so channel selection and owned-flag validation match normal launch.
|
|
46
|
-
const facts = claudeObservationHooks(config, { script: join(import.meta.dir, "facts-hook.ts"), stateDir });
|
|
47
|
-
launch = buildLaunch(tool, args, { preview: true, unattended, statusLine: { script: join(import.meta.dir, "statusline-tee.ts"), stateDir, ...(original ? { original: { command: original.command ? REDACTED : "", ...(typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}), ...(typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {}) } } : {}) }, ...(facts ? { facts } : {}) });
|
|
48
|
-
// Merged caller settings change the native argument count. Rebuild a redacted preview,
|
|
49
|
-
// retaining the managed hooks but never serializing caller settings or their file path.
|
|
50
|
-
const safeArgs = args.map((arg, i) => {
|
|
51
|
-
if (args[i - 1] === "--settings") return "{}";
|
|
52
|
-
if (arg.startsWith("--settings=")) return "--settings={}";
|
|
53
|
-
if (["--unattended", "--safe", "--new"].includes(arg)) return arg;
|
|
54
|
-
return publicArg(arg);
|
|
55
|
-
});
|
|
56
|
-
const publicLaunch = buildLaunch(tool, safeArgs, { preview: true, unattended, statusLine: { script: join(import.meta.dir, "statusline-tee.ts"), stateDir,
|
|
57
|
-
...(original ? { original: { command: original.command ? REDACTED : "", ...(typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}), ...(typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {}) } } : {}) }, facts });
|
|
58
|
-
publicLaunch.args.splice(0, 2, ...launch.args.slice(0, 2)); // channel selection was verified against the original MCP config
|
|
59
|
-
launch = publicLaunch;
|
|
60
|
-
} else if (tool === "codex") {
|
|
61
|
-
// A fresh native proxy is assigned by start, never guessed from an old status file.
|
|
62
|
-
launch = buildLaunch(tool, args, { unattended, proxyUrl: UNRESOLVED, codexBin: config.codex_bin === "codex" ? "codex" : REDACTED });
|
|
63
|
-
const passthrough = args.filter(arg => !["--unattended", "--safe", "--new"].includes(arg));
|
|
64
|
-
launch.args = launch.args.slice(0, launch.args.length - passthrough.length).concat(passthrough.map(publicArg));
|
|
65
|
-
unresolved.push({ field: "proxyUrl", reason: "assigned when the native adapter starts; preview does not start it" });
|
|
66
|
-
} else if (tool === "jcode") {
|
|
67
|
-
const mode = value("--mode") ?? "headless", model = value("--model"), profile = value("--provider-profile"), session = value("--session-id");
|
|
68
|
-
if (mode !== "headless") throw new Error("Jcode TUI driving is unverified");
|
|
69
|
-
for (let i = 0; i < args.length; i += 2) if (!["--model", "--mode", "--provider-profile", "--session-id"].includes(args[i]!) || !args[i + 1]) throw new Error("unknown Jcode option");
|
|
70
|
-
if (session !== undefined && !qwenSessionId(session)) throw new Error("invalid Jcode session identity");
|
|
71
|
-
const cmd = jcodeCommand(nativeCommand(config.jcode_cmd, ["jcode"]), "acp", UNRESOLVED, UNRESOLVED, model ? REDACTED : undefined, profile || config.jcode_provider_profile ? REDACTED : undefined);
|
|
72
|
-
launch = { cmd: cmd[0]!, args: cmd.slice(1) };
|
|
73
|
-
unresolved.push({ field: "ownedDaemon", reason: "private daemon/socket and source-pinned native identity verified at start; preview starts nothing" });
|
|
74
|
-
unresolved.push({ field: "hubTools", reason: "project MCP bridge required; native tool-call receipt remains unverified" });
|
|
75
|
-
} else if (kindOf(tool) === "qwen") {
|
|
76
|
-
const mode = value("--mode") ?? "headless", model = value("--model");
|
|
77
|
-
const adapter = definitionOf(tool)!;
|
|
78
|
-
if (!(adapter.start.forms as readonly string[]).includes(mode)) throw new Error("Qwen TUI is not offered before verification");
|
|
79
|
-
for (let i = 0; i < args.length; i += 2) if (!["--model", "--mode", "--session-id"].includes(args[i]!) || !args[i + 1]) throw new Error("unknown Qwen option");
|
|
80
|
-
const sessionId = value("--session-id");
|
|
81
|
-
if (sessionId !== undefined && !adapter.session.crashIdentity?.({ sessionId }).sessionId) throw new Error("invalid Qwen session identity");
|
|
82
|
-
if (sessionId) unresolved.push({ field: "sessionId", reason: "requested through ACP session/load at start; preview does not load or expose it" });
|
|
83
|
-
launch = adapter.start.launch({ instance: tool, form: "headless", projectRoot: cwd, stateDir, entrypoint: join(import.meta.dir, "main.js"),
|
|
84
|
-
userArgs: [], unattended: false, env: {}, preview: true, nativeCommand: nativeCommand(config.qwen_cmd, ["qwen", "--acp"]), model: model ? REDACTED : undefined });
|
|
85
|
-
unresolved.push({ field: "nativeIdentity", reason: "Qwen Code 0.25.0 identity and permission modes are checked at ACP initialization; preview starts nothing" });
|
|
86
|
-
} else if (tool === "kimi") {
|
|
87
|
-
const model = value("--model");
|
|
88
|
-
const mode = value("--mode") ?? startModeOf(config.peers, "kimi");
|
|
89
|
-
if (mode !== "tui" && mode !== "headless") throw new Error("invalid Kimi mode");
|
|
90
|
-
for (let i = 0; i < args.length; i += 2) if (!["--model", "--mode"].includes(args[i]!) || !args[i + 1]) throw new Error("unknown Kimi option");
|
|
91
|
-
if (mode === "headless") {
|
|
92
|
-
launch = buildKimiLaunch(nativeCommand(config.kimi_cmd, ["kimi", "acp"]), model ? REDACTED : undefined);
|
|
93
|
-
unresolved.push({ field: "sessionId", reason: "ACP session identity is available only after native initialization" });
|
|
94
|
-
} else {
|
|
95
|
-
const native = buildKimiTuiLaunch(config.kimi_cmd, model ? REDACTED : undefined);
|
|
96
|
-
launch = { ...native, cmd: native.cmd === "kimi" ? "kimi" : REDACTED, args: native.args.map(arg => arg === "--model" || arg === REDACTED ? arg : REDACTED) };
|
|
97
|
-
envNames = Object.keys(nativeLaunchEnv("kimi", childEnv())).concat(["AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR"]);
|
|
98
|
-
}
|
|
99
|
-
const { file, action, changed, tracked } = kimiMcpPlan(cwd, stateDir, join(import.meta.dir, "../../plugins/agent-hub/server.js"), mode);
|
|
100
|
-
kimiMetadata = { mode, mcp: { file, action, changed, tracked } };
|
|
101
|
-
} else {
|
|
102
|
-
const mode = value("--mode") ?? startModeOf(config.peers, "pi"); // #269: the same default `ahub pi` takes
|
|
103
|
-
const backend = value("--backend") ?? config.pi.backend;
|
|
104
|
-
if (!["headless", "tui"].includes(mode) || !["auto", "dgx", "mlx"].includes(backend) || (value("--session-id") && value("--session-file"))) throw new Error("invalid Pi options");
|
|
105
|
-
if (args.includes("--replace-session") && (!value("--session-file") || value("--session-id"))) throw new Error("--replace-session requires --session-file, without --session-id");
|
|
106
|
-
for (let i = 0; i < args.length; i++) {
|
|
107
|
-
if (args[i] === "--replace-session") continue;
|
|
108
|
-
if (!["--mode", "--backend", "--model", "--session-id", "--session-file"].includes(args[i]!) || !value(args[i]!)) throw new Error("unknown Pi option");
|
|
109
|
-
i++;
|
|
110
|
-
}
|
|
111
|
-
if (args.includes("--replace-session")) unresolved.push({ field: "session replacement", reason: "requires a person, a validated managed file and an offline settled owner at runtime; preview changes nothing" });
|
|
112
|
-
const models = relayModelIds({ enableHubAuto: true, mlx: config.mlx.enabled === false ? undefined : config.mlx, mlxAlias: "mlx/fast", allowedDGXmodels: { "dgx/coding": config.pi.dgx_coding, "dgx/fast": config.pi.dgx_fast } });
|
|
113
|
-
const pi = buildPiLaunch({
|
|
114
|
-
stateDir, cmd: nativeCommand(config.pi.cmd, ["pi"]), mode: mode as "headless" | "tui", backend: backend as "auto" | "dgx" | "mlx",
|
|
115
|
-
...(value("--model") ? { model: REDACTED } : {}),
|
|
116
|
-
...(value("--session-id") ? { sessionId: REDACTED } : {}),
|
|
117
|
-
...(value("--session-file") ? { sessionFile: REDACTED } : {}),
|
|
118
|
-
relay: { url: UNRESOLVED, token: UNRESOLVED, models: models.map(id => ({ id })) },
|
|
119
|
-
tools: [], preamble: REDACTED, maxSteps: config.pi.max_steps,
|
|
120
|
-
}, {}, resolve(join(import.meta.dir, "../pi/extension.ts")), UNRESOLVED, UNRESOLVED);
|
|
121
|
-
launch = pi;
|
|
122
|
-
envNames = Object.keys(pi.env).concat(["PATH", "HOME", "USER", "SHELL", "TMPDIR", "TERM", "TERM_PROGRAM", "LANG", "LC_ALL", "LC_CTYPE", "NO_COLOR", "CODEX_HOME"].filter(key => process.env[key] !== undefined));
|
|
123
|
-
if (mode === "tui") envNames.push("AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR");
|
|
124
|
-
unresolved.push({ field: "bridge/relay endpoints and owner credentials", reason: "assigned at runtime; preview does not bind a server or start a model" });
|
|
125
|
-
if (!value("--session-id") && !value("--session-file")) unresolved.push({ field: "sessionId", reason: "new native session identity is not allocated by preview" });
|
|
126
|
-
}
|
|
127
|
-
if (tool === "claude") envNames.push("AGENTHUB_CHANNEL");
|
|
128
|
-
if (tool === "claude" || tool === "codex") {
|
|
129
|
-
envNames.push("AGENTHUB_INSTANCE_ID", "AGENTHUB_LAUNCH_ID");
|
|
130
|
-
unresolved.push({ field: "env.AGENTHUB_INSTANCE_ID", reason: tool === "claude" ? "injected when an existing daemon instance is read at launch; preview does not read runtime state" : "injected when a verified Orca terminal launch is recorded against the daemon; preview does not record it" });
|
|
131
|
-
unresolved.push({ field: "env.AGENTHUB_LAUNCH_ID", reason: "allocated only for a verified Orca terminal launch; preview does not query Orca, allocate identity or write launch records" });
|
|
132
|
-
}
|
|
133
|
-
return { tool, ...(kimiMetadata ?? {}), cmd: launch.cmd, args: launch.args, settings: tool === "claude" && launch.args.includes("--settings") ? launch.args[launch.args.indexOf("--settings") + 1] : undefined, envNames: [...new Set(envNames)].sort(), ...(launch.warning ? { warning: launch.warning } : {}), unresolved, redaction: "Arbitrary user arguments, settings, commands and configured executable overrides are redacted. Environment values are never displayed.", needs: ["Native executable, accounts and runtime readiness are not checked."] };
|
|
16
|
+
const definition = definitionOf(tool), preview = builtinPreviewOf(tool);
|
|
17
|
+
if (!definition || !preview) throw new Error("native preview definition unavailable");
|
|
18
|
+
return preview({ tool, args, cwd, stateDir, cliDir: import.meta.dir, unattended, config, definition,
|
|
19
|
+
baseEnvNames: Object.keys(childEnv()).concat(["AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR"]),
|
|
20
|
+
helpers: { buildLaunch, claudeObservationHooks, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, childEnv,
|
|
21
|
+
startModeOf, kimiMcpPlan, jcodeCommand, qwenSessionId, relayModelIds, buildPiLaunch },
|
|
22
|
+
});
|
|
134
23
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ADAPTERS, definitionOf } from "../adapters/registry.ts";
|
|
1
2
|
import { recordedFileState } from "./recovery-session-file.ts";
|
|
2
3
|
import { decodeRestoreReceipt } from "./upgrade-restore-state.ts";
|
|
3
4
|
import { shellQuote } from "./terminal-recovery.ts";
|
|
@@ -25,11 +26,13 @@ export interface RecoveryGuidanceOptions {
|
|
|
25
26
|
|
|
26
27
|
const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
|
|
27
28
|
const argv = (...words: string[]): string => words.map(shellQuote).join(" ");
|
|
28
|
-
const peers:
|
|
29
|
+
const peers = (): RecoveryNativePeer[] => ADAPTERS.filter(adapter => adapter.session.launchRecord)
|
|
30
|
+
.sort((a, b) => Number(a.transport === "claude-channel") - Number(b.transport === "claude-channel"))
|
|
31
|
+
.map(adapter => adapter.kind as RecoveryNativePeer);
|
|
29
32
|
|
|
30
33
|
function sourceSession(peer: RecoveryNativePeer, source: RecoveryPeer | undefined): { sessionId?: string; sessionFile?: string } {
|
|
31
|
-
const sessionId = peer
|
|
32
|
-
const sessionFile = peer
|
|
34
|
+
const sessionId = source?.[definitionOf(peer)?.session.idField ?? "sessionId"];
|
|
35
|
+
const sessionFile = definitionOf(peer)?.session.crashIdentity ? source?.sessionFile ?? source?.args?.sessionFile : undefined;
|
|
33
36
|
return { ...(text(sessionId) ? { sessionId } : {}), ...(text(sessionFile) ? { sessionFile } : {}) };
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -70,7 +73,7 @@ export function resolveAcceptedBinding(planned: PlannedProject, progress: Projec
|
|
|
70
73
|
* current receipts and the original plan. A malformed current receipt otherwise stays unavailable.
|
|
71
74
|
*/
|
|
72
75
|
export function recordedPeerGuidance(planned: PlannedProject, progress: ProjectProgress, options: RecoveryGuidanceOptions = {}): RecordedPeerGuidance[] {
|
|
73
|
-
return peers.map(peer => {
|
|
76
|
+
return peers().map(peer => {
|
|
74
77
|
let session: { sessionId?: string; sessionFile?: string } | undefined;
|
|
75
78
|
let unavailable = false;
|
|
76
79
|
const override = options.recorded && Object.hasOwn(options.recorded, peer) ? options.recorded[peer] : undefined;
|
|
@@ -82,18 +85,19 @@ export function recordedPeerGuidance(planned: PlannedProject, progress: ProjectP
|
|
|
82
85
|
} catch { unavailable = true; }
|
|
83
86
|
}
|
|
84
87
|
const sessionId = text(session?.sessionId) ? session.sessionId : undefined;
|
|
85
|
-
const sessionFile = peer
|
|
88
|
+
const sessionFile = definitionOf(peer)?.session.crashIdentity && text(session?.sessionFile) ? session.sessionFile : undefined;
|
|
86
89
|
if (unavailable || (!sessionId && !sessionFile)) return { peer, note: `${peer}: recorded session identity unavailable; inspect the retained recovery receipt before choosing a conversation` };
|
|
87
90
|
const prefix = ["ahub", "--project", planned.project.root];
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
const adapter = definitionOf(peer)!;
|
|
92
|
+
const tail = adapter.session.resumeArgv?.({ sessionId: sessionId ?? "", ...(sessionFile ? { sessionFile } : {}) }, false);
|
|
93
|
+
const start = adapter.cli.startWords("tui");
|
|
94
|
+
// Person guidance preserves its legacy implied startup form; terminal creation pins the explicit form.
|
|
95
|
+
const personTail = tail && start.every((word, index) => tail[index] === word) ? [tail[0]!, ...tail.slice(start.length)] : tail;
|
|
96
|
+
const command = personTail ? argv(...prefix, ...personTail) : undefined;
|
|
97
|
+
let note = definitionOf(peer)?.transport === "claude-channel"
|
|
94
98
|
? "The existing Claude channel may reconnect while its native session remains open. Transcript availability is unknown; the recorded resume command is guidance, not proof that the transcript can be resumed."
|
|
95
|
-
: peer === "
|
|
96
|
-
:
|
|
99
|
+
: definitionOf(peer)?.session.idField === "threadId" ? `Recorded ${definitionOf(peer)!.displayName} thread; leave the old native client before resuming this conversation.`
|
|
100
|
+
: `Recorded ${definitionOf(peer)!.displayName} conversation; preserve this session when starting ${definitionOf(peer)!.displayName} again.`;
|
|
97
101
|
if (sessionFile && options.stopAndArchiveCommand && recordedFileState(sessionFile) === "missing") {
|
|
98
102
|
note = `Recorded Pi session file is positively missing. Fresh-session is not supported for Pi. ${piArchiveExit(planned, options.stopAndArchiveCommand)}`;
|
|
99
103
|
}
|
package/src/cli/setup.ts
CHANGED
|
@@ -1,80 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { realPath } from "../hub/project.ts";
|
|
4
|
-
import { VERSION } from "../version.ts";
|
|
5
|
-
|
|
6
|
-
export const PLUGIN = "agent-hub@agent-hub";
|
|
7
|
-
const MARKETPLACE = "agent-hub";
|
|
8
|
-
|
|
9
|
-
export interface InstalledPlugin {
|
|
10
|
-
id: string;
|
|
11
|
-
version?: string;
|
|
12
|
-
installPath?: string;
|
|
13
|
-
}
|
|
14
|
-
export interface Marketplace {
|
|
15
|
-
name: string;
|
|
16
|
-
path?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** `claude plugin list --json` / `claude plugin marketplace list --json`. Anything that does not parse is an empty list. */
|
|
20
|
-
export function parseList<T>(json: string): T[] {
|
|
21
|
-
try {
|
|
22
|
-
const d = JSON.parse(json);
|
|
23
|
-
return Array.isArray(d) ? d : [];
|
|
24
|
-
} catch {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const real = (p: string) => {
|
|
30
|
-
try {
|
|
31
|
-
return realPath(p);
|
|
32
|
-
} catch {
|
|
33
|
-
return p;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const read = (p: string) => {
|
|
37
|
-
try {
|
|
38
|
-
return readFileSync(p, "utf8");
|
|
39
|
-
} catch {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export type PluginState = { state: "missing" } | { state: "current"; version: string } | { state: "stale"; version: string; why: string };
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Is the installed Claude plugin the one this hub ships? The version is compared, and so is the bundle itself: Claude
|
|
48
|
-
* Code runs a cached copy, and a copy taken from an earlier build of the same version speaks an older wire protocol.
|
|
49
|
-
*/
|
|
50
|
-
export function pluginState(plugins: InstalledPlugin[], packageRoot: string): PluginState {
|
|
51
|
-
const mine = plugins.find((p) => p.id === PLUGIN);
|
|
52
|
-
if (!mine) return { state: "missing" };
|
|
53
|
-
const version = mine.version ?? "unknown";
|
|
54
|
-
if (version !== VERSION) return { state: "stale", version, why: `version ${version}, this hub is ${VERSION}` };
|
|
55
|
-
const shipped = read(join(packageRoot, "plugins", "agent-hub", "server.js"));
|
|
56
|
-
const cached = mine.installPath ? read(join(mine.installPath, "server.js")) : undefined;
|
|
57
|
-
if (shipped !== undefined && cached !== undefined && shipped !== cached) return { state: "stale", version, why: "same version, but the cached bundle differs from the one this hub ships" };
|
|
58
|
-
return { state: "current", version };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface SetupStep {
|
|
62
|
-
why: string;
|
|
63
|
-
argv: string[];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The next thing `ahub setup` has to do, or nothing. One step at a time: the state is read again after every step,
|
|
68
|
-
* because what a step leaves behind is Claude Code's business (removing a marketplace may uninstall its plugins).
|
|
69
|
-
*/
|
|
70
|
-
export function nextStep(plugins: InstalledPlugin[], marketplaces: Marketplace[], packageRoot: string): SetupStep | undefined {
|
|
71
|
-
const root = real(packageRoot);
|
|
72
|
-
const market = marketplaces.find((m) => m.name === MARKETPLACE);
|
|
73
|
-
if (market && real(market.path ?? "") !== root) return { why: `the ${MARKETPLACE} marketplace points at ${market.path ?? "another source"}, not at this package`, argv: ["claude", "plugin", "marketplace", "remove", MARKETPLACE] };
|
|
74
|
-
if (!market) return { why: "register this package as a Claude Code plugin marketplace", argv: ["claude", "plugin", "marketplace", "add", root] };
|
|
75
|
-
const state = pluginState(plugins, root);
|
|
76
|
-
if (state.state === "missing") return { why: "install the channel plugin", argv: ["claude", "plugin", "install", PLUGIN] };
|
|
77
|
-
// Uninstall rather than update: with an unchanged version number an update has nothing to do.
|
|
78
|
-
if (state.state === "stale") return { why: `replace the installed plugin (${state.why})`, argv: ["claude", "plugin", "uninstall", PLUGIN] };
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
1
|
+
/** Compatibility exports; native plugin policy belongs to its adapter definition leaf. */
|
|
2
|
+
export { PLUGIN, parseList, pluginState, nextStep, type InstalledPlugin, type Marketplace, type PluginState, type SetupStep } from "../adapters/definitions/native-cli-claude-setup.ts";
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { shellQuote } from "./shell-quote.ts";
|
|
2
|
+
export { shellQuote } from "./shell-quote.ts";
|
|
3
|
+
import { definitionOf } from "../adapters/registry.ts";
|
|
1
4
|
import { isAbsolute, join, resolve } from "node:path";
|
|
2
5
|
import { fileURLToPath } from "node:url";
|
|
3
6
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
@@ -231,9 +234,7 @@ export class OrcaCommandError extends Error {
|
|
|
231
234
|
}
|
|
232
235
|
|
|
233
236
|
/** POSIX quoting for the single command-string argument accepted by Orca. */
|
|
234
|
-
|
|
235
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
236
|
-
}
|
|
237
|
+
|
|
237
238
|
|
|
238
239
|
function normalizeOptions(options?: CommandRunner | TerminalRecoveryOptions): Required<TerminalRecoveryOptions> {
|
|
239
240
|
const config: TerminalRecoveryOptions = typeof options === "function" ? { runner: options } : options ?? {};
|
|
@@ -474,7 +475,7 @@ function identityFrom(terminal: Record<string, unknown>): TerminalPeer | undefin
|
|
|
474
475
|
const session = object(terminal.session);
|
|
475
476
|
const candidates = [terminal.agentIdentity, terminal.agent, wait.agentIdentity, wait.agent, session.agentIdentity, session.agent];
|
|
476
477
|
for (const candidate of candidates) {
|
|
477
|
-
if (
|
|
478
|
+
if (typeof candidate === "string" && definitionOf(candidate)?.session.launchRecord) return candidate as TerminalPeer;
|
|
478
479
|
}
|
|
479
480
|
return undefined;
|
|
480
481
|
}
|
|
@@ -504,13 +505,17 @@ function requireAbsoluteEntrypoint(value: string): string {
|
|
|
504
505
|
|
|
505
506
|
function launchFor(peer: TerminalPeer, projectRoot: string, sessionId: string, options: Required<TerminalRecoveryOptions>, source: Record<string, unknown>, recorded?: RecordedTerminalLaunch, ref?: SessionRef): LaunchMetadata {
|
|
506
507
|
const packageEntrypoint = requireAbsoluteEntrypoint(options.packageEntrypoint);
|
|
507
|
-
const argv = peer === "codex"
|
|
508
|
-
? ["bun", packageEntrypoint, "--project", projectRoot, "codex", "resume", sessionId]
|
|
509
|
-
: peer === "claude"
|
|
510
|
-
? ["bun", packageEntrypoint, "--project", projectRoot, "claude", "--resume", sessionId]
|
|
511
|
-
: ["bun", packageEntrypoint, "--project", projectRoot, "pi", "--mode", "tui", ...(ref?.backend ? ["--backend", ref.backend] : []), ...(ref?.model ? ["--model", ref.model] : []), ...(ref?.sessionFile ? ["--session-file", ref.sessionFile] : ["--session-id", sessionId])];
|
|
512
508
|
const env = { ...allowedEnv(source), ...(recorded?.env ?? {}) };
|
|
513
509
|
const prefix = ALLOWED_ENV.filter((name) => env[name] !== undefined).map((name) => `${name}=${shellQuote(env[name]!)}`);
|
|
510
|
+
const launch: LaunchMetadata = { packageEntrypoint, argv: ["bun", packageEntrypoint, "--project", projectRoot], env, command: "" };
|
|
511
|
+
launch.command = [...prefix, ...launch.argv.map(shellQuote)].join(" ");
|
|
512
|
+
// Identity and environment were independently verified before this pure two-phase launch construction.
|
|
513
|
+
const binding: TerminalBinding = { peer, projectRoot, sessionId, handle: nestedString(source, ["handle"])!,
|
|
514
|
+
incarnationId: nestedString(source, ["incarnationId"])!, worktreeId: nestedString(source, ["worktreeId"])!,
|
|
515
|
+
...(ref?.sessionFile ? { sessionFile: ref.sessionFile } : {}), ...(ref?.backend ? { backend: ref.backend } : {}), ...(ref?.model ? { model: ref.model } : {}), launch, launchMetadata: launch };
|
|
516
|
+
const tail = definitionOf(peer)?.session.resumeArgv?.(binding, false);
|
|
517
|
+
if (!tail) throw new Error(`${peer}: no recorded terminal resume adapter`);
|
|
518
|
+
const argv = [...launch.argv, ...tail];
|
|
514
519
|
const command = [...prefix, ...argv.map(shellQuote)].join(" ");
|
|
515
520
|
return { packageEntrypoint, command, argv, env };
|
|
516
521
|
}
|
|
@@ -669,7 +674,9 @@ export async function inspectTerminals(projectRoot: string, sessions: Partial<Re
|
|
|
669
674
|
continue;
|
|
670
675
|
}
|
|
671
676
|
const recorded = records.find((item) => item.peer === peer && item.handle === handle && item.worktreeId === terminal.worktreeId && item.incarnationId === terminal.incarnationId);
|
|
672
|
-
|
|
677
|
+
// Orca observation supplies file identity; launch arguments are not observed native session-file evidence.
|
|
678
|
+
const { args: _launchArgs, ...observedTerminal } = terminal;
|
|
679
|
+
const sessionFile = definitionOf(peer)?.session.crashIdentity?.(observedTerminal).sessionFile;
|
|
673
680
|
const launch = launchFor(peer, projectRoot, sessionId, config, terminal, recorded, { ...ref, ...(sessionFile ? { sessionFile } : {}) });
|
|
674
681
|
const binding: TerminalBinding = { peer, handle, incarnationId, worktreeId, projectRoot, sessionId, ...(sessionFile ? { sessionFile } : {}), ...(ref.backend ? { backend: ref.backend } : {}), ...(ref.model ? { model: ref.model } : {}), launch, launchMetadata: launch };
|
|
675
682
|
bindings.push(binding);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { definitionOf } from "../adapters/registry.ts";
|
|
1
2
|
import { renderPlan, renderOperation, renderBlocked, renderSummary, renderStep, renderWait, renderMenu, choiceInstruction, type RecoveryPresentationOptions, type MenuPresentationItem } from "./upgrade-presentation.ts";
|
|
2
3
|
import { quoted, wrap } from "./console-state.ts";
|
|
3
4
|
import { operationLines, stepLabel } from "./upgrade-labels.ts";
|
|
@@ -83,7 +84,8 @@ const left = (op: { id: string; sourceRoot?: string }, io: ScreenIO): string =>
|
|
|
83
84
|
export function peerKind(planned: PlannedProject, peer: RecoveryPeer): "tui" | "headless" | "unmanaged" | "offline" {
|
|
84
85
|
if (peer.state === "offline") return "offline";
|
|
85
86
|
if ((planned.terminals as { peer: string }[]).some((t) => t.peer === peer.id)) return "tui";
|
|
86
|
-
|
|
87
|
+
const unrecorded = definitionOf(peer.id)?.session.unrecordedTerminal;
|
|
88
|
+
if (unrecorded === "always" || unrecorded === "tui" && peer.args?.mode === "tui") return "unmanaged";
|
|
87
89
|
return "headless";
|
|
88
90
|
}
|
|
89
91
|
|
|
@@ -93,7 +95,7 @@ function peerAction(planned: PlannedProject, peer: RecoveryPeer): string {
|
|
|
93
95
|
if (planned.freshStart?.includes(peer.id)) return "restarts as a new session (no turn to lose)";
|
|
94
96
|
const terminal = (planned.terminals as { peer: string; handle: string }[]).find((t) => t.peer === peer.id);
|
|
95
97
|
if (terminal) return `resumes its session in a new terminal (replaces ${terminal.handle})`;
|
|
96
|
-
return peer.id
|
|
98
|
+
return definitionOf(peer.id)?.session.restoreOnStart ? "restarts headless on its recorded session" : "restarts headless as a new session";
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
const KINDS = { tui: "TUI", headless: "headless", unmanaged: "own", offline: "-" } as const;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { definitionOf } from "../adapters/registry.ts";
|
|
1
2
|
import type { TerminalBinding } from "./terminal-recovery.ts";
|
|
2
3
|
|
|
3
4
|
export type RestoreReceipt =
|
|
@@ -23,7 +24,7 @@ function malformed(): never { throw new Error("invalid restore receipt"); }
|
|
|
23
24
|
const object = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
|
|
24
25
|
const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
|
|
25
26
|
function terminalBinding(value: unknown): TerminalBinding {
|
|
26
|
-
if (!object(value) || typeof value.peer !== "string" || !
|
|
27
|
+
if (!object(value) || typeof value.peer !== "string" || !definitionOf(value.peer)?.session.launchRecord) malformed();
|
|
27
28
|
for (const key of ["handle", "incarnationId", "worktreeId", "projectRoot", "sessionId"]) if (!text(value[key])) malformed();
|
|
28
29
|
for (const key of ["sessionFile", "backend", "model"]) if (value[key] !== undefined && typeof value[key] !== "string") malformed();
|
|
29
30
|
const launch = value.launch;
|