@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.
- package/CHANGELOG.md +22 -0
- package/README.md +3 -3
- package/docs/agent-notes/adapters.md +9 -3
- package/docs/agent-notes/budget.md +7 -3
- package/docs/agent-notes/bus.md +2 -1
- package/docs/agent-notes/daemon.md +7 -3
- package/docs/agent-notes/local-worker.md +1 -1
- package/docs/agent-notes/tasks.md +6 -2
- package/docs/agent-notes/tests.md +2 -1
- package/docs/gsd-collaboration-analysis.md +5 -1
- package/docs/operations.md +80 -16
- package/docs/quickstart.md +10 -2
- package/docs/sdlc.md +59 -34
- package/docs/security.md +7 -4
- package/docs/smoke.md +1 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +407 -10
- package/docs/verification/2026-10-18-0.12.34.md +48 -0
- package/docs/verification/2026-10-18-0.12.35.md +49 -0
- package/docs/verification/2026-10-18-493-claude-busy-channel.md +91 -0
- package/docs/verified.json +35 -19
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +23567 -16606
- package/src/adapters/acp.ts +3 -1
- package/src/adapters/claude-channel.ts +59 -29
- package/src/adapters/codex-appserver.ts +82 -12
- 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 +34 -0
- package/src/adapters/definitions/native-cli-entry-jcode.ts +27 -0
- package/src/adapters/definitions/native-cli-entry-kimi.ts +57 -0
- package/src/adapters/definitions/native-cli-entry-local.ts +15 -0
- package/src/adapters/definitions/native-cli-entry-pi.ts +53 -0
- package/src/adapters/definitions/native-cli-entry-qwen.ts +21 -0
- package/src/adapters/definitions/native-cli-entry-types.ts +41 -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-native.ts +2 -2
- package/src/adapters/definitions/qwen.ts +8 -4
- package/src/adapters/local-worker.ts +3 -2
- package/src/adapters/pi.ts +3 -1
- package/src/adapters/registry-types.ts +100 -10
- package/src/adapters/registry.ts +127 -11
- package/src/cli/console-state.ts +5 -5
- package/src/cli/identity.ts +8 -7
- package/src/cli/jcode-mcp.ts +2 -58
- package/src/cli/launch.ts +41 -102
- package/src/cli/main.ts +126 -290
- package/src/cli/output.ts +3 -2
- package/src/cli/preview.ts +26 -126
- package/src/cli/recovery-guidance.ts +43 -22
- package/src/cli/setup.ts +2 -80
- package/src/cli/shell-quote.ts +3 -0
- package/src/cli/terminal-recovery.ts +81 -22
- package/src/cli/upgrade-interactive.ts +4 -2
- package/src/cli/upgrade-restore-state.ts +17 -7
- package/src/cli/upgrade-runtime.ts +126 -147
- package/src/cli/upgrade.ts +44 -27
- package/src/hub/budget.ts +59 -31
- package/src/hub/bus.ts +1 -1
- package/src/hub/conductor.ts +19 -9
- package/src/hub/config-apply.ts +22 -15
- package/src/hub/config-reload.ts +18 -3
- package/src/hub/config-trust.ts +1 -1
- package/src/hub/context-window.ts +3 -27
- package/src/hub/control-client.ts +1 -1
- package/src/hub/crash.ts +15 -39
- package/src/hub/daemon.ts +506 -341
- package/src/hub/execution-budget.ts +3 -2
- package/src/hub/hub-tools.ts +19 -8
- package/src/hub/instance-budget.ts +73 -0
- package/src/hub/instance-config.ts +47 -0
- package/src/hub/instance-execution.ts +50 -0
- package/src/hub/instances.ts +100 -0
- package/src/hub/lifecycle-schema.ts +7 -3
- package/src/hub/peer-catalog.ts +4 -13
- package/src/hub/permission-mode.ts +14 -17
- package/src/hub/recovery-continuity.ts +6 -5
- package/src/hub/report.ts +9 -8
- package/src/hub/roster.ts +9 -9
- package/src/hub/routing.ts +94 -19
- package/src/hub/sdlc-queue.ts +2 -2
- package/src/hub/sdlc.ts +84 -38
- package/src/hub/settings.ts +39 -10
- package/src/hub/settled-roles.ts +1 -1
- package/src/hub/snapshots.ts +40 -9
- package/src/hub/start-mode.ts +13 -8
- package/src/hub/status-roster.ts +8 -0
- package/src/hub/tasks.ts +60 -27
- package/src/hub/text-projection.ts +7 -3
- package/src/hub/tooling.ts +4 -60
- package/src/hub/ui.ts +3 -1
- package/src/hub/workflow.ts +30 -52
- package/src/memory/recall.ts +4 -5
- package/src/models/route/signals.ts +3 -1
- package/src/pi/launch.ts +5 -3
- package/src/ui/index.html +4 -3
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +16 -6
- package/templates/AGENTS.block.md +2 -1
- package/templates/routing.toml +2 -2
package/src/adapters/registry.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { builtinCliEntries } from "./definitions/native-cli-entry.ts";
|
|
2
|
+
import { builtinPreview } from "./definitions/native-cli-preview.ts";
|
|
3
|
+
import { DAEMON_PORTS } from "./definitions/native-factories.ts";
|
|
4
|
+
export { DAEMON_PORTS };
|
|
5
|
+
import { NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN } from "./definitions/builtin-metadata.ts";
|
|
2
6
|
import { QWEN_DEFINITION } from "./definitions/qwen.ts";
|
|
3
|
-
import type { AdapterDefinition } from "./registry-types.ts";
|
|
7
|
+
import type { AdapterDefinition, KindMap } from "./registry-types.ts";
|
|
8
|
+
import { types } from "node:util";
|
|
4
9
|
|
|
5
10
|
export type * from "./registry-types.ts";
|
|
6
11
|
export const ADAPTER_API = 1;
|
|
7
12
|
|
|
13
|
+
/** Ordered compatibility projections stay typed independently of definition implementation inference. */
|
|
14
|
+
export const KNOWN_PEERS = ["claude", "codex", "kimi", "pi", "local"] as const;
|
|
15
|
+
export const OPTIONAL_PEERS = ["qwen", "jcode"] as const;
|
|
16
|
+
export const SUPPORTED_PEERS = [...KNOWN_PEERS, ...OPTIONAL_PEERS] as const;
|
|
17
|
+
export const START_MODE_PEERS = ["pi", "codex", "kimi"] as const;
|
|
18
|
+
export const OFFICE_PEER_ORDER: readonly string[] = KNOWN_PEERS;
|
|
19
|
+
|
|
20
|
+
|
|
8
21
|
function immutable<T>(value: T, seen = new WeakSet<object>()): T {
|
|
9
22
|
if (!value || typeof value !== "object" || seen.has(value)) return value;
|
|
10
23
|
seen.add(value);
|
|
@@ -12,25 +25,128 @@ function immutable<T>(value: T, seen = new WeakSet<object>()): T {
|
|
|
12
25
|
return Object.freeze(value);
|
|
13
26
|
}
|
|
14
27
|
|
|
28
|
+
const fixedBuiltinDefinitions = new Set<AdapterDefinition>([...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN, QWEN_DEFINITION]);
|
|
29
|
+
|
|
15
30
|
/** Keep the native catalog's order, independently of the legacy default-role object order. */
|
|
16
31
|
export const ADAPTERS: readonly AdapterDefinition[] = immutable([
|
|
17
|
-
...
|
|
32
|
+
...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind !== "jcode"),
|
|
18
33
|
QWEN_DEFINITION,
|
|
19
|
-
...
|
|
34
|
+
...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind === "jcode"),
|
|
20
35
|
]);
|
|
21
36
|
const definitions = new Map<string, AdapterDefinition>();
|
|
22
37
|
for (const definition of ADAPTERS) {
|
|
38
|
+
assertDaemonEngine(definition);
|
|
39
|
+
assertNativeCapabilityScope(definition);
|
|
23
40
|
if (definition.api !== ADAPTER_API || definitions.has(definition.kind)) throw new Error("invalid builtin adapter registry");
|
|
24
41
|
definitions.set(definition.kind, definition);
|
|
25
42
|
}
|
|
26
43
|
|
|
27
|
-
/**
|
|
28
|
-
export
|
|
29
|
-
|
|
44
|
+
/** Native quota handoff platform names, distinct from per-delivery recall injection policy. */
|
|
45
|
+
export const QUOTA_HANDOFF_PLATFORMS: Readonly<Record<string, string>> = Object.freeze(Object.fromEntries(
|
|
46
|
+
ADAPTERS.flatMap(adapter => adapter.hooks?.recallPlatform ? [[adapter.kind, adapter.hooks.recallPlatform]] : []),
|
|
47
|
+
));
|
|
48
|
+
|
|
49
|
+
/** Declaration reads never execute caller accessors or inherit another project's mapping. */
|
|
50
|
+
function declaredKind(id: string, instances: KindMap | undefined): string | undefined {
|
|
51
|
+
if (!instances || typeof instances !== "object" || types.isProxy(instances) || Array.isArray(instances)
|
|
52
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(instances))) return undefined;
|
|
53
|
+
const field = Object.getOwnPropertyDescriptor(instances, id);
|
|
54
|
+
if (!field || !Object.hasOwn(field, "value") || !field.enumerable) return undefined;
|
|
55
|
+
const entry: unknown = field.value;
|
|
56
|
+
if (!entry || typeof entry !== "object" || types.isProxy(entry) || Array.isArray(entry)
|
|
57
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(entry))) return undefined;
|
|
58
|
+
const kind = Object.getOwnPropertyDescriptor(entry, "kind");
|
|
59
|
+
if (!kind || !Object.hasOwn(kind, "value") || !kind.enumerable || typeof kind.value !== "string" || !definitions.has(kind.value)) return undefined;
|
|
60
|
+
const prefix = `${kind.value}-`;
|
|
61
|
+
return /^[a-z][a-z0-9-]{0,31}$/.test(id) && id.startsWith(prefix) && /^[a-z0-9][a-z0-9-]*$/.test(id.slice(prefix.length)) ? kind.value : undefined;
|
|
62
|
+
}
|
|
63
|
+
/** Legacy ids retain their identity. Extra instances resolve only through the explicit project map. */
|
|
64
|
+
export function kindOf(id: string, instances?: KindMap): string | undefined {
|
|
65
|
+
return definitions.has(id) ? id : typeof id === "string" ? declaredKind(id, instances) : undefined;
|
|
66
|
+
}
|
|
67
|
+
export function definitionOf(id: string, instances?: KindMap): AdapterDefinition | undefined {
|
|
68
|
+
const kind = kindOf(id, instances);
|
|
69
|
+
return kind === undefined ? undefined : definitions.get(kind);
|
|
30
70
|
}
|
|
31
|
-
export function
|
|
32
|
-
return
|
|
71
|
+
export function familyOf(id: string, instances?: KindMap): string | undefined {
|
|
72
|
+
return definitionOf(id, instances)?.family;
|
|
33
73
|
}
|
|
34
|
-
|
|
35
|
-
|
|
74
|
+
|
|
75
|
+
/** Compatibility projection order is observable in default role maps and planner selection. */
|
|
76
|
+
const ROLE_ORDER = ["claude", "codex", "kimi", "local", "pi", "qwen", "jcode"] as const;
|
|
77
|
+
export const PLANNER_PEERS = ["claude", "codex", "kimi", "qwen", "jcode"] as const;
|
|
78
|
+
|
|
79
|
+
/** Callers own their role arrays, independent of immutable native policy. */
|
|
80
|
+
export function defaultPeerRoles(includeOptional = false): Record<string, string[]> {
|
|
81
|
+
return Object.fromEntries(ROLE_ORDER.filter(peer => includeOptional || !definitionOf(peer)!.optional)
|
|
82
|
+
.map(peer => [peer, [...definitionOf(peer)!.roles]]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Preserve the tool schema's legacy preferred order without spreading native names into tools. */
|
|
86
|
+
const CONDUCTOR_START_PRIORITY = ["local", "kimi", "pi"] as const;
|
|
87
|
+
export const CONDUCTOR_START_PEERS = [
|
|
88
|
+
...CONDUCTOR_START_PRIORITY,
|
|
89
|
+
...ADAPTERS.filter(adapter => !(CONDUCTOR_START_PRIORITY as readonly string[]).includes(adapter.kind)).map(adapter => adapter.kind),
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/** A missing definition retains external-id admission; Pi's legacy enabled switch controls its default only. */
|
|
93
|
+
export function defaultRosterEnabled(peer: string, piEnabled: boolean): boolean {
|
|
94
|
+
const definition = definitionOf(peer);
|
|
95
|
+
if (!definition) return true;
|
|
96
|
+
return definition.kind === "pi" ? piEnabled : definition.defaultOnRoster ?? false;
|
|
36
97
|
}
|
|
98
|
+
|
|
99
|
+
/** A factory must name one existing engine; metadata never creates a caller-controlled port. */
|
|
100
|
+
export function assertDaemonEngine(definition: Pick<AdapterDefinition, "start">): void {
|
|
101
|
+
const engine = definition.start.daemonPort;
|
|
102
|
+
if (definition.start.create && engine === undefined || engine !== undefined && !Object.hasOwn(DAEMON_PORTS, engine)) throw new Error("invalid adapter daemon engine");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Host-global native quirks are not capabilities a config-only or external definition may opt into. */
|
|
106
|
+
export function assertNativeCapabilityScope(definition: AdapterDefinition): void {
|
|
107
|
+
if (fixedBuiltinDefinitions.has(definition)) return;
|
|
108
|
+
const declared = (block: object | undefined, key: string): boolean => block !== undefined && key in block;
|
|
109
|
+
const restricted: [string, boolean][] = [
|
|
110
|
+
["start.acpCommand", declared(definition.start, "acpCommand")],
|
|
111
|
+
["attach.admission", declared(definition.attach, "admission")],
|
|
112
|
+
["budget.resetSoftTokensOnResume", declared(definition.budget, "resetSoftTokensOnResume")],
|
|
113
|
+
["session.crashIdentity", declared(definition.session, "crashIdentity")],
|
|
114
|
+
["start.daemonPort pi-rpc", definition.start.daemonPort === "pi-rpc"],
|
|
115
|
+
["routing", definition.routing !== undefined],
|
|
116
|
+
];
|
|
117
|
+
const field = restricted.find(([, present]) => present)?.[0];
|
|
118
|
+
if (field) throw new Error(`${field} is restricted to fixed builtin adapter definitions`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Ordered catalog projections consumed by generic core hosts. */
|
|
122
|
+
export const STARTABLE_PEERS = SUPPORTED_PEERS;
|
|
123
|
+
export type SupportedPeer = typeof SUPPORTED_PEERS[number];
|
|
124
|
+
export function isSupportedPeer(peer: unknown): peer is SupportedPeer { return typeof peer === "string" && definitionOf(peer) !== undefined; }
|
|
125
|
+
export function isStartablePeer(peer: unknown): peer is SupportedPeer { return typeof peer === "string" && definitionOf(peer)?.start.conductorStartable === true; }
|
|
126
|
+
|
|
127
|
+
/** Body-free browser metadata, derived from the same native participation and managed-tool contracts. */
|
|
128
|
+
export const ADAPTER_DISPLAY = Object.fromEntries(ADAPTERS.map(adapter => [adapter.kind, {
|
|
129
|
+
pullOnlyRead: adapter.attach?.pullOnlyForms?.includes("tui") === true,
|
|
130
|
+
terminalApproval: adapter.trust.hubSandboxed,
|
|
131
|
+
}]));
|
|
132
|
+
|
|
133
|
+
export { NATIVE_CONFIG_BLOCKS, nativeConfigurationDefaults, mergeNativeConfiguration, retainNativeRouting } from "./definitions/native-config-defaults.ts";
|
|
134
|
+
|
|
135
|
+
/** Private fixed-builtin preview implementation; not an adapter extension or configurable callback. */
|
|
136
|
+
export function builtinPreviewOf(id: string) {
|
|
137
|
+
const definition = definitionOf(id);
|
|
138
|
+
return definition ? builtinPreview(definition) : undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Fixed builtin doctor metadata and pure observed-value projections; the CLI retains all probe effects. */
|
|
142
|
+
export { DOCTOR_TOOL_PROBES, DOCTOR_MEMORY_CAPTURE_PEERS, DOCTOR_PLUGIN_PROBE, nativeDoctorCheck, nativeDoctorRecoveryIdentity, nativeDoctorOptionalProbes } from "./definitions/native-cli-doctor.ts";
|
|
143
|
+
|
|
144
|
+
/** Internal fixed builtin CLI entries; public AdapterDefinition and NativeHooks expose no execution extension. */
|
|
145
|
+
export const BUILTIN_CLI_ENTRIES = Object.freeze(builtinCliEntries.map(entry => {
|
|
146
|
+
const definition = definitionOf(entry.command);
|
|
147
|
+
if (!definition) throw new Error("builtin CLI entry has no adapter definition");
|
|
148
|
+
return Object.freeze({ ...entry, definition });
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
/** Private fixed-builtin stream presentation; does not widen the public palette object. */
|
|
152
|
+
export { nativeStreamTone } from "./definitions/native-display.ts";
|
package/src/cli/console-state.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { consoleNeeds, characterAttentionLine, attentionCommand } from "./attent
|
|
|
2
2
|
import { CHARACTERS, petFrame, petSize, petFooterRows, type CharacterId, type PetSize, type PetPose } from "../pet/frame.ts";
|
|
3
3
|
import { PEER_PALETTE } from "../ui/peer-palette.ts";
|
|
4
4
|
import { PERMISSION_MODES, permissionBoundary } from "../hub/permission-mode.ts";
|
|
5
|
-
import { PLANNER_PEERS } from "../
|
|
5
|
+
import { PLANNER_PEERS, definitionOf, nativeStreamTone } from "../adapters/registry.ts";
|
|
6
6
|
import { taskProgress } from "../ui/task-progress.ts";
|
|
7
7
|
import type { ProgressStage } from "../ui/task-progress.ts";
|
|
8
8
|
import { sanitize } from "../hub/envelope.ts";
|
|
@@ -114,7 +114,6 @@ export function paint(line: Span[], color: boolean): string {
|
|
|
114
114
|
export function streamTokens(value: string, eventTone?: Tone, kind?: string): Span[] {
|
|
115
115
|
const text = terminalText(value);
|
|
116
116
|
if (kind === "command" || kind === "console") return [{ text }];
|
|
117
|
-
const peers: Readonly<Record<string, Tone>> = { claude: "peerClaude", codex: "peerCodex" };
|
|
118
117
|
// Hub-written peer slots only; words such as local/pi/hub in a title do not identify a speaker.
|
|
119
118
|
const peerSlots = new Set<number>();
|
|
120
119
|
const leading = text.match(/^\s*(?:[.?*!]\s+)?([A-Za-z_][A-Za-z_0-9-]*)(?=:| is | asks permission:| permission )/);
|
|
@@ -143,6 +142,7 @@ export function streamTokens(value: string, eventTone?: Tone, kind?: string): Sp
|
|
|
143
142
|
const gap = text.slice(end, start);
|
|
144
143
|
if (gap) out.push({ text: gap });
|
|
145
144
|
let tone: Tone | undefined;
|
|
145
|
+
const peerTone = nativeStreamTone(token);
|
|
146
146
|
// ponytail: legacy notices have no typed reference ranges. Classify task/review prefixes textually;
|
|
147
147
|
// assign/which #N remains an issue reference until notices carry structured reference spans.
|
|
148
148
|
if (token.startsWith("#")) {
|
|
@@ -152,7 +152,7 @@ export function streamTokens(value: string, eventTone?: Tone, kind?: string): Sp
|
|
|
152
152
|
}
|
|
153
153
|
else if (start === taskKeywordAt) tone = "taskKeyword";
|
|
154
154
|
else if (/^[0-9]/.test(token) && !permissionIds.has(start)) tone = "number";
|
|
155
|
-
else if (peerSlots.has(start) &&
|
|
155
|
+
else if (peerSlots.has(start) && peerTone) tone = peerTone;
|
|
156
156
|
else if (start === markerAt || start === stateAt || (route && token === "->" && start < route[0].length)) tone = eventTone;
|
|
157
157
|
out.push({ text: token, ...(tone ? { tone } : {}) });
|
|
158
158
|
previousToken = token; previousStart = start; end = start + token.length;
|
|
@@ -749,7 +749,7 @@ export function cells(s: ConsoleState, row: any, selected: boolean, now: number,
|
|
|
749
749
|
: row.paused === "manual" ? "user" : row.paused ? String(row.paused).split(":")[0] : "-";
|
|
750
750
|
return [id(row.planner ? `${row.id} (planner)` : row.id, row.state === "offline" ? undefined : "info"), span(row.rostered === false ? `${row.state} off roster` : row.state, stateTone(row.state)),
|
|
751
751
|
...(showPermissionModes(s) ? [span(row.permissionMode && row.permissionMode !== "ask" ? row.permissionMode : "-", row.permissionMode === "never-ask" ? "attention" : undefined)] : []),
|
|
752
|
-
span(row.pullOnly && row.id
|
|
752
|
+
span(row.pullOnly && definitionOf(row.id)?.attach?.pullOnlyForms?.includes("tui") ? "pull-only" : row.toolsOnly ? "tools-only" : row.attached === false ? "detached" : "attached", row.toolsOnly ? "attention" : undefined),
|
|
753
753
|
span(count(row.queued)), span(count(row.queuedImportant), row.queuedImportant ? "attention" : undefined),
|
|
754
754
|
span(count(row.needsReview), row.needsReview ? "failure" : undefined), span(paused, paused === "-" ? undefined : "attention"),
|
|
755
755
|
span(b?.windows?.length ? b.windows.map((w: any) => `${w.id} ${Math.round(w.used * 100)}%${typeof w.resetsAt === "number" ? ` ${relative(w.resetsAt, now)}` : ""}`).join(", ") : "-"),
|
|
@@ -890,7 +890,7 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
890
890
|
if (p.permissionMode && p.permissionMode !== "ask") summary.push(span(` ${p.permissionMode}`, "attention"));
|
|
891
891
|
if (p.needsReview) summary.push(span(` review${p.needsReview}`, "failure"));
|
|
892
892
|
if (p.paused) summary.push(span(" paused", "attention"));
|
|
893
|
-
if (p.toolsOnly) summary.push(span(id
|
|
893
|
+
if (p.toolsOnly) summary.push(span(definitionOf(id)?.attach?.pullOnlyForms?.includes("tui") ? ` pull-only, read ${typeof p.lastInboxAt === "number" ? relative(p.lastInboxAt, now) : "never"}` : " tools-only: ahub claude", "attention"));
|
|
894
894
|
}
|
|
895
895
|
if (s.pendingRestart?.length) summary.push(span(`${summary.length ? " | " : ""}restart pending: ${s.pendingRestart.map(row => row.key).join(", ")}`, "attention"));
|
|
896
896
|
const quotas = Object.entries(s.budget).map(([id, b]) => `${id}:${b.windows?.map((w: any) => `${w.id} ${Math.round(w.used * 100)}%`).join("/") ?? "?"}`).join(" ");
|
package/src/cli/identity.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { recoveryPresentationFlag } from "./recovery-presentation-options.ts";
|
|
2
|
-
import {
|
|
2
|
+
import { definitionOf, kindOf, type KindMap } from "../adapters/registry.ts";
|
|
3
3
|
|
|
4
4
|
/** Recovery reads this declaration before delegating to another package's CLI. */
|
|
5
5
|
export const LIVE_TEST_SUPPORTED = true;
|
|
@@ -29,7 +29,7 @@ export type CliIdentity =
|
|
|
29
29
|
| { role: "invalid"; reason: string };
|
|
30
30
|
|
|
31
31
|
/** Honest defaults for native shells, not authentication against an agent that can edit its environment. */
|
|
32
|
-
export function detectCliIdentity(env: Readonly<Record<string, string | undefined
|
|
32
|
+
export function detectCliIdentity(env: Readonly<Record<string, string | undefined>>, instances?: KindMap): CliIdentity {
|
|
33
33
|
const hub = env.AGENTHUB_PEER_ID;
|
|
34
34
|
const claude = env.CLAUDECODE;
|
|
35
35
|
const session = env.CLAUDE_CODE_SESSION_ID;
|
|
@@ -42,7 +42,7 @@ export function detectCliIdentity(env: Readonly<Record<string, string | undefine
|
|
|
42
42
|
if (session !== undefined && claude === undefined) return invalid("CLAUDE_CODE_SESSION_ID without CLAUDECODE marker");
|
|
43
43
|
if (claude !== undefined && codex !== undefined) return invalid("conflicting native agent markers");
|
|
44
44
|
const native = claude !== undefined ? "claude" : codex !== undefined ? "codex" : undefined;
|
|
45
|
-
if (hub !== undefined && native !== undefined && hub !== native) return invalid("AGENTHUB_PEER_ID conflicts with native agent marker");
|
|
45
|
+
if (hub !== undefined && native !== undefined && (kindOf(hub, instances) ?? hub) !== native) return invalid("AGENTHUB_PEER_ID conflicts with native agent marker");
|
|
46
46
|
const peer = hub ?? native;
|
|
47
47
|
return peer === undefined ? { role: "console" } : { role: "tools", peer };
|
|
48
48
|
}
|
|
@@ -52,7 +52,8 @@ export type PeerCommandAccess = "allowed" | "conductor" | "console";
|
|
|
52
52
|
/** A closed list: unknown commands cannot acquire console authority through an agent shell. */
|
|
53
53
|
export function classifyPeerCommand(command: string, args: readonly string[]): PeerCommandAccess {
|
|
54
54
|
if (command === "recovery" && recoveryAction(args) === "reattach") return "console";
|
|
55
|
-
|
|
55
|
+
const native = definitionOf(command);
|
|
56
|
+
if (native?.cli.personOnlyFlags?.some(flag => args.includes(flag))) return "console";
|
|
56
57
|
if (command === "pet" && args.includes("--window")) return "console";
|
|
57
58
|
if (command === "permission" || command === "stop" || command === "watch") return "console";
|
|
58
59
|
if (args.some((arg) => arg === "--as-user" || arg.startsWith("--as-user="))) return "console";
|
|
@@ -72,8 +73,8 @@ export function classifyPeerCommand(command: string, args: readonly string[]): P
|
|
|
72
73
|
// The subcommand is the first argument that is not --json, which the command accepts anywhere.
|
|
73
74
|
if (command === "bench") return args.find((arg) => arg !== "--json") === "run" ? "console" : "allowed";
|
|
74
75
|
if (command === "budget") return !args.length || args.every((arg) => arg === "--json" || arg === "--full" || arg.startsWith("--color=")) ? "allowed" : "console";
|
|
75
|
-
if (
|
|
76
|
-
if (
|
|
76
|
+
if (native?.cli.authority === "person") return "console";
|
|
77
|
+
if (native || ["pause", "resume"].includes(command)) return "conductor";
|
|
77
78
|
if (command === "board" && args[0] === "import") return "console";
|
|
78
79
|
if (["help", "version", "--version", "say", "status", "pet", "board", "report", "turns", "review", "remember", "facts", "check-path"].includes(command)) return "allowed";
|
|
79
80
|
return "console";
|
|
@@ -84,7 +85,7 @@ export function cliCommandLabel(command: string, args: readonly string[]): strin
|
|
|
84
85
|
if (command === "backlog") return ["list", "add", "pull", "pull-ready", "split", "return", "plan", "pin", "undo"].includes(args[0] ?? "") ? `backlog ${args[0]}` : "backlog";
|
|
85
86
|
if (command === "board" && args[0] === "import") return "board import";
|
|
86
87
|
if (command === "release") return ["new", "list", "show", "plan", "activate", "freeze", "ship", "drop"].includes(args[0] ?? "") ? `release ${args[0]}` : "release";
|
|
87
|
-
if (!["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "
|
|
88
|
+
if (!definitionOf(command) && !["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "stop", "models", "say", "queue", "tail", "watch", "budget", "board", "task", "review", "remember", "ask", "route", "pause", "resume", "permit", "permission", "settings", "roster", "status", "pet", "logs", "export", "report", "research", "memory", "goal", "bench", "sdlc", "role", "facts", "check-path", "turns", "undo", "kill", "reset", "doctor"].includes(command)) return "unknown";
|
|
88
89
|
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench"].includes(command)) {
|
|
89
90
|
const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
|
|
90
91
|
if (sub && ["propose", "show", "assign", "unassign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "reattach", "setup", "start", "stop", "remove", "export", "backfill", "addons", "run", "report", "compare", "search", "graph", "promote", "demote", "forget", "supersede", "dedupe", "decay", "rebuild", "sync", "machine", "import", "suggestions", "confirm", "reject", "new", "link", "state", "done", "take"].includes(sub)) return `${command} ${sub}`;
|
package/src/cli/jcode-mcp.ts
CHANGED
|
@@ -1,58 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
}
|
|
1
|
+
/** Compatibility exports; the project MCP layer is native adapter policy. */
|
|
2
|
+
export { JCODE_MCP_FILE, JCODE_MCP_NAME, jcodeMcpEntry, jcodeMcpReady, configureJcodeMcp } from "../adapters/definitions/native-cli-jcode-mcp.ts";
|
package/src/cli/launch.ts
CHANGED
|
@@ -6,38 +6,46 @@ 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 type { InstancesConfig } from "../hub/instances.ts";
|
|
11
|
+
import { kimiAcpCommand } from "../adapters/definitions/native-daemon-kimi.ts";
|
|
9
12
|
export const CLAUDE_CHANNEL = "plugin:agent-hub@agent-hub";
|
|
10
13
|
|
|
14
|
+
/** Consume only ahub's selector, never native arguments after the passthrough delimiter. */
|
|
15
|
+
export function instanceLaunchArguments(command: string, raw: readonly string[], instances?: InstancesConfig): { instance?: string; args: string[] } {
|
|
16
|
+
if (!definitionOf(command)) return { args: [...raw] };
|
|
17
|
+
const args: string[] = [];
|
|
18
|
+
let instance: string | undefined;
|
|
19
|
+
for (let index = 0; index < raw.length; index++) {
|
|
20
|
+
const arg = raw[index]!;
|
|
21
|
+
if (arg === "--") { args.push(...raw.slice(index)); break; }
|
|
22
|
+
if (arg !== "--as" && !arg.startsWith("--as=")) { args.push(arg); continue; }
|
|
23
|
+
if (instance !== undefined) throw new Error("--as may appear only once");
|
|
24
|
+
const id = arg === "--as" ? raw[++index] : arg.slice("--as=".length);
|
|
25
|
+
if (!id || id.startsWith("--")) throw new Error("--as needs a declared instance id");
|
|
26
|
+
const declaration = instances && Object.hasOwn(instances, id) ? instances[id] : undefined;
|
|
27
|
+
if (!declaration || declaration.kind !== command || !definitionOf(id, instances)) throw new Error("--as must name a declared instance of this command's native kind");
|
|
28
|
+
instance = id;
|
|
29
|
+
}
|
|
30
|
+
return { ...(instance ? { instance } : {}), args };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Only an authenticated daemon hello can authorize extra-instance effects. */
|
|
34
|
+
export function requirePeerInstances(features: unknown): void {
|
|
35
|
+
if (!Array.isArray(features) || !features.includes("peer_instances")) throw new Error("running hub lacks peer_instances; upgrade it before starting an extra instance");
|
|
36
|
+
}
|
|
37
|
+
|
|
11
38
|
/** Launch identity belongs to the native child, never to the agent that invoked the wrapper. */
|
|
12
|
-
export function nativeLaunchEnv(tool:
|
|
39
|
+
export function nativeLaunchEnv(tool: string, source: NodeJS.ProcessEnv, instances?: InstancesConfig): NodeJS.ProcessEnv {
|
|
13
40
|
const env = peerChildEnv(tool, source);
|
|
14
41
|
delete env.AGENTHUB_UNATTENDED;
|
|
15
42
|
// Channel evidence for the plugin (issue #205): set beside the development-channel flag `buildLaunch` always passes.
|
|
16
|
-
|
|
17
|
-
if (
|
|
43
|
+
const definition = definitionOf(tool, instances);
|
|
44
|
+
if (definition?.identity.mcp === "channel-plugin") env.AGENTHUB_CHANNEL = "1";
|
|
45
|
+
if (definition?.attach?.pullOnlyForms?.includes("tui")) delete env.AGENTHUB_MODE; // its MCP child must attach pull-only, never as a tools socket
|
|
18
46
|
return env;
|
|
19
47
|
}
|
|
20
48
|
|
|
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
49
|
export const UNATTENDED_WARNING =
|
|
42
50
|
"WARNING: --unattended disables permission prompts. Other agents' messages are untrusted input and this agent will act on its own judgment without asking you.";
|
|
43
51
|
|
|
@@ -306,91 +314,22 @@ export function sessionSettings(tee?: StatusLineTee, facts?: FactsHook): string
|
|
|
306
314
|
export function buildLaunch(
|
|
307
315
|
tool: "claude" | "codex",
|
|
308
316
|
userArgs: string[],
|
|
309
|
-
ctx: { unattended: boolean; proxyUrl?: string; codexBin?: string; statusLine?: StatusLineTee; facts?: FactsHook; preview?: boolean },
|
|
317
|
+
ctx: { instance?: string; unattended: boolean; proxyUrl?: string; codexBin?: string; statusLine?: StatusLineTee; facts?: FactsHook; preview?: boolean },
|
|
310
318
|
): 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
|
-
};
|
|
319
|
+
const adapter = definitionOf(tool);
|
|
320
|
+
if (!adapter) throw new Error("native launch definition unavailable");
|
|
321
|
+
const { metadata: _metadata, ...launch } = adapter.start.launch({
|
|
322
|
+
instance: ctx.instance ?? tool, form: "tui", projectRoot: process.cwd(), stateDir: ctx.facts?.stateDir ?? ctx.statusLine?.stateDir ?? "",
|
|
323
|
+
entrypoint: resolve(import.meta.dir, "main.js"), userArgs, env: {}, ...ctx,
|
|
324
|
+
});
|
|
325
|
+
return launch;
|
|
387
326
|
}
|
|
388
327
|
|
|
389
328
|
|
|
390
329
|
/** The daemon and preview use exactly the same ACP command insertion. */
|
|
391
330
|
export function buildKimiLaunch(command: string[], model?: string): Launch {
|
|
392
|
-
const [cmd, ...args] = command;
|
|
393
|
-
return { cmd: cmd!, args
|
|
331
|
+
const [cmd, ...args] = kimiAcpCommand(command, model);
|
|
332
|
+
return { cmd: cmd!, args };
|
|
394
333
|
}
|
|
395
334
|
|
|
396
335
|
export { buildQwenLaunch } from "../adapters/definitions/qwen-native.ts";
|