@staix/agent-hub 0.12.33 → 0.12.35

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.
Files changed (126) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -3
  3. package/docs/agent-notes/adapters.md +9 -3
  4. package/docs/agent-notes/budget.md +7 -3
  5. package/docs/agent-notes/bus.md +2 -1
  6. package/docs/agent-notes/daemon.md +7 -3
  7. package/docs/agent-notes/local-worker.md +1 -1
  8. package/docs/agent-notes/tasks.md +6 -2
  9. package/docs/agent-notes/tests.md +2 -1
  10. package/docs/gsd-collaboration-analysis.md +5 -1
  11. package/docs/operations.md +80 -16
  12. package/docs/quickstart.md +10 -2
  13. package/docs/sdlc.md +59 -34
  14. package/docs/security.md +7 -4
  15. package/docs/smoke.md +1 -1
  16. package/docs/specs/2026-09-19-agent-hub-design.md +407 -10
  17. package/docs/verification/2026-10-18-0.12.34.md +48 -0
  18. package/docs/verification/2026-10-18-0.12.35.md +49 -0
  19. package/docs/verification/2026-10-18-493-claude-busy-channel.md +91 -0
  20. package/docs/verified.json +35 -19
  21. package/package.json +1 -1
  22. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  23. package/plugins/agent-hub/server.js +23567 -16606
  24. package/src/adapters/acp.ts +3 -1
  25. package/src/adapters/claude-channel.ts +59 -29
  26. package/src/adapters/codex-appserver.ts +82 -12
  27. package/src/adapters/definitions/builtin-metadata.ts +101 -7
  28. package/src/adapters/definitions/native-cli-claude-launch.ts +90 -0
  29. package/src/adapters/definitions/native-cli-claude-setup.ts +80 -0
  30. package/src/adapters/definitions/native-cli-codex-launch.ts +14 -0
  31. package/src/adapters/definitions/native-cli-doctor.ts +46 -0
  32. package/src/adapters/definitions/native-cli-entry-claude.ts +52 -0
  33. package/src/adapters/definitions/native-cli-entry-codex.ts +34 -0
  34. package/src/adapters/definitions/native-cli-entry-jcode.ts +27 -0
  35. package/src/adapters/definitions/native-cli-entry-kimi.ts +57 -0
  36. package/src/adapters/definitions/native-cli-entry-local.ts +15 -0
  37. package/src/adapters/definitions/native-cli-entry-pi.ts +53 -0
  38. package/src/adapters/definitions/native-cli-entry-qwen.ts +21 -0
  39. package/src/adapters/definitions/native-cli-entry-types.ts +41 -0
  40. package/src/adapters/definitions/native-cli-entry.ts +19 -0
  41. package/src/adapters/definitions/native-cli-jcode-mcp.ts +58 -0
  42. package/src/adapters/definitions/native-cli-launch-common.ts +17 -0
  43. package/src/adapters/definitions/native-cli-pi.ts +33 -0
  44. package/src/adapters/definitions/native-cli-preview-claude.ts +37 -0
  45. package/src/adapters/definitions/native-cli-preview-codex.ts +20 -0
  46. package/src/adapters/definitions/native-cli-preview-common.ts +57 -0
  47. package/src/adapters/definitions/native-cli-preview-jcode.ts +20 -0
  48. package/src/adapters/definitions/native-cli-preview-kimi.ts +28 -0
  49. package/src/adapters/definitions/native-cli-preview-pi.ts +36 -0
  50. package/src/adapters/definitions/native-cli-preview-qwen.ts +22 -0
  51. package/src/adapters/definitions/native-cli-preview.ts +16 -0
  52. package/src/adapters/definitions/native-config-defaults.ts +19 -0
  53. package/src/adapters/definitions/native-config-policy.ts +35 -0
  54. package/src/adapters/definitions/native-context.ts +35 -0
  55. package/src/adapters/definitions/native-daemon-kimi.ts +5 -0
  56. package/src/adapters/definitions/native-daemon-policy.ts +18 -0
  57. package/src/adapters/definitions/native-display.ts +18 -0
  58. package/src/adapters/definitions/native-factories.ts +48 -0
  59. package/src/adapters/definitions/native-policy-aux.ts +130 -0
  60. package/src/adapters/definitions/native-quota.ts +29 -0
  61. package/src/adapters/definitions/native-recovery-continuity.ts +12 -0
  62. package/src/adapters/definitions/native-recovery.ts +88 -0
  63. package/src/adapters/definitions/qwen-native.ts +2 -2
  64. package/src/adapters/definitions/qwen.ts +8 -4
  65. package/src/adapters/local-worker.ts +3 -2
  66. package/src/adapters/pi.ts +3 -1
  67. package/src/adapters/registry-types.ts +100 -10
  68. package/src/adapters/registry.ts +127 -11
  69. package/src/cli/console-state.ts +5 -5
  70. package/src/cli/identity.ts +8 -7
  71. package/src/cli/jcode-mcp.ts +2 -58
  72. package/src/cli/launch.ts +41 -102
  73. package/src/cli/main.ts +126 -290
  74. package/src/cli/output.ts +3 -2
  75. package/src/cli/preview.ts +26 -126
  76. package/src/cli/recovery-guidance.ts +43 -22
  77. package/src/cli/setup.ts +2 -80
  78. package/src/cli/shell-quote.ts +3 -0
  79. package/src/cli/terminal-recovery.ts +81 -22
  80. package/src/cli/upgrade-interactive.ts +4 -2
  81. package/src/cli/upgrade-restore-state.ts +17 -7
  82. package/src/cli/upgrade-runtime.ts +126 -147
  83. package/src/cli/upgrade.ts +44 -27
  84. package/src/hub/budget.ts +59 -31
  85. package/src/hub/bus.ts +1 -1
  86. package/src/hub/conductor.ts +19 -9
  87. package/src/hub/config-apply.ts +22 -15
  88. package/src/hub/config-reload.ts +18 -3
  89. package/src/hub/config-trust.ts +1 -1
  90. package/src/hub/context-window.ts +3 -27
  91. package/src/hub/control-client.ts +1 -1
  92. package/src/hub/crash.ts +15 -39
  93. package/src/hub/daemon.ts +506 -341
  94. package/src/hub/execution-budget.ts +3 -2
  95. package/src/hub/hub-tools.ts +19 -8
  96. package/src/hub/instance-budget.ts +73 -0
  97. package/src/hub/instance-config.ts +47 -0
  98. package/src/hub/instance-execution.ts +50 -0
  99. package/src/hub/instances.ts +100 -0
  100. package/src/hub/lifecycle-schema.ts +7 -3
  101. package/src/hub/peer-catalog.ts +4 -13
  102. package/src/hub/permission-mode.ts +14 -17
  103. package/src/hub/recovery-continuity.ts +6 -5
  104. package/src/hub/report.ts +9 -8
  105. package/src/hub/roster.ts +9 -9
  106. package/src/hub/routing.ts +94 -19
  107. package/src/hub/sdlc-queue.ts +2 -2
  108. package/src/hub/sdlc.ts +84 -38
  109. package/src/hub/settings.ts +39 -10
  110. package/src/hub/settled-roles.ts +1 -1
  111. package/src/hub/snapshots.ts +40 -9
  112. package/src/hub/start-mode.ts +13 -8
  113. package/src/hub/status-roster.ts +8 -0
  114. package/src/hub/tasks.ts +60 -27
  115. package/src/hub/text-projection.ts +7 -3
  116. package/src/hub/tooling.ts +4 -60
  117. package/src/hub/ui.ts +3 -1
  118. package/src/hub/workflow.ts +30 -52
  119. package/src/memory/recall.ts +4 -5
  120. package/src/models/route/signals.ts +3 -1
  121. package/src/pi/launch.ts +5 -3
  122. package/src/ui/index.html +4 -3
  123. package/src/ui/office-model.ts +3 -1
  124. package/src/ui/peer-palette.ts +16 -6
  125. package/templates/AGENTS.block.md +2 -1
  126. package/templates/routing.toml +2 -2
