@kal-elsam/kairo-runtime 0.4.2 → 0.5.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.
Files changed (103) hide show
  1. package/README.md +124 -17
  2. package/global-template/components/catalog.json +63 -6
  3. package/global-template/components/sdd-core/personas/teaching.md +42 -0
  4. package/global-template/components/sdd-core/skills/sdd-apply/SKILL.md +34 -0
  5. package/global-template/components/sdd-core/skills/sdd-apply/references/contract.md +4 -0
  6. package/global-template/components/sdd-core/skills/sdd-archive/SKILL.md +34 -0
  7. package/global-template/components/sdd-core/skills/sdd-archive/references/contract.md +4 -0
  8. package/global-template/components/sdd-core/skills/sdd-design/SKILL.md +34 -0
  9. package/global-template/components/sdd-core/skills/sdd-design/references/contract.md +4 -0
  10. package/global-template/components/sdd-core/skills/sdd-explore/SKILL.md +34 -0
  11. package/global-template/components/sdd-core/skills/sdd-explore/references/contract.md +4 -0
  12. package/global-template/components/sdd-core/skills/sdd-init/SKILL.md +34 -0
  13. package/global-template/components/sdd-core/skills/sdd-init/references/contract.md +4 -0
  14. package/global-template/components/sdd-core/skills/sdd-propose/SKILL.md +35 -0
  15. package/global-template/components/sdd-core/skills/sdd-propose/references/contract.md +4 -0
  16. package/global-template/components/sdd-core/skills/sdd-spec/SKILL.md +34 -0
  17. package/global-template/components/sdd-core/skills/sdd-spec/references/contract.md +4 -0
  18. package/global-template/components/sdd-core/skills/sdd-tasks/SKILL.md +34 -0
  19. package/global-template/components/sdd-core/skills/sdd-tasks/references/contract.md +4 -0
  20. package/global-template/components/sdd-core/skills/sdd-verify/SKILL.md +34 -0
  21. package/global-template/components/sdd-core/skills/sdd-verify/references/contract.md +4 -0
  22. package/package.json +1 -1
  23. package/scripts/cockpit-smoke.mjs +29 -8
  24. package/scripts/smoke-test.sh +9 -0
  25. package/src/cli.js +54 -5
  26. package/src/global/action-planner.js +17 -4
  27. package/src/global/component-authoring.js +1 -2
  28. package/src/global/component-ecosystem-checks.js +74 -15
  29. package/src/global/component-health.js +56 -0
  30. package/src/global/component-integration-cli.js +302 -0
  31. package/src/global/component-manifest.js +214 -0
  32. package/src/global/component-registry.js +19 -2
  33. package/src/global/component-resolve.js +51 -0
  34. package/src/global/components/sdd-core.js +6 -0
  35. package/src/global/control-plane-proposals.js +229 -0
  36. package/src/global/control-plane-snapshot.js +212 -0
  37. package/src/global/dashboard-guidance.js +27 -3
  38. package/src/global/diff.js +2 -5
  39. package/src/global/global-cli.js +16 -1
  40. package/src/global/global-doctor.js +75 -2
  41. package/src/global/global-installer.js +43 -6
  42. package/src/global/governance-actions.js +212 -0
  43. package/src/global/governance-repair.js +13 -0
  44. package/src/global/ink/cockpit-changes.js +124 -0
  45. package/src/global/ink/cockpit-control-center.js +106 -0
  46. package/src/global/ink/cockpit-controller.js +17 -0
  47. package/src/global/ink/cockpit-enter.js +27 -0
  48. package/src/global/ink/cockpit-focus.js +7 -1
  49. package/src/global/ink/cockpit-models.js +98 -46
  50. package/src/global/ink/cockpit-proposals.js +82 -0
  51. package/src/global/ink/cockpit-recovery.js +120 -0
  52. package/src/global/ink/cockpit-runs.js +47 -0
  53. package/src/global/ink/cockpit-scan.js +60 -0
  54. package/src/global/ink/cockpit-views.js +144 -74
  55. package/src/global/ink/orchestrator-app.js +156 -42
  56. package/src/global/ink/orchestrator-state.js +22 -1
  57. package/src/global/ink/use-orchestrator-data.js +239 -17
  58. package/src/global/integrations/engram-apply.js +105 -0
  59. package/src/global/integrations/engram-evidence.js +218 -0
  60. package/src/global/integrations/engram-exec.js +107 -0
  61. package/src/global/integrations/engram-plan.js +60 -0
  62. package/src/global/integrations/engram-provider.js +22 -0
  63. package/src/global/integrations/engram-receipts.js +116 -0
  64. package/src/global/integrations/engram-rollback.js +131 -0
  65. package/src/global/integrations/index.js +12 -0
  66. package/src/global/integrations/provider-contract.js +83 -0
  67. package/src/global/integrations/provider-registry.js +47 -0
  68. package/src/global/integrations/sdd-apply.js +190 -0
  69. package/src/global/integrations/sdd-destinations.js +95 -0
  70. package/src/global/integrations/sdd-evidence.js +89 -0
  71. package/src/global/integrations/sdd-fs-guard.js +183 -0
  72. package/src/global/integrations/sdd-lifecycle.js +84 -0
  73. package/src/global/integrations/sdd-persona.js +71 -0
  74. package/src/global/integrations/sdd-plan.js +87 -0
  75. package/src/global/integrations/sdd-provider.js +21 -0
  76. package/src/global/integrations/sdd-receipts.js +44 -0
  77. package/src/global/integrations/sdd-rollback.js +269 -0
  78. package/src/global/integrations/sdd-skill-files.js +61 -0
  79. package/src/global/integrations/sdd-state.js +175 -0
  80. package/src/global/integrations/sdd-verify.js +122 -0
  81. package/src/global/intelligence/backends/opencode-catalog.js +212 -0
  82. package/src/global/intelligence/backends/opencode-cli.js +247 -0
  83. package/src/global/intelligence/backends/opencode-evidence.js +129 -0
  84. package/src/global/intelligence/backends/opencode-http.js +152 -0
  85. package/src/global/intelligence/backends/opencode-invoke.js +83 -0
  86. package/src/global/intelligence/backends/opencode-providers.js +67 -0
  87. package/src/global/intelligence/backends/opencode-runtime.js +242 -0
  88. package/src/global/intelligence/context-compiler.js +95 -50
  89. package/src/global/intelligence/index.js +38 -1
  90. package/src/global/intelligence/orchestrate.js +4 -2
  91. package/src/global/intelligence/registry.js +62 -6
  92. package/src/global/intelligence/router.js +188 -70
  93. package/src/global/intelligence/transport-registry.js +147 -0
  94. package/src/global/intelligence/types.js +21 -2
  95. package/src/global/intelligence-cli.js +48 -17
  96. package/src/global/intelligence-session.js +37 -0
  97. package/src/global/json-output.js +1 -0
  98. package/src/global/load-component-catalog.js +10 -28
  99. package/src/global/load-workspace-component-catalog.js +22 -63
  100. package/src/global/state-migration.js +21 -1
  101. package/src/global/state.js +13 -8
  102. package/src/global/status.js +4 -17
  103. package/src/global/sync.js +2 -5
