@kal-elsam/kairo-runtime 0.1.4 → 0.1.5
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/package.json +1 -1
- package/src/cli.js +59 -3
- package/src/global/action-planner.js +178 -0
- package/src/global/agent-capabilities/create-capability-adapter.js +224 -0
- package/src/global/agent-capabilities/index.js +54 -0
- package/src/global/brand/cli.js +11 -7
- package/src/global/capability-registry.js +80 -0
- package/src/global/capability-states.js +30 -0
- package/src/global/cli-probe.js +47 -0
- package/src/global/ink/orchestrator-app.js +209 -0
- package/src/global/ink/orchestrator-state.js +78 -0
- package/src/global/ink/run-orchestrator-ink.js +29 -0
- package/src/global/orchestrator.js +139 -0
- package/src/global/paths.js +1 -0
- package/src/global/profile.js +146 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Gemini, Copilot, Engram, and Graphify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Kal-elSam/harness#readme",
|
package/src/cli.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { applyPolicyToOptions, loadPolicyFile } from "./global/policy.js";
|
|
31
31
|
import { resolveHomeDir } from "./global/paths.js";
|
|
32
32
|
import { runWorkspaceDetect, runWorkspaceDoctor, runWorkspaceInit, runWorkspaceUpdate } from "./workspace-cli.js";
|
|
33
|
+
import { runOrchestratorDiagnostics, runOrchestratorShell } from "./global/orchestrator.js";
|
|
33
34
|
import {
|
|
34
35
|
LEGACY_PACKAGE_NAME,
|
|
35
36
|
PACKAGE_NAME,
|
|
@@ -66,6 +67,24 @@ export async function runCli(argv) {
|
|
|
66
67
|
const invoke = resolveSuggestedInvocation(packageManifest.name);
|
|
67
68
|
|
|
68
69
|
switch (command) {
|
|
70
|
+
case "shell":
|
|
71
|
+
await runOrchestratorShell({
|
|
72
|
+
packageRoot,
|
|
73
|
+
packageManifest,
|
|
74
|
+
workspaceRoot: optionsWithPolicy.cwd,
|
|
75
|
+
interactive: optionsWithPolicy.interactive
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
case "orchestrator":
|
|
79
|
+
await runOrchestratorDiagnostics({
|
|
80
|
+
homeDir: resolveHomeDir(),
|
|
81
|
+
workspaceRoot: optionsWithPolicy.cwd,
|
|
82
|
+
packageName: packageManifest.name,
|
|
83
|
+
packageRoot,
|
|
84
|
+
cliVersion: packageManifest.version,
|
|
85
|
+
json: optionsWithPolicy.json
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
69
88
|
case "setup":
|
|
70
89
|
await runGlobalSetup(optionsWithPolicy, packageManifest, packageRoot);
|
|
71
90
|
return;
|
|
@@ -219,7 +238,37 @@ function resolveImplicitCommand(args) {
|
|
|
219
238
|
if (argsWantsWorkspaceScope(args)) {
|
|
220
239
|
return "init";
|
|
221
240
|
}
|
|
222
|
-
|
|
241
|
+
if (hasImplicitSetupFlags(args)) {
|
|
242
|
+
return "setup";
|
|
243
|
+
}
|
|
244
|
+
return "shell";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function hasImplicitSetupFlags(args) {
|
|
248
|
+
const setupFlags = new Set([
|
|
249
|
+
"--dry-run",
|
|
250
|
+
"--yes",
|
|
251
|
+
"-y",
|
|
252
|
+
"--confirm",
|
|
253
|
+
"--simple",
|
|
254
|
+
"--no-preflight",
|
|
255
|
+
"--all-adapters",
|
|
256
|
+
"--no-default-components",
|
|
257
|
+
"--detect"
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
261
|
+
const arg = args[index];
|
|
262
|
+
if (setupFlags.has(arg)) return true;
|
|
263
|
+
if (arg.startsWith("--mode=")) return true;
|
|
264
|
+
if (arg.startsWith("--adapters=") || arg.startsWith("--agents=")) return true;
|
|
265
|
+
if (arg.startsWith("--components=")) return true;
|
|
266
|
+
if (arg === "--mode" || arg === "--adapters" || arg === "--agents" || arg === "--components") {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return false;
|
|
223
272
|
}
|
|
224
273
|
|
|
225
274
|
function argsWantsWorkspaceScope(args) {
|
|
@@ -461,6 +510,8 @@ function normalizeCommand(command) {
|
|
|
461
510
|
if (!command) return "install";
|
|
462
511
|
|
|
463
512
|
if (command === "install" || command === "i") return "install";
|
|
513
|
+
if (command === "shell") return "shell";
|
|
514
|
+
if (command === "orchestrator") return "orchestrator";
|
|
464
515
|
if (command === "setup") return "setup";
|
|
465
516
|
if (command === "status") return "status";
|
|
466
517
|
if (command === "sync") return "sync";
|
|
@@ -517,8 +568,11 @@ sections, components, backups, and drift repair under ~/.harness.
|
|
|
517
568
|
Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
|
|
518
569
|
|
|
519
570
|
Usage:
|
|
520
|
-
${cli}
|
|
571
|
+
${cli} Interactive orchestrator shell (TTY)
|
|
572
|
+
${cli} --dry-run Setup dry-run (scriptable)
|
|
521
573
|
${cli} --version
|
|
574
|
+
${cli} shell Interactive orchestrator shell (TTY)
|
|
575
|
+
${cli} orchestrator [--json] Read-only agent capability diagnostics
|
|
522
576
|
${cli} setup [--dry-run] [--yes] [--confirm] [--simple] [--no-preflight] [--agents <list|all>] [--components <list>]
|
|
523
577
|
${cli} status [--json]
|
|
524
578
|
${cli} sync [--dry-run] [--yes] [--confirm] [--json] [--no-preflight]
|
|
@@ -552,7 +606,9 @@ Scopes:
|
|
|
552
606
|
Explicit --scope=workspace only.
|
|
553
607
|
|
|
554
608
|
Commands:
|
|
555
|
-
|
|
609
|
+
shell Interactive Ink orchestrator (TTY). Bare ${cli} opens this in TTY sessions.
|
|
610
|
+
orchestrator Read-only capability registry diagnostics (--json supported).
|
|
611
|
+
setup Managed ecosystem setup. Interactive Ink UI (TTY). Use --simple for Clack prompts.
|
|
556
612
|
status Control panel: agents, components, drift, backups, next action.
|
|
557
613
|
sync Converge managed content (repair drift), then show status.
|
|
558
614
|
upgrade Preview or apply ecosystem updates (apply requires --yes).
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { CAPABILITY_STATES } from "./capability-states.js";
|
|
2
|
+
import { inspectAllCapabilities } from "./capability-registry.js";
|
|
3
|
+
import { resolveProfile, resolveProfileAgents } from "./profile.js";
|
|
4
|
+
import { formatCliCommand } from "./brand/cli.js";
|
|
5
|
+
|
|
6
|
+
export const PLAN_ACTIONS = {
|
|
7
|
+
DIAGNOSE: "diagnose",
|
|
8
|
+
SETUP: "setup",
|
|
9
|
+
SYNC: "sync",
|
|
10
|
+
INSTALL: "install",
|
|
11
|
+
STATUS: "status"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function buildReadOnlyDiagnostics({
|
|
15
|
+
homeDir,
|
|
16
|
+
workspaceRoot,
|
|
17
|
+
packageName,
|
|
18
|
+
packageRoot,
|
|
19
|
+
cliVersion
|
|
20
|
+
}) {
|
|
21
|
+
const [{ profile, sources }, capabilities] = await Promise.all([
|
|
22
|
+
resolveProfile({ homeDir, workspaceRoot }),
|
|
23
|
+
inspectAllCapabilities({ homeDir, workspaceRoot, packageName })
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const detectedIds = capabilities.filter((entry) => entry.detected).map((entry) => entry.id);
|
|
27
|
+
const profileAgents = resolveProfileAgents(profile, detectedIds);
|
|
28
|
+
const diagnostics = summarizeDiagnostics(capabilities);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
readOnly: true,
|
|
32
|
+
cliVersion,
|
|
33
|
+
profile: { ...profile, sources },
|
|
34
|
+
capabilities,
|
|
35
|
+
profileAgents,
|
|
36
|
+
diagnostics,
|
|
37
|
+
recommendations: buildDiagnosticRecommendations({ capabilities, profile, diagnostics })
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function buildActionPlan({
|
|
42
|
+
action,
|
|
43
|
+
homeDir,
|
|
44
|
+
workspaceRoot,
|
|
45
|
+
packageName,
|
|
46
|
+
options = {}
|
|
47
|
+
}) {
|
|
48
|
+
const diagnostics = await buildReadOnlyDiagnostics({
|
|
49
|
+
homeDir,
|
|
50
|
+
workspaceRoot,
|
|
51
|
+
packageName,
|
|
52
|
+
packageRoot: options.packageRoot ?? null,
|
|
53
|
+
cliVersion: options.cliVersion ?? null
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const steps = [];
|
|
57
|
+
const warnings = [];
|
|
58
|
+
|
|
59
|
+
switch (action) {
|
|
60
|
+
case PLAN_ACTIONS.DIAGNOSE:
|
|
61
|
+
case PLAN_ACTIONS.STATUS:
|
|
62
|
+
return {
|
|
63
|
+
action,
|
|
64
|
+
readOnly: true,
|
|
65
|
+
requiresConfirmation: false,
|
|
66
|
+
steps: ["Show ecosystem diagnostics (read-only)."],
|
|
67
|
+
diagnostics,
|
|
68
|
+
warnings
|
|
69
|
+
};
|
|
70
|
+
case PLAN_ACTIONS.SETUP:
|
|
71
|
+
case PLAN_ACTIONS.INSTALL:
|
|
72
|
+
steps.push(`Target agents: ${diagnostics.profileAgents.join(", ")}`);
|
|
73
|
+
steps.push("Preview managed section changes under ~/.harness and agent config files.");
|
|
74
|
+
steps.push("Create backups before writing managed content.");
|
|
75
|
+
if (options.dryRun) {
|
|
76
|
+
steps.push("Dry run: no files will be written.");
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
action,
|
|
80
|
+
readOnly: Boolean(options.dryRun),
|
|
81
|
+
requiresConfirmation: !options.dryRun,
|
|
82
|
+
steps,
|
|
83
|
+
diagnostics,
|
|
84
|
+
warnings: collectCapabilityWarnings(diagnostics.capabilities)
|
|
85
|
+
};
|
|
86
|
+
case PLAN_ACTIONS.SYNC:
|
|
87
|
+
steps.push("Compare managed content against bundled assets.");
|
|
88
|
+
steps.push("Repair drift in agent config files when differences are found.");
|
|
89
|
+
if (options.dryRun) {
|
|
90
|
+
steps.push("Dry run: show planned repairs only.");
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
action,
|
|
94
|
+
readOnly: Boolean(options.dryRun),
|
|
95
|
+
requiresConfirmation: !options.dryRun,
|
|
96
|
+
steps,
|
|
97
|
+
diagnostics,
|
|
98
|
+
warnings: collectCapabilityWarnings(diagnostics.capabilities)
|
|
99
|
+
};
|
|
100
|
+
default: {
|
|
101
|
+
const _exhaustive = action;
|
|
102
|
+
throw new Error(`Unknown plan action "${_exhaustive}".`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function shouldExecutePlan(plan, { confirmed = false } = {}) {
|
|
108
|
+
if (plan.readOnly) return true;
|
|
109
|
+
return confirmed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatActionPlan(plan) {
|
|
113
|
+
const lines = [];
|
|
114
|
+
lines.push(`Action: ${plan.action}`);
|
|
115
|
+
lines.push(`Mode: ${plan.readOnly ? "read-only" : "write"}`);
|
|
116
|
+
lines.push(`Confirmation required: ${plan.requiresConfirmation ? "yes" : "no"}`);
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push("Steps:");
|
|
119
|
+
for (const step of plan.steps) {
|
|
120
|
+
lines.push(` - ${step}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (plan.warnings.length > 0) {
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push("Warnings:");
|
|
126
|
+
for (const warning of plan.warnings) {
|
|
127
|
+
lines.push(` - ${warning}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (plan.diagnostics.recommendations.length > 0) {
|
|
132
|
+
lines.push("");
|
|
133
|
+
lines.push("Recommendations:");
|
|
134
|
+
for (const recommendation of plan.diagnostics.recommendations) {
|
|
135
|
+
lines.push(` - ${recommendation}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function summarizeDiagnostics(capabilities) {
|
|
143
|
+
return {
|
|
144
|
+
detected: capabilities.filter((entry) => entry.detected).length,
|
|
145
|
+
available: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.AVAILABLE).length,
|
|
146
|
+
unknown: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.UNKNOWN).length,
|
|
147
|
+
errors: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.ERROR).length
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildDiagnosticRecommendations({ capabilities, profile, diagnostics }) {
|
|
152
|
+
const recommendations = [];
|
|
153
|
+
|
|
154
|
+
if (diagnostics.detected === 0) {
|
|
155
|
+
recommendations.push(`No agents detected. Run ${formatCliCommand("detect")} or install a supported agent CLI.`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (profile.coordinator) {
|
|
159
|
+
const coordinator = capabilities.find((entry) => entry.id === profile.coordinator);
|
|
160
|
+
if (!coordinator?.detected && coordinator?.state === CAPABILITY_STATES.UNKNOWN) {
|
|
161
|
+
recommendations.push(`Profile coordinator "${profile.coordinator}" is not detected on this machine.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const capability of capabilities) {
|
|
166
|
+
if (capability.recommendation) {
|
|
167
|
+
recommendations.push(`${capability.label}: ${capability.recommendation}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return [...new Set(recommendations)];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function collectCapabilityWarnings(capabilities) {
|
|
175
|
+
return capabilities
|
|
176
|
+
.filter((entry) => entry.state === CAPABILITY_STATES.ERROR || entry.state === CAPABILITY_STATES.UNKNOWN)
|
|
177
|
+
.map((entry) => `${entry.label} is ${entry.state}${entry.error ? ` (${entry.error})` : ""}.`);
|
|
178
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { CAPABILITY_STATES } from "../capability-states.js";
|
|
2
|
+
import {
|
|
3
|
+
isExecutableAvailable,
|
|
4
|
+
parseVersionFromOutput,
|
|
5
|
+
probeCommand,
|
|
6
|
+
resolveProbeState
|
|
7
|
+
} from "../cli-probe.js";
|
|
8
|
+
|
|
9
|
+
export function createAgentCapabilityAdapter({
|
|
10
|
+
id,
|
|
11
|
+
label,
|
|
12
|
+
managedAdapter,
|
|
13
|
+
executable = null,
|
|
14
|
+
versionArgs = ["--version"],
|
|
15
|
+
authArgs = null,
|
|
16
|
+
modelsArgs = null,
|
|
17
|
+
opaqueAuth = false,
|
|
18
|
+
runExecutable = null
|
|
19
|
+
}) {
|
|
20
|
+
const cliName = runExecutable ?? executable;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
label,
|
|
25
|
+
managedAdapter,
|
|
26
|
+
|
|
27
|
+
detect(context) {
|
|
28
|
+
return managedAdapter.detect(context);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
inspect(context, { probeImpl = defaultProbe } = {}) {
|
|
32
|
+
const detected = managedAdapter.detect(context);
|
|
33
|
+
const cliAvailable = executable ? isExecutableAvailable(executable) : false;
|
|
34
|
+
|
|
35
|
+
if (!detected && !cliAvailable) {
|
|
36
|
+
return buildInspection({
|
|
37
|
+
id,
|
|
38
|
+
label,
|
|
39
|
+
state: CAPABILITY_STATES.UNKNOWN,
|
|
40
|
+
detected: false,
|
|
41
|
+
cliAvailable: false,
|
|
42
|
+
version: null,
|
|
43
|
+
authenticated: null,
|
|
44
|
+
recommendation: `Install ${label} or run ${formatCliCommand("setup")} to configure managed sections.`
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (opaqueAuth) {
|
|
49
|
+
return buildInspection({
|
|
50
|
+
id,
|
|
51
|
+
label,
|
|
52
|
+
state: detected || cliAvailable ? CAPABILITY_STATES.DETECTED : CAPABILITY_STATES.UNKNOWN,
|
|
53
|
+
detected,
|
|
54
|
+
cliAvailable,
|
|
55
|
+
version: null,
|
|
56
|
+
authenticated: null,
|
|
57
|
+
recommendation: detected
|
|
58
|
+
? `${label} config detected. Authentication status is provider-managed.`
|
|
59
|
+
: `Install ${label} to enable managed configuration.`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return probeImpl({
|
|
64
|
+
id,
|
|
65
|
+
label,
|
|
66
|
+
detected,
|
|
67
|
+
cliAvailable,
|
|
68
|
+
executable,
|
|
69
|
+
versionArgs,
|
|
70
|
+
authArgs
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
listModels(context, { probeImpl = defaultProbeModels } = {}) {
|
|
75
|
+
if (!modelsArgs || !executable) return null;
|
|
76
|
+
if (!isExecutableAvailable(executable)) return null;
|
|
77
|
+
return probeImpl({ executable, modelsArgs, context });
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
run(context, { args = [], cwd = context.workspaceRoot ?? process.cwd(), spawnImpl = probeCommand } = {}) {
|
|
81
|
+
if (!cliName) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
state: CAPABILITY_STATES.ERROR,
|
|
85
|
+
message: `${label} does not expose a delegatable CLI through Kairo.`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!isExecutableAvailable(cliName)) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
state: CAPABILITY_STATES.ERROR,
|
|
93
|
+
message: `${label} CLI "${cliName}" is not on PATH. Install the agent or add it to PATH.`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = spawnImpl(cliName, args, { cwd, env: process.env });
|
|
98
|
+
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
const detail = result.stderr || result.stdout || result.error || "unknown error";
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
state: CAPABILITY_STATES.ERROR,
|
|
104
|
+
message: `${label} CLI failed: ${detail}`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
state: CAPABILITY_STATES.AVAILABLE,
|
|
111
|
+
stdout: result.stdout,
|
|
112
|
+
stderr: result.stderr
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function defaultProbe({ id, label, detected, cliAvailable, executable, versionArgs, authArgs }) {
|
|
119
|
+
let version = null;
|
|
120
|
+
let authenticated = null;
|
|
121
|
+
let probeError = null;
|
|
122
|
+
let authReady = false;
|
|
123
|
+
|
|
124
|
+
if (cliAvailable && executable) {
|
|
125
|
+
const versionResult = probeCommand(executable, versionArgs);
|
|
126
|
+
if (versionResult.timedOut || versionResult.error) {
|
|
127
|
+
probeError = versionResult.error ?? "probe timed out";
|
|
128
|
+
} else if (versionResult.ok) {
|
|
129
|
+
version = parseVersionFromOutput(versionResult.stdout) ?? versionResult.stdout.split("\n")[0] ?? null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (authArgs) {
|
|
133
|
+
const authResult = probeCommand(executable, authArgs);
|
|
134
|
+
if (authResult.timedOut || authResult.error) {
|
|
135
|
+
probeError = probeError ?? authResult.error ?? "auth probe timed out";
|
|
136
|
+
} else {
|
|
137
|
+
authenticated = authResult.ok;
|
|
138
|
+
authReady = authResult.ok;
|
|
139
|
+
}
|
|
140
|
+
} else if (version) {
|
|
141
|
+
authenticated = null;
|
|
142
|
+
authReady = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const state = resolveProbeState({
|
|
147
|
+
detected,
|
|
148
|
+
cliAvailable,
|
|
149
|
+
authReady,
|
|
150
|
+
probeError,
|
|
151
|
+
opaque: false
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return buildInspection({
|
|
155
|
+
id,
|
|
156
|
+
label,
|
|
157
|
+
state,
|
|
158
|
+
detected,
|
|
159
|
+
cliAvailable,
|
|
160
|
+
version,
|
|
161
|
+
authenticated,
|
|
162
|
+
error: probeError,
|
|
163
|
+
recommendation: buildRecommendation({ label, state, detected, cliAvailable, authenticated })
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function defaultProbeModels({ executable, modelsArgs }) {
|
|
168
|
+
const result = probeCommand(executable, modelsArgs);
|
|
169
|
+
if (!result.ok) return null;
|
|
170
|
+
|
|
171
|
+
const lines = result.stdout
|
|
172
|
+
.split("\n")
|
|
173
|
+
.map((line) => line.trim())
|
|
174
|
+
.filter(Boolean);
|
|
175
|
+
|
|
176
|
+
return lines.length > 0 ? lines : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildInspection(fields) {
|
|
180
|
+
return {
|
|
181
|
+
id: fields.id,
|
|
182
|
+
label: fields.label,
|
|
183
|
+
state: fields.state,
|
|
184
|
+
detected: fields.detected,
|
|
185
|
+
cliAvailable: fields.cliAvailable,
|
|
186
|
+
version: fields.version ?? null,
|
|
187
|
+
authenticated: fields.authenticated ?? null,
|
|
188
|
+
models: fields.models ?? null,
|
|
189
|
+
error: fields.error ?? null,
|
|
190
|
+
recommendation: fields.recommendation ?? null
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildRecommendation({ label, state, detected, cliAvailable, authenticated }) {
|
|
195
|
+
if (state === CAPABILITY_STATES.ERROR) {
|
|
196
|
+
return `Re-run detection or check ${label} CLI logs for details.`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (state === CAPABILITY_STATES.UNKNOWN) {
|
|
200
|
+
return `Install ${label} or run ${formatCliCommand("detect")} after setup.`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (state === CAPABILITY_STATES.AVAILABLE) {
|
|
204
|
+
return `${label} is ready. Delegate tasks through its CLI.`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (authenticated === false) {
|
|
208
|
+
return `Authenticate with ${label} (provider login) before delegating work.`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (detected && !cliAvailable) {
|
|
212
|
+
return `${label} config detected but CLI not on PATH.`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (cliAvailable && !detected) {
|
|
216
|
+
return `${label} CLI found. Run ${formatCliCommand("setup")} to add managed sections.`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return `${label} detected. Run ${formatCliCommand("status")} for ecosystem health.`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function formatCliCommand(command) {
|
|
223
|
+
return `kairo ${command}`;
|
|
224
|
+
}
|
|
@@ -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
|
+
}
|
package/src/global/brand/cli.js
CHANGED
|
@@ -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 =
|
|
56
|
-
const userAgent =
|
|
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(
|
|
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,209 @@
|
|
|
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
|
+
formatPlanLines,
|
|
11
|
+
formatProfileLines
|
|
12
|
+
} from "./orchestrator-state.js";
|
|
13
|
+
|
|
14
|
+
const COLORS = {
|
|
15
|
+
accent: "cyan",
|
|
16
|
+
success: "green",
|
|
17
|
+
warning: "yellow",
|
|
18
|
+
danger: "red",
|
|
19
|
+
muted: "gray"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function OrchestratorApp({
|
|
23
|
+
homeDir,
|
|
24
|
+
workspaceRoot,
|
|
25
|
+
packageName,
|
|
26
|
+
packageRoot,
|
|
27
|
+
cliVersion,
|
|
28
|
+
onComplete
|
|
29
|
+
}) {
|
|
30
|
+
const { exit } = useApp();
|
|
31
|
+
const [view, setView] = useState(ORCHESTRATOR_VIEWS.HOME);
|
|
32
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
33
|
+
const [loading, setLoading] = useState(true);
|
|
34
|
+
const [error, setError] = useState(null);
|
|
35
|
+
const [diagnostics, setDiagnostics] = useState(null);
|
|
36
|
+
const [profileJson, setProfileJson] = useState(null);
|
|
37
|
+
const [plan, setPlan] = useState(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let cancelled = false;
|
|
41
|
+
|
|
42
|
+
async function load() {
|
|
43
|
+
try {
|
|
44
|
+
const [diag, profileResolved] = await Promise.all([
|
|
45
|
+
buildReadOnlyDiagnostics({ homeDir, workspaceRoot, packageName, packageRoot, cliVersion }),
|
|
46
|
+
resolveProfile({ homeDir, workspaceRoot })
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
if (cancelled) return;
|
|
50
|
+
setDiagnostics(diag);
|
|
51
|
+
setProfileJson(buildProfileJson(profileResolved));
|
|
52
|
+
setLoading(false);
|
|
53
|
+
} catch (loadError) {
|
|
54
|
+
if (cancelled) return;
|
|
55
|
+
setError(loadError instanceof Error ? loadError.message : String(loadError));
|
|
56
|
+
setLoading(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
load();
|
|
61
|
+
return () => {
|
|
62
|
+
cancelled = true;
|
|
63
|
+
};
|
|
64
|
+
}, [homeDir, workspaceRoot, packageName, packageRoot, cliVersion]);
|
|
65
|
+
|
|
66
|
+
const finish = (outcome) => {
|
|
67
|
+
onComplete(outcome);
|
|
68
|
+
exit();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
useInput((inputKey, key) => {
|
|
72
|
+
if (key.escape) {
|
|
73
|
+
if (view === ORCHESTRATOR_VIEWS.HOME) {
|
|
74
|
+
finish({ cancelled: true });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setView(ORCHESTRATOR_VIEWS.HOME);
|
|
78
|
+
setPlan(null);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (loading || error) return;
|
|
83
|
+
|
|
84
|
+
if (view === ORCHESTRATOR_VIEWS.CONFIRM) {
|
|
85
|
+
if (inputKey.toLowerCase() === "y") {
|
|
86
|
+
finish({ cancelled: false, action: plan?.action ?? PLAN_ACTIONS.SETUP, confirmed: true, plan });
|
|
87
|
+
}
|
|
88
|
+
if (inputKey.toLowerCase() === "n") {
|
|
89
|
+
setView(ORCHESTRATOR_VIEWS.HOME);
|
|
90
|
+
setPlan(null);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (view !== ORCHESTRATOR_VIEWS.HOME) return;
|
|
96
|
+
|
|
97
|
+
if (key.upArrow) {
|
|
98
|
+
setMenuIndex((index) => Math.max(0, index - 1));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (key.downArrow) {
|
|
103
|
+
setMenuIndex((index) => Math.min(ORCHESTRATOR_MENU.length - 1, index + 1));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!key.return) return;
|
|
108
|
+
|
|
109
|
+
const item = ORCHESTRATOR_MENU[menuIndex];
|
|
110
|
+
if (item.action === "setup") {
|
|
111
|
+
buildActionPlan({
|
|
112
|
+
action: PLAN_ACTIONS.SETUP,
|
|
113
|
+
homeDir,
|
|
114
|
+
workspaceRoot,
|
|
115
|
+
packageName,
|
|
116
|
+
options: { packageRoot, cliVersion }
|
|
117
|
+
}).then((builtPlan) => {
|
|
118
|
+
setPlan(builtPlan);
|
|
119
|
+
setView(ORCHESTRATOR_VIEWS.CONFIRM);
|
|
120
|
+
}).catch((planError) => {
|
|
121
|
+
setError(planError instanceof Error ? planError.message : String(planError));
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setView(item.view);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (loading) {
|
|
130
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
131
|
+
React.createElement(Text, { bold: true, color: COLORS.accent }, BRAND.displayName),
|
|
132
|
+
React.createElement(Text, { color: COLORS.muted }, "Loading agent capabilities…")
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (error) {
|
|
137
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
138
|
+
React.createElement(Text, { bold: true, color: COLORS.danger }, "Orchestrator error"),
|
|
139
|
+
React.createElement(Text, null, error),
|
|
140
|
+
React.createElement(Text, { dimColor: true }, "Esc to exit")
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
145
|
+
React.createElement(Text, { bold: true, color: COLORS.accent }, `${BRAND.displayName} orchestrator`),
|
|
146
|
+
React.createElement(Text, { color: COLORS.muted }, "Provider-neutral agent coordinator · no model imposed"),
|
|
147
|
+
React.createElement(Text, null, ""),
|
|
148
|
+
renderView({ view, diagnostics, profileJson, plan, menuIndex }),
|
|
149
|
+
React.createElement(Text, null, ""),
|
|
150
|
+
React.createElement(Text, { dimColor: true }, footerHint(view))
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderView({ view, diagnostics, profileJson, plan, menuIndex }) {
|
|
155
|
+
switch (view) {
|
|
156
|
+
case ORCHESTRATOR_VIEWS.HOME:
|
|
157
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
158
|
+
React.createElement(Text, { bold: true }, "Menu"),
|
|
159
|
+
ORCHESTRATOR_MENU.map((item, index) =>
|
|
160
|
+
React.createElement(Text, {
|
|
161
|
+
key: item.id,
|
|
162
|
+
color: index === menuIndex ? COLORS.accent : undefined,
|
|
163
|
+
bold: index === menuIndex
|
|
164
|
+
}, `${index === menuIndex ? "› " : " "}${item.label}`)
|
|
165
|
+
),
|
|
166
|
+
React.createElement(Text, null, ""),
|
|
167
|
+
React.createElement(Text, { bold: true }, "Snapshot"),
|
|
168
|
+
React.createElement(Text, null, `Agents detected: ${diagnostics.diagnostics.detected}/${diagnostics.capabilities.length}`),
|
|
169
|
+
React.createElement(Text, null, `Available: ${diagnostics.diagnostics.available}`)
|
|
170
|
+
);
|
|
171
|
+
case ORCHESTRATOR_VIEWS.AGENTS:
|
|
172
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
173
|
+
React.createElement(Text, { bold: true }, "Agent capabilities"),
|
|
174
|
+
formatAgentStatusLines(diagnostics.capabilities)
|
|
175
|
+
.map((line) => React.createElement(Text, { key: line }, line))
|
|
176
|
+
);
|
|
177
|
+
case ORCHESTRATOR_VIEWS.PROFILE:
|
|
178
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
179
|
+
React.createElement(Text, { bold: true }, "Profile"),
|
|
180
|
+
formatProfileLines(profileJson)
|
|
181
|
+
.map((line) => React.createElement(Text, { key: line }, line))
|
|
182
|
+
);
|
|
183
|
+
case ORCHESTRATOR_VIEWS.PLAN:
|
|
184
|
+
case ORCHESTRATOR_VIEWS.CONFIRM:
|
|
185
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
186
|
+
React.createElement(Text, { bold: true }, view === ORCHESTRATOR_VIEWS.CONFIRM ? "Confirm plan" : "Plan"),
|
|
187
|
+
plan && formatPlanLines(plan).map((line) => React.createElement(Text, { key: line }, line)),
|
|
188
|
+
view === ORCHESTRATOR_VIEWS.CONFIRM && React.createElement(Text, { color: COLORS.warning }, "Y confirm · N decline")
|
|
189
|
+
);
|
|
190
|
+
case ORCHESTRATOR_VIEWS.HELP:
|
|
191
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
192
|
+
React.createElement(Text, { bold: true }, "Help"),
|
|
193
|
+
React.createElement(Text, null, "Kairo coordinates installed agent CLIs — it is not a coding model."),
|
|
194
|
+
React.createElement(Text, null, "Use explicit commands for scripts: setup, install, status, doctor, sync."),
|
|
195
|
+
React.createElement(Text, null, "Profiles: ~/.harness/profile.json and .harness/kairo.json (project wins)."),
|
|
196
|
+
React.createElement(Text, null, "Credentials are never stored by Kairo.")
|
|
197
|
+
);
|
|
198
|
+
default: {
|
|
199
|
+
const _exhaustive = view;
|
|
200
|
+
return React.createElement(Text, null, `Unknown view: ${_exhaustive}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function footerHint(view) {
|
|
206
|
+
if (view === ORCHESTRATOR_VIEWS.HOME) return "↑↓ navigate · Enter select · Esc quit";
|
|
207
|
+
if (view === ORCHESTRATOR_VIEWS.CONFIRM) return "Y confirm · N decline · Esc back";
|
|
208
|
+
return "Esc back to menu";
|
|
209
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
};
|
|
21
|
+
|
|
22
|
+
export const ORCHESTRATOR_MENU = [
|
|
23
|
+
{ id: "status", label: "Diagnostics", view: ORCHESTRATOR_VIEWS.HOME },
|
|
24
|
+
{ id: "agents", label: "Agents", view: ORCHESTRATOR_VIEWS.AGENTS },
|
|
25
|
+
{ id: "profile", label: "Profile", view: ORCHESTRATOR_VIEWS.PROFILE },
|
|
26
|
+
{ id: "plan-setup", label: "Plan setup", view: ORCHESTRATOR_VIEWS.PLAN, action: "setup" },
|
|
27
|
+
{ id: "help", label: "Help", view: ORCHESTRATOR_VIEWS.HELP }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function formatAgentStatusLines(capabilities) {
|
|
31
|
+
return capabilities.map((entry) => {
|
|
32
|
+
const auth = entry.authenticated == null ? "n/a" : (entry.authenticated ? "yes" : "no");
|
|
33
|
+
const version = entry.version ?? "unknown";
|
|
34
|
+
return `${entry.label.padEnd(14)} ${entry.state.padEnd(14)} v${version} auth=${auth}`;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatProfileLines(profileJson) {
|
|
39
|
+
const lines = [
|
|
40
|
+
`Coordinator: ${profileJson.coordinator ?? "none"}`,
|
|
41
|
+
`Default agents: ${formatAgentsLabel(profileJson.defaultAgents)}`,
|
|
42
|
+
`Apply mode: ${profileJson.applyMode}`
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
if (profileJson.sources.global) {
|
|
46
|
+
lines.push(`Global: ${profileJson.sources.global}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (profileJson.sources.project) {
|
|
50
|
+
lines.push(`Project: ${profileJson.sources.project}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines.push(`Precedence: ${profileJson.sources.precedence}`);
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatPlanLines(plan) {
|
|
58
|
+
const lines = [`Action: ${plan.action}`, ""];
|
|
59
|
+
for (const step of plan.steps) {
|
|
60
|
+
lines.push(` • ${step}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (plan.warnings.length > 0) {
|
|
64
|
+
lines.push("", "Warnings:");
|
|
65
|
+
for (const warning of plan.warnings) {
|
|
66
|
+
lines.push(` ! ${warning}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatAgentsLabel(agents) {
|
|
74
|
+
if (agents === "detected") return "detected";
|
|
75
|
+
if (agents === "all") return "all";
|
|
76
|
+
if (Array.isArray(agents)) return agents.join(", ");
|
|
77
|
+
return String(agents);
|
|
78
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import { resolveHomeDir } from "./paths.js";
|
|
3
|
+
import { runGlobalSetup } from "./global-cli.js";
|
|
4
|
+
import { PLAN_ACTIONS, buildReadOnlyDiagnostics, shouldExecutePlan } from "./action-planner.js";
|
|
5
|
+
import { canUseOrchestratorShell } from "./ink/orchestrator-state.js";
|
|
6
|
+
import { runOrchestratorInk as defaultRunOrchestratorInk } from "./ink/run-orchestrator-ink.js";
|
|
7
|
+
import { formatCliCommand } from "./brand/cli.js";
|
|
8
|
+
import { BRAND } from "./brand/index.js";
|
|
9
|
+
|
|
10
|
+
export { canUseOrchestratorShell };
|
|
11
|
+
|
|
12
|
+
export function shouldOpenOrchestratorShell({
|
|
13
|
+
interactive = Boolean(input.isTTY && output.isTTY),
|
|
14
|
+
json = false,
|
|
15
|
+
hasImplicitFlags = false
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (!interactive || json) return false;
|
|
18
|
+
if (hasImplicitFlags) return false;
|
|
19
|
+
return canUseOrchestratorShell({ interactive });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runOrchestratorShell({
|
|
23
|
+
packageRoot,
|
|
24
|
+
packageManifest,
|
|
25
|
+
workspaceRoot,
|
|
26
|
+
interactive = Boolean(input.isTTY && output.isTTY),
|
|
27
|
+
runOrchestratorInkImpl = defaultRunOrchestratorInk
|
|
28
|
+
}) {
|
|
29
|
+
if (!interactive) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Non-interactive shell requires an explicit command. Try ${formatCliCommand("help")} or ${formatCliCommand("setup --yes")}.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!canUseOrchestratorShell({ interactive })) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Interactive shell requires a capable TTY. Use ${formatCliCommand("setup --simple")} or explicit commands.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const homeDir = resolveHomeDir();
|
|
42
|
+
const outcome = await runOrchestratorInkImpl({
|
|
43
|
+
homeDir,
|
|
44
|
+
workspaceRoot,
|
|
45
|
+
packageRoot,
|
|
46
|
+
packageName: packageManifest.name,
|
|
47
|
+
cliVersion: packageManifest.version
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (outcome.error) {
|
|
51
|
+
throw outcome.error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (outcome.cancelled) {
|
|
55
|
+
return { cancelled: true, wrote: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (outcome.confirmed && outcome.action === PLAN_ACTIONS.SETUP) {
|
|
59
|
+
const setupOutcome = await runGlobalSetup(
|
|
60
|
+
{
|
|
61
|
+
cwd: workspaceRoot,
|
|
62
|
+
adapters: null,
|
|
63
|
+
components: null,
|
|
64
|
+
noDefaultComponents: false,
|
|
65
|
+
dryRun: false,
|
|
66
|
+
yes: false,
|
|
67
|
+
confirm: false,
|
|
68
|
+
preflight: true,
|
|
69
|
+
preflightExplicit: false,
|
|
70
|
+
yesExplicit: false,
|
|
71
|
+
confirmExplicit: false,
|
|
72
|
+
json: false,
|
|
73
|
+
interactive: true,
|
|
74
|
+
simple: false
|
|
75
|
+
},
|
|
76
|
+
packageManifest,
|
|
77
|
+
packageRoot
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
cancelled: Boolean(setupOutcome.cancelled),
|
|
82
|
+
wrote: !setupOutcome.cancelled && !setupOutcome.result?.dryRun,
|
|
83
|
+
action: PLAN_ACTIONS.SETUP,
|
|
84
|
+
setupOutcome
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { cancelled: false, wrote: false, action: outcome.action ?? null };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function runOrchestratorDiagnostics({
|
|
92
|
+
homeDir,
|
|
93
|
+
workspaceRoot,
|
|
94
|
+
packageName,
|
|
95
|
+
packageRoot,
|
|
96
|
+
cliVersion,
|
|
97
|
+
json = false
|
|
98
|
+
}) {
|
|
99
|
+
const diagnostics = await buildReadOnlyDiagnostics({
|
|
100
|
+
homeDir,
|
|
101
|
+
workspaceRoot,
|
|
102
|
+
packageName,
|
|
103
|
+
packageRoot,
|
|
104
|
+
cliVersion
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (json) {
|
|
108
|
+
return diagnostics;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(commandHeader("orchestrator — agent capability diagnostics"));
|
|
112
|
+
console.log(`Home: ${homeDir}`);
|
|
113
|
+
console.log(`Workspace: ${workspaceRoot}`);
|
|
114
|
+
console.log("");
|
|
115
|
+
|
|
116
|
+
for (const capability of diagnostics.capabilities) {
|
|
117
|
+
console.log(
|
|
118
|
+
` ${capability.label.padEnd(14)} ${capability.state.padEnd(14)} detected=${capability.detected ? "yes" : "no"}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log("");
|
|
123
|
+
console.log("Recommendations:");
|
|
124
|
+
for (const recommendation of diagnostics.recommendations) {
|
|
125
|
+
console.log(` - ${recommendation}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return diagnostics;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function commandHeader(title) {
|
|
132
|
+
return `${BRAND.displayName} ${title}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function assertPlanExecution(plan, { confirmed = false } = {}) {
|
|
136
|
+
if (!shouldExecutePlan(plan, { confirmed })) {
|
|
137
|
+
throw new Error("Plan declined. No writes or installations were performed.");
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/global/paths.js
CHANGED
|
@@ -15,6 +15,7 @@ export function harnessHomePaths(homeDir) {
|
|
|
15
15
|
root,
|
|
16
16
|
statePath: join(root, "state.json"),
|
|
17
17
|
policyPath: join(root, "policy.json"),
|
|
18
|
+
profilePath: join(root, "profile.json"),
|
|
18
19
|
historyPath: join(root, "history.jsonl"),
|
|
19
20
|
coreDir: join(root, "core"),
|
|
20
21
|
backupsDir: join(root, "backups")
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { harnessHomePaths } from "./paths.js";
|
|
5
|
+
import { AGENT_CAPABILITY_IDS } from "./agent-capabilities/index.js";
|
|
6
|
+
|
|
7
|
+
export const PROFILE_KEYS = new Set([
|
|
8
|
+
"coordinator",
|
|
9
|
+
"defaultAgents",
|
|
10
|
+
"defaultComponents",
|
|
11
|
+
"applyMode"
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_PROFILE = {
|
|
15
|
+
coordinator: null,
|
|
16
|
+
defaultAgents: "detected",
|
|
17
|
+
defaultComponents: null,
|
|
18
|
+
applyMode: "prompt"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const APPLY_MODES = new Set(["prompt", "confirm"]);
|
|
22
|
+
|
|
23
|
+
export function getGlobalProfilePath(homeDir) {
|
|
24
|
+
return join(harnessHomePaths(homeDir).root, "profile.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getProjectProfilePath(workspaceRoot) {
|
|
28
|
+
return join(workspaceRoot, ".harness", "kairo.json");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function loadProfileFile(filePath) {
|
|
32
|
+
if (!existsSync(filePath)) return null;
|
|
33
|
+
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error(`Invalid profile file at ${filePath}: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
42
|
+
throw new Error(`Invalid profile file at ${filePath}: expected a JSON object.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function loadGlobalProfile(homeDir) {
|
|
49
|
+
return loadProfileFile(getGlobalProfilePath(homeDir));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function loadProjectProfile(workspaceRoot) {
|
|
53
|
+
return loadProfileFile(getProjectProfilePath(workspaceRoot));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function resolveProfile({ homeDir, workspaceRoot }) {
|
|
57
|
+
const globalRaw = await loadGlobalProfile(homeDir);
|
|
58
|
+
const projectRaw = await loadProjectProfile(workspaceRoot);
|
|
59
|
+
|
|
60
|
+
const merged = {
|
|
61
|
+
...DEFAULT_PROFILE,
|
|
62
|
+
...(globalRaw ?? {}),
|
|
63
|
+
...(projectRaw ?? {})
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
validateProfile(merged);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
profile: merged,
|
|
70
|
+
sources: {
|
|
71
|
+
global: globalRaw ? getGlobalProfilePath(homeDir) : null,
|
|
72
|
+
project: projectRaw ? getProjectProfilePath(workspaceRoot) : null
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildProfileJson(resolved) {
|
|
78
|
+
const { profile, sources } = resolved;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
coordinator: profile.coordinator,
|
|
82
|
+
defaultAgents: profile.defaultAgents,
|
|
83
|
+
defaultComponents: profile.defaultComponents,
|
|
84
|
+
applyMode: profile.applyMode,
|
|
85
|
+
sources: {
|
|
86
|
+
global: sources.global,
|
|
87
|
+
project: sources.project,
|
|
88
|
+
precedence: "project overrides global overrides defaults"
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function saveGlobalProfile(homeDir, profile) {
|
|
94
|
+
validateProfile(profile);
|
|
95
|
+
const { root } = harnessHomePaths(homeDir);
|
|
96
|
+
const profilePath = getGlobalProfilePath(homeDir);
|
|
97
|
+
await mkdir(root, { recursive: true });
|
|
98
|
+
await writeFile(profilePath, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
|
|
99
|
+
return profilePath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resolveProfileAgents(profile, detectedAgentIds) {
|
|
103
|
+
if (profile.defaultAgents === "detected") {
|
|
104
|
+
return detectedAgentIds.length > 0 ? [...detectedAgentIds] : [...AGENT_CAPABILITY_IDS];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (profile.defaultAgents === "all") {
|
|
108
|
+
return [...AGENT_CAPABILITY_IDS];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (Array.isArray(profile.defaultAgents)) {
|
|
112
|
+
return profile.defaultAgents.filter((id) => AGENT_CAPABILITY_IDS.includes(id));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [...AGENT_CAPABILITY_IDS];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateProfile(profile) {
|
|
119
|
+
if (profile.coordinator != null && !AGENT_CAPABILITY_IDS.includes(profile.coordinator)) {
|
|
120
|
+
throw new Error(`Unknown coordinator "${profile.coordinator}". Use ${AGENT_CAPABILITY_IDS.join(", ")} or null.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!APPLY_MODES.has(profile.applyMode)) {
|
|
124
|
+
throw new Error(`Invalid applyMode "${profile.applyMode}". Use prompt or confirm.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
profile.defaultAgents !== "detected"
|
|
129
|
+
&& profile.defaultAgents !== "all"
|
|
130
|
+
&& !Array.isArray(profile.defaultAgents)
|
|
131
|
+
) {
|
|
132
|
+
throw new Error('Profile defaultAgents must be "detected", "all", or an agent list.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (Array.isArray(profile.defaultAgents)) {
|
|
136
|
+
for (const agent of profile.defaultAgents) {
|
|
137
|
+
if (!AGENT_CAPABILITY_IDS.includes(agent)) {
|
|
138
|
+
throw new Error(`Unknown agent "${agent}" in profile.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (profile.defaultComponents != null && !Array.isArray(profile.defaultComponents)) {
|
|
144
|
+
throw new Error("Profile defaultComponents must be an array or null.");
|
|
145
|
+
}
|
|
146
|
+
}
|