@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.
- package/README.md +23 -0
- package/package.json +1 -1
- package/src/cli.js +113 -4
- package/src/global/action-planner.js +230 -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 +224 -0
- package/src/global/ink/orchestrator-state.js +107 -0
- package/src/global/ink/run-orchestrator-ink.js +29 -0
- package/src/global/intelligence/backends/custom-http.js +175 -0
- package/src/global/intelligence/backends/ollama.js +164 -0
- package/src/global/intelligence/backends/openrouter.js +198 -0
- package/src/global/intelligence/context-compiler.js +338 -0
- package/src/global/intelligence/custom-url.js +82 -0
- package/src/global/intelligence/http.js +63 -0
- package/src/global/intelligence/index.js +38 -0
- package/src/global/intelligence/orchestrate.js +189 -0
- package/src/global/intelligence/registry.js +77 -0
- package/src/global/intelligence/router.js +191 -0
- package/src/global/intelligence/types.js +99 -0
- package/src/global/intelligence-cli.js +323 -0
- package/src/global/orchestrator.js +150 -0
- package/src/global/paths.js +1 -0
- package/src/global/profile.js +297 -0
package/README.md
CHANGED
|
@@ -161,6 +161,14 @@ kairo install --agents all
|
|
|
161
161
|
kairo install --agents cursor,codex --components orchestrator,sdd-core
|
|
162
162
|
kairo doctor
|
|
163
163
|
kairo doctor --json
|
|
164
|
+
kairo orchestrator --json
|
|
165
|
+
kairo intelligence status
|
|
166
|
+
kairo intelligence models
|
|
167
|
+
kairo intelligence context --json
|
|
168
|
+
kairo intelligence route --task "explain architecture"
|
|
169
|
+
kairo intelligence ask --prompt "Summarize project risks" --json
|
|
170
|
+
# Cloud (OpenRouter/free) only after explicit consent + confirm:
|
|
171
|
+
# OPENROUTER_API_KEY=... kairo intelligence ask --prompt "..." --cloud-consent --yes
|
|
164
172
|
kairo update # technical alias; prefer sync
|
|
165
173
|
kairo detect
|
|
166
174
|
kairo components
|
|
@@ -180,6 +188,21 @@ kairo uninstall
|
|
|
180
188
|
kairo install --scope=workspace # opt-in / legacy
|
|
181
189
|
```
|
|
182
190
|
|
|
191
|
+
### Intelligence layer (0.2.0)
|
|
192
|
+
|
|
193
|
+
Kairo owns **Harness Engineering** governance: compile relevant project context, route to a backend, and require human confirmation for cloud transmission. It does not store credentials.
|
|
194
|
+
|
|
195
|
+
| Backend | Detection | Invoke |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| Ollama | `GET $OLLAMA_HOST/api/tags` (default `http://127.0.0.1:11434`) | Local chat |
|
|
198
|
+
| OpenRouter | `OPENROUTER_API_KEY` in env | `openrouter/free` after `--cloud-consent` + `--yes` |
|
|
199
|
+
| Custom HTTP | Profile `customProviders` (`baseUrl`, `modelId`, optional local-only `apiKeyEnv`) | OpenAI-compatible `/chat/completions` |
|
|
200
|
+
|
|
201
|
+
Routing order: user override → Ollama → OpenRouter free (consent) → diagnostics mode.
|
|
202
|
+
|
|
203
|
+
Private paths (`.env`, secrets, keys) are excluded from context packs unless `--include-private`.
|
|
204
|
+
Remote custom providers require explicit cloud consent and cannot receive an `apiKeyEnv` credential in 0.2.0; use a built-in provider or a local custom endpoint for env-backed authentication.
|
|
205
|
+
|
|
183
206
|
Legacy CLI aliases (backward compatible): `harness`, `agentic-harness`, `sgs-harness`, `harness-sgs`
|
|
184
207
|
|
|
185
208
|
`kairo help` lists commands and JSON support; longer examples live in this README.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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,8 @@ 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";
|
|
34
|
+
import { runIntelligenceCli } from "./global/intelligence-cli.js";
|
|
33
35
|
import {
|
|
34
36
|
LEGACY_PACKAGE_NAME,
|
|
35
37
|
PACKAGE_NAME,
|
|
@@ -66,6 +68,27 @@ export async function runCli(argv) {
|
|
|
66
68
|
const invoke = resolveSuggestedInvocation(packageManifest.name);
|
|
67
69
|
|
|
68
70
|
switch (command) {
|
|
71
|
+
case "shell":
|
|
72
|
+
await runOrchestratorShell({
|
|
73
|
+
packageRoot,
|
|
74
|
+
packageManifest,
|
|
75
|
+
workspaceRoot: optionsWithPolicy.cwd,
|
|
76
|
+
interactive: optionsWithPolicy.interactive
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
case "orchestrator":
|
|
80
|
+
await runOrchestratorDiagnostics({
|
|
81
|
+
homeDir: resolveHomeDir(),
|
|
82
|
+
workspaceRoot: optionsWithPolicy.cwd,
|
|
83
|
+
packageName: packageManifest.name,
|
|
84
|
+
packageRoot,
|
|
85
|
+
cliVersion: packageManifest.version,
|
|
86
|
+
json: optionsWithPolicy.json
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
case "intelligence":
|
|
90
|
+
await runIntelligenceCli(optionsWithPolicy, packageManifest);
|
|
91
|
+
return;
|
|
69
92
|
case "setup":
|
|
70
93
|
await runGlobalSetup(optionsWithPolicy, packageManifest, packageRoot);
|
|
71
94
|
return;
|
|
@@ -219,7 +242,37 @@ function resolveImplicitCommand(args) {
|
|
|
219
242
|
if (argsWantsWorkspaceScope(args)) {
|
|
220
243
|
return "init";
|
|
221
244
|
}
|
|
222
|
-
|
|
245
|
+
if (hasImplicitSetupFlags(args)) {
|
|
246
|
+
return "setup";
|
|
247
|
+
}
|
|
248
|
+
return "shell";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function hasImplicitSetupFlags(args) {
|
|
252
|
+
const setupFlags = new Set([
|
|
253
|
+
"--dry-run",
|
|
254
|
+
"--yes",
|
|
255
|
+
"-y",
|
|
256
|
+
"--confirm",
|
|
257
|
+
"--simple",
|
|
258
|
+
"--no-preflight",
|
|
259
|
+
"--all-adapters",
|
|
260
|
+
"--no-default-components",
|
|
261
|
+
"--detect"
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
265
|
+
const arg = args[index];
|
|
266
|
+
if (setupFlags.has(arg)) return true;
|
|
267
|
+
if (arg.startsWith("--mode=")) return true;
|
|
268
|
+
if (arg.startsWith("--adapters=") || arg.startsWith("--agents=")) return true;
|
|
269
|
+
if (arg.startsWith("--components=")) return true;
|
|
270
|
+
if (arg === "--mode" || arg === "--adapters" || arg === "--agents" || arg === "--components") {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return false;
|
|
223
276
|
}
|
|
224
277
|
|
|
225
278
|
function argsWantsWorkspaceScope(args) {
|
|
@@ -275,7 +328,13 @@ export function parseArgs(argv) {
|
|
|
275
328
|
simple: false,
|
|
276
329
|
help: false,
|
|
277
330
|
version: false,
|
|
278
|
-
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
331
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
332
|
+
intelligenceAction: null,
|
|
333
|
+
intelligenceTask: null,
|
|
334
|
+
intelligencePrompt: null,
|
|
335
|
+
intelligencePaths: [],
|
|
336
|
+
includePrivate: false,
|
|
337
|
+
cloudConsent: false
|
|
279
338
|
};
|
|
280
339
|
|
|
281
340
|
if (command === "components") {
|
|
@@ -290,6 +349,10 @@ export function parseArgs(argv) {
|
|
|
290
349
|
parseHistoryAction(args, options);
|
|
291
350
|
}
|
|
292
351
|
|
|
352
|
+
if (command === "intelligence") {
|
|
353
|
+
parseIntelligenceAction(args, options);
|
|
354
|
+
}
|
|
355
|
+
|
|
293
356
|
for (let index = 0; index < args.length; index += 1) {
|
|
294
357
|
const arg = args[index];
|
|
295
358
|
|
|
@@ -351,6 +414,14 @@ export function parseArgs(argv) {
|
|
|
351
414
|
else if (arg === "--out") options.outPath = resolve(args[++index]);
|
|
352
415
|
else if (arg.startsWith("--out=")) options.outPath = resolve(arg.slice("--out=".length));
|
|
353
416
|
else if (arg === "--simple") options.simple = true;
|
|
417
|
+
else if (arg === "--task") options.intelligenceTask = args[++index];
|
|
418
|
+
else if (arg.startsWith("--task=")) options.intelligenceTask = arg.slice("--task=".length);
|
|
419
|
+
else if (arg === "--prompt") options.intelligencePrompt = args[++index];
|
|
420
|
+
else if (arg.startsWith("--prompt=")) options.intelligencePrompt = arg.slice("--prompt=".length);
|
|
421
|
+
else if (arg === "--paths") options.intelligencePaths = parsePathList(args[++index]);
|
|
422
|
+
else if (arg.startsWith("--paths=")) options.intelligencePaths = parsePathList(arg.slice("--paths=".length));
|
|
423
|
+
else if (arg === "--include-private") options.includePrivate = true;
|
|
424
|
+
else if (arg === "--cloud-consent") options.cloudConsent = true;
|
|
354
425
|
else if (arg === "--help" || arg === "-h") options.help = true;
|
|
355
426
|
else if (arg === "--version" || arg === "-v") options.version = true;
|
|
356
427
|
else throw new Error(`Unknown option "${arg}".`);
|
|
@@ -441,6 +512,31 @@ function parseHistoryAction(args, options) {
|
|
|
441
512
|
throw new Error(`Unknown history action "${action}". Use last or omit for the full log.`);
|
|
442
513
|
}
|
|
443
514
|
|
|
515
|
+
function parseIntelligenceAction(args, options) {
|
|
516
|
+
const action = args[0];
|
|
517
|
+
if (!action || action.startsWith("-")) {
|
|
518
|
+
options.intelligenceAction = "status";
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
args.shift();
|
|
523
|
+
const allowed = new Set(["status", "models", "context", "route", "ask"]);
|
|
524
|
+
if (!allowed.has(action)) {
|
|
525
|
+
throw new Error(`Unknown intelligence action "${action}". Use status, models, context, route, or ask.`);
|
|
526
|
+
}
|
|
527
|
+
options.intelligenceAction = action;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function parsePathList(value) {
|
|
531
|
+
if (!value) return [];
|
|
532
|
+
return [...new Set(
|
|
533
|
+
value
|
|
534
|
+
.split(",")
|
|
535
|
+
.map((item) => item.trim())
|
|
536
|
+
.filter(Boolean)
|
|
537
|
+
)];
|
|
538
|
+
}
|
|
539
|
+
|
|
444
540
|
function parsePositiveInt(value, label) {
|
|
445
541
|
const parsed = Number.parseInt(value, 10);
|
|
446
542
|
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
@@ -461,6 +557,9 @@ function normalizeCommand(command) {
|
|
|
461
557
|
if (!command) return "install";
|
|
462
558
|
|
|
463
559
|
if (command === "install" || command === "i") return "install";
|
|
560
|
+
if (command === "shell") return "shell";
|
|
561
|
+
if (command === "orchestrator") return "orchestrator";
|
|
562
|
+
if (command === "intelligence" || command === "intel") return "intelligence";
|
|
464
563
|
if (command === "setup") return "setup";
|
|
465
564
|
if (command === "status") return "status";
|
|
466
565
|
if (command === "sync") return "sync";
|
|
@@ -517,8 +616,13 @@ sections, components, backups, and drift repair under ~/.harness.
|
|
|
517
616
|
Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
|
|
518
617
|
|
|
519
618
|
Usage:
|
|
520
|
-
${cli}
|
|
619
|
+
${cli} Interactive orchestrator shell (TTY)
|
|
620
|
+
${cli} --dry-run Setup dry-run (scriptable)
|
|
521
621
|
${cli} --version
|
|
622
|
+
${cli} shell Interactive orchestrator shell (TTY)
|
|
623
|
+
${cli} orchestrator [--json] Read-only agent capability diagnostics
|
|
624
|
+
${cli} intelligence [status|models|context|route|ask] [--json]
|
|
625
|
+
${cli} intelligence ask --prompt "..." [--cloud-consent] [--yes] [--paths a,b]
|
|
522
626
|
${cli} setup [--dry-run] [--yes] [--confirm] [--simple] [--no-preflight] [--agents <list|all>] [--components <list>]
|
|
523
627
|
${cli} status [--json]
|
|
524
628
|
${cli} sync [--dry-run] [--yes] [--confirm] [--json] [--no-preflight]
|
|
@@ -552,7 +656,12 @@ Scopes:
|
|
|
552
656
|
Explicit --scope=workspace only.
|
|
553
657
|
|
|
554
658
|
Commands:
|
|
555
|
-
|
|
659
|
+
shell Interactive Ink orchestrator (TTY). Bare ${cli} opens this in TTY sessions.
|
|
660
|
+
orchestrator Read-only capability registry diagnostics (--json supported).
|
|
661
|
+
intelligence Harness Engineering layer: backends, context packs, routing, budgets.
|
|
662
|
+
Local-first (Ollama). Cloud (OpenRouter/free) only with --cloud-consent.
|
|
663
|
+
Credentials via env only (OPENROUTER_API_KEY, OLLAMA_HOST). Never stored.
|
|
664
|
+
setup Managed ecosystem setup. Interactive Ink UI (TTY). Use --simple for Clack prompts.
|
|
556
665
|
status Control panel: agents, components, drift, backups, next action.
|
|
557
666
|
sync Converge managed content (repair drift), then show status.
|
|
558
667
|
upgrade Preview or apply ecosystem updates (apply requires --yes).
|
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
import {
|
|
6
|
+
inspectIntelligenceBackends,
|
|
7
|
+
summarizeIntelligenceBackends,
|
|
8
|
+
resolveRoutingDecision
|
|
9
|
+
} from "./intelligence/index.js";
|
|
10
|
+
|
|
11
|
+
export const PLAN_ACTIONS = {
|
|
12
|
+
DIAGNOSE: "diagnose",
|
|
13
|
+
SETUP: "setup",
|
|
14
|
+
SYNC: "sync",
|
|
15
|
+
INSTALL: "install",
|
|
16
|
+
STATUS: "status"
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function buildReadOnlyDiagnostics({
|
|
20
|
+
homeDir,
|
|
21
|
+
workspaceRoot,
|
|
22
|
+
packageName,
|
|
23
|
+
packageRoot,
|
|
24
|
+
cliVersion,
|
|
25
|
+
env = process.env,
|
|
26
|
+
fetchImpl = globalThis.fetch
|
|
27
|
+
}) {
|
|
28
|
+
const [{ profile, sources }, capabilities] = await Promise.all([
|
|
29
|
+
resolveProfile({ homeDir, workspaceRoot }),
|
|
30
|
+
inspectAllCapabilities({ homeDir, workspaceRoot, packageName })
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const intelligence = await inspectIntelligenceBackends({
|
|
34
|
+
env,
|
|
35
|
+
fetchImpl,
|
|
36
|
+
customProviders: profile.customProviders ?? []
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const detectedIds = capabilities.filter((entry) => entry.detected).map((entry) => entry.id);
|
|
40
|
+
const profileAgents = resolveProfileAgents(profile, detectedIds);
|
|
41
|
+
const diagnostics = summarizeDiagnostics(capabilities);
|
|
42
|
+
const intelligenceSummary = summarizeIntelligenceBackends(intelligence);
|
|
43
|
+
const routingPreview = resolveRoutingDecision({
|
|
44
|
+
backends: intelligence,
|
|
45
|
+
profile,
|
|
46
|
+
// Session consent only — profile.cloudConsent is a preference, never live authorization.
|
|
47
|
+
cloudConsent: false,
|
|
48
|
+
tokenBudget: profile.tokenBudget
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
readOnly: true,
|
|
53
|
+
cliVersion,
|
|
54
|
+
profile: { ...profile, sources },
|
|
55
|
+
capabilities,
|
|
56
|
+
profileAgents,
|
|
57
|
+
diagnostics,
|
|
58
|
+
intelligence: {
|
|
59
|
+
backends: intelligence,
|
|
60
|
+
summary: intelligenceSummary,
|
|
61
|
+
routingPreview
|
|
62
|
+
},
|
|
63
|
+
recommendations: buildDiagnosticRecommendations({
|
|
64
|
+
capabilities,
|
|
65
|
+
profile,
|
|
66
|
+
diagnostics,
|
|
67
|
+
intelligenceSummary,
|
|
68
|
+
routingPreview
|
|
69
|
+
})
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function buildActionPlan({
|
|
74
|
+
action,
|
|
75
|
+
homeDir,
|
|
76
|
+
workspaceRoot,
|
|
77
|
+
packageName,
|
|
78
|
+
options = {}
|
|
79
|
+
}) {
|
|
80
|
+
const diagnostics = await buildReadOnlyDiagnostics({
|
|
81
|
+
homeDir,
|
|
82
|
+
workspaceRoot,
|
|
83
|
+
packageName,
|
|
84
|
+
packageRoot: options.packageRoot ?? null,
|
|
85
|
+
cliVersion: options.cliVersion ?? null
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const steps = [];
|
|
89
|
+
const warnings = [];
|
|
90
|
+
|
|
91
|
+
switch (action) {
|
|
92
|
+
case PLAN_ACTIONS.DIAGNOSE:
|
|
93
|
+
case PLAN_ACTIONS.STATUS:
|
|
94
|
+
return {
|
|
95
|
+
action,
|
|
96
|
+
readOnly: true,
|
|
97
|
+
requiresConfirmation: false,
|
|
98
|
+
steps: ["Show ecosystem diagnostics (read-only)."],
|
|
99
|
+
diagnostics,
|
|
100
|
+
warnings
|
|
101
|
+
};
|
|
102
|
+
case PLAN_ACTIONS.SETUP:
|
|
103
|
+
case PLAN_ACTIONS.INSTALL:
|
|
104
|
+
steps.push(`Target agents: ${diagnostics.profileAgents.join(", ")}`);
|
|
105
|
+
steps.push("Preview managed section changes under ~/.harness and agent config files.");
|
|
106
|
+
steps.push("Create backups before writing managed content.");
|
|
107
|
+
if (options.dryRun) {
|
|
108
|
+
steps.push("Dry run: no files will be written.");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
action,
|
|
112
|
+
readOnly: Boolean(options.dryRun),
|
|
113
|
+
requiresConfirmation: !options.dryRun,
|
|
114
|
+
steps,
|
|
115
|
+
diagnostics,
|
|
116
|
+
warnings: collectCapabilityWarnings(diagnostics.capabilities)
|
|
117
|
+
};
|
|
118
|
+
case PLAN_ACTIONS.SYNC:
|
|
119
|
+
steps.push("Compare managed content against bundled assets.");
|
|
120
|
+
steps.push("Repair drift in agent config files when differences are found.");
|
|
121
|
+
if (options.dryRun) {
|
|
122
|
+
steps.push("Dry run: show planned repairs only.");
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
action,
|
|
126
|
+
readOnly: Boolean(options.dryRun),
|
|
127
|
+
requiresConfirmation: !options.dryRun,
|
|
128
|
+
steps,
|
|
129
|
+
diagnostics,
|
|
130
|
+
warnings: collectCapabilityWarnings(diagnostics.capabilities)
|
|
131
|
+
};
|
|
132
|
+
default: {
|
|
133
|
+
const _exhaustive = action;
|
|
134
|
+
throw new Error(`Unknown plan action "${_exhaustive}".`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function shouldExecutePlan(plan, { confirmed = false } = {}) {
|
|
140
|
+
if (plan.readOnly) return true;
|
|
141
|
+
return confirmed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function formatActionPlan(plan) {
|
|
145
|
+
const lines = [];
|
|
146
|
+
lines.push(`Action: ${plan.action}`);
|
|
147
|
+
lines.push(`Mode: ${plan.readOnly ? "read-only" : "write"}`);
|
|
148
|
+
lines.push(`Confirmation required: ${plan.requiresConfirmation ? "yes" : "no"}`);
|
|
149
|
+
lines.push("");
|
|
150
|
+
lines.push("Steps:");
|
|
151
|
+
for (const step of plan.steps) {
|
|
152
|
+
lines.push(` - ${step}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (plan.warnings.length > 0) {
|
|
156
|
+
lines.push("");
|
|
157
|
+
lines.push("Warnings:");
|
|
158
|
+
for (const warning of plan.warnings) {
|
|
159
|
+
lines.push(` - ${warning}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (plan.diagnostics.recommendations.length > 0) {
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push("Recommendations:");
|
|
166
|
+
for (const recommendation of plan.diagnostics.recommendations) {
|
|
167
|
+
lines.push(` - ${recommendation}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function summarizeDiagnostics(capabilities) {
|
|
175
|
+
return {
|
|
176
|
+
detected: capabilities.filter((entry) => entry.detected).length,
|
|
177
|
+
available: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.AVAILABLE).length,
|
|
178
|
+
unknown: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.UNKNOWN).length,
|
|
179
|
+
errors: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.ERROR).length
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildDiagnosticRecommendations({
|
|
184
|
+
capabilities,
|
|
185
|
+
profile,
|
|
186
|
+
diagnostics,
|
|
187
|
+
intelligenceSummary,
|
|
188
|
+
routingPreview
|
|
189
|
+
}) {
|
|
190
|
+
const recommendations = [];
|
|
191
|
+
|
|
192
|
+
if (diagnostics.detected === 0) {
|
|
193
|
+
recommendations.push(`No agents detected. Run ${formatCliCommand("detect")} or install a supported agent CLI.`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (profile.coordinator) {
|
|
197
|
+
const coordinator = capabilities.find((entry) => entry.id === profile.coordinator);
|
|
198
|
+
if (!coordinator?.detected && coordinator?.state === CAPABILITY_STATES.UNKNOWN) {
|
|
199
|
+
recommendations.push(`Profile coordinator "${profile.coordinator}" is not detected on this machine.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const capability of capabilities) {
|
|
204
|
+
if (capability.recommendation) {
|
|
205
|
+
recommendations.push(`${capability.label}: ${capability.recommendation}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!intelligenceSummary?.localAvailable && !intelligenceSummary?.cloudAuthenticated) {
|
|
210
|
+
recommendations.push(
|
|
211
|
+
"No intelligence backend available. Start Ollama or set OPENROUTER_API_KEY (env only). Diagnostics mode remains available."
|
|
212
|
+
);
|
|
213
|
+
} else if (!intelligenceSummary?.localAvailable && intelligenceSummary?.cloudAuthenticated) {
|
|
214
|
+
recommendations.push(
|
|
215
|
+
`OpenRouter is authenticated. Cloud invoke still requires session flags: ${formatCliCommand("intelligence ask --cloud-consent --yes")}.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (routingPreview && !routingPreview.canInvoke) {
|
|
220
|
+
recommendations.push(`Routing: ${routingPreview.reason}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return [...new Set(recommendations)];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function collectCapabilityWarnings(capabilities) {
|
|
227
|
+
return capabilities
|
|
228
|
+
.filter((entry) => entry.state === CAPABILITY_STATES.ERROR || entry.state === CAPABILITY_STATES.UNKNOWN)
|
|
229
|
+
.map((entry) => `${entry.label} is ${entry.state}${entry.error ? ` (${entry.error})` : ""}.`);
|
|
230
|
+
}
|
|
@@ -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
|
+
}
|