@kal-elsam/kairo-runtime 0.1.4 → 0.2.0

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.
@@ -0,0 +1,54 @@
1
+ import cursorManaged from "../adapters/cursor.js";
2
+ import codexManaged from "../adapters/codex.js";
3
+ import opencodeManaged from "../adapters/opencode.js";
4
+ import claudeManaged from "../adapters/claude.js";
5
+ import { createAgentCapabilityAdapter } from "./create-capability-adapter.js";
6
+
7
+ const cursor = createAgentCapabilityAdapter({
8
+ id: "cursor",
9
+ label: "Cursor",
10
+ managedAdapter: cursorManaged,
11
+ executable: "cursor",
12
+ opaqueAuth: true
13
+ });
14
+
15
+ const codex = createAgentCapabilityAdapter({
16
+ id: "codex",
17
+ label: "Codex",
18
+ managedAdapter: codexManaged,
19
+ executable: "codex",
20
+ runExecutable: "codex"
21
+ });
22
+
23
+ const opencode = createAgentCapabilityAdapter({
24
+ id: "opencode",
25
+ label: "OpenCode",
26
+ managedAdapter: opencodeManaged,
27
+ executable: "opencode",
28
+ runExecutable: "opencode"
29
+ });
30
+
31
+ const claude = createAgentCapabilityAdapter({
32
+ id: "claude",
33
+ label: "Claude Code",
34
+ managedAdapter: claudeManaged,
35
+ executable: "claude",
36
+ authArgs: ["auth", "status"],
37
+ runExecutable: "claude"
38
+ });
39
+
40
+ const CAPABILITY_ADAPTERS = [cursor, codex, opencode, claude];
41
+
42
+ export const AGENT_CAPABILITY_IDS = CAPABILITY_ADAPTERS.map((adapter) => adapter.id);
43
+
44
+ export function listCapabilityAdapters() {
45
+ return [...CAPABILITY_ADAPTERS];
46
+ }
47
+
48
+ export function resolveCapabilityAdapter(id) {
49
+ const adapter = CAPABILITY_ADAPTERS.find((candidate) => candidate.id === id);
50
+ if (!adapter) {
51
+ throw new Error(`Unknown agent capability "${id}". Use ${AGENT_CAPABILITY_IDS.join(", ")}.`);
52
+ }
53
+ return adapter;
54
+ }
@@ -51,9 +51,9 @@ export function formatCliCommand(subcommand, cliName = PREFERRED_CLI) {
51
51
  return trimmed ? `${cliName} ${trimmed}` : cliName;
52
52
  }
53
53
 