@@ -0,0 +1,58 @@
1
+ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import { hubGitSync } from "../../hub/git.ts";
5
+
6
+ export const JCODE_MCP_FILE = ".jcode/mcp.json", JCODE_MCP_NAME = "agent-hub-jcode";
7
+ type Document = Record<string, unknown>;
8
+ const object = (value: unknown): value is Document => !!value && typeof value === "object" && !Array.isArray(value);
9
+
10
+ function document(file: string): Document {
11
+ const stat = lstatSync(file, { throwIfNoEntry: false });
12
+ if (!stat) return {};
13
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 256 * 1024) throw new Error("Jcode MCP configuration must be a bounded regular file, not a link");
14
+ let value: unknown; try { value = JSON.parse(readFileSync(file, "utf8")); } catch { throw new Error("Jcode MCP configuration is invalid JSON"); }
15
+ if (!object(value)) throw new Error("Jcode MCP configuration must be an object");
16
+ if (value.servers !== undefined && value.mcpServers !== undefined) throw new Error("Jcode MCP configuration has ambiguous server maps");
17
+ for (const key of ["servers", "mcpServers"]) if (value[key] !== undefined && !object(value[key])) throw new Error("Jcode MCP server map must be an object");
18
+ return value;
19
+ }
20
+ const map = (doc: Document): Document => (doc.mcpServers ?? doc.servers ?? {}) as Document;
21
+ const owned = (entry: unknown, cwd: string): entry is Document => object(entry) && object(entry.env) && entry.env.AGENTHUB_MODE === "tools" && entry.env.AGENTHUB_PEER_ID === "jcode" && entry.env.AGENTHUB_PROJECT_DIR === cwd;
22
+
23
+ export function jcodeMcpEntry(cwd: string, stateDir: string, bundle: string) {
24
+ return { command: "bun", args: ["run", bundle], shared: false, env: { AGENTHUB_MODE: "tools", AGENTHUB_DELIVERY_ONLY: "true", AGENTHUB_PEER_ID: "jcode", AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } };
25
+ }
26
+ /** The native loader reads later project files last. Never shadow a foreign definition. */
27
+ function inspect(cwd: string): Document {
28
+ const directory = lstatSync(join(cwd, ".jcode"), { throwIfNoEntry: false });
29
+ if (directory && (!directory.isDirectory() || directory.isSymbolicLink())) throw new Error(".jcode must be a project directory, not a link");
30
+ for (const relative of [".mcp.json", ".claude/mcp.json"]) {
31
+ const parent = relative.startsWith(".claude/") ? lstatSync(join(cwd, ".claude"), { throwIfNoEntry: false }) : undefined;
32
+ if (parent?.isSymbolicLink()) throw new Error("Jcode MCP override layer is a link; inspect it before launching");
33
+ if (Object.hasOwn(map(document(join(cwd, relative))), JCODE_MCP_NAME)) throw new Error("a later project MCP layer overrides agent-hub-jcode; resolve it in your editor");
34
+ }
35
+ return document(join(cwd, JCODE_MCP_FILE));
36
+ }
37
+ export function jcodeMcpReady(cwd: string, stateDir: string, bundle: string): boolean {
38
+ const entry = map(inspect(cwd))[JCODE_MCP_NAME];
39
+ return JSON.stringify(entry) === JSON.stringify(jcodeMcpEntry(cwd, stateDir, bundle));
40
+ }
41
+ /** Only the console launcher calls this; daemon starts use jcodeMcpReady read-only. */
42
+ export function configureJcodeMcp(cwd: string, stateDir: string, bundle: string, remove = false): { changed: boolean; tracked: boolean } {
43
+ const current = inspect(cwd), servers = { ...map(current) }, existing = servers[JCODE_MCP_NAME];
44
+ if (existing !== undefined && !owned(existing, cwd)) throw new Error("agent-hub-jcode is a foreign MCP entry; it was not changed");
45
+ if (remove) delete servers[JCODE_MCP_NAME]; else servers[JCODE_MCP_NAME] = jcodeMcpEntry(cwd, stateDir, bundle);
46
+ const key = current.servers !== undefined ? "servers" : "mcpServers";
47
+ const next = { ...current, ...(current[key] !== undefined || !remove ? { [key]: servers } : {}) };
48
+ const tracked = hubGitSync(["ls-files", "--error-unmatch", "--", JCODE_MCP_FILE], { cwd, encoding: "utf8", timeout: 3000 }).status === 0;
49
+ if (JSON.stringify(next) === JSON.stringify(current)) return { changed: false, tracked };
50
+ mkdirSync(join(cwd, ".jcode"), { recursive: true });
51
+ const file = join(cwd, JCODE_MCP_FILE), temporary = `${file}.${randomUUID()}.tmp`;
52
+ try {
53
+ const fd = openSync(temporary, "wx", 0o600);
54
+ try { writeFileSync(fd, `${JSON.stringify(next, null, 2)}\n`); } finally { closeSync(fd); }
55
+ renameSync(temporary, file);
56
+ } finally { if (existsSync(temporary)) unlinkSync(temporary); }
57
+ return { changed: true, tracked };
58
+ }
@@ -0,0 +1,17 @@
1
+ import type { LaunchContext, LaunchPlan } from "../registry-types.ts";
2
+ import type { Launch } from "../../cli/launch.ts";
3
+ import { UNATTENDED_WARNING } from "../../cli/launch.ts";
4
+ import { shellQuote } from "../../cli/shell-quote.ts";
5
+
6
+ export interface NativeLaunchPolicy { hubSwitches: readonly string[]; ownedFlags: readonly string[] }
7
+ export function nativeLaunchArguments(tool: string, ctx: LaunchContext, policy: NativeLaunchPolicy) {
8
+ const passthrough = ctx.userArgs.filter(arg => !policy.hubSwitches.includes(arg));
9
+ const dup = passthrough.find(arg => policy.ownedFlags.some(flag => arg === flag || arg.startsWith(`${flag}=`)));
10
+ if (dup) throw new Error(`${dup} is managed by the hub; use "hub ${tool} --unattended" or drop the flag`);
11
+ const unattended = ctx.unattended || ctx.userArgs.includes("--unattended");
12
+ return { passthrough, unattended, warning: unattended ? { warning: UNATTENDED_WARNING } : {} };
13
+ }
14
+ export function nativeLaunchPlan(launch: Launch, ctx: LaunchContext): LaunchPlan {
15
+ const argv = [launch.cmd, ...launch.args];
16
+ return { ...launch, metadata: { packageEntrypoint: ctx.entrypoint, argv, env: {}, command: argv.map(shellQuote).join(" ") } };
17
+ }
@@ -0,0 +1,33 @@
1
+ export function parsePiFlags(args: string[], fail: (message: string) => never): { mode?: "headless" | "tui"; backend?: "auto" | "dgx" | "mlx"; model?: string; sessionId?: string; sessionFile?: string; replaceSession?: boolean } {
2
+ 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>]";
3
+ if (args.includes("--headless")) {
4
+ if (args.includes("--mode")) fail(usage);
5
+ args.splice(args.indexOf("--headless"), 1, "--mode", "headless");
6
+ }
7
+ for (const flag of ["--mode", "--backend", "--model", "--session-id", "--session-file"]) {
8
+ const index = args.indexOf(flag);
9
+ if (index >= 0 && (!args[index + 1] || args[index + 1]!.startsWith("--"))) fail(`${flag} needs a value`);
10
+ }
11
+ const mode = (args.includes("--mode") ? args[args.indexOf("--mode") + 1] : undefined) as string | undefined;
12
+ const backend = (args.includes("--backend") ? args[args.indexOf("--backend") + 1] : undefined) as string | undefined;
13
+ const model = args.includes("--model") ? args[args.indexOf("--model") + 1] : undefined;
14
+ const sessionId = args.includes("--session-id") ? args[args.indexOf("--session-id") + 1] : undefined;
15
+ const sessionFile = args.includes("--session-file") ? args[args.indexOf("--session-file") + 1] : undefined;
16
+ const replaceSession = args.includes("--replace-session");
17
+ if (replaceSession && (!sessionFile || sessionId)) fail("--replace-session requires --session-file, without --session-id");
18
+ const valueFlags = new Set(["--mode", "--backend", "--model", "--session-id", "--session-file"]);
19
+ for (let i = 0; i < args.length; i++) {
20
+ if (args[i] === "--replace-session") continue;
21
+ if (!valueFlags.has(args[i]!)) fail(`unknown Pi option: ${args[i]}`);
22
+ i++;
23
+ }
24
+ if ((mode !== undefined && !["headless", "tui"].includes(mode)) || (backend !== undefined && !["auto", "dgx", "mlx"].includes(backend)) || (sessionId && sessionFile)) fail(usage);
25
+ return { ...(replaceSession ? { replaceSession: true } : {}), ...(mode ? { mode: mode as "headless" | "tui" } : {}), ...(backend ? { backend: backend as "auto" | "dgx" | "mlx" } : {}), ...(model ? { model } : {}), ...(sessionId ? { sessionId } : {}), ...(sessionFile ? { sessionFile } : {}) };
26
+ }
27
+
28
+ /** The conductor can repeat only the selected form; native startup options remain person-only. */
29
+ export function piConductorStartArgs(args: string[]): { mode?: "headless" | "tui" } | Error {
30
+ if (args.length === 1 && args[0] === "--headless") args.splice(0, 1, "--mode", "headless");
31
+ if (args.length && !(args.length === 2 && args[0] === "--mode" && ["headless", "tui"].includes(args[1] ?? ""))) return new Error("conductor starts accept no launch overrides; use ahub pi [--mode headless|tui]");
32
+ return args[1] ? { mode: args[1] as "headless" | "tui" } : {};
33
+ }
@@ -0,0 +1,37 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { Launch } from "../../cli/launch.ts";
4
+ import { REDACTED, publicArg, previewState, finishPreview, type NativePreviewContext } from "./native-cli-preview-common.ts";
5
+
6
+ export function previewClaude(ctx: NativePreviewContext) {
7
+ const { args, cwd, stateDir, cliDir, unattended, config } = ctx;
8
+ const { buildLaunch, claudeObservationHooks } = ctx.helpers;
9
+ const state = previewState(args, ctx.baseEnvNames), { unresolved } = state;
10
+ let envNames = state.envNames;
11
+ let launch: Launch;
12
+ let original: { command?: string; refreshInterval?: number; padding?: number } | undefined;
13
+ for (const file of [join(cwd, ".claude/settings.local.json"), join(cwd, ".claude/settings.json"), join(process.env.HOME ?? "", ".claude/settings.json")]) {
14
+ try { original ??= JSON.parse(readFileSync(file, "utf8")).statusLine; } catch { /* optional */ }
15
+ }
16
+ // Build with original inputs so channel selection and owned-flag validation match normal launch.
17
+ const facts = claudeObservationHooks(config, { script: join(cliDir, "facts-hook.ts"), stateDir });
18
+ launch = buildLaunch("claude", args, { preview: true, unattended, statusLine: { script: join(cliDir, "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 } : {}) });
19
+ // Merged caller settings change the native argument count. Rebuild a redacted preview,
20
+ // retaining the managed hooks but never serializing caller settings or their file path.
21
+ const safeArgs = args.map((arg, i) => {
22
+ if (args[i - 1] === "--settings") return "{}";
23
+ if (arg.startsWith("--settings=")) return "--settings={}";
24
+ if (["--unattended", "--safe", "--new"].includes(arg)) return arg;
25
+ return publicArg(arg);
26
+ });
27
+ const publicLaunch = buildLaunch("claude", safeArgs, { preview: true, unattended, statusLine: { script: join(cliDir, "statusline-tee.ts"), stateDir,
28
+ ...(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 });
29
+ publicLaunch.args.splice(0, 2, ...launch.args.slice(0, 2)); // channel selection was verified against the original MCP config
30
+ launch = publicLaunch;
31
+
32
+ envNames.push("AGENTHUB_CHANNEL");
33
+ envNames.push("AGENTHUB_INSTANCE_ID", "AGENTHUB_LAUNCH_ID");
34
+ unresolved.push({ field: "env.AGENTHUB_INSTANCE_ID", reason: "injected when an existing daemon instance is read at launch; preview does not read runtime state" });
35
+ 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" });
36
+ return finishPreview(ctx, launch, envNames, unresolved, undefined, launch.args.includes("--settings") ? launch.args[launch.args.indexOf("--settings") + 1] : undefined);
37
+ }
@@ -0,0 +1,20 @@
1
+ import type { Launch } from "../../cli/launch.ts";
2
+ import { REDACTED, UNRESOLVED, publicArg, previewState, finishPreview, type NativePreviewContext } from "./native-cli-preview-common.ts";
3
+
4
+ export function previewCodex(ctx: NativePreviewContext) {
5
+ const { args, unattended, config } = ctx;
6
+ const { buildLaunch } = ctx.helpers;
7
+ const state = previewState(args, ctx.baseEnvNames), { unresolved } = state;
8
+ let envNames = state.envNames;
9
+ let launch: Launch;
10
+ // A fresh native proxy is assigned by start, never guessed from an old status file.
11
+ launch = buildLaunch("codex", args, { unattended, proxyUrl: UNRESOLVED, codexBin: config.codex_bin === "codex" ? "codex" : REDACTED });
12
+ const passthrough = args.filter(arg => !["--unattended", "--safe", "--new"].includes(arg));
13
+ launch.args = launch.args.slice(0, launch.args.length - passthrough.length).concat(passthrough.map(publicArg));
14
+ unresolved.push({ field: "proxyUrl", reason: "assigned when the native adapter starts; preview does not start it" });
15
+
16
+ envNames.push("AGENTHUB_INSTANCE_ID", "AGENTHUB_LAUNCH_ID");
17
+ unresolved.push({ field: "env.AGENTHUB_INSTANCE_ID", reason: "injected when a verified Orca terminal launch is recorded against the daemon; preview does not record it" });
18
+ 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" });
19
+ return finishPreview(ctx, launch, envNames, unresolved, undefined);
20
+ }
@@ -0,0 +1,57 @@
1
+ import type { loadConfig } from "../../hub/daemon.ts";
2
+ import type { AdapterDefinition } from "../registry-types.ts";
3
+ import type { Launch } from "../../cli/launch.ts";
4
+
5
+ export const REDACTED = "[redacted]";
6
+ export const UNRESOLVED = "[unresolved]";
7
+ /** Fixed, pure/read-only CLI planning helpers. No command executor, control client or writer enters this port. */
8
+ export interface NativePreviewHelpers {
9
+ buildLaunch: typeof import("../../cli/launch.ts").buildLaunch;
10
+ claudeObservationHooks: typeof import("../../cli/launch.ts").claudeObservationHooks;
11
+ buildKimiLaunch: typeof import("../../cli/launch.ts").buildKimiLaunch;
12
+ buildKimiTuiLaunch: typeof import("../../cli/launch.ts").buildKimiTuiLaunch;
13
+ nativeLaunchEnv: typeof import("../../cli/launch.ts").nativeLaunchEnv;
14
+ childEnv: typeof import("../../hub/child-process.ts").childEnv;
15
+ startModeOf: typeof import("../../hub/start-mode.ts").startModeOf;
16
+ kimiMcpPlan: typeof import("../../cli/kimi-mcp.ts").kimiMcpPlan;
17
+ jcodeCommand: typeof import("../jcode.ts").jcodeCommand;
18
+ qwenSessionId: typeof import("../../hub/crash.ts").qwenSessionId;
19
+ relayModelIds: typeof import("../../models/relay.ts").relayModelIds;
20
+ buildPiLaunch: typeof import("../../pi/launch.ts").buildPiLaunch;
21
+ }
22
+ export interface NativePreviewContext {
23
+ tool: string; args: string[]; cwd: string; stateDir: string; cliDir: string; unattended: boolean;
24
+ config: ReturnType<typeof loadConfig>; definition: AdapterDefinition; baseEnvNames: string[]; helpers: NativePreviewHelpers;
25
+ }
26
+ export interface NativePreviewMetadata { mode: "tui" | "headless"; mcp: { file: string; action: string; changed: boolean; tracked: boolean } }
27
+ export type NativePreviewBuilder = (ctx: NativePreviewContext) => NativePreview;
28
+ export interface NativePreview {
29
+ tool: string; mode?: "tui" | "headless"; mcp?: NativePreviewMetadata["mcp"];
30
+ cmd: string; args: string[]; settings: string | undefined; envNames: string[]; warning?: string;
31
+ unresolved: { field: string; reason: string }[]; redaction: string; needs: string[];
32
+ }
33
+ /** Arbitrary native arguments can contain secrets even without a credential-looking key. */
34
+ export function publicArg(arg: string): string {
35
+ const flags = new Set(["--settings", "--model", "--mcp-config", "--session-id", "--session-file", "--resume", "--continue", "--print", "--safe", "--new"]);
36
+ const key = arg.split("=", 1)[0]!;
37
+ return flags.has(key) ? (arg.includes("=") ? key + "=" + REDACTED : key) : REDACTED;
38
+ }
39
+ export function nativeCommand(command: string[], trusted: string[]): string[] {
40
+ return command.map((value, i) => value === trusted[i] ? value : REDACTED);
41
+ }
42
+
43
+ export function previewState(args: readonly string[], baseEnvNames: readonly string[]) {
44
+ const unresolved: { field: string; reason: string }[] = [];
45
+ const envNames = [...baseEnvNames];
46
+ const value = (flag: string) => {
47
+ const at = args.indexOf(flag);
48
+ if (at < 0) return undefined;
49
+ const next = args[at + 1];
50
+ if (!next || next.startsWith("--")) throw new Error("missing option value");
51
+ return next;
52
+ };
53
+ return { unresolved, envNames, value };
54
+ }
55
+ export function finishPreview(ctx: NativePreviewContext, launch: Launch, envNames: string[], unresolved: NativePreview["unresolved"], metadata?: NativePreviewMetadata, settings?: string): NativePreview {
56
+ return { tool: ctx.tool, ...(metadata ?? {}), cmd: launch.cmd, args: launch.args, settings, 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."] };
57
+ }
@@ -0,0 +1,20 @@
1
+ import type { Launch } from "../../cli/launch.ts";
2
+ import { REDACTED, UNRESOLVED, nativeCommand, previewState, finishPreview, type NativePreviewContext } from "./native-cli-preview-common.ts";
3
+
4
+ export function previewJcode(ctx: NativePreviewContext) {
5
+ const { args, config } = ctx;
6
+ const { jcodeCommand, qwenSessionId } = ctx.helpers;
7
+ const state = previewState(args, ctx.baseEnvNames), { unresolved, value } = state;
8
+ let envNames = state.envNames;
9
+ let launch: Launch;
10
+ const mode = value("--mode") ?? "headless", model = value("--model"), profile = value("--provider-profile"), session = value("--session-id");
11
+ if (mode !== "headless") throw new Error("Jcode TUI driving is unverified");
12
+ 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");
13
+ if (session !== undefined && !qwenSessionId(session)) throw new Error("invalid Jcode session identity");
14
+ const cmd = jcodeCommand(nativeCommand(config.jcode_cmd, ["jcode"]), "acp", UNRESOLVED, UNRESOLVED, model ? REDACTED : undefined, profile || config.jcode_provider_profile ? REDACTED : undefined);
15
+ launch = { cmd: cmd[0]!, args: cmd.slice(1) };
16
+ unresolved.push({ field: "ownedDaemon", reason: "private daemon/socket and source-pinned native identity verified at start; preview starts nothing" });
17
+ unresolved.push({ field: "hubTools", reason: "project MCP bridge required; native tool-call receipt remains unverified" });
18
+
19
+ return finishPreview(ctx, launch, envNames, unresolved, undefined);
20
+ }
@@ -0,0 +1,28 @@
1
+ import { join } from "node:path";
2
+ import type { Launch } from "../../cli/launch.ts";
3
+ import { REDACTED, nativeCommand, previewState, finishPreview, type NativePreviewContext, type NativePreviewMetadata } from "./native-cli-preview-common.ts";
4
+
5
+ export function previewKimi(ctx: NativePreviewContext) {
6
+ const { args, cwd, stateDir, cliDir, config } = ctx;
7
+ const { childEnv, startModeOf, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, kimiMcpPlan } = ctx.helpers;
8
+ const state = previewState(args, ctx.baseEnvNames), { unresolved, value } = state;
9
+ let envNames = state.envNames;
10
+ let launch: Launch;
11
+ let kimiMetadata: NativePreviewMetadata | undefined;
12
+ const model = value("--model");
13
+ const mode = value("--mode") ?? startModeOf(config.peers, "kimi");
14
+ if (mode !== "tui" && mode !== "headless") throw new Error("invalid Kimi mode");
15
+ for (let i = 0; i < args.length; i += 2) if (!["--model", "--mode"].includes(args[i]!) || !args[i + 1]) throw new Error("unknown Kimi option");
16
+ if (mode === "headless") {
17
+ launch = buildKimiLaunch(nativeCommand(config.kimi_cmd, ["kimi", "acp"]), model ? REDACTED : undefined);
18
+ unresolved.push({ field: "sessionId", reason: "ACP session identity is available only after native initialization" });
19
+ } else {
20
+ const native = buildKimiTuiLaunch(config.kimi_cmd, model ? REDACTED : undefined);
21
+ launch = { ...native, cmd: native.cmd === "kimi" ? "kimi" : REDACTED, args: native.args.map(arg => arg === "--model" || arg === REDACTED ? arg : REDACTED) };
22
+ envNames = Object.keys(nativeLaunchEnv("kimi", childEnv())).concat(["AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR"]);
23
+ }
24
+ const { file, action, changed, tracked } = kimiMcpPlan(cwd, stateDir, join(cliDir, "../../plugins/agent-hub/server.js"), mode);
25
+ kimiMetadata = { mode, mcp: { file, action, changed, tracked } };
26
+
27
+ return finishPreview(ctx, launch, envNames, unresolved, kimiMetadata);
28
+ }
@@ -0,0 +1,36 @@
1
+ import { join, resolve } from "node:path";
2
+ import type { Launch } from "../../cli/launch.ts";
3
+ import { REDACTED, UNRESOLVED, nativeCommand, previewState, finishPreview, type NativePreviewContext } from "./native-cli-preview-common.ts";
4
+
5
+ export function previewPi(ctx: NativePreviewContext) {
6
+ const { args, stateDir, cliDir, config } = ctx;
7
+ const { relayModelIds, buildPiLaunch, startModeOf } = ctx.helpers;
8
+ const state = previewState(args, ctx.baseEnvNames), { unresolved, value } = state;
9
+ let envNames = state.envNames;
10
+ let launch: Launch;
11
+ const mode = value("--mode") ?? startModeOf(config.peers, "pi"); // #269: the same default `ahub pi` takes
12
+ const backend = value("--backend") ?? config.pi.backend;
13
+ if (!["headless", "tui"].includes(mode) || !["auto", "dgx", "mlx"].includes(backend) || (value("--session-id") && value("--session-file"))) throw new Error("invalid Pi options");
14
+ if (args.includes("--replace-session") && (!value("--session-file") || value("--session-id"))) throw new Error("--replace-session requires --session-file, without --session-id");
15
+ for (let i = 0; i < args.length; i++) {
16
+ if (args[i] === "--replace-session") continue;
17
+ if (!["--mode", "--backend", "--model", "--session-id", "--session-file"].includes(args[i]!) || !value(args[i]!)) throw new Error("unknown Pi option");
18
+ i++;
19
+ }
20
+ 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" });
21
+ 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 } });
22
+ const pi = buildPiLaunch({
23
+ stateDir, cmd: nativeCommand(config.pi.cmd, ["pi"]), mode: mode as "headless" | "tui", backend: backend as "auto" | "dgx" | "mlx",
24
+ ...(value("--model") ? { model: REDACTED } : {}),
25
+ ...(value("--session-id") ? { sessionId: REDACTED } : {}),
26
+ ...(value("--session-file") ? { sessionFile: REDACTED } : {}),
27
+ relay: { url: UNRESOLVED, token: UNRESOLVED, models: models.map(id => ({ id })) },
28
+ tools: [], preamble: REDACTED, maxSteps: config.pi.max_steps,
29
+ }, {}, resolve(join(cliDir, "../pi/extension.ts")), UNRESOLVED, UNRESOLVED);
30
+ launch = pi;
31
+ 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));
32
+ if (mode === "tui") envNames.push("AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR");
33
+ unresolved.push({ field: "bridge/relay endpoints and owner credentials", reason: "assigned at runtime; preview does not bind a server or start a model" });
34
+ if (!value("--session-id") && !value("--session-file")) unresolved.push({ field: "sessionId", reason: "new native session identity is not allocated by preview" });
35
+ return finishPreview(ctx, launch, envNames, unresolved, undefined);
36
+ }
@@ -0,0 +1,22 @@
1
+ import { join } from "node:path";
2
+ import type { Launch } from "../../cli/launch.ts";
3
+ import { REDACTED, UNRESOLVED, nativeCommand, previewState, finishPreview, type NativePreviewContext } from "./native-cli-preview-common.ts";
4
+
5
+ export function previewQwen(ctx: NativePreviewContext) {
6
+ const { tool, args, cwd, stateDir, cliDir, config } = ctx;
7
+ const state = previewState(args, ctx.baseEnvNames), { unresolved, value } = state;
8
+ let envNames = state.envNames;
9
+ let launch: Launch;
10
+ const mode = value("--mode") ?? "headless", model = value("--model");
11
+ const adapter = ctx.definition;
12
+ if (!(adapter.start.forms as readonly string[]).includes(mode)) throw new Error("Qwen TUI is not offered before verification");
13
+ 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");
14
+ const sessionId = value("--session-id");
15
+ if (sessionId !== undefined && !adapter.session.crashIdentity?.({ sessionId }).sessionId) throw new Error("invalid Qwen session identity");
16
+ if (sessionId) unresolved.push({ field: "sessionId", reason: "requested through ACP session/load at start; preview does not load or expose it" });
17
+ launch = adapter.start.launch({ instance: tool, form: "headless", projectRoot: cwd, stateDir, entrypoint: join(cliDir, "main.js"),
18
+ userArgs: [], unattended: false, env: {}, preview: true, nativeCommand: nativeCommand(config.qwen_cmd, ["qwen", "--acp"]), model: model ? REDACTED : undefined });
19
+ unresolved.push({ field: "nativeIdentity", reason: "Qwen Code 0.25.0 identity and permission modes are checked at ACP initialization; preview starts nothing" });
20
+
21
+ return finishPreview(ctx, launch, envNames, unresolved, undefined);
22
+ }
@@ -0,0 +1,16 @@
1
+ import type { AdapterDefinition } from "../registry-types.ts";
2
+ import type { NativePreviewBuilder } from "./native-cli-preview-common.ts";
3
+ import { previewClaude } from "./native-cli-preview-claude.ts";
4
+ import { previewCodex } from "./native-cli-preview-codex.ts";
5
+ import { previewKimi } from "./native-cli-preview-kimi.ts";
6
+ import { previewPi } from "./native-cli-preview-pi.ts";
7
+ import { previewQwen } from "./native-cli-preview-qwen.ts";
8
+ import { previewJcode } from "./native-cli-preview-jcode.ts";
9
+
10
+ // Private implementations of the fixed builtin set; neither config nor an external definition can add callbacks.
11
+ const previews: Readonly<Record<string, NativePreviewBuilder>> = Object.freeze({
12
+ claude: previewClaude, codex: previewCodex, kimi: previewKimi, pi: previewPi, qwen: previewQwen, jcode: previewJcode,
13
+ });
14
+ export function builtinPreview(definition: AdapterDefinition): NativePreviewBuilder | undefined {
15
+ return Object.hasOwn(previews, definition.kind) ? previews[definition.kind] : undefined;
16
+ }
@@ -0,0 +1,19 @@
1
+ import { join } from "node:path";
2
+ import type { HubConfig } from "../../hub/daemon.ts";
3
+ import type { Routing } from "../../hub/routing.ts";
4
+
5
+ /** Preserve the legacy config block order and shallow native merges. */
6
+ export const NATIVE_CONFIG_BLOCKS = ["local", "pi"] as const;
7
+ export function nativeConfigurationDefaults(ctx: { home: string; env: Readonly<Record<string, string | undefined>>; networkAllow: string[] }) {
8
+ return {
9
+ commands: { kimi_cmd: ["kimi", "acp"], qwen_cmd: ["qwen", "--acp"], jcode_cmd: ["jcode"], jcode_home: ctx.env.JCODE_HOME ?? join(ctx.home, ".jcode"), jcode_provider_profile: "" },
10
+ proxy: { codex_bin: "codex" },
11
+ rpc: { pi: { enabled: false, auto_start: false, cmd: ["pi"], backend: "auto" as const, dgx_coding: "coding", dgx_fast: "fast", max_steps: 30 } },
12
+ sandbox: { local: { deny: [] as string[], bash_network: false, network_allow: ctx.networkAllow, max_steps: 30, read_allow: [] as string[] } },
13
+ };
14
+ }
15
+ export function mergeNativeConfiguration(file: Record<string, any>, defaults: HubConfig): Pick<HubConfig, "local" | "pi"> {
16
+ return { local: { ...defaults.local, ...file.local }, pi: { ...defaults.pi, ...file.pi } };
17
+ }
18
+ /** The published RPC relay stays bound while other routing leaves are reloaded. */
19
+ export function retainNativeRouting(running: Routing): Pick<Routing, "pi"> { return { pi: running.pi }; }
@@ -0,0 +1,35 @@
1
+ import type { NativeConfigCheck, NativeConfigValidators } from "../registry-types.ts";
2
+ import type { SettingDef } from "../../hub/settings.ts";
3
+
4
+ /** Native config uses the host's checked schema vocabulary, retaining its shape/path introspection. */
5
+ export function piConfigSchema(v: NativeConfigValidators): Record<string, NativeConfigCheck> {
6
+ return { pi: v.block({ enabled: v.bool, auto_start: v.bool, cmd: v.argv, backend: v.words(["auto", "dgx", "mlx"]), dgx_coding: v.text, dgx_fast: v.text, max_steps: v.number(1, 1_000_000, true) }) };
7
+ }
8
+ export function localConfigSchema(v: NativeConfigValidators): Record<string, NativeConfigCheck> {
9
+ return { local: v.block({ deny: v.strings, bash_network: (value, key) => { if (value !== "direct") v.bool(value, key); }, network_allow: v.strings, max_steps: v.number(1, 1_000_000, true), read_allow: v.strings }) };
10
+ }
11
+ export function piRoutingSchema(v: NativeConfigValidators): Record<string, NativeConfigCheck> {
12
+ return { pi: v.block({ dgx_max_context_tokens: v.number(), mlx_max_context_tokens: v.number(), efficient_wait_ms: v.number(0, 119999) }) };
13
+ }
14
+ export function localRoutingSchema(v: NativeConfigValidators): Record<string, NativeConfigCheck> {
15
+ return { local: v.block({ route: v.text, fixed_model: v.text }) };
16
+ }
17
+ export const PI_SETTINGS: readonly SettingDef[] = [
18
+ { key: "pi.auto_start", group: "Start", label: "Start Pi with the hub", type: "boolean", store: "config", applies: "next use", risk: "raises", floor: false, path: ["pi", "auto_start"] },
19
+ ];
20
+ export const PI_RESTART_REASONS: Readonly<Record<string, string>> = {
21
+ "pi.dgx_coding": "Published Pi relay model catalogue requires a new daemon relay",
22
+ "pi.dgx_fast": "Published Pi relay model catalogue requires a new daemon relay",
23
+ "routing.pi": "Pi relay context and model publication are bound to the running relay",
24
+ };
25
+ export const LOCAL_RESTART_REASONS: Readonly<Record<string, string>> = {
26
+ "local.deny": "Existing native sessions retain their sandbox deny policy", "local.read_allow": "Existing native sessions retain their sandbox read grants",
27
+ "local.bash_network": "Existing sandbox and egress proxy contracts require a new instance", "local.network_allow": "Existing egress proxy ownership requires a new instance",
28
+ };
29
+ export function piEffectiveConfig(raw: Record<string, unknown>, fail: (key: string, reason: string) => never): void {
30
+ const object = (value: unknown, key: string): Record<string, unknown> => {
31
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail(key, "must be an object");
32
+ return value as Record<string, unknown>;
33
+ };
34
+ if (object(raw.mlx, "mlx").enabled === false && object(raw.pi, "pi").backend === "mlx") fail("pi.backend", "conflicts with disabled mlx");
35
+ }
@@ -0,0 +1,35 @@
1
+ import type { ContextReading } from "../../hub/context-window.ts";
2
+
3
+ export const nonnegative = (n: unknown): n is number => typeof n === "number" && Number.isFinite(n) && n >= 0;
4
+ export const positive = (n: unknown): n is number => nonnegative(n) && n > 0;
5
+ const record = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === "object" && !Array.isArray(v);
6
+ const object = (v: unknown): Record<string, unknown> => record(v) ? v : {};
7
+ export function claudeContext(value: unknown, sessionId: string, measuredAt: number): ContextReading {
8
+ const c = object(value);
9
+ const current = object(c.current_usage);
10
+ const parts = [current.input_tokens, current.cache_creation_input_tokens, current.cache_read_input_tokens];
11
+ const window = positive(c.context_window_size) ? c.context_window_size : null;
12
+ const sum = c.current_usage != null && parts.every(nonnegative) ? parts.reduce<number>((sum, n) => sum + (n as number), 0) : null;
13
+ const tokens = nonnegative(sum) ? sum : null;
14
+ const used = record(c.current_usage) && tokens !== null && nonnegative(c.used_percentage) && c.used_percentage <= 100 && window !== null ? c.used_percentage / 100 : null;
15
+ return { source: "claude_statusline", sessionId, measuredAt, tokens, window, used };
16
+ }
17
+ export function codexContext(value: unknown, sessionId: string, measuredAt: number): ContextReading {
18
+ const c = object(value), last = object(c.last);
19
+ const window = positive(c.modelContextWindow) ? c.modelContextWindow : null;
20
+ const tokens = nonnegative(last.totalTokens) ? last.totalTokens : null;
21
+ return { source: "codex_token_usage", sessionId, measuredAt, tokens, window, used: tokens !== null && window !== null ? Math.min(1, tokens / window) : null };
22
+ }
23
+ /** An ACP `usage_update` of the `{ used, size }` shape (Kimi 2.x): context occupancy, not consumption (#167, #285). */
24
+ export function acpContext(usage: { used: number; size: number }, sessionId: string, measuredAt: number): ContextReading {
25
+ const window = positive(usage.size) ? usage.size : null;
26
+ const tokens = nonnegative(usage.used) ? usage.used : null;
27
+ // If a report ever shows used > size, the fraction clamps so a bar renders; tokens keeps the reported number.
28
+ return { source: "acp_usage_update", sessionId, measuredAt, tokens, window, used: tokens !== null && window !== null ? Math.min(1, tokens / window) : null };
29
+ }
30
+
31
+ /** The coordinator supplies validated occupancy; malformed public input remains unknown. */
32
+ export function acpContextValue(value: unknown, sessionId: string, measuredAt: number): ContextReading {
33
+ const raw = object(value);
34
+ return acpContext({ used: raw.used as number, size: raw.size as number }, sessionId, measuredAt);
35
+ }
@@ -0,0 +1,5 @@
1
+ /** Kimi puts its optional model switch before the configured ACP subcommand. */
2
+ export function kimiAcpCommand(command: readonly string[], model?: string): string[] {
3
+ const [cmd, ...args] = command;
4
+ return [cmd!, ...(model ? ["--model", model, ...args] : args)];
5
+ }
@@ -0,0 +1,18 @@
1
+ import type { StartForm, StartRequest, VersionContext } from "../registry-types.ts";
2
+
3
+ /** Native serialization and person-facing guidance only; the daemon retains every ownership decision. */
4
+ export const claudeRecoveryLaunch = (instance: string): Record<string, unknown> => ({ kind: "claude-channel", peer: instance });
5
+ export const kimiRecoveryLaunch = (instance: string): Record<string, unknown> => ({ kind: "kimi-tui", peer: instance, mode: "tui" });
6
+ export const claudeRetainedTerminalNotice = (instance: string): string =>
7
+ `Stop ${instance}'s existing work in its retained terminal; removal detaches the hub connection and does not stop that terminal's work.`;
8
+ export const piRetainedTerminalNotice = (instance: string, form: StartForm | undefined): string | undefined =>
9
+ form === "tui" ? claudeRetainedTerminalNotice(instance) : undefined;
10
+ export const codexRetainedTerminalNotice = (_instance: string, _form: StartForm | undefined, resumeCommand: string): string =>
11
+ `Check codex's existing terminal and stop any remaining work. Its app-server and proxy stay available, but Codex 0.146.0 exits its TUI on disconnect and does not reconnect automatically; after ahub roster add codex, resume the previous session with ${resumeCommand}.`;
12
+ export const codexRosterReturnNotice = (resumeCommand: string): string =>
13
+ `Codex 0.146.0 needs a manual return: resume the previous native session with ${resumeCommand}.`;
14
+ export const jcodeStartPreflight = (request: StartRequest): string | undefined =>
15
+ request.form !== undefined && request.form !== "headless" ? "Jcode native TUI driving is unverified; use headless" : undefined;
16
+ export const claudeObservedVersion = (context: VersionContext): string | undefined => context.observedVersion;
17
+ export const codexObservedVersion = (context: VersionContext): string | undefined =>
18
+ (context.peer as { version?: string } | undefined)?.version ?? context.observedVersion;
@@ -0,0 +1,18 @@
1
+ /** Existing native identity swatches; hub and unknown-peer styling stays in the UI. */
2
+ export const CLAUDE_DISPLAY = { palette: { fg: "#6495ed", bg: "transparent", tone: "peerClaude", ansi: "\x1b[94m" } } as const;
3
+ export const CODEX_DISPLAY = { palette: { fg: "#22b8cf", bg: "transparent", tone: "peerCodex", ansi: "\x1b[96m" } } as const;
4
+ export const KIMI_DISPLAY = { palette: { fg: "#bc73ca", bg: "transparent", tone: "taskKeyword", ansi: "\x1b[35m" } } as const;
5
+ export const PI_DISPLAY = { palette: { fg: "#c79936", bg: "transparent", tone: "attention", ansi: "\x1b[33m" } } as const;
6
+ export const LOCAL_DISPLAY = { palette: { fg: "#4caa77", bg: "transparent", tone: "success", ansi: "\x1b[32m" } } as const;
7
+ // These two kinds deliberately retain the existing 'other' fallback, not new public swatches.
8
+ export const QWEN_DISPLAY = { palette: { fg: "#8a8f99", bg: "transparent", tone: "muted", ansi: "\x1b[90m" } } as const;
9
+ export const JCODE_DISPLAY = { palette: { fg: "#8a8f99", bg: "transparent", tone: "muted", ansi: "\x1b[90m" } } as const;
10
+
11
+ /** Kinds with a public named swatch; keep their exact literal types in compatibility projections. */
12
+ export const NATIVE_DISPLAYS = { claude: CLAUDE_DISPLAY, codex: CODEX_DISPLAY, kimi: KIMI_DISPLAY, pi: PI_DISPLAY, local: LOCAL_DISPLAY } as const;
13
+
14
+ const STREAM_TONES = { claude: "peerClaude", codex: "peerCodex" } as const;
15
+ /** Stream headers historically color only these speakers, independently of their board palette. */
16
+ export function nativeStreamTone(peer: string): (typeof STREAM_TONES)[keyof typeof STREAM_TONES] | undefined {
17
+ return Object.hasOwn(STREAM_TONES, peer) ? STREAM_TONES[peer as keyof typeof STREAM_TONES] : undefined;
18
+ }
@@ -0,0 +1,48 @@
1
+ import type { CreateContext, DaemonEngine, DaemonPorts } from "../registry-types.ts";
2
+ import type { PeerAdapter } from "../../hub/peers.ts";
3
+
4
+ // Load only fixed builtin modules after the pre-effect guards. A top-level LocalPeer import would
5
+ // cycle through hub-tools and the registry while its definitions are still being initialized.
6
+ export function createCodex(ctx: CreateContext): PeerAdapter {
7
+ if (!("codex" in ctx.daemon)) throw new Error("Codex requires its Codex transport options");
8
+ if (ctx.permission === "read-only") throw new Error("Codex has no verified read-only permission mapping");
9
+ const { CodexPeer } = require("../codex-appserver.ts") as typeof import("../codex-appserver.ts");
10
+ return new CodexPeer(ctx.instance, ctx.daemon.codex);
11
+ }
12
+
13
+ export function createKimi(ctx: CreateContext): PeerAdapter {
14
+ if (!("acp" in ctx.daemon)) throw new Error("Kimi requires its ACP transport options");
15
+ if (ctx.permission === "read-only") throw new Error("Kimi has no verified read-only permission mapping");
16
+ const { AcpPeer } = require("../acp.ts") as typeof import("../acp.ts");
17
+ return new AcpPeer(ctx.instance, ctx.daemon.acp);
18
+ }
19
+
20
+ export function createJcode(ctx: CreateContext): PeerAdapter {
21
+ if (!("jcode" in ctx.daemon)) throw new Error("Jcode requires its Jcode transport options");
22
+ if (ctx.permission === "read-only") throw new Error("Jcode has no verified read-only permission mapping");
23
+ const { JcodePeer } = require("../jcode.ts") as typeof import("../jcode.ts");
24
+ return new JcodePeer(ctx.daemon.jcode);
25
+ }
26
+
27
+ export function createPi(ctx: CreateContext): PeerAdapter {
28
+ if (!("pi" in ctx.daemon)) throw new Error("Pi requires its Pi transport options");
29
+ if (ctx.permission === "read-only") throw new Error("Pi has no verified read-only permission mapping");
30
+ const { PiPeer } = require("../pi.ts") as typeof import("../pi.ts");
31
+ return new PiPeer(ctx.instance, ctx.daemon.pi);
32
+ }
33
+
34
+ export function createLocal(ctx: CreateContext): PeerAdapter {
35
+ if (!("local" in ctx.daemon)) throw new Error("Local worker requires its local transport options");
36
+ if (ctx.permission === "read-only") throw new Error("Local worker has no verified read-only permission mapping");
37
+ const { LocalPeer } = require("../local-worker.ts") as typeof import("../local-worker.ts");
38
+ return new LocalPeer(ctx.instance, ctx.daemon.local);
39
+ }
40
+
41
+ /** Preserve existing transport option types and object identity; selection stays host-owned. */
42
+ export const DAEMON_PORTS = {
43
+ "codex-appserver": (options: Extract<DaemonPorts, { codex: unknown }>["codex"]) => ({ codex: options }),
44
+ acp: (options: Extract<DaemonPorts, { acp: unknown }>["acp"]) => ({ acp: options }),
45
+ "owned-acp": (options: Extract<DaemonPorts, { jcode: unknown }>["jcode"]) => ({ jcode: options }),
46
+ "pi-rpc": (options: Extract<DaemonPorts, { pi: unknown }>["pi"]) => ({ pi: options }),
47
+ "local-worker": (options: Extract<DaemonPorts, { local: unknown }>["local"]) => ({ local: options }),
48
+ } satisfies Record<DaemonEngine, unknown>;