@kal-elsam/kairo-runtime 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ export const BACKEND_IDS = {
2
+ OLLAMA: "ollama",
3
+ OPENROUTER: "openrouter",
4
+ CUSTOM: "custom"
5
+ };
6
+
7
+ export const COST_CLASSES = {
8
+ FREE: "free",
9
+ LOCAL: "local",
10
+ PAID: "paid",
11
+ UNKNOWN: "unknown"
12
+ };
13
+
14
+ export const PRIVACY_CLASSES = {
15
+ LOCAL: "local",
16
+ CLOUD: "cloud",
17
+ UNKNOWN: "unknown"
18
+ };
19
+
20
+ export const ROUTING_MODES = {
21
+ DIAGNOSTICS: "diagnostics",
22
+ LOCAL: "local",
23
+ CLOUD_CONSENT: "cloud_consent",
24
+ USER_OVERRIDE: "user_override"
25
+ };
26
+
27
+ export const OPENROUTER_FREE_MODEL = "openrouter/free";
28
+
29
+ export const DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434";
30
+
31
+ export function createModelDescriptor({
32
+ provider,
33
+ modelId,
34
+ local = false,
35
+ costClass = COST_CLASSES.UNKNOWN,
36
+ privacyClass = PRIVACY_CLASSES.UNKNOWN,
37
+ contextLimit = null,
38
+ tools = false,
39
+ reasoning = false,
40
+ rateLimits = null,
41
+ opaque = false
42
+ }) {
43
+ return {
44
+ provider,
45
+ modelId,
46
+ local,
47
+ costClass,
48
+ privacyClass,
49
+ contextLimit,
50
+ tools,
51
+ reasoning,
52
+ rateLimits,
53
+ opaque
54
+ };
55
+ }
56
+
57
+ export function createRoutingDecision({
58
+ backendId,
59
+ model,
60
+ reason,
61
+ estimatedTokens = null,
62
+ privacyImpact = PRIVACY_CLASSES.UNKNOWN,
63
+ fallback = null,
64
+ mode = ROUTING_MODES.DIAGNOSTICS,
65
+ requiresCloudConsent = false,
66
+ canInvoke = false
67
+ }) {
68
+ return {
69
+ backendId,
70
+ model,
71
+ reason,
72
+ estimatedTokens,
73
+ privacyImpact,
74
+ fallback,
75
+ mode,
76
+ requiresCloudConsent,
77
+ canInvoke
78
+ };
79
+ }
80
+
81
+ export function createUsageTelemetry({
82
+ inputTokens = null,
83
+ outputTokens = null,
84
+ cachedTokens = null,
85
+ estimatedCost = null,
86
+ model = null,
87
+ backendId = null,
88
+ fallbackUsed = false
89
+ } = {}) {
90
+ return {
91
+ inputTokens,
92
+ outputTokens,
93
+ cachedTokens,
94
+ estimatedCost,
95
+ model,
96
+ backendId,
97
+ fallbackUsed
98
+ };
99
+ }
@@ -0,0 +1,323 @@
1
+ import { resolveHomeDir } from "./paths.js";
2
+ import { resolveProfile, buildProfileJson } from "./profile.js";
3
+ import {
4
+ compileContextPack,
5
+ inspectIntelligenceBackends,
6
+ resolveRoutingDecision,
7
+ runIntelligenceRequest,
8
+ summarizeIntelligenceBackends
9
+ } from "./intelligence/index.js";
10
+ import { BRAND } from "./brand/index.js";
11
+ import { formatCliCommand } from "./brand/cli.js";
12
+
13
+ export async function runIntelligenceCli(options, packageManifest) {
14
+ const action = options.intelligenceAction ?? "status";
15
+ const homeDir = resolveHomeDir();
16
+ const workspaceRoot = options.cwd;
17
+ const { profile, sources } = await resolveProfile({ homeDir, workspaceRoot });
18
+
19
+ switch (action) {
20
+ case "status":
21
+ return printIntelligenceStatus({
22
+ homeDir,
23
+ workspaceRoot,
24
+ profile,
25
+ sources,
26
+ json: options.json,
27
+ env: process.env
28
+ });
29
+ case "models":
30
+ return printIntelligenceModels({
31
+ profile,
32
+ json: options.json,
33
+ env: process.env
34
+ });
35
+ case "context":
36
+ return printIntelligenceContext({
37
+ workspaceRoot,
38
+ profile,
39
+ task: options.intelligenceTask,
40
+ relevantPaths: options.intelligencePaths ?? [],
41
+ includePrivate: options.includePrivate,
42
+ confirmed: options.yes || options.confirm,
43
+ json: options.json
44
+ });
45
+ case "route":
46
+ return printIntelligenceRoute({
47
+ workspaceRoot,
48
+ profile,
49
+ task: options.intelligenceTask,
50
+ cloudConsent: options.cloudConsent,
51
+ json: options.json,
52
+ env: process.env
53
+ });
54
+ case "ask":
55
+ return runIntelligenceAsk({
56
+ workspaceRoot,
57
+ profile,
58
+ task: options.intelligenceTask,
59
+ prompt: options.intelligencePrompt,
60
+ relevantPaths: options.intelligencePaths ?? [],
61
+ includePrivate: options.includePrivate,
62
+ cloudConsent: options.cloudConsent,
63
+ confirmed: options.yes || options.confirm,
64
+ json: options.json,
65
+ env: process.env,
66
+ packageManifest
67
+ });
68
+ default:
69
+ throw new Error(
70
+ `Unknown intelligence action "${action}". Use status, models, context, route, or ask.`
71
+ );
72
+ }
73
+ }
74
+
75
+ async function printIntelligenceStatus({ homeDir, workspaceRoot, profile, sources, json, env }) {
76
+ const backends = await inspectIntelligenceBackends({
77
+ env,
78
+ customProviders: profile.customProviders
79
+ });
80
+ const summary = summarizeIntelligenceBackends(backends);
81
+ const routing = resolveRoutingDecision({
82
+ backends,
83
+ profile,
84
+ cloudConsent: false
85
+ });
86
+
87
+ const payload = {
88
+ readOnly: true,
89
+ homeDir,
90
+ workspaceRoot,
91
+ profile: buildProfileJson({ profile, sources }),
92
+ backends,
93
+ summary,
94
+ routing
95
+ };
96
+
97
+ if (json) {
98
+ console.log(JSON.stringify(payload, null, 2));
99
+ return payload;
100
+ }
101
+
102
+ console.log(`${BRAND.displayName} intelligence — status`);
103
+ console.log(`Home: ${homeDir}`);
104
+ console.log(`Workspace: ${workspaceRoot}`);
105
+ console.log("");
106
+ for (const backend of backends) {
107
+ const models = backend.models?.length ?? 0;
108
+ console.log(
109
+ ` ${backend.label.padEnd(14)} ${backend.state.padEnd(14)} models=${models}`
110
+ );
111
+ }
112
+ console.log("");
113
+ console.log(`Routing: ${routing.reason}`);
114
+ console.log(`Can invoke: ${routing.canInvoke ? "yes" : "no"}`);
115
+ if (!summary.localAvailable && !summary.cloudAuthenticated) {
116
+ console.log("");
117
+ console.log("Diagnostics mode: configure Ollama or OPENROUTER_API_KEY to enable inference.");
118
+ }
119
+ return payload;
120
+ }
121
+
122
+ async function printIntelligenceModels({ profile, json, env }) {
123
+ const backends = await inspectIntelligenceBackends({
124
+ env,
125
+ customProviders: profile.customProviders
126
+ });
127
+
128
+ const models = backends.flatMap((backend) =>
129
+ (backend.models ?? []).map((model) => ({
130
+ ...model,
131
+ backendState: backend.state,
132
+ available: backend.available
133
+ }))
134
+ );
135
+
136
+ const payload = { readOnly: true, models, backends: backends.map((entry) => ({
137
+ id: entry.id,
138
+ state: entry.state,
139
+ available: entry.available
140
+ })) };
141
+
142
+ if (json) {
143
+ console.log(JSON.stringify(payload, null, 2));
144
+ return payload;
145
+ }
146
+
147
+ console.log(`${BRAND.displayName} intelligence — models`);
148
+ if (models.length === 0) {
149
+ console.log(" No models detected. Start Ollama or set OPENROUTER_API_KEY.");
150
+ } else {
151
+ for (const model of models) {
152
+ console.log(
153
+ ` ${model.provider.padEnd(12)} ${model.modelId.padEnd(32)} ${model.privacyClass}/${model.costClass}`
154
+ );
155
+ }
156
+ }
157
+ return payload;
158
+ }
159
+
160
+ async function printIntelligenceContext({
161
+ workspaceRoot,
162
+ profile,
163
+ task,
164
+ relevantPaths,
165
+ includePrivate,
166
+ confirmed,
167
+ json
168
+ }) {
169
+ const privateConfirmationRequired = includePrivate && !confirmed;
170
+ const pack = await compileContextPack({
171
+ workspaceRoot,
172
+ task,
173
+ relevantPaths,
174
+ includePrivate: includePrivate && confirmed,
175
+ stableBudgetTokens: profile.stableContextBudget ?? undefined,
176
+ requestBudgetTokens: profile.requestContextBudget ?? undefined
177
+ });
178
+
179
+ const payload = {
180
+ readOnly: true,
181
+ estimatedTokens: pack.estimatedTokens,
182
+ project: pack.stable.project,
183
+ evidence: pack.evidence,
184
+ privacy: pack.privacy,
185
+ skills: pack.stable.skills,
186
+ sdd: pack.stable.sdd,
187
+ tdd: pack.stable.tdd,
188
+ graphify: pack.stable.graphify,
189
+ hasAgentsMd: Boolean(pack.stable.agentsMd),
190
+ relevantFiles: pack.perRequest.files.map((file) => file.path)
191
+ };
192
+
193
+ if (privateConfirmationRequired) {
194
+ payload.privateConfirmationRequired = true;
195
+ payload.error = "Including private context requires explicit confirmation (--include-private --yes / --confirm).";
196
+ }
197
+
198
+ if (json) {
199
+ console.log(JSON.stringify(payload, null, 2));
200
+ return payload;
201
+ }
202
+
203
+ console.log(`${BRAND.displayName} intelligence — context`);
204
+ console.log(`Estimated tokens: ${pack.estimatedTokens}`);
205
+ console.log(`Project: ${pack.stable.project.name} (${pack.stable.project.stack})`);
206
+ console.log(`Evidence files: ${pack.evidence.filter((entry) => entry.kind === "file").length}`);
207
+ if (privateConfirmationRequired) {
208
+ console.log(`Blocked: ${payload.error}`);
209
+ }
210
+ if (pack.privacy.excludedPrivate.length > 0) {
211
+ console.log(`Excluded private: ${pack.privacy.excludedPrivate.join(", ")}`);
212
+ }
213
+ return payload;
214
+ }
215
+
216
+ async function printIntelligenceRoute({
217
+ workspaceRoot,
218
+ profile,
219
+ task,
220
+ cloudConsent,
221
+ json,
222
+ env
223
+ }) {
224
+ const backends = await inspectIntelligenceBackends({
225
+ env,
226
+ customProviders: profile.customProviders
227
+ });
228
+ const pack = await compileContextPack({ workspaceRoot, task });
229
+ const routing = resolveRoutingDecision({
230
+ backends,
231
+ profile,
232
+ contextPack: pack,
233
+ task,
234
+ cloudConsent: Boolean(cloudConsent)
235
+ });
236
+
237
+ const payload = {
238
+ readOnly: true,
239
+ routing,
240
+ estimatedTokens: pack.estimatedTokens,
241
+ evidenceUsed: pack.evidence.filter((entry) => entry.kind === "file").map((entry) => entry.path)
242
+ };
243
+
244
+ if (json) {
245
+ console.log(JSON.stringify(payload, null, 2));
246
+ return payload;
247
+ }
248
+
249
+ console.log(`${BRAND.displayName} intelligence — routing`);
250
+ console.log(`Backend: ${routing.backendId ?? "none"}`);
251
+ console.log(`Model: ${routing.model?.modelId ?? "none"}`);
252
+ console.log(`Reason: ${routing.reason}`);
253
+ console.log(`Estimated tokens: ${routing.estimatedTokens}`);
254
+ console.log(`Privacy: ${routing.privacyImpact}`);
255
+ console.log(`Can invoke: ${routing.canInvoke ? "yes" : "no"}`);
256
+ return payload;
257
+ }
258
+
259
+ async function runIntelligenceAsk({
260
+ workspaceRoot,
261
+ profile,
262
+ task,
263
+ prompt,
264
+ relevantPaths,
265
+ includePrivate,
266
+ cloudConsent,
267
+ confirmed,
268
+ json,
269
+ env
270
+ }) {
271
+ if (!prompt && !task) {
272
+ throw new Error(`Missing prompt. Use: ${formatCliCommand("intelligence ask --prompt \"...\"")}`);
273
+ }
274
+
275
+ const outcome = await runIntelligenceRequest({
276
+ workspaceRoot,
277
+ profile,
278
+ task,
279
+ prompt: prompt ?? task,
280
+ relevantPaths,
281
+ includePrivate,
282
+ cloudConsent,
283
+ confirmed,
284
+ env
285
+ });
286
+
287
+ if (json) {
288
+ console.log(JSON.stringify({
289
+ ok: outcome.ok,
290
+ mode: outcome.mode,
291
+ diagnosticsOnly: outcome.diagnosticsOnly,
292
+ routing: outcome.routing,
293
+ explanation: outcome.explanation,
294
+ telemetry: outcome.telemetry,
295
+ content: outcome.result?.content ?? null,
296
+ error: outcome.error
297
+ }, null, 2));
298
+ return outcome;
299
+ }
300
+
301
+ console.log(`${BRAND.displayName} intelligence — ask`);
302
+ console.log(`Routing: ${outcome.explanation.reason}`);
303
+ console.log(`Estimated tokens: ${outcome.explanation.estimatedTokens}`);
304
+ console.log(`Privacy: ${outcome.explanation.privacyImpact}`);
305
+ console.log("");
306
+
307
+ if (!outcome.ok) {
308
+ console.log(`Blocked: ${outcome.error}`);
309
+ if (outcome.routing?.requiresCloudConsent) {
310
+ console.log(`Retry with --cloud-consent --yes after reviewing context (${formatCliCommand("intelligence context")}).`);
311
+ }
312
+ return outcome;
313
+ }
314
+
315
+ console.log(outcome.result.content);
316
+ if (outcome.telemetry) {
317
+ console.log("");
318
+ console.log(
319
+ `Usage: in=${outcome.telemetry.inputTokens ?? "?"} out=${outcome.telemetry.outputTokens ?? "?"} model=${outcome.telemetry.model}`
320
+ );
321
+ }
322
+ return outcome;
323
+ }
@@ -119,6 +119,17 @@ export async function runOrchestratorDiagnostics({
119
119
  );
120
120
  }