54
- function detectInvocationPackageManager() {
55
- const execPath = process.env.npm_execpath ?? "";
56
- const userAgent = process.env.npm_config_user_agent ?? "";
54
+ function detectInvocationPackageManager(env = process.env) {
55
+ const execPath = env.npm_execpath ?? "";
56
+ const userAgent = env.npm_config_user_agent ?? "";
57
57
 
58
58
  if (execPath.includes("pnpm") || userAgent.startsWith("pnpm/")) return "pnpm";
59
59
  if (execPath.includes("yarn") || userAgent.startsWith("yarn/")) return "yarn";
@@ -61,7 +61,11 @@ function detectInvocationPackageManager() {
61
61
  return "npm";
62
62
  }
63
63
 
64
- export function resolveSuggestedInvocation(packageName = PACKAGE_NAME, argv = process.argv) {
64
+ export function resolveSuggestedInvocation(
65
+ packageName = PACKAGE_NAME,
66
+ argv = process.argv,
67
+ { env = process.env } = {}
68
+ ) {
65
69
  const invokedPath = argv[1] ?? PREFERRED_CLI;
66
70
  const invokedBase = basename(invokedPath);
67
71
 
@@ -69,7 +73,7 @@ export function resolveSuggestedInvocation(packageName = PACKAGE_NAME, argv = pr
69
73
  return invokedBase;
70
74
  }
71
75
 
72
- const packageManager = detectInvocationPackageManager();
76
+ const packageManager = detectInvocationPackageManager(env);
73
77
 
74
78
  switch (packageManager) {
75
79
  case "pnpm":
@@ -85,9 +89,9 @@ export function resolveSuggestedInvocation(packageName = PACKAGE_NAME, argv = pr
85
89
 
86
90
  export function formatSuggestedCliCommand(
87
91
  subcommand,
88
- { packageName = PACKAGE_NAME, argv = process.argv, suggestedInvocation } = {}
92
+ { packageName = PACKAGE_NAME, argv = process.argv, suggestedInvocation, env = process.env } = {}
89
93
  ) {
90
- const invoke = suggestedInvocation ?? resolveSuggestedInvocation(packageName, argv);
94
+ const invoke = suggestedInvocation ?? resolveSuggestedInvocation(packageName, argv, { env });
91
95
  const trimmed = subcommand.trim();
92
96
  return trimmed ? `${invoke} ${trimmed}` : invoke;
93
97
  }
@@ -0,0 +1,80 @@
1
+ import { buildAdapterContext } from "./adapter-context.js";
2
+ import { CAPABILITY_STATES } from "./capability-states.js";
3
+ import {
4
+ AGENT_CAPABILITY_IDS,
5
+ listCapabilityAdapters,
6
+ resolveCapabilityAdapter
7
+ } from "./agent-capabilities/index.js";
8
+
9
+ export async function inspectAllCapabilities({
10
+ homeDir,
11
+ workspaceRoot = process.cwd(),
12
+ packageName = "@kal-elsam/kairo-runtime",
13
+ probeOverrides = null
14
+ } = {}) {
15
+ const context = buildAdapterContext({ homeDir, workspaceRoot, packageName });
16
+ const adapters = listCapabilityAdapters();
17
+
18
+ return Promise.all(adapters.map(async (adapter) => {
19
+ const probeOptions = probeOverrides?.[adapter.id] ?? {};
20
+ const inspection = adapter.inspect(context, probeOptions);
21
+ const models = adapter.listModels?.(context, probeOptions) ?? null;
22
+
23
+ return {
24
+ ...inspection,
25
+ models
26
+ };
27
+ }));
28
+ }
29
+
30
+ export async function inspectCapability(agentId, context, probeOptions = {}) {
31
+ const adapter = resolveCapabilityAdapter(agentId);
32
+ const inspection = adapter.inspect(context, probeOptions);
33
+ const models = adapter.listModels?.(context, probeOptions) ?? null;
34
+ return { ...inspection, models };
35
+ }
36
+
37
+ export function summarizeCapabilityRegistry(capabilities) {
38
+ const byState = {};
39
+ for (const state of Object.values(CAPABILITY_STATES)) {
40
+ byState[state] = 0;
41
+ }
42
+
43
+ for (const capability of capabilities) {
44
+ if (byState[capability.state] != null) {
45
+ byState[capability.state] += 1;
46
+ }
47
+ }
48
+
49
+ return {
50
+ total: capabilities.length,
51
+ supported: AGENT_CAPABILITY_IDS.length,
52
+ detected: capabilities.filter((entry) => entry.detected).length,
53
+ available: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.AVAILABLE).length,
54
+ byState
55
+ };
56
+ }
57
+
58
+ export function buildCapabilityDiagnostics(capabilities) {
59
+ const recommendations = capabilities
60
+ .map((entry) => entry.recommendation)
61
+ .filter(Boolean);
62
+
63
+ const errors = capabilities
64
+ .filter((entry) => entry.state === CAPABILITY_STATES.ERROR)
65
+ .map((entry) => ({
66
+ agent: entry.id,
67
+ message: entry.error ?? entry.recommendation ?? "Inspection failed."
68
+ }));
69
+
70
+ return {
71
+ recommendations,
72
+ errors,
73
+ hasActionableErrors: errors.length > 0
74
+ };
75
+ }
76
+
77
+ export function delegateToAgent(agentId, context, runOptions = {}) {
78
+ const adapter = resolveCapabilityAdapter(agentId);
79
+ return adapter.run(context, runOptions);
80
+ }
@@ -0,0 +1,30 @@
1
+ export const CAPABILITY_STATES = {
2
+ DETECTED: "detected",
3
+ AUTHENTICATED: "authenticated",
4
+ AVAILABLE: "available",
5
+ UNKNOWN: "unknown",
6
+ ERROR: "error"
7
+ };
8
+
9
+ export function isCapabilityState(value) {
10
+ return Object.values(CAPABILITY_STATES).includes(value);
11
+ }
12
+
13
+ export function formatCapabilityState(state) {
14
+ switch (state) {
15
+ case CAPABILITY_STATES.DETECTED:
16
+ return "detected";
17
+ case CAPABILITY_STATES.AUTHENTICATED:
18
+ return "authenticated";
19
+ case CAPABILITY_STATES.AVAILABLE:
20
+ return "available";
21
+ case CAPABILITY_STATES.UNKNOWN:
22
+ return "unknown";
23
+ case CAPABILITY_STATES.ERROR:
24
+ return "error";
25
+ default: {
26
+ const _exhaustive = state;
27
+ return String(_exhaustive);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,47 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ const DEFAULT_TIMEOUT_MS = 5000;
4
+
5
+ export function isExecutableAvailable(command, { env = process.env } = {}) {
6
+ const result = spawnSync("which", [command], { encoding: "utf8", env });
7
+ return result.status === 0 && result.stdout.trim().length > 0;
8
+ }
9
+
10
+ export function probeCommand(command, args = [], {
11
+ cwd = process.cwd(),
12
+ env = process.env,
13
+ timeoutMs = DEFAULT_TIMEOUT_MS
14
+ } = {}) {
15
+ const result = spawnSync(command, args, {
16
+ cwd,
17
+ env,
18
+ encoding: "utf8",
19
+ timeout: timeoutMs
20
+ });
21
+
22
+ return {
23
+ ok: result.status === 0,
24
+ status: result.status,
25
+ stdout: (result.stdout ?? "").trim(),
26
+ stderr: (result.stderr ?? "").trim(),
27
+ error: result.error?.message ?? null,
28
+ timedOut: result.error?.code === "ETIMEDOUT"
29
+ };
30
+ }
31
+
32
+ export function parseVersionFromOutput(output) {
33
+ if (!output) return null;
34
+
35
+ const match = output.match(/(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
36
+ return match?.[1] ?? null;
37
+ }
38
+
39
+ export function resolveProbeState({ detected, cliAvailable, authReady, probeError, opaque = false }) {
40
+ if (probeError) return "error";
41
+ if (opaque) return "unknown";
42
+ if (!detected && !cliAvailable) return "unknown";
43
+ if (cliAvailable && authReady) return "available";
44
+ if (authReady) return "authenticated";
45
+ if (detected || cliAvailable) return "detected";
46
+ return "unknown";
47
+ }
@@ -0,0 +1,224 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Box, Text, useApp, useInput } from "ink";
3
+ import { BRAND } from "../brand/index.js";
4
+ import { PLAN_ACTIONS, buildActionPlan, buildReadOnlyDiagnostics } from "../action-planner.js";
5
+ import { buildProfileJson, resolveProfile } from "../profile.js";
6
+ import {
7
+ ORCHESTRATOR_MENU,
8
+ ORCHESTRATOR_VIEWS,
9
+ formatAgentStatusLines,
10
+ formatIntelligenceLines,
11
+ formatPlanLines,
12
+ formatProfileLines
13
+ } from "./orchestrator-state.js";
14
+
15
+ const COLORS = {
16
+ accent: "cyan",
17
+ success: "green",
18
+ warning: "yellow",
19
+ danger: "red",
20
+ muted: "gray"
21
+ };
22
+
23
+ export function OrchestratorApp({
24
+ homeDir,
25
+ workspaceRoot,
26
+ packageName,
27
+ packageRoot,
28
+ cliVersion,
29
+ onComplete
30
+ }) {
31
+ const { exit } = useApp();
32
+ const [view, setView] = useState(ORCHESTRATOR_VIEWS.HOME);
33
+ const [menuIndex, setMenuIndex] = useState(0);
34
+ const [loading, setLoading] = useState(true);
35
+ const [error, setError] = useState(null);
36
+ const [diagnostics, setDiagnostics] = useState(null);
37
+ const [profileJson, setProfileJson] = useState(null);
38
+ const [plan, setPlan] = useState(null);
39
+
40
+ useEffect(() => {
41
+ let cancelled = false;
42
+
43
+ async function load() {
44
+ try {
45
+ const [diag, profileResolved] = await Promise.all([
46
+ buildReadOnlyDiagnostics({ homeDir, workspaceRoot, packageName, packageRoot, cliVersion }),
47
+ resolveProfile({ homeDir, workspaceRoot })
48
+ ]);
49
+
50
+ if (cancelled) return;
51
+ setDiagnostics(diag);
52
+ setProfileJson(buildProfileJson(profileResolved));
53
+ setLoading(false);
54
+ } catch (loadError) {
55
+ if (cancelled) return;
56
+ setError(loadError instanceof Error ? loadError.message : String(loadError));
57
+ setLoading(false);
58
+ }
59
+ }
60
+
61
+ load();
62
+ return () => {
63
+ cancelled = true;
64
+ };
65
+ }, [homeDir, workspaceRoot, packageName, packageRoot, cliVersion]);
66
+
67
+ const finish = (outcome) => {
68
+ onComplete(outcome);
69
+ exit();
70
+ };
71
+
72
+ useInput((inputKey, key) => {
73
+ if (key.escape) {
74
+ if (view === ORCHESTRATOR_VIEWS.HOME) {
75
+ finish({ cancelled: true });
76
+ return;
77
+ }
78
+ setView(ORCHESTRATOR_VIEWS.HOME);
79
+ setPlan(null);
80
+ return;
81
+ }
82
+
83
+ if (loading || error) return;
84
+
85
+ if (view === ORCHESTRATOR_VIEWS.CONFIRM) {
86
+ if (inputKey.toLowerCase() === "y") {
87
+ finish({ cancelled: false, action: plan?.action ?? PLAN_ACTIONS.SETUP, confirmed: true, plan });
88
+ }
89
+ if (inputKey.toLowerCase() === "n") {
90
+ setView(ORCHESTRATOR_VIEWS.HOME);
91
+ setPlan(null);
92
+ }
93
+ return;
94
+ }
95
+
96
+ if (view !== ORCHESTRATOR_VIEWS.HOME) return;
97
+
98
+ if (key.upArrow) {
99
+ setMenuIndex((index) => Math.max(0, index - 1));
100
+ return;
101
+ }
102
+
103
+ if (key.downArrow) {
104
+ setMenuIndex((index) => Math.min(ORCHESTRATOR_MENU.length - 1, index + 1));
105
+ return;
106
+ }
107
+
108
+ if (!key.return) return;
109
+
110
+ const item = ORCHESTRATOR_MENU[menuIndex];
111
+ if (item.action === "setup") {
112
+ buildActionPlan({
113
+ action: PLAN_ACTIONS.SETUP,
114
+ homeDir,
115
+ workspaceRoot,
116
+ packageName,
117
+ options: { packageRoot, cliVersion }
118
+ }).then((builtPlan) => {
119
+ setPlan(builtPlan);
120
+ setView(ORCHESTRATOR_VIEWS.CONFIRM);
121
+ }).catch((planError) => {
122
+ setError(planError instanceof Error ? planError.message : String(planError));
123
+ });
124
+ return;
125
+ }
126
+
127
+ setView(item.view);
128
+ });
129
+
130
+ if (loading) {
131
+ return React.createElement(Box, { flexDirection: "column" },
132
+ React.createElement(Text, { bold: true, color: COLORS.accent }, BRAND.displayName),
133
+ React.createElement(Text, { color: COLORS.muted }, "Loading agent capabilities…")
134
+ );
135
+ }
136
+
137
+ if (error) {
138
+ return React.createElement(Box, { flexDirection: "column" },
139
+ React.createElement(Text, { bold: true, color: COLORS.danger }, "Orchestrator error"),
140
+ React.createElement(Text, null, error),
141
+ React.createElement(Text, { dimColor: true }, "Esc to exit")
142
+ );
143
+ }
144
+
145
+ return React.createElement(Box, { flexDirection: "column" },
146
+ React.createElement(Text, { bold: true, color: COLORS.accent }, `${BRAND.displayName} orchestrator`),
147
+ React.createElement(Text, { color: COLORS.muted }, "Harness Engineering · local-first · cloud opt-in"),
148
+ React.createElement(Text, null, ""),
149
+ renderView({ view, diagnostics, profileJson, plan, menuIndex }),
150
+ React.createElement(Text, null, ""),
151
+ React.createElement(Text, { dimColor: true }, footerHint(view))
152
+ );
153
+ }
154
+
155
+ function renderView({ view, diagnostics, profileJson, plan, menuIndex }) {
156
+ switch (view) {
157
+ case ORCHESTRATOR_VIEWS.HOME:
158
+ return React.createElement(Box, { flexDirection: "column" },
159
+ React.createElement(Text, { bold: true }, "Menu"),
160
+ ORCHESTRATOR_MENU.map((item, index) =>
161
+ React.createElement(Text, {
162
+ key: item.id,
163
+ color: index === menuIndex ? COLORS.accent : undefined,
164
+ bold: index === menuIndex
165
+ }, `${index === menuIndex ? "› " : " "}${item.label}`)
166
+ ),
167
+ React.createElement(Text, null, ""),
168
+ React.createElement(Text, { bold: true }, "Snapshot"),
169
+ React.createElement(Text, null, `Agents detected: ${diagnostics.diagnostics.detected}/${diagnostics.capabilities.length}`),
170
+ React.createElement(Text, null, `Available: ${diagnostics.diagnostics.available}`),
171
+ diagnostics.intelligence && React.createElement(
172
+ Text,
173
+ null,
174
+ `Intelligence: local=${diagnostics.intelligence.summary.localAvailable ? "yes" : "no"} cloud=${diagnostics.intelligence.summary.cloudAuthenticated ? "yes" : "no"}`
175
+ )
176
+ );
177
+ case ORCHESTRATOR_VIEWS.AGENTS:
178
+ return React.createElement(Box, { flexDirection: "column" },
179
+ React.createElement(Text, { bold: true }, "Agent capabilities"),
180
+ formatAgentStatusLines(diagnostics.capabilities)
181
+ .map((line) => React.createElement(Text, { key: line }, line))
182
+ );
183
+ case ORCHESTRATOR_VIEWS.INTELLIGENCE:
184
+ return React.createElement(Box, { flexDirection: "column" },
185
+ React.createElement(Text, { bold: true }, "Intelligence backends"),
186
+ formatIntelligenceLines(diagnostics)
187
+ .map((line) => React.createElement(Text, { key: line }, line)),
188
+ React.createElement(Text, null, ""),
189
+ React.createElement(Text, { dimColor: true }, "CLI: kairo intelligence status|models|context|route|ask")
190
+ );
191
+ case ORCHESTRATOR_VIEWS.PROFILE:
192
+ return React.createElement(Box, { flexDirection: "column" },
193
+ React.createElement(Text, { bold: true }, "Profile"),
194
+ formatProfileLines(profileJson)
195
+ .map((line) => React.createElement(Text, { key: line }, line))
196
+ );
197
+ case ORCHESTRATOR_VIEWS.PLAN:
198
+ case ORCHESTRATOR_VIEWS.CONFIRM:
199
+ return React.createElement(Box, { flexDirection: "column" },
200
+ React.createElement(Text, { bold: true }, view === ORCHESTRATOR_VIEWS.CONFIRM ? "Confirm plan" : "Plan"),
201
+ plan && formatPlanLines(plan).map((line) => React.createElement(Text, { key: line }, line)),
202
+ view === ORCHESTRATOR_VIEWS.CONFIRM && React.createElement(Text, { color: COLORS.warning }, "Y confirm · N decline")
203
+ );
204
+ case ORCHESTRATOR_VIEWS.HELP:
205
+ return React.createElement(Box, { flexDirection: "column" },
206
+ React.createElement(Text, { bold: true }, "Help"),
207
+ React.createElement(Text, null, "Kairo coordinates installed agent CLIs and governs project intelligence."),
208
+ React.createElement(Text, null, "Local-first: Ollama when available. Cloud (OpenRouter/free) needs consent."),
209
+ React.createElement(Text, null, "Use: intelligence status|models|context|route|ask"),
210
+ React.createElement(Text, null, "Profiles: ~/.harness/profile.json and .harness/kairo.json (project wins)."),
211
+ React.createElement(Text, null, "Credentials are never stored by Kairo — use environment variables.")
212
+ );
213
+ default: {
214
+ const _exhaustive = view;
215
+ return React.createElement(Text, null, `Unknown view: ${_exhaustive}`);
216
+ }
217
+ }
218
+ }
219
+
220
+ function footerHint(view) {
221
+ if (view === ORCHESTRATOR_VIEWS.HOME) return "↑↓ navigate · Enter select · Esc quit";
222
+ if (view === ORCHESTRATOR_VIEWS.CONFIRM) return "Y confirm · N decline · Esc back";
223
+ return "Esc back to menu";
224
+ }
@@ -0,0 +1,107 @@
1
+ import { stdin as input, stdout as output } from "node:process";
2
+ import { canUseSetupInk } from "./terminal.js";
3
+
4
+ export function canUseOrchestratorShell({
5
+ interactive = Boolean(input.isTTY && output.isTTY),
6
+ term = process.env.TERM ?? "",
7
+ columns = output.columns ?? 80,
8
+ forceInk = process.env.HARNESS_INK !== "0"
9
+ } = {}) {
10
+ return canUseSetupInk({ interactive, term, columns, forceInk });
11
+ }
12
+
13
+ export const ORCHESTRATOR_VIEWS = {
14
+ HOME: "home",
15
+ AGENTS: "agents",
16
+ PROFILE: "profile",
17
+ PLAN: "plan",
18
+ CONFIRM: "confirm",
19
+ HELP: "help",
20
+ INTELLIGENCE: "intelligence"
21
+ };
22
+
23
+ export const ORCHESTRATOR_MENU = [
24
+ { id: "status", label: "Diagnostics", view: ORCHESTRATOR_VIEWS.HOME },
25
+ { id: "agents", label: "Agents", view: ORCHESTRATOR_VIEWS.AGENTS },
26
+ { id: "intelligence", label: "Intelligence", view: ORCHESTRATOR_VIEWS.INTELLIGENCE },
27
+ { id: "profile", label: "Profile", view: ORCHESTRATOR_VIEWS.PROFILE },
28
+ { id: "plan-setup", label: "Plan setup", view: ORCHESTRATOR_VIEWS.PLAN, action: "setup" },
29
+ { id: "help", label: "Help", view: ORCHESTRATOR_VIEWS.HELP }
30
+ ];
31
+
32
+ export function formatAgentStatusLines(capabilities) {
33
+ return capabilities.map((entry) => {
34
+ const auth = entry.authenticated == null ? "n/a" : (entry.authenticated ? "yes" : "no");
35
+ const version = entry.version ?? "unknown";
36
+ return `${entry.label.padEnd(14)} ${entry.state.padEnd(14)} v${version} auth=${auth}`;
37
+ });
38
+ }
39
+
40
+ export function formatProfileLines(profileJson) {
41
+ const lines = [
42
+ `Coordinator: ${profileJson.coordinator ?? "none"}`,
43
+ `Default agents: ${formatAgentsLabel(profileJson.defaultAgents)}`,
44
+ `Apply mode: ${profileJson.applyMode}`,
45
+ `Preferred backend: ${profileJson.preferredBackend ?? "auto"}`,
46
+ `Preferred model: ${profileJson.preferredModel ?? "auto"}`,
47
+ `Cloud consent preference: ${profileJson.cloudConsent ? "recorded (session --cloud-consent still required)" : "no"}`,
48
+ `Token budget: ${profileJson.tokenBudget ?? "none"}`
49
+ ];
50
+
51
+ if (profileJson.sources.global) {
52
+ lines.push(`Global: ${profileJson.sources.global}`);
53
+ }
54
+
55
+ if (profileJson.sources.project) {
56
+ lines.push(`Project: ${profileJson.sources.project}`);
57
+ }
58
+
59
+ lines.push(`Precedence: ${profileJson.sources.precedence}`);
60
+ return lines;
61
+ }
62
+
63
+ export function formatIntelligenceLines(diagnostics) {
64
+ const intelligence = diagnostics?.intelligence;
65
+ if (!intelligence) {
66
+ return ["Intelligence layer unavailable."];
67
+ }
68
+
69
+ const lines = [
70
+ `Local available: ${intelligence.summary.localAvailable ? "yes" : "no"}`,
71
+ `Cloud authenticated: ${intelligence.summary.cloudAuthenticated ? "yes" : "no"}`,
72
+ `Routing: ${intelligence.routingPreview?.reason ?? "n/a"}`,
73
+ `Can invoke: ${intelligence.routingPreview?.canInvoke ? "yes" : "no"}`,
74
+ ""
75
+ ];
76
+
77
+ for (const backend of intelligence.backends ?? []) {
78
+ lines.push(
79
+ `${backend.label.padEnd(14)} ${backend.state.padEnd(14)} models=${backend.models?.length ?? 0}`
80
+ );
81
+ }
82
+
83
+ return lines;
84
+ }
85
+
86
+ export function formatPlanLines(plan) {
87
+ const lines = [`Action: ${plan.action}`, ""];
88
+ for (const step of plan.steps) {
89
+ lines.push(` • ${step}`);
90
+ }
91
+
92
+ if (plan.warnings.length > 0) {
93
+ lines.push("", "Warnings:");
94
+ for (const warning of plan.warnings) {
95
+ lines.push(` ! ${warning}`);
96
+ }
97
+ }
98
+
99
+ return lines;
100
+ }
101
+
102
+ function formatAgentsLabel(agents) {
103
+ if (agents === "detected") return "detected";
104
+ if (agents === "all") return "all";
105
+ if (Array.isArray(agents)) return agents.join(", ");
106
+ return String(agents);
107
+ }
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { render } from "ink";
3
+ import { OrchestratorApp } from "./orchestrator-app.js";
4
+
5
+ export async function runOrchestratorInk({
6
+ homeDir,
7
+ workspaceRoot,
8
+ packageRoot,
9
+ packageName,
10
+ cliVersion,
11
+ renderImpl = render
12
+ }) {
13
+ return new Promise((resolve) => {
14
+ const { waitUntilExit } = renderImpl(
15
+ React.createElement(OrchestratorApp, {
16
+ homeDir,
17
+ workspaceRoot,
18
+ packageRoot,
19
+ packageName,
20
+ cliVersion,
21
+ onComplete: resolve
22
+ })
23
+ );
24
+
25
+ waitUntilExit().catch((error) => {
26
+ resolve({ cancelled: true, error });
27
+ });
28
+ });
29
+ }