@@ -0,0 +1,247 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const EXECUTABLE = "opencode";
4
+ const DEFAULT_TIMEOUT_MS = 120_000;
5
+ const DEFAULT_TERMINATION_GRACE_MS = 1000;
6
+ const DEFAULT_KILL_GRACE_MS = 1000;
7
+ const STDERR_LIMIT = 480;
8
+ const STDOUT_BUFFER_LIMIT = 1_048_576;
9
+ const STDERR_BUFFER_LIMIT = STDERR_LIMIT;
10
+
11
+ function isSensitiveEnvName(name) {
12
+ return /(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/i.test(String(name ?? ""));
13
+ }
14
+
15
+ function appendLimited(current, chunk, limit) {
16
+ if (current.length >= limit) return current;
17
+ const next = current + String(chunk);
18
+ return next.length > limit ? next.slice(0, limit) : next;
19
+ }
20
+
21
+ /**
22
+ * Supervised, non-mutating OpenCode CLI invocation.
23
+ * Always uses `opencode run --format json --model` — never `--auto`.
24
+ * Timeout: SIGTERM → grace → SIGKILL → grace → unref + terminationFailed.
25
+ */
26
+ export function runOpencodeJson({
27
+ modelRef,
28
+ prompt,
29
+ cwd,
30
+ env,
31
+ spawnImpl = spawn,
32
+ timeoutMs = DEFAULT_TIMEOUT_MS,
33
+ terminationGraceMs = DEFAULT_TERMINATION_GRACE_MS,
34
+ killGraceMs = DEFAULT_KILL_GRACE_MS
35
+ } = {}) {
36
+ if (!modelRef || typeof modelRef !== "string") {
37
+ return Promise.reject(new Error("runOpencodeJson requires modelRef."));
38
+ }
39
+ if (prompt == null) {
40
+ return Promise.reject(new Error("runOpencodeJson requires prompt."));
41
+ }
42
+
43
+ const args = ["run", "--format", "json", "--model", modelRef, String(prompt)];
44
+
45
+ return new Promise((resolve, reject) => {
46
+ const child = spawnImpl(EXECUTABLE, args, {
47
+ cwd,
48
+ env,
49
+ stdio: ["ignore", "pipe", "pipe"]
50
+ });
51
+
52
+ let stdout = "";
53
+ let stderr = "";
54
+ let settled = false;
55
+ let timedOut = false;
56
+ let timers = [];
57
+
58
+ const clearTimers = () => {
59
+ for (const timer of timers) clearTimeout(timer);
60
+ timers = [];
61
+ };
62
+
63
+ const settle = (result) => {
64
+ if (settled) return;
65
+ settled = true;
66
+ clearTimers();
67
+ detachIo();
68
+ resolve(result);
69
+ };
70
+
71
+ const schedule = (fn, ms) => {
72
+ const timer = setTimeout(fn, ms);
73
+ timers.push(timer);
74
+ return timer;
75
+ };
76
+
77
+ const finish = ({ status = null, signal = null, terminationFailed = false } = {}) => {
78
+ settle({
79
+ stdout,
80
+ stderr,
81
+ status,
82
+ signal,
83
+ timedOut,
84
+ terminationFailed
85
+ });
86
+ };
87
+
88
+ const forceUnref = () => {
89
+ try {
90
+ child.unref?.();
91
+ } catch {
92
+ /* ignore unref failures */
93
+ }
94
+ };
95
+
96
+ const onStdout = (chunk) => {
97
+ stdout = appendLimited(stdout, chunk, STDOUT_BUFFER_LIMIT);
98
+ };
99
+ const onStderr = (chunk) => {
100
+ stderr = appendLimited(stderr, chunk, STDERR_BUFFER_LIMIT);
101
+ };
102
+ // Keep listening after settle so late errors never crash Node.
103
+ const onError = (error) => {
104
+ if (settled) return;
105
+ settled = true;
106
+ clearTimers();
107
+ detachIo();
108
+ reject(error);
109
+ };
110
+ const onClose = (status, signal) => {
111
+ finish({
112
+ status: status ?? null,
113
+ signal: signal ?? null,
114
+ terminationFailed: false
115
+ });
116
+ };
117
+
118
+ const detachIo = () => {
119
+ child.stdout?.off?.("data", onStdout);
120
+ child.stderr?.off?.("data", onStderr);
121
+ child.off?.("close", onClose);
122
+ };
123
+
124
+ const onTimeout = () => {
125
+ if (settled) return;
126
+ timedOut = true;
127
+ safeKill(child, "SIGTERM");
128
+ schedule(() => {
129
+ if (settled) return;
130
+ safeKill(child, "SIGKILL");
131
+ schedule(() => {
132
+ if (settled) return;
133
+ forceUnref();
134
+ finish({ terminationFailed: true });
135
+ }, killGraceMs);
136
+ }, terminationGraceMs);
137
+ };
138
+
139
+ schedule(onTimeout, timeoutMs);
140
+
141
+ child.stdout?.on("data", onStdout);
142
+ child.stderr?.on("data", onStderr);
143
+ child.on("error", onError);
144
+ child.on("close", onClose);
145
+ });
146
+ }
147
+
148
+ export function sanitizeCliStderr(text, { limit = STDERR_LIMIT } = {}) {
149
+ let out = String(text ?? "").replace(/\s+/g, " ").trim();
150
+ out = out.replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]");
151
+ out = out.replace(
152
+ /\b([A-Z][A-Z0-9_]*)\s*[:=]\s*(["'])(?:\\.|(?!\2).)*\2/g,
153
+ (match, name) => (isSensitiveEnvName(name) ? `${name}=[REDACTED]` : match)
154
+ );
155
+ out = out.replace(/\b([A-Z][A-Z0-9_]*)\s*[:=]\s*\S+/g, (match, name) => (
156
+ isSensitiveEnvName(name) ? `${name}=[REDACTED]` : match
157
+ ));
158
+ if (out.length > limit) {
159
+ if (limit <= 0) return "";
160
+ if (limit === 1) return "…";
161
+ out = `${out.slice(0, limit - 1)}…`;
162
+ }
163
+ return out;
164
+ }
165
+
166
+ function safeKill(child, signal) {
167
+ try {
168
+ if (typeof child.kill !== "function") return false;
169
+ return child.kill(signal) !== false;
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Parse NDJSON OpenCode `--format json` events into text, usage, and errors.
177
+ * Rejects malformed stdout explicitly (any non-empty line that is not JSON).
178
+ */
179
+ export function parseOpencodeJsonEvents(stdout) {
180
+ const events = [];
181
+ const texts = [];
182
+ let error = null;
183
+ const usage = {
184
+ inputTokens: null,
185
+ outputTokens: null,
186
+ cachedTokens: null,
187
+ estimatedCost: null
188
+ };
189
+
190
+ const lines = String(stdout ?? "")
191
+ .split(/\r?\n/)
192
+ .map((line) => line.trim())
193
+ .filter(Boolean);
194
+
195
+ for (const line of lines) {
196
+ let event;
197
+ try {
198
+ event = JSON.parse(line);
199
+ } catch {
200
+ return {
201
+ content: null,
202
+ error: "OpenCode CLI returned malformed JSON events.",
203
+ events,
204
+ usage
205
+ };
206
+ }
207
+
208
+ events.push(event);
209
+
210
+ if (event.type === "error" || event.error) {
211
+ error =
212
+ event.error?.message ??
213
+ event.message ??
214
+ "OpenCode CLI error event";
215
+ continue;
216
+ }
217
+
218
+ if (event.type === "text") {
219
+ if (typeof event.part?.text === "string") texts.push(event.part.text);
220
+ else if (typeof event.text === "string") texts.push(event.text);
221
+ }
222
+
223
+ if (event.type === "step_finish" && event.part) {
224
+ applyStepFinishUsage(usage, event.part);
225
+ }
226
+ }
227
+
228
+ return {
229
+ content: texts.length > 0 ? texts.join("") : null,
230
+ error,
231
+ events,
232
+ usage
233
+ };
234
+ }
235
+
236
+ function applyStepFinishUsage(usage, part) {
237
+ const tokens = part.tokens ?? part.usage ?? {};
238
+ if (tokens.input != null || tokens.prompt != null) {
239
+ usage.inputTokens = tokens.input ?? tokens.prompt;
240
+ }
241
+ if (tokens.output != null || tokens.completion != null) {
242
+ usage.outputTokens = tokens.output ?? tokens.completion;
243
+ }
244
+ const cached = tokens.cache?.read ?? tokens.cached;
245
+ if (cached != null) usage.cachedTokens = cached;
246
+ if (typeof part.cost === "number") usage.estimatedCost = part.cost;
247
+ }
@@ -0,0 +1,129 @@
1
+ import { probeCommand, isExecutableAvailable } from "../../cli-probe.js";
2
+
3
+ export const ENTITLEMENT_STATES = Object.freeze({
4
+ UNKNOWN: "unknown",
5
+ UNVERIFIED: "entitlement_unverified",
6
+ LIMIT_REACHED: "limit_reached"
7
+ });
8
+
9
+ export const BILLING_MODELS = Object.freeze({
10
+ GO_PLAN: "go_plan",
11
+ ZEN_CREDITS: "zen_credits",
12
+ CLI_RUNTIME: "cli_runtime"
13
+ });
14
+
15
+ /**
16
+ * Safe OpenCode evidence: CLI presence + `opencode auth list` + env key.
17
+ * Never reads auth.json. Never claims subscription/balance without HTTP evidence.
18
+ */
19
+ export function collectOpencodeCliEvidence({
20
+ env = process.env,
21
+ whichImpl = isExecutableAvailable,
22
+ probeImpl = probeCommand
23
+ } = {}) {
24
+ const cliInstalled = whichImpl("opencode", { env });
25
+ if (!cliInstalled) {
26
+ return {
27
+ cliInstalled: false,
28
+ authListOk: false,
29
+ authProviders: [],
30
+ error: null
31
+ };
32
+ }
33
+
34
+ const result = probeImpl("opencode", ["auth", "list"], {
35
+ env,
36
+ timeoutMs: 8000
37
+ });
38
+
39
+ if (!result.ok) {
40
+ return {
41
+ cliInstalled: true,
42
+ authListOk: false,
43
+ authProviders: [],
44
+ error: result.error ?? result.stderr ?? "opencode auth list failed"
45
+ };
46
+ }
47
+
48
+ return {
49
+ cliInstalled: true,
50
+ authListOk: true,
51
+ authProviders: parseAuthListProviders(result.stdout),
52
+ error: null
53
+ };
54
+ }
55
+
56
+ export function parseAuthListProviders(rawOutput) {
57
+ const plain = String(rawOutput ?? "").replace(/\u001b\[[0-9;]*m/g, "");
58
+ const found = new Set();
59
+
60
+ for (const line of plain.split(/\r?\n/)) {
61
+ const trimmed = line.trim();
62
+ if (!trimmed || /credentials|environment|outro|intro/i.test(trimmed)) continue;
63
+
64
+ const match = trimmed.match(/^(?:[●*•]\s*)?(.+?)\s+(oauth|api)\s*$/i);
65
+ if (match) {
66
+ found.add(normalizeProviderLabel(match[1]));
67
+ continue;
68
+ }
69
+
70
+ if (/opencode\s+go/i.test(trimmed)) found.add("OpenCode Go");
71
+ if (/opencode\s+zen/i.test(trimmed)) found.add("OpenCode Zen");
72
+ }
73
+
74
+ return [...found].sort((a, b) => a.localeCompare(b));
75
+ }
76
+
77
+ export function hasAuthProvider(providers, product) {
78
+ const list = Array.isArray(providers) ? providers : [];
79
+ if (product === "go") {
80
+ return list.some((entry) => /opencode\s*go/i.test(entry) || /^go$/i.test(entry));
81
+ }
82
+ if (product === "zen") {
83
+ return list.some((entry) => /opencode\s*zen/i.test(entry) || /^zen$/i.test(entry));
84
+ }
85
+ return list.length > 0;
86
+ }
87
+
88
+ export function classifyModelsProbeStatus(status) {
89
+ if (status === 429) return ENTITLEMENT_STATES.LIMIT_REACHED;
90
+ if (status === 401 || status === 403) return ENTITLEMENT_STATES.UNKNOWN;
91
+ if (status >= 200 && status < 300) return ENTITLEMENT_STATES.UNVERIFIED;
92
+ return ENTITLEMENT_STATES.UNKNOWN;
93
+ }
94
+
95
+ export function buildDirectEvidenceBase({
96
+ product,
97
+ hasApiKey,
98
+ cliEvidence = null,
99
+ modelsHttpStatus = null,
100
+ modelsOk = false
101
+ }) {
102
+ const authProviders = cliEvidence?.authProviders ?? [];
103
+ const configuredViaCli = hasAuthProvider(authProviders, product);
104
+ const configured = Boolean(hasApiKey) || configuredViaCli;
105
+ const authenticated = Boolean(hasApiKey) && modelsOk;
106
+ const entitlement = modelsHttpStatus === 429
107
+ ? ENTITLEMENT_STATES.LIMIT_REACHED
108
+ : (authenticated || configured
109
+ ? ENTITLEMENT_STATES.UNVERIFIED
110
+ : ENTITLEMENT_STATES.UNKNOWN);
111
+
112
+ return {
113
+ configured,
114
+ authenticated,
115
+ entitlement,
116
+ billingModel: product === "go" ? BILLING_MODELS.GO_PLAN : BILLING_MODELS.ZEN_CREDITS,
117
+ evidence: {
118
+ hasApiKey: Boolean(hasApiKey),
119
+ cliInstalled: Boolean(cliEvidence?.cliInstalled),
120
+ authListOk: Boolean(cliEvidence?.authListOk),
121
+ authProviders,
122
+ modelsHttpStatus
123
+ }
124
+ };
125
+ }
126
+
127
+ function normalizeProviderLabel(value) {
128
+ return String(value ?? "").replace(/\s+/g, " ").trim();
129
+ }
@@ -0,0 +1,152 @@
1
+ import { TRANSPORT_KINDS } from "../types.js";
2
+ import { fetchJson } from "../http.js";
3
+
4
+ export async function invokeChatCompletions({
5
+ baseUrl,
6
+ apiKey,
7
+ modelId,
8
+ contextPack,
9
+ request,
10
+ fetchImpl
11
+ }) {
12
+ const result = await fetchJson(`${baseUrl}/chat/completions`, {
13
+ method: "POST",
14
+ headers: bearerHeaders(apiKey),
15
+ body: {
16
+ model: modelId,
17
+ messages: buildChatMessages(contextPack, request),
18
+ stream: false
19
+ },
20
+ timeoutMs: request.timeoutMs ?? 120000,
21
+ fetchImpl
22
+ });
23
+
24
+ if (!result.ok) {
25
+ return {
26
+ ok: false,
27
+ error: redactSecret(result.error ?? "chat completions failed", apiKey)
28
+ };
29
+ }
30
+
31
+ const usage = result.data?.usage ?? {};
32
+ return {
33
+ ok: true,
34
+ model: result.data?.model ?? modelId,
35
+ content: result.data?.choices?.[0]?.message?.content ?? "",
36
+ usage: {
37
+ inputTokens: usage.prompt_tokens ?? null,
38
+ outputTokens: usage.completion_tokens ?? null,
39
+ cachedTokens: usage.prompt_tokens_details?.cached_tokens ?? null,
40
+ estimatedCost: null
41
+ },
42
+ raw: result.data
43
+ };
44
+ }
45
+
46
+ export async function invokeResponses({
47
+ baseUrl,
48
+ apiKey,
49
+ modelId,
50
+ contextPack,
51
+ request,
52
+ fetchImpl
53
+ }) {
54
+ const result = await fetchJson(`${baseUrl}/responses`, {
55
+ method: "POST",
56
+ headers: bearerHeaders(apiKey),
57
+ body: {
58
+ model: modelId,
59
+ input: buildResponsesInput(contextPack, request)
60
+ },
61
+ timeoutMs: request.timeoutMs ?? 120000,
62
+ fetchImpl
63
+ });
64
+
65
+ if (!result.ok) {
66
+ return {
67
+ ok: false,
68
+ error: redactSecret(result.error ?? "responses request failed", apiKey)
69
+ };
70
+ }
71
+
72
+ const usage = result.data?.usage ?? {};
73
+ return {
74
+ ok: true,
75
+ model: result.data?.model ?? modelId,
76
+ content: extractResponsesContent(result.data),
77
+ usage: {
78
+ inputTokens: usage.input_tokens ?? usage.prompt_tokens ?? null,
79
+ outputTokens: usage.output_tokens ?? usage.completion_tokens ?? null,
80
+ cachedTokens: usage.input_tokens_details?.cached_tokens ?? null,
81
+ estimatedCost: null
82
+ },
83
+ raw: result.data
84
+ };
85
+ }
86
+
87
+ export function invokeViaTransport(transport, options) {
88
+ if (transport === TRANSPORT_KINDS.RESPONSES) {
89
+ return invokeResponses(options);
90
+ }
91
+ return invokeChatCompletions(options);
92
+ }
93
+
94
+ function bearerHeaders(apiKey) {
95
+ return {
96
+ Authorization: `Bearer ${apiKey}`,
97
+ "Content-Type": "application/json"
98
+ };
99
+ }
100
+
101
+ function redactSecret(message, secret) {
102
+ if (!message || !secret) return message ?? null;
103
+ return String(message).split(String(secret)).join("[REDACTED]");
104
+ }
105
+
106
+ function buildChatMessages(contextPack, request) {
107
+ const messages = [];
108
+ if (contextPack?.systemPrompt) {
109
+ messages.push({ role: "system", content: contextPack.systemPrompt });
110
+ }
111
+ if (Array.isArray(request.messages) && request.messages.length > 0) {
112
+ messages.push(...request.messages);
113
+ } else if (request.prompt) {
114
+ messages.push({ role: "user", content: request.prompt });
115
+ }
116
+ return messages;
117
+ }
118
+
119
+ function buildResponsesInput(contextPack, request) {
120
+ const parts = [];
121
+ if (contextPack?.systemPrompt) {
122
+ parts.push({ role: "system", content: contextPack.systemPrompt });
123
+ }
124
+ if (Array.isArray(request.messages) && request.messages.length > 0) {
125
+ parts.push(...request.messages);
126
+ } else if (request.prompt) {
127
+ parts.push({ role: "user", content: request.prompt });
128
+ }
129
+ return parts;
130
+ }
131
+
132
+ function extractResponsesContent(data) {
133
+ if (!data) return "";
134
+ if (typeof data.output_text === "string") return data.output_text;
135
+
136
+ if (Array.isArray(data.output)) {
137
+ const texts = [];
138
+ for (const item of data.output) {
139
+ if (item?.type === "message" && Array.isArray(item.content)) {
140
+ for (const part of item.content) {
141
+ if (part?.type === "output_text" && part.text) texts.push(part.text);
142
+ else if (typeof part?.text === "string") texts.push(part.text);
143
+ }
144
+ } else if (typeof item?.text === "string") {
145
+ texts.push(item.text);
146
+ }
147
+ }
148
+ if (texts.length > 0) return texts.join("");
149
+ }
150
+
151
+ return data?.choices?.[0]?.message?.content ?? "";
152
+ }
@@ -0,0 +1,83 @@
1
+ import { OPENCODE_API_KEY_ENV } from "../types.js";
2
+ import {
3
+ isDirectTransport,
4
+ normalizeModelId,
5
+ resolveOpencodeTransport,
6
+ toRuntimeModelRef
7
+ } from "../transport-registry.js";
8
+ import { invokeViaTransport } from "./opencode-http.js";
9
+ import { resolveOpencodeApiKey, redactSecret } from "./opencode-catalog.js";
10
+
11
+ /**
12
+ * Compose direct HTTP invoke onto an OpenCode catalog backend.
13
+ * No Go→Zen fallback here — routing owns that policy.
14
+ */
15
+ export function withOpencodeDirectInvoke(catalog, {
16
+ env = process.env,
17
+ apiKey = null,
18
+ fetchImpl = globalThis.fetch,
19
+ defaultModelId = null
20
+ } = {}) {
21
+ const resolvedKey = resolveOpencodeApiKey({ env, apiKey });
22
+ const { id, label, product, baseUrl } = catalog;
23
+
24
+ return {
25
+ ...catalog,
26
+
27
+ async invoke(contextPack, request = {}) {
28
+ if (!resolvedKey) {
29
+ return invokeError(id, `${OPENCODE_API_KEY_ENV} is not set. Kairo never stores credentials.`);
30
+ }
31
+
32
+ const modelId = normalizeModelId(request.modelId) ?? defaultModelId;
33
+ const transport = resolveOpencodeTransport(product, modelId);
34
+
35
+ if (!isDirectTransport(transport)) {
36
+ const runtimeRef = toRuntimeModelRef(product, modelId) ?? modelId;
37
+ return invokeError(
38
+ id,
39
+ `Model ${modelId} has no direct ${label} transport. Use --backend opencode --model ${runtimeRef} with OpenCode CLI installed and authenticated.`,
40
+ modelId
41
+ );
42
+ }
43
+
44
+ const result = await invokeViaTransport(transport, {
45
+ baseUrl,
46
+ apiKey: resolvedKey,
47
+ modelId,
48
+ contextPack,
49
+ request,
50
+ fetchImpl
51
+ });
52
+
53
+ if (!result.ok) {
54
+ return invokeError(id, redactSecret(result.error, resolvedKey), modelId);
55
+ }
56
+
57
+ return {
58
+ ok: true,
59
+ backendId: id,
60
+ model: result.model,
61
+ content: result.content,
62
+ usage: {
63
+ ...result.usage,
64
+ model: result.model,
65
+ backendId: id,
66
+ fallbackUsed: false
67
+ },
68
+ raw: result.raw
69
+ };
70
+ }
71
+ };
72
+ }
73
+
74
+ function invokeError(backendId, message, model = null) {
75
+ return {
76
+ ok: false,
77
+ backendId,
78
+ model,
79
+ content: null,
80
+ error: message,
81
+ usage: null
82
+ };
83
+ }
@@ -0,0 +1,67 @@
1
+ import {
2
+ BACKEND_IDS,
3
+ OPENCODE_GO_BASE_URL,
4
+ OPENCODE_GO_DEFAULT_MODEL,
5
+ OPENCODE_ZEN_BASE_URL,
6
+ OPENCODE_ZEN_DEFAULT_FREE_MODEL
7
+ } from "../types.js";
8
+ import {
9
+ createOpencodeCatalogBackend,
10
+ goCostClassForModel,
11
+ zenCostClassForModel
12
+ } from "./opencode-catalog.js";
13
+ import { withOpencodeDirectInvoke } from "./opencode-invoke.js";
14
+
15
+ export function createOpencodeGoBackend({
16
+ env = process.env,
17
+ fetchImpl = globalThis.fetch,
18
+ apiKey = null,
19
+ baseUrl = OPENCODE_GO_BASE_URL,
20
+ collectCliEvidence
21
+ } = {}) {
22
+ const catalog = createOpencodeCatalogBackend({
23
+ id: BACKEND_IDS.OPENCODE_GO,
24
+ label: "OpenCode Go",
25
+ product: "go",
26
+ baseUrl,
27
+ defaultModelId: OPENCODE_GO_DEFAULT_MODEL,
28
+ costClassForModel: goCostClassForModel,
29
+ env,
30
+ fetchImpl,
31
+ apiKey,
32
+ collectCliEvidence
33
+ });
34
+ return withOpencodeDirectInvoke(catalog, {
35
+ env,
36
+ apiKey,
37
+ fetchImpl,
38
+ defaultModelId: OPENCODE_GO_DEFAULT_MODEL
39
+ });
40
+ }
41
+
42
+ export function createOpencodeZenBackend({
43
+ env = process.env,
44
+ fetchImpl = globalThis.fetch,
45
+ apiKey = null,
46
+ baseUrl = OPENCODE_ZEN_BASE_URL,
47
+ collectCliEvidence
48
+ } = {}) {
49
+ const catalog = createOpencodeCatalogBackend({
50
+ id: BACKEND_IDS.OPENCODE_ZEN,
51
+ label: "OpenCode Zen",
52
+ product: "zen",
53
+ baseUrl,
54
+ defaultModelId: OPENCODE_ZEN_DEFAULT_FREE_MODEL,
55
+ costClassForModel: zenCostClassForModel,
56
+ env,
57
+ fetchImpl,
58
+ apiKey,
59
+ collectCliEvidence
60
+ });
61
+ return withOpencodeDirectInvoke(catalog, {
62
+ env,
63
+ apiKey,
64
+ fetchImpl,
65
+ defaultModelId: OPENCODE_ZEN_DEFAULT_FREE_MODEL
66
+ });
67
+ }