121
121
 
122
+ if (diagnostics.intelligence) {
123
+ console.log("");
124
+ console.log("Intelligence backends:");
125
+ for (const backend of diagnostics.intelligence.backends) {
126
+ console.log(
127
+ ` ${backend.label.padEnd(14)} ${backend.state.padEnd(14)} models=${backend.models?.length ?? 0}`
128
+ );
129
+ }
130
+ console.log(` Routing: ${diagnostics.intelligence.routingPreview?.reason ?? "n/a"}`);
131
+ }
132
+
122
133
  console.log("");
123
134
  console.log("Recommendations:");
124
135
  for (const recommendation of diagnostics.recommendations) {
@@ -3,19 +3,34 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { harnessHomePaths } from "./paths.js";
5
5
  import { AGENT_CAPABILITY_IDS } from "./agent-capabilities/index.js";
6
+ import { classifyCustomBaseUrl, isValidEnvironmentName } from "./intelligence/custom-url.js";
6
7
 
7
8
  export const PROFILE_KEYS = new Set([
8
9
  "coordinator",
9
10
  "defaultAgents",
10
11
  "defaultComponents",
11
- "applyMode"
12
+ "applyMode",
13
+ "preferredBackend",
14
+ "preferredModel",
15
+ "cloudConsent",
16
+ "tokenBudget",
17
+ "stableContextBudget",
18
+ "requestContextBudget",
19
+ "customProviders"
12
20
  ]);
13
21
 
14
22
  export const DEFAULT_PROFILE = {
15
23
  coordinator: null,
16
24
  defaultAgents: "detected",
17
25
  defaultComponents: null,
18
- applyMode: "prompt"
26
+ applyMode: "prompt",
27
+ preferredBackend: null,
28
+ preferredModel: null,
29
+ cloudConsent: false,
30
+ tokenBudget: null,
31
+ stableContextBudget: null,
32
+ requestContextBudget: null,
33
+ customProviders: []
19
34
  };
20
35
 
21
36
  const APPLY_MODES = new Set(["prompt", "confirm"]);
@@ -64,6 +79,7 @@ export async function resolveProfile({ homeDir, workspaceRoot }) {
64
79
  };
65
80
 
66
81
  validateProfile(merged);
82
+ merged.customProviders = sanitizeCustomProviders(merged.customProviders);
67
83
 
68
84
  return {
69
85
  profile: merged,
@@ -82,6 +98,13 @@ export function buildProfileJson(resolved) {
82
98
  defaultAgents: profile.defaultAgents,
83
99
  defaultComponents: profile.defaultComponents,
84
100
  applyMode: profile.applyMode,
101
+ preferredBackend: profile.preferredBackend,
102
+ preferredModel: profile.preferredModel,
103
+ cloudConsent: profile.cloudConsent,
104
+ tokenBudget: profile.tokenBudget,
105
+ stableContextBudget: profile.stableContextBudget,
106
+ requestContextBudget: profile.requestContextBudget,
107
+ customProviders: sanitizeCustomProviders(profile.customProviders),
85
108
  sources: {
86
109
  global: sources.global,
87
110
  project: sources.project,
@@ -143,4 +166,132 @@ function validateProfile(profile) {
143
166
  if (profile.defaultComponents != null && !Array.isArray(profile.defaultComponents)) {
144
167
  throw new Error("Profile defaultComponents must be an array or null.");
145
168
  }
169
+
170
+ if (profile.cloudConsent != null && typeof profile.cloudConsent !== "boolean") {
171
+ throw new Error("Profile cloudConsent must be a boolean.");
172
+ }
173
+
174
+ for (const key of ["tokenBudget", "stableContextBudget", "requestContextBudget"]) {
175
+ if (profile[key] != null && (!Number.isFinite(profile[key]) || profile[key] < 1)) {
176
+ throw new Error(`Profile ${key} must be a positive number or null.`);
177
+ }
178
+ }
179
+
180
+ if (profile.preferredBackend != null && typeof profile.preferredBackend !== "string") {
181
+ throw new Error("Profile preferredBackend must be a string or null.");
182
+ }
183
+
184
+ if (profile.preferredModel != null && typeof profile.preferredModel !== "string") {
185
+ throw new Error("Profile preferredModel must be a string or null.");
186
+ }
187
+
188
+ validateNoSecrets(profile);
189
+ validateCustomProviders(profile.customProviders);
190
+ }
191
+
192
+ /**
193
+ * Secret-looking key segments after camelCase → snake_case normalization.
194
+ * `api_key_env` is the only credential-related key allowed (env var name, not a secret).
195
+ */
196
+ const SECRET_KEY_PATTERN = /(^|_)(api_?key|api_?token|access_?token|auth_?token|client_?secret|private_?key|authorization(_header)?|password|secrets?|credentials?|bearer|token)(_|$)/;
197
+ const SECRET_KEY_ALLOWLIST = new Set([
198
+ "api_key_env",
199
+ "token_budget",
200
+ "stable_context_budget",
201
+ "request_context_budget"
202
+ ]);
203
+ const SECRET_VALUE_PATTERN = /^(sk-[A-Za-z0-9]|sk-or-|gh[pousr]_|xox[baprs]-|AKIA[0-9A-Z]{16}\b|Bearer\s+\S+|eyJ[A-Za-z0-9_-]+\.)|-----BEGIN [A-Z ]*PRIVATE KEY-----/i;
204
+
205
+ export function normalizeProfileKey(key) {
206
+ return String(key)
207
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
208
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
209
+ .replace(/-/g, "_")
210
+ .toLowerCase();
211
+ }
212
+
213
+ export function isForbiddenSecretKey(key) {
214
+ const normalized = normalizeProfileKey(key);
215
+ if (SECRET_KEY_ALLOWLIST.has(normalized)) return false;
216
+ return SECRET_KEY_PATTERN.test(normalized);
217
+ }
218
+
219
+ function validateNoSecrets(profile) {
220
+ walkForSecrets(profile);
221
+ }
222
+
223
+ function walkForSecrets(value) {
224
+ if (typeof value === "string") {
225
+ if (SECRET_VALUE_PATTERN.test(value)) {
226
+ throw new Error("Profile must not store credential-like values. Use environment variables.");
227
+ }
228
+ return;
229
+ }
230
+ if (value == null || typeof value !== "object") return;
231
+
232
+ for (const [key, nested] of Object.entries(value)) {
233
+ const normalizedKey = normalizeProfileKey(key);
234
+
235
+ // apiKeyEnv is the only allowed credential-related key: it names an env var.
236
+ if (normalizedKey === "api_key_env") {
237
+ if (typeof nested === "string" && SECRET_VALUE_PATTERN.test(nested)) {
238
+ throw new Error("Profile must not store credential-like values. Use environment variables.");
239
+ }
240
+ continue;
241
+ }
242
+
243
+ if (isForbiddenSecretKey(key)) {
244
+ throw new Error(`Profile must not store credentials (rejected key "${key}"). Use environment variables.`);
245
+ }
246
+ if (typeof nested === "string" && SECRET_VALUE_PATTERN.test(nested)) {
247
+ throw new Error("Profile must not store credential-like values. Use environment variables.");
248
+ }
249
+ walkForSecrets(nested);
250
+ }
251
+ }
252
+
253
+ function validateCustomProviders(providers) {
254
+ if (providers == null) return;
255
+ if (!Array.isArray(providers)) {
256
+ throw new Error("Profile customProviders must be an array.");
257
+ }
258
+
259
+ for (const provider of providers) {
260
+ if (provider == null || typeof provider !== "object" || Array.isArray(provider)) {
261
+ throw new Error("Each customProviders entry must be an object.");
262
+ }
263
+ if (!provider.baseUrl || typeof provider.baseUrl !== "string") {
264
+ throw new Error("customProviders entries require baseUrl.");
265
+ }
266
+ for (const key of Object.keys(provider)) {
267
+ if (normalizeProfileKey(key) === "api_key_env") continue;
268
+ if (isForbiddenSecretKey(key)) {
269
+ throw new Error(`customProviders must not include "${key}". Use apiKeyEnv to name an environment variable.`);
270
+ }
271
+ }
272
+ if (provider.apiKey != null || provider.token != null || provider.secret != null) {
273
+ throw new Error("customProviders must not embed secrets. Set apiKeyEnv to an environment variable name.");
274
+ }
275
+ if (provider.apiKeyEnv != null && !isValidEnvironmentName(provider.apiKeyEnv)) {
276
+ throw new Error("customProviders apiKeyEnv must be a valid uppercase environment variable name.");
277
+ }
278
+ const location = classifyCustomBaseUrl(provider.baseUrl);
279
+ if (provider.apiKeyEnv && !location.local) {
280
+ throw new Error(
281
+ "Remote custom providers cannot use apiKeyEnv in 0.2.0; use a built-in provider or a local endpoint."
282
+ );
283
+ }
284
+ }
285
+ }
286
+
287
+ function sanitizeCustomProviders(providers) {
288
+ if (!Array.isArray(providers)) return [];
289
+ return providers.map((provider) => ({
290
+ id: provider.id ?? "custom",
291
+ label: provider.label ?? "Custom provider",
292
+ baseUrl: provider.baseUrl,
293
+ modelId: provider.modelId ?? null,
294
+ apiKeyEnv: provider.apiKeyEnv ?? null,
295
+ local: classifyCustomBaseUrl(provider.baseUrl).local
296
+ }));
146
297
  }