@kal-elsam/kairo-runtime 0.1.5 → 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 +54 -1
- package/src/global/action-planner.js +55 -3
- package/src/global/ink/orchestrator-app.js +20 -5
- package/src/global/ink/orchestrator-state.js +31 -2
- 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 +11 -0
- package/src/global/profile.js +153 -2
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
|
@@ -31,6 +31,7 @@ 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
33
|
import { runOrchestratorDiagnostics, runOrchestratorShell } from "./global/orchestrator.js";
|
|
34
|
+
import { runIntelligenceCli } from "./global/intelligence-cli.js";
|
|
34
35
|
import {
|
|
35
36
|
LEGACY_PACKAGE_NAME,
|
|
36
37
|
PACKAGE_NAME,
|
|
@@ -85,6 +86,9 @@ export async function runCli(argv) {
|
|
|
85
86
|
json: optionsWithPolicy.json
|
|
86
87
|
});
|
|
87
88
|
return;
|
|
89
|
+
case "intelligence":
|
|
90
|
+
await runIntelligenceCli(optionsWithPolicy, packageManifest);
|
|
91
|
+
return;
|
|
88
92
|
case "setup":
|
|
89
93
|
await runGlobalSetup(optionsWithPolicy, packageManifest, packageRoot);
|
|
90
94
|
return;
|
|
@@ -324,7 +328,13 @@ export function parseArgs(argv) {
|
|
|
324
328
|
simple: false,
|
|
325
329
|
help: false,
|
|
326
330
|
version: false,
|
|
327
|
-
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
|
|
328
338
|
};
|
|
329
339
|
|
|
330
340
|
if (command === "components") {
|
|
@@ -339,6 +349,10 @@ export function parseArgs(argv) {
|
|
|
339
349
|
parseHistoryAction(args, options);
|
|
340
350
|
}
|
|
341
351
|
|
|
352
|
+
if (command === "intelligence") {
|
|
353
|
+
parseIntelligenceAction(args, options);
|
|
354
|
+
}
|
|
355
|
+
|
|
342
356
|
for (let index = 0; index < args.length; index += 1) {
|
|
343
357
|
const arg = args[index];
|
|
344
358
|
|
|
@@ -400,6 +414,14 @@ export function parseArgs(argv) {
|
|
|
400
414
|
else if (arg === "--out") options.outPath = resolve(args[++index]);
|
|
401
415
|
else if (arg.startsWith("--out=")) options.outPath = resolve(arg.slice("--out=".length));
|
|
402
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;
|
|
403
425
|
else if (arg === "--help" || arg === "-h") options.help = true;
|
|
404
426
|
else if (arg === "--version" || arg === "-v") options.version = true;
|
|
405
427
|
else throw new Error(`Unknown option "${arg}".`);
|
|
@@ -490,6 +512,31 @@ function parseHistoryAction(args, options) {
|
|
|
490
512
|
throw new Error(`Unknown history action "${action}". Use last or omit for the full log.`);
|
|
491
513
|
}
|
|
492
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
|
+
|
|
493
540
|
function parsePositiveInt(value, label) {
|
|
494
541
|
const parsed = Number.parseInt(value, 10);
|
|
495
542
|
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
@@ -512,6 +559,7 @@ function normalizeCommand(command) {
|
|
|
512
559
|
if (command === "install" || command === "i") return "install";
|
|
513
560
|
if (command === "shell") return "shell";
|
|
514
561
|
if (command === "orchestrator") return "orchestrator";
|
|
562
|
+
if (command === "intelligence" || command === "intel") return "intelligence";
|
|
515
563
|
if (command === "setup") return "setup";
|
|
516
564
|
if (command === "status") return "status";
|
|
517
565
|
if (command === "sync") return "sync";
|
|
@@ -573,6 +621,8 @@ Usage:
|
|
|
573
621
|
${cli} --version
|
|
574
622
|
${cli} shell Interactive orchestrator shell (TTY)
|
|
575
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]
|
|
576
626
|
${cli} setup [--dry-run] [--yes] [--confirm] [--simple] [--no-preflight] [--agents <list|all>] [--components <list>]
|
|
577
627
|
${cli} status [--json]
|
|
578
628
|
${cli} sync [--dry-run] [--yes] [--confirm] [--json] [--no-preflight]
|
|
@@ -608,6 +658,9 @@ Scopes:
|
|
|
608
658
|
Commands:
|
|
609
659
|
shell Interactive Ink orchestrator (TTY). Bare ${cli} opens this in TTY sessions.
|
|
610
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.
|
|
611
664
|
setup Managed ecosystem setup. Interactive Ink UI (TTY). Use --simple for Clack prompts.
|
|
612
665
|
status Control panel: agents, components, drift, backups, next action.
|
|
613
666
|
sync Converge managed content (repair drift), then show status.
|
|
@@ -2,6 +2,11 @@ import { CAPABILITY_STATES } from "./capability-states.js";
|
|
|
2
2
|
import { inspectAllCapabilities } from "./capability-registry.js";
|
|
3
3
|
import { resolveProfile, resolveProfileAgents } from "./profile.js";
|
|
4
4
|
import { formatCliCommand } from "./brand/cli.js";
|
|
5
|
+
import {
|
|
6
|
+
inspectIntelligenceBackends,
|
|
7
|
+
summarizeIntelligenceBackends,
|
|
8
|
+
resolveRoutingDecision
|
|
9
|
+
} from "./intelligence/index.js";
|
|
5
10
|
|
|
6
11
|
export const PLAN_ACTIONS = {
|
|
7
12
|
DIAGNOSE: "diagnose",
|
|
@@ -16,16 +21,32 @@ export async function buildReadOnlyDiagnostics({
|
|
|
16
21
|
workspaceRoot,
|
|
17
22
|
packageName,
|
|
18
23
|
packageRoot,
|
|
19
|
-
cliVersion
|
|
24
|
+
cliVersion,
|
|
25
|
+
env = process.env,
|
|
26
|
+
fetchImpl = globalThis.fetch
|
|
20
27
|
}) {
|
|
21
28
|
const [{ profile, sources }, capabilities] = await Promise.all([
|
|
22
29
|
resolveProfile({ homeDir, workspaceRoot }),
|
|
23
30
|
inspectAllCapabilities({ homeDir, workspaceRoot, packageName })
|
|
24
31
|
]);
|
|
25
32
|
|
|
33
|
+
const intelligence = await inspectIntelligenceBackends({
|
|
34
|
+
env,
|
|
35
|
+
fetchImpl,
|
|
36
|
+
customProviders: profile.customProviders ?? []
|
|
37
|
+
});
|
|
38
|
+
|
|
26
39
|
const detectedIds = capabilities.filter((entry) => entry.detected).map((entry) => entry.id);
|
|
27
40
|
const profileAgents = resolveProfileAgents(profile, detectedIds);
|
|
28
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
|
+
});
|
|
29
50
|
|
|
30
51
|
return {
|
|
31
52
|
readOnly: true,
|
|
@@ -34,7 +55,18 @@ export async function buildReadOnlyDiagnostics({
|
|
|
34
55
|
capabilities,
|
|
35
56
|
profileAgents,
|
|
36
57
|
diagnostics,
|
|
37
|
-
|
|
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
|
+
})
|
|
38
70
|
};
|
|
39
71
|
}
|
|
40
72
|
|
|
@@ -148,7 +180,13 @@ function summarizeDiagnostics(capabilities) {
|
|
|
148
180
|
};
|
|
149
181
|
}
|
|
150
182
|
|
|
151
|
-
function buildDiagnosticRecommendations({
|
|
183
|
+
function buildDiagnosticRecommendations({
|
|
184
|
+
capabilities,
|
|
185
|
+
profile,
|
|
186
|
+
diagnostics,
|
|
187
|
+
intelligenceSummary,
|
|
188
|
+
routingPreview
|
|
189
|
+
}) {
|
|
152
190
|
const recommendations = [];
|
|
153
191
|
|
|
154
192
|
if (diagnostics.detected === 0) {
|
|
@@ -168,6 +206,20 @@ function buildDiagnosticRecommendations({ capabilities, profile, diagnostics })
|
|
|
168
206
|
}
|
|
169
207
|
}
|
|
170
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
|
+
|
|
171
223
|
return [...new Set(recommendations)];
|
|
172
224
|
}
|
|
173
225
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ORCHESTRATOR_MENU,
|
|
8
8
|
ORCHESTRATOR_VIEWS,
|
|
9
9
|
formatAgentStatusLines,
|
|
10
|
+
formatIntelligenceLines,
|
|
10
11
|
formatPlanLines,
|
|
11
12
|
formatProfileLines
|
|
12
13
|
} from "./orchestrator-state.js";
|
|
@@ -143,7 +144,7 @@ export function OrchestratorApp({
|
|
|
143
144
|
|
|
144
145
|
return React.createElement(Box, { flexDirection: "column" },
|
|
145
146
|
React.createElement(Text, { bold: true, color: COLORS.accent }, `${BRAND.displayName} orchestrator`),
|
|
146
|
-
React.createElement(Text, { color: COLORS.muted }, "
|
|
147
|
+
React.createElement(Text, { color: COLORS.muted }, "Harness Engineering · local-first · cloud opt-in"),
|
|
147
148
|
React.createElement(Text, null, ""),
|
|
148
149
|
renderView({ view, diagnostics, profileJson, plan, menuIndex }),
|
|
149
150
|
React.createElement(Text, null, ""),
|
|
@@ -166,7 +167,12 @@ function renderView({ view, diagnostics, profileJson, plan, menuIndex }) {
|
|
|
166
167
|
React.createElement(Text, null, ""),
|
|
167
168
|
React.createElement(Text, { bold: true }, "Snapshot"),
|
|
168
169
|
React.createElement(Text, null, `Agents detected: ${diagnostics.diagnostics.detected}/${diagnostics.capabilities.length}`),
|
|
169
|
-
React.createElement(Text, null, `Available: ${diagnostics.diagnostics.available}`)
|
|
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
|
+
)
|
|
170
176
|
);
|
|
171
177
|
case ORCHESTRATOR_VIEWS.AGENTS:
|
|
172
178
|
return React.createElement(Box, { flexDirection: "column" },
|
|
@@ -174,6 +180,14 @@ function renderView({ view, diagnostics, profileJson, plan, menuIndex }) {
|
|
|
174
180
|
formatAgentStatusLines(diagnostics.capabilities)
|
|
175
181
|
.map((line) => React.createElement(Text, { key: line }, line))
|
|
176
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
|
+
);
|
|
177
191
|
case ORCHESTRATOR_VIEWS.PROFILE:
|
|
178
192
|
return React.createElement(Box, { flexDirection: "column" },
|
|
179
193
|
React.createElement(Text, { bold: true }, "Profile"),
|
|
@@ -190,10 +204,11 @@ function renderView({ view, diagnostics, profileJson, plan, menuIndex }) {
|
|
|
190
204
|
case ORCHESTRATOR_VIEWS.HELP:
|
|
191
205
|
return React.createElement(Box, { flexDirection: "column" },
|
|
192
206
|
React.createElement(Text, { bold: true }, "Help"),
|
|
193
|
-
React.createElement(Text, null, "Kairo coordinates installed agent CLIs
|
|
194
|
-
React.createElement(Text, null, "
|
|
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"),
|
|
195
210
|
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.")
|
|
211
|
+
React.createElement(Text, null, "Credentials are never stored by Kairo — use environment variables.")
|
|
197
212
|
);
|
|
198
213
|
default: {
|
|
199
214
|
const _exhaustive = view;
|
|
@@ -16,12 +16,14 @@ export const ORCHESTRATOR_VIEWS = {
|
|
|
16
16
|
PROFILE: "profile",
|
|
17
17
|
PLAN: "plan",
|
|
18
18
|
CONFIRM: "confirm",
|
|
19
|
-
HELP: "help"
|
|
19
|
+
HELP: "help",
|
|
20
|
+
INTELLIGENCE: "intelligence"
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export const ORCHESTRATOR_MENU = [
|
|
23
24
|
{ id: "status", label: "Diagnostics", view: ORCHESTRATOR_VIEWS.HOME },
|
|
24
25
|
{ id: "agents", label: "Agents", view: ORCHESTRATOR_VIEWS.AGENTS },
|
|
26
|
+
{ id: "intelligence", label: "Intelligence", view: ORCHESTRATOR_VIEWS.INTELLIGENCE },
|
|
25
27
|
{ id: "profile", label: "Profile", view: ORCHESTRATOR_VIEWS.PROFILE },
|
|
26
28
|
{ id: "plan-setup", label: "Plan setup", view: ORCHESTRATOR_VIEWS.PLAN, action: "setup" },
|
|
27
29
|
{ id: "help", label: "Help", view: ORCHESTRATOR_VIEWS.HELP }
|
|
@@ -39,7 +41,11 @@ export function formatProfileLines(profileJson) {
|
|
|
39
41
|
const lines = [
|
|
40
42
|
`Coordinator: ${profileJson.coordinator ?? "none"}`,
|
|
41
43
|
`Default agents: ${formatAgentsLabel(profileJson.defaultAgents)}`,
|
|
42
|
-
`Apply mode: ${profileJson.applyMode}
|
|
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"}`
|
|
43
49
|
];
|
|
44
50
|
|
|
45
51
|
if (profileJson.sources.global) {
|
|
@@ -54,6 +60,29 @@ export function formatProfileLines(profileJson) {
|
|
|
54
60
|
return lines;
|
|
55
61
|
}
|
|
56
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
|
+
|
|
57
86
|
export function formatPlanLines(plan) {
|
|
58
87
|
const lines = [`Action: ${plan.action}`, ""];
|
|
59
88
|
for (const step of plan.steps) {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKEND_IDS,
|
|
3
|
+
COST_CLASSES,
|
|
4
|
+
PRIVACY_CLASSES,
|
|
5
|
+
createModelDescriptor
|
|
6
|
+
} from "../types.js";
|
|
7
|
+
import { fetchJson } from "../http.js";
|
|
8
|
+
import { CAPABILITY_STATES } from "../../capability-states.js";
|
|
9
|
+
import { classifyCustomBaseUrl, isValidEnvironmentName } from "../custom-url.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OpenAI-compatible HTTP backend. API keys are read from env by name only —
|
|
13
|
+
* never from profile JSON or disk.
|
|
14
|
+
*/
|
|
15
|
+
export function createCustomHttpBackend({
|
|
16
|
+
id = BACKEND_IDS.CUSTOM,
|
|
17
|
+
label = "Custom provider",
|
|
18
|
+
baseUrl,
|
|
19
|
+
modelId,
|
|
20
|
+
apiKeyEnv = null,
|
|
21
|
+
local: _local = false,
|
|
22
|
+
fetchImpl = globalThis.fetch,
|
|
23
|
+
env = process.env
|
|
24
|
+
} = {}) {
|
|
25
|
+
if (!baseUrl) {
|
|
26
|
+
throw new Error("Custom HTTP backend requires baseUrl.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const location = classifyCustomBaseUrl(baseUrl);
|
|
30
|
+
const normalizedBase = location.normalizedBaseUrl;
|
|
31
|
+
if (apiKeyEnv != null && !isValidEnvironmentName(apiKeyEnv)) {
|
|
32
|
+
throw new Error("Custom provider apiKeyEnv must be a valid uppercase environment variable name.");
|
|
33
|
+
}
|
|
34
|
+
if (apiKeyEnv && !location.local) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Remote custom providers cannot use apiKeyEnv in 0.2.0; use a built-in provider or a local endpoint."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const apiKey = apiKeyEnv ? (env[apiKeyEnv] ?? null) : null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
label,
|
|
44
|
+
local: location.local,
|
|
45
|
+
|
|
46
|
+
async detect() {
|
|
47
|
+
if (apiKeyEnv && !apiKey) {
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
label,
|
|
51
|
+
state: CAPABILITY_STATES.UNKNOWN,
|
|
52
|
+
detected: false,
|
|
53
|
+
available: false,
|
|
54
|
+
hasApiKey: false,
|
|
55
|
+
error: null,
|
|
56
|
+
recommendation: `Set ${apiKeyEnv} in the environment. Kairo never stores credentials.`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
label,
|
|
63
|
+
state: CAPABILITY_STATES.DETECTED,
|
|
64
|
+
detected: true,
|
|
65
|
+
available: Boolean(modelId),
|
|
66
|
+
hasApiKey: apiKeyEnv ? Boolean(apiKey) : null,
|
|
67
|
+
error: null,
|
|
68
|
+
recommendation: modelId
|
|
69
|
+
? `Custom provider configured (${modelId}).`
|
|
70
|
+
: "Custom provider baseUrl set; configure modelId in profile."
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async listModels() {
|
|
75
|
+
if (!modelId) return [];
|
|
76
|
+
return [
|
|
77
|
+
createModelDescriptor({
|
|
78
|
+
provider: id,
|
|
79
|
+
modelId,
|
|
80
|
+
local: location.local,
|
|
81
|
+
costClass: location.local ? COST_CLASSES.LOCAL : COST_CLASSES.UNKNOWN,
|
|
82
|
+
privacyClass: location.local ? PRIVACY_CLASSES.LOCAL : PRIVACY_CLASSES.CLOUD,
|
|
83
|
+
opaque: true
|
|
84
|
+
})
|
|
85
|
+
];
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async capabilities() {
|
|
89
|
+
const detection = await this.detect();
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
local: location.local,
|
|
93
|
+
cloud: !location.local,
|
|
94
|
+
requiresApiKey: Boolean(apiKeyEnv),
|
|
95
|
+
requiresConsent: !location.local,
|
|
96
|
+
streaming: false,
|
|
97
|
+
tools: false,
|
|
98
|
+
state: detection.state,
|
|
99
|
+
baseUrl: normalizedBase,
|
|
100
|
+
modelId
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async invoke(contextPack, request = {}) {
|
|
105
|
+
if (apiKeyEnv && !apiKey) {
|
|
106
|
+
return invokeError(id, `Missing ${apiKeyEnv}. Kairo never stores credentials.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const resolvedModel = request.modelId ?? modelId;
|
|
110
|
+
if (!resolvedModel) {
|
|
111
|
+
return invokeError(id, "Custom invoke requires modelId.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const messages = [];
|
|
115
|
+
if (contextPack?.systemPrompt) {
|
|
116
|
+
messages.push({ role: "system", content: contextPack.systemPrompt });
|
|
117
|
+
}
|
|
118
|
+
if (Array.isArray(request.messages) && request.messages.length > 0) {
|
|
119
|
+
messages.push(...request.messages);
|
|
120
|
+
} else if (request.prompt) {
|
|
121
|
+
messages.push({ role: "user", content: request.prompt });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const headers = { "Content-Type": "application/json" };
|
|
125
|
+
if (apiKey && location.credentialSafe) headers.Authorization = `Bearer ${apiKey}`;
|
|
126
|
+
|
|
127
|
+
const result = await fetchJson(`${normalizedBase}/chat/completions`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers,
|
|
130
|
+
body: {
|
|
131
|
+
model: resolvedModel,
|
|
132
|
+
messages,
|
|
133
|
+
stream: false
|
|
134
|
+
},
|
|
135
|
+
timeoutMs: request.timeoutMs ?? 120000,
|
|
136
|
+
fetchImpl
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!result.ok) {
|
|
140
|
+
return invokeError(id, result.error ?? "Custom provider chat failed.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const content = result.data?.choices?.[0]?.message?.content ?? "";
|
|
144
|
+
const usage = result.data?.usage ?? {};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
backendId: id,
|
|
149
|
+
model: resolvedModel,
|
|
150
|
+
content,
|
|
151
|
+
usage: {
|
|
152
|
+
inputTokens: usage.prompt_tokens ?? null,
|
|
153
|
+
outputTokens: usage.completion_tokens ?? null,
|
|
154
|
+
cachedTokens: null,
|
|
155
|
+
estimatedCost: null,
|
|
156
|
+
model: resolvedModel,
|
|
157
|
+
backendId: id,
|
|
158
|
+
fallbackUsed: false
|
|
159
|
+
},
|
|
160
|
+
raw: result.data
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function invokeError(backendId, message) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
backendId,
|
|
170
|
+
model: null,
|
|
171
|
+
content: null,
|
|
172
|
+
error: message,
|
|
173
|
+
usage: null
|
|
174
|
+
};
|
|
175
|
+
}
|