@smartmemory/compose 0.1.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 (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1014 -0
  3. package/bin/compose.js +1515 -0
  4. package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
  5. package/dist/assets/arc-SxJ2J1sh.js +1 -0
  6. package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
  7. package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
  8. package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
  9. package/dist/assets/channel-DGElom1e.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
  11. package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
  12. package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
  13. package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
  14. package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
  15. package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
  16. package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
  17. package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
  18. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
  19. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
  20. package/dist/assets/clone-DUJKJXd7.js +1 -0
  21. package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
  22. package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
  23. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  24. package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
  25. package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
  26. package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
  27. package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
  28. package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
  29. package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
  30. package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
  31. package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
  32. package/dist/assets/graph-D0Cfv00Y.js +1 -0
  33. package/dist/assets/index-CUd6pFGF.css +1 -0
  34. package/dist/assets/index-DReRlzZI.js +1144 -0
  35. package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
  38. package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
  39. package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-Bj72wOEB.js +1 -0
  42. package/dist/assets/linear-BRFo114D.js +1 -0
  43. package/dist/assets/min-GCHnKlJS.js +1 -0
  44. package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
  45. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  46. package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
  47. package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
  48. package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
  49. package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
  50. package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
  51. package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
  52. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
  53. package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
  54. package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
  55. package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
  56. package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
  57. package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
  58. package/dist/index.html +30 -0
  59. package/lib/agent-chains.js +65 -0
  60. package/lib/agent-string.js +86 -0
  61. package/lib/budget-ledger.js +86 -0
  62. package/lib/build-all.js +162 -0
  63. package/lib/build-dag.js +120 -0
  64. package/lib/build-stream-writer.js +190 -0
  65. package/lib/build.js +2997 -0
  66. package/lib/capability-checker.js +53 -0
  67. package/lib/cert-inject.js +38 -0
  68. package/lib/cli-progress.js +483 -0
  69. package/lib/constants.js +69 -0
  70. package/lib/cross-layer-audit.js +84 -0
  71. package/lib/debug-discipline.js +173 -0
  72. package/lib/feature-json.js +106 -0
  73. package/lib/gate-prompt.js +291 -0
  74. package/lib/gate-tiers.js +194 -0
  75. package/lib/health-history.js +119 -0
  76. package/lib/health-score.js +227 -0
  77. package/lib/ideabox.js +570 -0
  78. package/lib/import.js +244 -0
  79. package/lib/migrate-roadmap.js +94 -0
  80. package/lib/model-pricing.js +67 -0
  81. package/lib/new.js +413 -0
  82. package/lib/pipeline-cli.js +489 -0
  83. package/lib/plan-parser.js +103 -0
  84. package/lib/qa-scoping.js +474 -0
  85. package/lib/questionnaire.js +200 -0
  86. package/lib/resolve-port.js +7 -0
  87. package/lib/result-normalizer.js +349 -0
  88. package/lib/review-lenses.js +166 -0
  89. package/lib/roadmap-gen.js +210 -0
  90. package/lib/roadmap-parser.js +176 -0
  91. package/lib/server-probe.js +23 -0
  92. package/lib/staleness.js +87 -0
  93. package/lib/step-prompt.js +260 -0
  94. package/lib/step-validator.js +49 -0
  95. package/lib/stratum-mcp-client.js +365 -0
  96. package/lib/team-flag.js +46 -0
  97. package/lib/test-bootstrap.js +401 -0
  98. package/lib/triage.js +274 -0
  99. package/lib/vision-writer.js +391 -0
  100. package/package.json +111 -0
  101. package/pipelines/bug-fix.stratum.yaml +230 -0
  102. package/pipelines/build.stratum.yaml +498 -0
  103. package/pipelines/content.stratum.yaml +112 -0
  104. package/pipelines/coverage-sweep.stratum.yaml +52 -0
  105. package/pipelines/refactor.stratum.yaml +169 -0
  106. package/pipelines/research.stratum.yaml +88 -0
  107. package/pipelines/review-fix.stratum.yaml +109 -0
  108. package/presets/team-feature.stratum.yaml +105 -0
  109. package/presets/team-research.stratum.yaml +108 -0
  110. package/presets/team-review.stratum.yaml +106 -0
  111. package/scripts/agent-activity-hook.sh +31 -0
  112. package/scripts/agent-error-hook.sh +28 -0
  113. package/scripts/analyze-orphans.mjs +50 -0
  114. package/scripts/find-orphans.mjs +26 -0
  115. package/scripts/fix-phases.mjs +49 -0
  116. package/scripts/generate-stratum-spec.mjs +137 -0
  117. package/scripts/import-roadmap.mjs +116 -0
  118. package/scripts/phase-audit.mjs +33 -0
  119. package/scripts/run-pipeline.mjs +314 -0
  120. package/scripts/session-end-hook.sh +18 -0
  121. package/scripts/session-start-hook.sh +38 -0
  122. package/scripts/vision-hook.sh +104 -0
  123. package/scripts/vision-track.mjs +554 -0
  124. package/scripts/wire-all-orphans.mjs +108 -0
  125. package/scripts/wire-orphans.mjs +164 -0
  126. package/server/activity-routes.js +123 -0
  127. package/server/agent-health.js +197 -0
  128. package/server/agent-hooks.js +102 -0
  129. package/server/agent-mcp.js +10 -0
  130. package/server/agent-registry.js +95 -0
  131. package/server/agent-server.js +290 -0
  132. package/server/agent-spawn.js +251 -0
  133. package/server/agent-templates.js +77 -0
  134. package/server/artifact-manager.js +247 -0
  135. package/server/artifact-templates/architecture.md +28 -0
  136. package/server/artifact-templates/blueprint.md +21 -0
  137. package/server/artifact-templates/design.md +36 -0
  138. package/server/artifact-templates/plan.md +25 -0
  139. package/server/artifact-templates/prd.md +43 -0
  140. package/server/artifact-templates/report.md +40 -0
  141. package/server/block-tracker.js +90 -0
  142. package/server/build-stream-bridge.js +502 -0
  143. package/server/coalescing-buffer.js +46 -0
  144. package/server/compose-mcp-tools.js +479 -0
  145. package/server/compose-mcp.js +324 -0
  146. package/server/connectors/agent-connector.js +78 -0
  147. package/server/connectors/claude-sdk-connector.js +198 -0
  148. package/server/connectors/codex-connector.js +240 -0
  149. package/server/connectors/connector-discovery.js +18 -0
  150. package/server/connectors/connector-runtime.js +13 -0
  151. package/server/connectors/opencode-connector.js +200 -0
  152. package/server/design-routes.js +540 -0
  153. package/server/design-session.js +161 -0
  154. package/server/feature-scan.js +593 -0
  155. package/server/file-watcher.js +284 -0
  156. package/server/find-root.js +29 -0
  157. package/server/graph-export.js +343 -0
  158. package/server/ideabox-cache.js +77 -0
  159. package/server/ideabox-routes.js +294 -0
  160. package/server/index.js +156 -0
  161. package/server/model-tiers.js +49 -0
  162. package/server/pipeline-routes.js +288 -0
  163. package/server/policy-evaluator.js +36 -0
  164. package/server/project-root.js +122 -0
  165. package/server/security.js +23 -0
  166. package/server/session-manager.js +403 -0
  167. package/server/session-routes.js +190 -0
  168. package/server/session-store.js +107 -0
  169. package/server/settings-routes.js +35 -0
  170. package/server/settings-store.js +234 -0
  171. package/server/stratum-api.js +102 -0
  172. package/server/stratum-client.js +192 -0
  173. package/server/stratum-sync.js +193 -0
  174. package/server/summarizer.js +139 -0
  175. package/server/supervisor.js +196 -0
  176. package/server/vision-routes.js +668 -0
  177. package/server/vision-server.js +393 -0
  178. package/server/vision-store.js +360 -0
  179. package/server/vision-utils.js +179 -0
  180. package/server/worktree-gc.js +137 -0
  181. package/templates/ROADMAP.md +46 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * CodexConnector — spawns the official `codex exec --json` CLI for each prompt.
3
+ *
4
+ * Replaces the previous opencode-backed implementation. Uses the OpenAI Codex
5
+ * CLI (`codex`, installed via `npm i -g @openai/codex` or `brew install codex`)
6
+ * which streams structured JSONL events to stdout.
7
+ *
8
+ * Auth: run `codex login` once (ChatGPT OAuth), or set OPENAI_API_KEY.
9
+ *
10
+ * Model IDs use the form `<model>` or `<model>/<effort>` where effort is one
11
+ * of `minimal|low|medium|high|xhigh`. The effort suffix is split off and
12
+ * passed as `-c model_reasoning_effort=<effort>`.
13
+ */
14
+
15
+ import { spawn } from 'node:child_process';
16
+ import { createInterface } from 'node:readline';
17
+ import { AgentConnector, injectSchema } from './agent-connector.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Supported Codex model IDs (model + optional /effort suffix)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export const CODEX_MODEL_IDS = new Set([
24
+ 'gpt-5.4',
25
+ 'gpt-5.4/low',
26
+ 'gpt-5.4/medium',
27
+ 'gpt-5.4/high',
28
+ 'gpt-5.4/xhigh',
29
+ 'gpt-5.2-codex',
30
+ 'gpt-5.2-codex/low',
31
+ 'gpt-5.2-codex/medium',
32
+ 'gpt-5.2-codex/high',
33
+ 'gpt-5.2-codex/xhigh',
34
+ 'gpt-5.1-codex-max',
35
+ 'gpt-5.1-codex-max/low',
36
+ 'gpt-5.1-codex-max/medium',
37
+ 'gpt-5.1-codex-max/high',
38
+ 'gpt-5.1-codex-max/xhigh',
39
+ 'gpt-5.1-codex',
40
+ 'gpt-5.1-codex/low',
41
+ 'gpt-5.1-codex/medium',
42
+ 'gpt-5.1-codex/high',
43
+ 'gpt-5.1-codex-mini',
44
+ 'gpt-5.1-codex-mini/medium',
45
+ 'gpt-5.1-codex-mini/high',
46
+ ]);
47
+
48
+ const DEFAULT_MODEL_ID = process.env.CODEX_MODEL || 'gpt-5.4';
49
+ const AGENT_NAME = 'codex';
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // CodexConnector
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export class CodexConnector extends AgentConnector {
56
+ _defaultModelID;
57
+ _cwd;
58
+ #proc = null;
59
+
60
+ /**
61
+ * @param {object} [opts]
62
+ * @param {string} [opts.modelID] — Codex model ID; must be in CODEX_MODEL_IDS
63
+ * @param {string} [opts.cwd] — default working directory
64
+ * @throws {Error} if modelID is not a recognized Codex model
65
+ */
66
+ constructor({ modelID = DEFAULT_MODEL_ID, cwd = process.cwd() } = {}) {
67
+ super();
68
+ _assertCodexModel(modelID);
69
+ this._defaultModelID = modelID;
70
+ this._cwd = cwd;
71
+ }
72
+
73
+ // ── Runtime ────────────────────────────────────────────────────────────────
74
+
75
+ async *run(prompt, { schema, modelID, cwd } = {}) {
76
+ if (this.#proc) {
77
+ throw new Error(`${AGENT_NAME}: run() already active. Call interrupt() first.`);
78
+ }
79
+
80
+ const resolvedModelID = modelID ?? this._defaultModelID;
81
+ _assertCodexModel(resolvedModelID);
82
+ const resolvedCwd = cwd ?? this._cwd;
83
+ const actualPrompt = schema ? injectSchema(prompt, schema) : prompt;
84
+
85
+ const [baseModel, effort] = resolvedModelID.split('/');
86
+
87
+ yield {
88
+ type: 'system', subtype: 'init',
89
+ agent: AGENT_NAME, model: resolvedModelID,
90
+ };
91
+
92
+ const args = [
93
+ 'exec',
94
+ '--json',
95
+ '--skip-git-repo-check',
96
+ '--sandbox', 'read-only',
97
+ '-m', baseModel,
98
+ '-C', resolvedCwd,
99
+ ];
100
+ if (effort) {
101
+ args.push('-c', `model_reasoning_effort="${effort}"`);
102
+ }
103
+ args.push('-'); // read prompt from stdin
104
+
105
+ const proc = spawn('codex', args, {
106
+ cwd: resolvedCwd,
107
+ stdio: ['pipe', 'pipe', 'pipe'],
108
+ env: process.env,
109
+ });
110
+ this.#proc = proc;
111
+
112
+ // Write prompt via stdin to avoid argv length and quoting issues
113
+ proc.stdin.end(actualPrompt);
114
+
115
+ const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
116
+ const textParts = [];
117
+ const stderrChunks = [];
118
+
119
+ // Stream stderr — surface auth/rate-limit errors immediately
120
+ proc.stderr.on('data', chunk => {
121
+ stderrChunks.push(chunk);
122
+ const text = chunk.toString();
123
+ const lower = text.toLowerCase();
124
+ if (lower.includes('rate limit') || lower.includes('rate_limit') ||
125
+ lower.includes('quota') || lower.includes('insufficient_quota') ||
126
+ lower.includes('unauthorized') || lower.includes('401') ||
127
+ lower.includes('403') || lower.includes('authentication') ||
128
+ lower.includes('not logged in') || lower.includes('login required') ||
129
+ lower.includes('billing') || lower.includes('exceeded')) {
130
+ process.stderr.write(`\n⚠ ${AGENT_NAME}: ${text.trim()}\n`);
131
+ process.stderr.write(` → Check login: codex login status\n`);
132
+ process.stderr.write(` → Re-auth: codex login\n\n`);
133
+ }
134
+ });
135
+
136
+ // Stall detection — warn if no stdout events for 120s
137
+ let lastEventAt = Date.now();
138
+ const stallTimer = setInterval(() => {
139
+ const silent = Math.round((Date.now() - lastEventAt) / 1000);
140
+ if (silent >= 120) {
141
+ process.stderr.write(`\n⚠ ${AGENT_NAME}: no response for ${silent}s — may be stalled or rate-limited\n`);
142
+ process.stderr.write(` → Press s to skip, or Ctrl+C to abort\n\n`);
143
+ }
144
+ }, 30_000);
145
+
146
+ try {
147
+ for await (const line of rl) {
148
+ if (!line.trim()) continue;
149
+ lastEventAt = Date.now();
150
+
151
+ let event;
152
+ try { event = JSON.parse(line); } catch { continue; }
153
+
154
+ // codex exec --json event shapes:
155
+ // { type: 'thread.started', thread_id }
156
+ // { type: 'turn.started' }
157
+ // { type: 'item.started' | 'item.updated' | 'item.completed', item: {...} }
158
+ // { type: 'turn.completed', usage: { input_tokens, cached_input_tokens, output_tokens } }
159
+ // { type: 'error', message }
160
+ const t = event.type;
161
+
162
+ if (t === 'item.completed' && event.item) {
163
+ const item = event.item;
164
+ if (item.type === 'agent_message' && item.text) {
165
+ textParts.push(item.text);
166
+ yield { type: 'assistant', content: item.text };
167
+ } else if (item.type === 'command_execution') {
168
+ const cmd = item.command ?? item.input?.command ?? '';
169
+ yield { type: 'tool_use', tool: 'bash', input: { command: cmd } };
170
+ const out = item.aggregated_output ?? item.output ?? '';
171
+ if (out) {
172
+ const short = out.length > 80 ? out.slice(0, 77) + '...' : out;
173
+ yield { type: 'tool_use_summary', summary: short, output: String(out).slice(0, 2048) };
174
+ }
175
+ } else if (item.type === 'file_change') {
176
+ yield { type: 'tool_use', tool: 'edit', input: { path: item.path ?? '' } };
177
+ } else if (item.type === 'reasoning' && item.text) {
178
+ // Surface reasoning as assistant content for visibility
179
+ yield { type: 'assistant', content: item.text };
180
+ }
181
+ } else if (t === 'turn.completed' && event.usage) {
182
+ const u = event.usage;
183
+ yield {
184
+ type: 'usage',
185
+ input_tokens: u.input_tokens ?? 0,
186
+ output_tokens: u.output_tokens ?? 0,
187
+ cache_creation_input_tokens: 0,
188
+ cache_read_input_tokens: u.cached_input_tokens ?? 0,
189
+ cost_usd: 0,
190
+ model: resolvedModelID,
191
+ };
192
+ } else if (t === 'error') {
193
+ yield { type: 'error', message: event.message || 'codex error' };
194
+ }
195
+ }
196
+
197
+ const exitCode = await new Promise(resolve => proc.on('close', resolve));
198
+
199
+ if (exitCode !== 0 && textParts.length === 0) {
200
+ const stderr = Buffer.concat(stderrChunks).toString();
201
+ yield { type: 'error', message: stderr || `codex exited with code ${exitCode}` };
202
+ } else {
203
+ const fullText = textParts.join('');
204
+ if (fullText) yield { type: 'result', content: fullText };
205
+ yield { type: 'system', subtype: 'complete', agent: AGENT_NAME };
206
+ }
207
+ } catch (err) {
208
+ if (err?.name !== 'AbortError') {
209
+ yield { type: 'error', message: err.message || String(err) };
210
+ }
211
+ } finally {
212
+ clearInterval(stallTimer);
213
+ this.#proc = null;
214
+ }
215
+ }
216
+
217
+ interrupt() {
218
+ if (this.#proc) {
219
+ try { this.#proc.kill('SIGTERM'); } catch { /* ignore */ }
220
+ this.#proc = null;
221
+ }
222
+ }
223
+
224
+ get isRunning() {
225
+ return this.#proc !== null;
226
+ }
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Guard
231
+ // ---------------------------------------------------------------------------
232
+
233
+ function _assertCodexModel(modelID) {
234
+ if (!CODEX_MODEL_IDS.has(modelID)) {
235
+ throw new Error(
236
+ `CodexConnector: '${modelID}' is not a supported Codex model.\n` +
237
+ `Supported models: ${[...CODEX_MODEL_IDS].join(', ')}`
238
+ );
239
+ }
240
+ }
@@ -0,0 +1,18 @@
1
+ // compose/server/connectors/connector-discovery.js
2
+ /**
3
+ * @interface ConnectorDiscovery
4
+ *
5
+ * Stateless vendor capability contract.
6
+ * Implementations must not hold execution state.
7
+ *
8
+ * All three concrete connectors (ClaudeSDKConnector, CodexConnector, OpencodeConnector)
9
+ * satisfy this interface. Shape verified by test/connector-shape.test.js.
10
+ */
11
+ export const ConnectorDiscoveryInterface = {
12
+ /** @returns {string[]} model IDs available for this vendor */
13
+ listModels() {},
14
+ /** @param {string} modelId @returns {boolean} */
15
+ supportsModel(_modelId) {},
16
+ /** @param {string} sessionId @returns {Promise<object[]>} message history */
17
+ async loadHistory(_sessionId) { return []; },
18
+ };
@@ -0,0 +1,13 @@
1
+ // compose/server/connectors/connector-runtime.js
2
+ /**
3
+ * @interface ConnectorRuntime
4
+ *
5
+ * Stateful execution contract.
6
+ * See agent-connector.js for the message envelope spec.
7
+ */
8
+ export const ConnectorRuntimeInterface = {
9
+ /** @yields typed message envelopes */
10
+ async *run(_prompt, _opts) {},
11
+ interrupt() {},
12
+ get isRunning() { return false; },
13
+ };
@@ -0,0 +1,200 @@
1
+ /**
2
+ * OpencodeConnector — spawns `opencode run` for each prompt.
3
+ *
4
+ * Model-agnostic base for any non-Anthropic agent running through OpenCode.
5
+ * NOT exposed as an MCP tool directly — subclasses (e.g. CodexConnector)
6
+ * are exposed after constraining to a specific provider/model set.
7
+ *
8
+ * Uses `opencode run --format json` which streams structured JSON events
9
+ * (step_start, text, tool_use, step_finish) to stdout. This is more reliable
10
+ * than the SDK's serve mode which has event stream issues.
11
+ */
12
+
13
+ import { spawn } from 'node:child_process';
14
+ import { createInterface } from 'node:readline';
15
+ import { AgentConnector, injectSchema } from './agent-connector.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // OpencodeConnector
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export class OpencodeConnector extends AgentConnector {
22
+ // ── Discovery ──────────────────────────────────────────────────────────────
23
+ // No overrides — inherits stubs from AgentConnector. See agent-connector.js.
24
+
25
+ _defaultProviderID;
26
+ _defaultModelID;
27
+ _cwd;
28
+ _agentName;
29
+ #proc = null;
30
+
31
+ /**
32
+ * @param {object} opts
33
+ * @param {string} opts.providerID — OpenCode provider ID (e.g. 'openai')
34
+ * @param {string} opts.modelID — model ID (e.g. 'gpt-5.4')
35
+ * @param {string} [opts.cwd] — default working directory
36
+ * @param {string} [opts.agentName] — label used in system messages
37
+ */
38
+ constructor({ providerID, modelID, cwd = process.cwd(), agentName = 'opencode' }) {
39
+ super();
40
+ this._defaultProviderID = providerID;
41
+ this._defaultModelID = modelID;
42
+ this._cwd = cwd;
43
+ this._agentName = agentName;
44
+ }
45
+
46
+ // ── Runtime ────────────────────────────────────────────────────────────────
47
+
48
+ async *run(prompt, { schema, modelID, providerID, cwd } = {}) {
49
+ if (this.#proc) {
50
+ throw new Error(`${this._agentName}: run() already active. Call interrupt() first.`);
51
+ }
52
+
53
+ const resolvedProviderID = providerID ?? this._defaultProviderID;
54
+ const resolvedModelID = modelID ?? this._defaultModelID;
55
+ const resolvedCwd = cwd ?? this._cwd;
56
+ const actualPrompt = schema ? injectSchema(prompt, schema) : prompt;
57
+
58
+ yield {
59
+ type: 'system', subtype: 'init',
60
+ agent: this._agentName, model: `${resolvedProviderID}/${resolvedModelID}`,
61
+ };
62
+
63
+ // Build clean env: remove OPENAI_API_KEY so opencode uses OAuth
64
+ // (API key overrides OAuth and may lack Codex model access)
65
+ const cleanEnv = { ...process.env };
66
+ delete cleanEnv.OPENAI_API_KEY;
67
+
68
+ const proc = spawn('opencode', [
69
+ 'run',
70
+ '-m', `${resolvedProviderID}/${resolvedModelID}`,
71
+ '--format', 'json',
72
+ actualPrompt,
73
+ ], {
74
+ cwd: resolvedCwd,
75
+ stdio: ['ignore', 'pipe', 'pipe'],
76
+ env: cleanEnv,
77
+ });
78
+ this.#proc = proc;
79
+
80
+ // Read JSON events line-by-line from stdout
81
+ const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
82
+
83
+ const textParts = [];
84
+ let stderrChunks = [];
85
+
86
+ // Stream stderr live — detect rate-limit, auth, and quota errors immediately
87
+ proc.stderr.on('data', chunk => {
88
+ stderrChunks.push(chunk);
89
+ const text = chunk.toString();
90
+ const lower = text.toLowerCase();
91
+ // Check for actionable errors that should surface immediately
92
+ if (lower.includes('rate limit') || lower.includes('rate_limit') ||
93
+ lower.includes('quota') || lower.includes('insufficient_quota') ||
94
+ lower.includes('unauthorized') || lower.includes('401') ||
95
+ lower.includes('403') || lower.includes('authentication') ||
96
+ lower.includes('auth') || lower.includes('billing') ||
97
+ lower.includes('exceeded') || lower.includes('capacity')) {
98
+ process.stderr.write(`\n⚠ ${this._agentName}: ${text.trim()}\n`);
99
+ process.stderr.write(` → Check account: opencode auth status\n`);
100
+ process.stderr.write(` → Switch account: opencode auth login\n\n`);
101
+ }
102
+ });
103
+
104
+ // Stall detection — warn if no stdout events for 120s
105
+ let lastEventAt = Date.now();
106
+ const stallTimer = setInterval(() => {
107
+ const silent = Math.round((Date.now() - lastEventAt) / 1000);
108
+ if (silent >= 120) {
109
+ process.stderr.write(`\n⚠ ${this._agentName}: no response for ${silent}s — may be stalled or rate-limited\n`);
110
+ process.stderr.write(` → Press s to skip, or Ctrl+C to abort\n\n`);
111
+ }
112
+ }, 30_000);
113
+
114
+ try {
115
+ for await (const line of rl) {
116
+ if (!line.trim()) continue;
117
+ lastEventAt = Date.now();
118
+
119
+ let event;
120
+ try {
121
+ event = JSON.parse(line);
122
+ } catch {
123
+ continue; // skip non-JSON lines
124
+ }
125
+
126
+ if (event.type === 'text') {
127
+ const text = event.part?.text ?? '';
128
+ if (text) {
129
+ textParts.push(text);
130
+ yield { type: 'assistant', content: text };
131
+ }
132
+ } else if (event.type === 'tool_use') {
133
+ const tool = event.part?.tool ?? event.part?.state?.input?.command ? 'bash' : 'unknown';
134
+ const input = event.part?.state?.input ?? {};
135
+ yield { type: 'tool_use', tool, input };
136
+
137
+ // If tool has output, yield a summary
138
+ const output = event.part?.state?.output;
139
+ if (output && typeof output === 'string') {
140
+ const short = output.length > 80 ? output.slice(0, 77) + '...' : output;
141
+ yield { type: 'tool_use_summary', summary: short, output: output.slice(0, 2048) };
142
+ }
143
+ } else if (event.type === 'step_finish') {
144
+ // step_finish includes cost and token info — forward as usage event
145
+ const part = event.part;
146
+ if (part && (part.cost != null || part.tokens != null)) {
147
+ if (process.env.COMPOSE_DEBUG) {
148
+ process.stderr.write(` [${this._agentName}] cost=$${(part.cost ?? 0).toFixed(4)} tokens=${part.tokens?.total}\n`);
149
+ }
150
+ yield {
151
+ type: 'usage',
152
+ input_tokens: part.tokens?.input ?? 0,
153
+ output_tokens: part.tokens?.output ?? 0,
154
+ cache_creation_input_tokens: part.tokens?.cache_write ?? 0,
155
+ cache_read_input_tokens: part.tokens?.cache_read ?? 0,
156
+ cost_usd: part.cost ?? 0,
157
+ model: resolvedModelID,
158
+ };
159
+ }
160
+ }
161
+ // step_start is ignored (already yielded init)
162
+ }
163
+
164
+ // Wait for process to exit
165
+ const exitCode = await new Promise((resolve) => {
166
+ proc.on('close', resolve);
167
+ });
168
+
169
+ if (exitCode !== 0 && textParts.length === 0) {
170
+ const stderr = Buffer.concat(stderrChunks).toString();
171
+ yield { type: 'error', message: stderr || `opencode exited with code ${exitCode}` };
172
+ } else {
173
+ // Yield the full concatenated text as a result
174
+ const fullText = textParts.join('');
175
+ if (fullText) {
176
+ yield { type: 'result', content: fullText };
177
+ }
178
+ yield { type: 'system', subtype: 'complete', agent: this._agentName };
179
+ }
180
+ } catch (err) {
181
+ if (err?.name !== 'AbortError') {
182
+ yield { type: 'error', message: err.message || String(err) };
183
+ }
184
+ } finally {
185
+ clearInterval(stallTimer);
186
+ this.#proc = null;
187
+ }
188
+ }
189
+
190
+ interrupt() {
191
+ if (this.#proc) {
192
+ try { this.#proc.kill('SIGTERM'); } catch { /* ignore */ }
193
+ this.#proc = null;
194
+ }
195
+ }
196
+
197
+ get isRunning() {
198
+ return this.#proc !== null;
199
+ }
200
+ }