@axhub/genie 0.2.6 → 0.2.8

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 (102) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-CTKZtqB1.js +460 -0
  3. package/dist/assets/{ReviewApp-BEicSBzW.js → ReviewApp-DM6BNAzR.js} +1 -1
  4. package/dist/assets/{_basePickBy-DkiHsp3X.js → _basePickBy-CqJbRZ9y.js} +1 -1
  5. package/dist/assets/{_baseUniq-7ElXb2sX.js → _baseUniq-BS8YH8jO.js} +1 -1
  6. package/dist/assets/{arc-CEsS3MdK.js → arc-BBmKEN-S.js} +1 -1
  7. package/dist/assets/{architectureDiagram-2XIMDMQ5-BubZ7T3U.js → architectureDiagram-2XIMDMQ5-N5lcb82R.js} +1 -1
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-Cza6M6Ht.js → blockDiagram-WCTKOSBZ-DTMwHuLn.js} +1 -1
  9. package/dist/assets/{c4Diagram-IC4MRINW-jhjtOQ12.js → c4Diagram-IC4MRINW-BTKlkXI9.js} +1 -1
  10. package/dist/assets/channel-1oJBvF-0.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB--HkodwbY.js → chunk-4BX2VUAB-DUdoTxAc.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-CyBuez4e.js → chunk-55IACEB6-Bm_92xe4.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-CuzG4iAl.js → chunk-FMBD7UC4-CGW0g62g.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-BNi8S861.js → chunk-JSJVCQXG-DYkTH3w1.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-D817O-GT.js → chunk-KX2RTZJC-C9oTlISU.js} +1 -1
  16. package/dist/assets/{chunk-NQ4KR5QH-DyujyOvx.js → chunk-NQ4KR5QH-CM50ygWP.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-VMEn-zxh.js → chunk-QZHKN3VN-7dzpYeNJ.js} +1 -1
  18. package/dist/assets/{chunk-WL4C6EOR-CQHHFLvx.js → chunk-WL4C6EOR-Cm9nQrsr.js} +1 -1
  19. package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +1 -0
  21. package/dist/assets/clone-CinxIlEu.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-qykDd54p.js → cose-bilkent-S5V4N54A-Ccp_p0JZ.js} +1 -1
  23. package/dist/assets/{dagre-KLK3FWXG-Bqp7DjEa.js → dagre-KLK3FWXG-fBwTLUp9.js} +1 -1
  24. package/dist/assets/{diagram-E7M64L7V-BKtx468K.js → diagram-E7M64L7V-CeNVmFUp.js} +1 -1
  25. package/dist/assets/{diagram-IFDJBPK2--fHfW6V2.js → diagram-IFDJBPK2-CtavyLGa.js} +1 -1
  26. package/dist/assets/{diagram-P4PSJMXO-D1kQI5RB.js → diagram-P4PSJMXO-CpQTjQwc.js} +1 -1
  27. package/dist/assets/{erDiagram-INFDFZHY-DT9YzdNw.js → erDiagram-INFDFZHY-B8R5vwhd.js} +1 -1
  28. package/dist/assets/{flowDiagram-PKNHOUZH-DWeNr4yg.js → flowDiagram-PKNHOUZH-BvkVVwIQ.js} +1 -1
  29. package/dist/assets/{ganttDiagram-A5KZAMGK--IgwcUhI.js → ganttDiagram-A5KZAMGK-DOu3hSNa.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-B5a8UWjN.js → gitGraphDiagram-K3NZZRJ6-C7zT67YE.js} +1 -1
  31. package/dist/assets/{graph-Cw1rYoD9.js → graph-D11wiwHo.js} +1 -1
  32. package/dist/assets/{highlighted-body-TPN3WLV5-BCxJHuqY.js → highlighted-body-TPN3WLV5-Babpthg-.js} +1 -1
  33. package/dist/assets/index-DFxzgWoO.js +2 -0
  34. package/dist/assets/index-YCFGDVKw.css +1 -0
  35. package/dist/assets/{infoDiagram-LFFYTUFH-D2u70rhN.js → infoDiagram-LFFYTUFH-BmA7IpQG.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-Cl8yrezU.js → ishikawaDiagram-PHBUUO56-BEquZd3E.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-ddP0AMU9.js → journeyDiagram-4ABVD52K-BfemGz7f.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-DbVt0v29.js → kanban-definition-K7BYSVSG-CWja3mln.js} +1 -1
  39. package/dist/assets/{layout-W_tRx4UV.js → layout-BLUNf-PJ.js} +1 -1
  40. package/dist/assets/{linear-CcMb2ay-.js → linear-DukIV_Xv.js} +1 -1
  41. package/dist/assets/{mermaid-O7DHMXV3-BBJqt8pT.js → mermaid-O7DHMXV3-SgtM28qI.js} +265 -215
  42. package/dist/assets/{mindmap-definition-YRQLILUH-BGhZa7Na.js → mindmap-definition-YRQLILUH-4UjqXITU.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CDyJaACv.js → pieDiagram-SKSYHLDU-8AxqJd0M.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-BSYuqf0Q.js → quadrantDiagram-337W2JSQ-D60m8V8r.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-Cfi9YX9H.js → requirementDiagram-Z7DCOOCP-zqh9jBVf.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-Di1ShaMF.js → sankeyDiagram-WA2Y5GQK-CDZILTLI.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-CYTTG38e.js → sequenceDiagram-2WXFIKYE-7BReFd0L.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-CVZYMqyW.js → stateDiagram-RAJIS63D-HPTVdIG4.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-B1sdb5mK.js → timeline-definition-YZTLITO2-CTVllFgr.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-CGG4gx3C.js → treemap-KZPCXAKY-BtyxboJZ.js} +1 -1
  52. package/dist/assets/{vennDiagram-LZ73GAT5-Dds37L2k.js → vennDiagram-LZ73GAT5-D96ZI6Mg.js} +1 -1
  53. package/dist/assets/{xychartDiagram-JWTSCODW-C8QKSyRR.js → xychartDiagram-JWTSCODW-eRk-39YO.js} +1 -1
  54. package/dist/index.html +2 -2
  55. package/package.json +35 -33
  56. package/server/_legacy-providers/README.md +30 -0
  57. package/server/_legacy-providers/claude-sdk.js +956 -0
  58. package/server/_legacy-providers/gemini-cli.js +368 -0
  59. package/server/_legacy-providers/openai-codex.js +705 -0
  60. package/server/_legacy-providers/opencode-cli.js +674 -0
  61. package/server/acp-runtime/client.js +1872 -0
  62. package/server/acp-runtime/index.js +408 -0
  63. package/server/acp-runtime/registry.js +45 -0
  64. package/server/acp-runtime/session-store.js +254 -0
  65. package/server/channels/runtime/AgentRuntimeAdapter.js +22 -80
  66. package/server/claude-sdk.js +24 -946
  67. package/server/cli.js +140 -2
  68. package/server/external-agent/service.js +52 -63
  69. package/server/gemini-cli.js +21 -360
  70. package/server/index.js +133 -58
  71. package/server/openai-codex.js +19 -695
  72. package/server/opencode-cli.js +68 -640
  73. package/server/projects.js +128 -85
  74. package/server/routes/agent.js +2 -0
  75. package/server/routes/cc-connect.js +1131 -0
  76. package/server/routes/cli-auth.js +1 -73
  77. package/server/routes/commands.js +4 -9
  78. package/server/routes/git.js +3 -20
  79. package/server/routes/projects.js +45 -24
  80. package/server/routes/session-core.js +44 -10
  81. package/server/session-core/abortSession.js +2 -18
  82. package/server/session-core/eventStore.js +5 -1
  83. package/server/session-core/providerAdapters.js +98 -10
  84. package/server/session-core/providerDiscovery.js +8 -3
  85. package/server/session-core/runtimeState.js +16 -17
  86. package/server/session-core/runtimeWriter.js +19 -12
  87. package/server/utils/ccConnectManager.js +390 -0
  88. package/server/utils/ccConnectState.js +575 -0
  89. package/server/utils/resolveCommandPath.js +71 -0
  90. package/server/utils/workspaceRoots.js +154 -0
  91. package/shared/conversationEvents.js +347 -10
  92. package/dist/assets/App-CYTE30Cf.js +0 -484
  93. package/dist/assets/channel-RmqTALN0.js +0 -1
  94. package/dist/assets/classDiagram-VBA2DB6C-wvVV1ggz.js +0 -1
  95. package/dist/assets/classDiagram-v2-RAHNMMFH-wvVV1ggz.js +0 -1
  96. package/dist/assets/clone-oT5aWXpf.js +0 -1
  97. package/dist/assets/index-CBuAXA5S.js +0 -2
  98. package/dist/assets/index-CyLWKyxy.css +0 -1
  99. package/dist/assets/stateDiagram-v2-FVOUBMTO-Bbl0b4-i.js +0 -1
  100. package/server/cli.test.js +0 -76
  101. package/server/external-agent/service.test.js +0 -53
  102. package/server/external-agent/ws.test.js +0 -289
@@ -0,0 +1,674 @@
1
+ import { spawn } from 'child_process';
2
+ import crossSpawn from 'cross-spawn';
3
+ import crypto from 'crypto';
4
+ import readline from 'readline';
5
+ import fetch from 'node-fetch';
6
+ import { promises as fs } from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import { resolveWorkingDirectory } from './utils/defaultWorkingDirectory.js';
10
+
11
+ const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
12
+
13
+ const activeOpencodeRuns = new Map();
14
+
15
+ const SERVER_READY_PREFIX = 'opencode server listening on ';
16
+ const LOCAL_SESSION_DIR = path.join(os.homedir(), '.opencode', 'sessions');
17
+
18
+ function generatePassword() {
19
+ return crypto.randomBytes(24).toString('base64url');
20
+ }
21
+
22
+ function parseModel(model) {
23
+ if (!model || typeof model !== 'string') return null;
24
+ const normalized = model.trim();
25
+ if (!normalized.includes('/')) return null;
26
+ const [providerID, modelID] = normalized.split('/');
27
+ if (!providerID || !modelID) return null;
28
+ return { providerID, modelID };
29
+ }
30
+
31
+ function getEventSessionId(payload) {
32
+ return (
33
+ payload?.properties?.sessionID ||
34
+ payload?.properties?.info?.sessionID ||
35
+ payload?.properties?.part?.sessionID ||
36
+ payload?.sessionID ||
37
+ null
38
+ );
39
+ }
40
+
41
+ function collectTextChunks(value) {
42
+ if (!value) return [];
43
+ if (typeof value === 'string') return value.trim() ? [value] : [];
44
+ if (Array.isArray(value)) return value.flatMap(collectTextChunks);
45
+ if (typeof value !== 'object') return [];
46
+
47
+ const chunks = [];
48
+ const candidates = [
49
+ value.text,
50
+ value.content,
51
+ value.message,
52
+ value.delta,
53
+ value.parts,
54
+ value.part,
55
+ value.properties,
56
+ value.data
57
+ ];
58
+ candidates.forEach((candidate) => {
59
+ chunks.push(...collectTextChunks(candidate));
60
+ });
61
+ return chunks;
62
+ }
63
+
64
+ function extractPartText(part) {
65
+ if (!part || typeof part !== 'object') return '';
66
+
67
+ if (typeof part.text === 'string') return part.text;
68
+ if (typeof part.content === 'string') return part.content;
69
+
70
+ if (Array.isArray(part.text)) {
71
+ return part.text
72
+ .map((item) => (typeof item === 'string' ? item : item?.text || item?.content || ''))
73
+ .join('');
74
+ }
75
+
76
+ if (Array.isArray(part.content)) {
77
+ return part.content
78
+ .map((item) => (typeof item === 'string' ? item : item?.text || item?.content || ''))
79
+ .join('');
80
+ }
81
+
82
+ return '';
83
+ }
84
+
85
+ function resolvePartRole(payload, part) {
86
+ return String(
87
+ part?.role ||
88
+ part?.message?.role ||
89
+ payload?.properties?.message?.role ||
90
+ payload?.properties?.info?.role ||
91
+ ''
92
+ ).toLowerCase();
93
+ }
94
+
95
+ function isReasoningPart(payload, part) {
96
+ const typeHints = [
97
+ part?.type,
98
+ part?.message?.type,
99
+ payload?.properties?.part?.type,
100
+ payload?.properties?.message?.type,
101
+ payload?.properties?.type,
102
+ payload?.type
103
+ ]
104
+ .filter(Boolean)
105
+ .map((value) => String(value).toLowerCase());
106
+
107
+ return typeHints.some((hint) =>
108
+ hint.includes('reasoning') || hint.includes('thinking') || hint.includes('thought')
109
+ );
110
+ }
111
+
112
+ function isUnsupportedImageInputText(text) {
113
+ return /this model does not support image input/i.test(String(text || ''));
114
+ }
115
+
116
+ function sendTextDelta(ws, sessionId, text) {
117
+ if (!text) return;
118
+ ws.send({
119
+ type: 'claude-response',
120
+ provider: 'opencode',
121
+ sessionId,
122
+ data: {
123
+ type: 'content_block_delta',
124
+ delta: {
125
+ type: 'text_delta',
126
+ text
127
+ }
128
+ }
129
+ });
130
+ }
131
+
132
+ function sendContentStop(ws, sessionId) {
133
+ ws.send({
134
+ type: 'claude-response',
135
+ provider: 'opencode',
136
+ sessionId,
137
+ data: { type: 'content_block_stop' }
138
+ });
139
+ }
140
+
141
+ function terminateProcess(child) {
142
+ if (!child || child.killed) return;
143
+ try {
144
+ child.kill('SIGTERM');
145
+ } catch {}
146
+ }
147
+
148
+ function localSessionFilePath(sessionId) {
149
+ return path.join(LOCAL_SESSION_DIR, `${sessionId}.jsonl`);
150
+ }
151
+
152
+ async function appendLocalSessionEvent(sessionId, event) {
153
+ if (!sessionId) return;
154
+ try {
155
+ await fs.mkdir(LOCAL_SESSION_DIR, { recursive: true });
156
+ const line = `${JSON.stringify(event)}\n`;
157
+ await fs.appendFile(localSessionFilePath(sessionId), line, 'utf8');
158
+ } catch {
159
+ }
160
+ }
161
+
162
+ async function waitForServerUrl(child, timeoutMs = 30000) {
163
+ return new Promise((resolve, reject) => {
164
+ let settled = false;
165
+ const captured = [];
166
+
167
+ const cleanup = () => {
168
+ clearTimeout(timer);
169
+ child.stdout?.off('error', onStreamError);
170
+ child.stderr?.off('error', onStreamError);
171
+ child.off('error', onProcessError);
172
+ child.off('close', onClose);
173
+ stdoutRl?.close();
174
+ stderrRl?.close();
175
+ };
176
+
177
+ const done = (fn) => {
178
+ if (settled) return;
179
+ settled = true;
180
+ cleanup();
181
+ fn();
182
+ };
183
+
184
+ const onStreamError = (error) => {
185
+ done(() => reject(error));
186
+ };
187
+
188
+ const onProcessError = (error) => {
189
+ done(() => reject(error));
190
+ };
191
+
192
+ const onClose = (code) => {
193
+ done(() => {
194
+ reject(new Error(`OpenCode server exited before ready (code ${code}). Output: ${captured.slice(-8).join(' | ')}`));
195
+ });
196
+ };
197
+
198
+ const onLine = (line) => {
199
+ if (!line) return;
200
+ captured.push(line);
201
+ if (captured.length > 64) captured.shift();
202
+
203
+ const trimmed = line.trim();
204
+ if (trimmed.startsWith(SERVER_READY_PREFIX)) {
205
+ const baseUrl = trimmed.slice(SERVER_READY_PREFIX.length).trim();
206
+ done(() => resolve(baseUrl));
207
+ }
208
+ };
209
+
210
+ const stdoutRl = child.stdout
211
+ ? readline.createInterface({ input: child.stdout })
212
+ : null;
213
+ const stderrRl = child.stderr
214
+ ? readline.createInterface({ input: child.stderr })
215
+ : null;
216
+
217
+ stdoutRl?.on('line', onLine);
218
+ stderrRl?.on('line', onLine);
219
+ child.stdout?.on('error', onStreamError);
220
+ child.stderr?.on('error', onStreamError);
221
+ child.on('error', onProcessError);
222
+ child.on('close', onClose);
223
+
224
+ const timer = setTimeout(() => {
225
+ done(() => {
226
+ reject(new Error(`Timed out waiting OpenCode server URL. Output: ${captured.slice(-8).join(' | ')}`));
227
+ });
228
+ }, timeoutMs);
229
+ });
230
+ }
231
+
232
+ function spawnServerCommand(command, args, workingDir, env) {
233
+ return spawnFunction(command, args, {
234
+ cwd: workingDir,
235
+ stdio: ['ignore', 'pipe', 'pipe'],
236
+ env
237
+ });
238
+ }
239
+
240
+ async function startOpenCodeServer(workingDir) {
241
+ const password = generatePassword();
242
+ const env = {
243
+ ...process.env,
244
+ NO_COLOR: '1',
245
+ NPM_CONFIG_LOGLEVEL: 'error',
246
+ NODE_NO_WARNINGS: '1',
247
+ OPENCODE_SERVER_USERNAME: 'opencode',
248
+ OPENCODE_SERVER_PASSWORD: password
249
+ };
250
+
251
+ const trySpawn = async (command, args) => {
252
+ const child = spawnServerCommand(command, args, workingDir, env);
253
+ const baseUrl = await waitForServerUrl(child);
254
+ return { child, baseUrl, password };
255
+ };
256
+
257
+ try {
258
+ return await trySpawn('opencode', ['serve', '--hostname', '127.0.0.1', '--port', '0']);
259
+ } catch (error) {
260
+ return trySpawn('npx', ['-y', 'opencode-ai@latest', 'serve', '--hostname', '127.0.0.1', '--port', '0']);
261
+ }
262
+ }
263
+
264
+ function authHeaders(password, workingDir) {
265
+ const basic = Buffer.from(`opencode:${password}`).toString('base64');
266
+ return {
267
+ Authorization: `Basic ${basic}`,
268
+ 'x-opencode-directory': workingDir,
269
+ Accept: 'application/json'
270
+ };
271
+ }
272
+
273
+ async function requestJson(baseUrl, path, { method = 'GET', headers = {}, body, signal } = {}) {
274
+ const response = await fetch(`${baseUrl}${path}`, {
275
+ method,
276
+ headers: {
277
+ ...headers,
278
+ ...(body ? { 'Content-Type': 'application/json' } : {})
279
+ },
280
+ body: body ? JSON.stringify(body) : undefined,
281
+ signal
282
+ });
283
+
284
+ const text = await response.text();
285
+ if (!response.ok) {
286
+ throw new Error(`OpenCode request failed: ${response.status} ${text}`);
287
+ }
288
+
289
+ if (!text.trim()) return null;
290
+ try {
291
+ return JSON.parse(text);
292
+ } catch {
293
+ return null;
294
+ }
295
+ }
296
+
297
+ export async function listOpencodeModels(options = {}) {
298
+ const { cwd, projectPath } = options;
299
+ const workingDir = resolveWorkingDirectory({ cwd, projectPath });
300
+ let server = null;
301
+
302
+ try {
303
+ server = await startOpenCodeServer(workingDir);
304
+ const headers = authHeaders(server.password, workingDir);
305
+ const directoryParam = encodeURIComponent(workingDir);
306
+
307
+ const configProviders = await requestJson(
308
+ server.baseUrl,
309
+ `/config/providers?directory=${directoryParam}`,
310
+ { headers }
311
+ );
312
+
313
+ const providerList = await requestJson(
314
+ server.baseUrl,
315
+ `/provider?directory=${directoryParam}`,
316
+ { headers }
317
+ ).catch((err) => {
318
+ console.warn('OpenCode provider list fetch failed:', err.message);
319
+ return null;
320
+ });
321
+
322
+ const providers = Array.isArray(configProviders?.providers)
323
+ ? configProviders.providers
324
+ : [];
325
+
326
+ const models = providers
327
+ .slice()
328
+ .sort((a, b) => String(a?.id || '').localeCompare(String(b?.id || '')))
329
+ .flatMap((provider) => {
330
+ const providerId = String(provider?.id || '').trim();
331
+ if (!providerId) return [];
332
+
333
+ const modelIds = Object.keys(provider?.models || {}).sort();
334
+ return modelIds.map((modelId) => `${providerId}/${modelId}`);
335
+ });
336
+
337
+ return {
338
+ models,
339
+ defaults: configProviders?.default || {},
340
+ connected: Array.isArray(providerList?.connected) ? providerList.connected : []
341
+ };
342
+ } finally {
343
+ if (server?.child) {
344
+ terminateProcess(server.child);
345
+ }
346
+ }
347
+ }
348
+
349
+ async function createOrReuseSession({ baseUrl, headers, workingDir, sessionId }) {
350
+ if (sessionId) return sessionId;
351
+ const created = await requestJson(baseUrl, `/session?directory=${encodeURIComponent(workingDir)}`, {
352
+ method: 'POST',
353
+ headers,
354
+ body: {}
355
+ });
356
+ const id = created?.id;
357
+ if (!id) {
358
+ throw new Error('OpenCode session.create did not return session id');
359
+ }
360
+ return id;
361
+ }
362
+
363
+ function createSSEReader(responseBody) {
364
+ const decoder = new TextDecoder('utf-8');
365
+ let buffer = '';
366
+ let eventData = '';
367
+ let eventId = '';
368
+
369
+ const flushEvent = () => {
370
+ if (!eventData) return null;
371
+ const payload = eventData.endsWith('\n') ? eventData.slice(0, -1) : eventData;
372
+ const event = { id: eventId || null, data: payload };
373
+ eventData = '';
374
+ eventId = '';
375
+ return event;
376
+ };
377
+
378
+ return {
379
+ async *read() {
380
+ for await (const chunk of responseBody) {
381
+ buffer += decoder.decode(chunk, { stream: true });
382
+
383
+ while (true) {
384
+ const idx = buffer.indexOf('\n');
385
+ if (idx === -1) break;
386
+
387
+ const line = buffer.slice(0, idx).replace(/\r$/, '');
388
+ buffer = buffer.slice(idx + 1);
389
+
390
+ if (line === '') {
391
+ const evt = flushEvent();
392
+ if (evt) yield evt;
393
+ continue;
394
+ }
395
+
396
+ if (line.startsWith('id:')) {
397
+ eventId = line.slice(3).trim();
398
+ } else if (line.startsWith('data:')) {
399
+ eventData += `${line.slice(5).trim()}\n`;
400
+ }
401
+ }
402
+ }
403
+
404
+ if (buffer.trim()) {
405
+ if (buffer.startsWith('data:')) {
406
+ eventData += `${buffer.slice(5).trim()}\n`;
407
+ }
408
+ }
409
+
410
+ const evt = flushEvent();
411
+ if (evt) yield evt;
412
+ }
413
+ };
414
+ }
415
+
416
+ async function subscribeEventStream({ baseUrl, headers, workingDir, sessionId, ws, abortSignal, promptText }) {
417
+ const response = await fetch(`${baseUrl}/event?directory=${encodeURIComponent(workingDir)}`, {
418
+ method: 'GET',
419
+ headers: {
420
+ ...headers,
421
+ Accept: 'text/event-stream'
422
+ },
423
+ signal: abortSignal
424
+ });
425
+
426
+ if (!response.ok || !response.body) {
427
+ const text = await response.text().catch(() => '');
428
+ throw new Error(`OpenCode event stream failed: ${response.status} ${text}`);
429
+ }
430
+
431
+ let gotIdle = false;
432
+ const sentTextByPart = new Map();
433
+ let assistantText = '';
434
+ let hasSentUnsupportedImageNotice = false;
435
+ for await (const evt of createSSEReader(response.body).read()) {
436
+ if (!evt?.data) continue;
437
+ let payload;
438
+ try {
439
+ payload = JSON.parse(evt.data);
440
+ } catch {
441
+ continue;
442
+ }
443
+
444
+ const eventType = payload?.type;
445
+ const eventSessionId = getEventSessionId(payload);
446
+ if (eventSessionId && eventSessionId !== sessionId) {
447
+ continue;
448
+ }
449
+
450
+ if (eventType === 'session.error') {
451
+ const errorMessage =
452
+ payload?.properties?.error?.message ||
453
+ payload?.properties?.error?.data?.message ||
454
+ 'OpenCode session error';
455
+ ws.send({
456
+ type: 'claude-error',
457
+ provider: 'opencode',
458
+ sessionId,
459
+ error: errorMessage
460
+ });
461
+ throw new Error(errorMessage);
462
+ }
463
+
464
+ if (eventType === 'session.idle') {
465
+ gotIdle = true;
466
+ break;
467
+ }
468
+
469
+ if (eventType === 'message.part.updated') {
470
+ const part = payload?.properties?.part;
471
+ if (isReasoningPart(payload, part)) {
472
+ continue;
473
+ }
474
+
475
+ const role = resolvePartRole(payload, part);
476
+ const isAssistantPart = role === 'assistant' || (!role && part?.type !== 'input_image');
477
+ if (!isAssistantPart) continue;
478
+
479
+ const partId = String(part?.id || part?.partID || part?.messageID || evt.id || 'unknown');
480
+ const currentText = extractPartText(part);
481
+ if (!currentText) continue;
482
+
483
+ if (isUnsupportedImageInputText(currentText)) {
484
+ if (!hasSentUnsupportedImageNotice) {
485
+ hasSentUnsupportedImageNotice = true;
486
+ ws.send({
487
+ type: 'claude-error',
488
+ provider: 'opencode',
489
+ sessionId,
490
+ error: 'Current model does not support image input. Remove attached images or switch to a vision-capable model.'
491
+ });
492
+ }
493
+ continue;
494
+ }
495
+
496
+ if (!role && promptText && currentText.trim() === promptText.trim()) {
497
+ continue;
498
+ }
499
+
500
+ const previousText = sentTextByPart.get(partId) || '';
501
+ let delta = '';
502
+
503
+ if (currentText.startsWith(previousText)) {
504
+ delta = currentText.slice(previousText.length);
505
+ } else if (currentText !== previousText) {
506
+ let commonPrefix = 0;
507
+ const maxPrefix = Math.min(previousText.length, currentText.length);
508
+ while (commonPrefix < maxPrefix && previousText[commonPrefix] === currentText[commonPrefix]) {
509
+ commonPrefix += 1;
510
+ }
511
+ delta = currentText.length > previousText.length
512
+ ? currentText.slice(commonPrefix)
513
+ : '';
514
+ }
515
+
516
+ sentTextByPart.set(partId, currentText);
517
+ if (delta) {
518
+ assistantText += delta;
519
+ sendTextDelta(ws, sessionId, delta);
520
+ }
521
+ }
522
+ }
523
+
524
+ if (!gotIdle && !abortSignal.aborted) {
525
+ throw new Error('OpenCode event stream closed before session.idle');
526
+ }
527
+
528
+ return { assistantText };
529
+ }
530
+
531
+ export async function queryOpencode(command, options = {}, ws) {
532
+ const {
533
+ sessionId,
534
+ cwd,
535
+ projectPath,
536
+ model,
537
+ permissionMode = 'default'
538
+ } = options;
539
+
540
+ const workingDir = resolveWorkingDirectory({ cwd, projectPath });
541
+ const abortController = new AbortController();
542
+ let server = null;
543
+ let resolvedSessionId = sessionId || null;
544
+
545
+ try {
546
+ server = await startOpenCodeServer(workingDir);
547
+ const headers = authHeaders(server.password, workingDir);
548
+
549
+ resolvedSessionId = await createOrReuseSession({
550
+ baseUrl: server.baseUrl,
551
+ headers,
552
+ workingDir,
553
+ sessionId
554
+ });
555
+
556
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
557
+ ws.setSessionId(resolvedSessionId);
558
+ }
559
+
560
+ if (!sessionId) {
561
+ ws.send({
562
+ type: 'session-created',
563
+ provider: 'opencode',
564
+ sessionId: resolvedSessionId
565
+ });
566
+ }
567
+
568
+ await appendLocalSessionEvent(resolvedSessionId, {
569
+ sessionId: resolvedSessionId,
570
+ cwd: workingDir,
571
+ timestamp: new Date().toISOString(),
572
+ role: 'user',
573
+ content: command || 'Continue'
574
+ });
575
+
576
+ activeOpencodeRuns.set(resolvedSessionId, {
577
+ abortController,
578
+ server,
579
+ workingDir,
580
+ sessionId: resolvedSessionId
581
+ });
582
+
583
+ const eventTask = subscribeEventStream({
584
+ baseUrl: server.baseUrl,
585
+ headers,
586
+ workingDir,
587
+ sessionId: resolvedSessionId,
588
+ ws,
589
+ abortSignal: abortController.signal,
590
+ promptText: command || ''
591
+ });
592
+
593
+ const modelSpec = parseModel(model);
594
+ await requestJson(
595
+ server.baseUrl,
596
+ `/session/${encodeURIComponent(resolvedSessionId)}/message?directory=${encodeURIComponent(workingDir)}`,
597
+ {
598
+ method: 'POST',
599
+ headers,
600
+ body: {
601
+ ...(modelSpec ? { model: modelSpec } : {}),
602
+ parts: [{ type: 'text', text: command || 'Continue' }]
603
+ },
604
+ signal: abortController.signal
605
+ }
606
+ );
607
+
608
+ const eventResult = await eventTask;
609
+
610
+ if (eventResult?.assistantText?.trim()) {
611
+ await appendLocalSessionEvent(resolvedSessionId, {
612
+ sessionId: resolvedSessionId,
613
+ cwd: workingDir,
614
+ timestamp: new Date().toISOString(),
615
+ role: 'assistant',
616
+ content: eventResult.assistantText
617
+ });
618
+ }
619
+
620
+ sendContentStop(ws, resolvedSessionId);
621
+ ws.send({
622
+ type: 'claude-complete',
623
+ provider: 'opencode',
624
+ sessionId: resolvedSessionId,
625
+ exitCode: 0,
626
+ isNewSession: !sessionId
627
+ });
628
+ } catch (error) {
629
+ if (abortController.signal.aborted) {
630
+ return;
631
+ }
632
+
633
+ ws.send({
634
+ type: 'claude-error',
635
+ provider: 'opencode',
636
+ sessionId: resolvedSessionId,
637
+ error: error.message || String(error)
638
+ });
639
+ throw error;
640
+ } finally {
641
+ if (resolvedSessionId) {
642
+ activeOpencodeRuns.delete(resolvedSessionId);
643
+ }
644
+ abortController.abort();
645
+ if (server?.child) {
646
+ terminateProcess(server.child);
647
+ }
648
+ }
649
+ }
650
+
651
+ export function abortOpencodeSession(sessionId) {
652
+ const run = activeOpencodeRuns.get(sessionId);
653
+ if (!run) return false;
654
+
655
+ run.abortController.abort();
656
+
657
+ const headers = authHeaders(run.server.password, run.workingDir || process.cwd());
658
+ fetch(`${run.server.baseUrl}/session/${encodeURIComponent(sessionId)}/abort?directory=${encodeURIComponent(run.workingDir || process.cwd())}`, {
659
+ method: 'POST',
660
+ headers
661
+ }).catch(() => {});
662
+
663
+ terminateProcess(run.server.child);
664
+ activeOpencodeRuns.delete(sessionId);
665
+ return true;
666
+ }
667
+
668
+ export function isOpencodeSessionActive(sessionId) {
669
+ return activeOpencodeRuns.has(sessionId);
670
+ }
671
+
672
+ export function getActiveOpencodeSessions() {
673
+ return Array.from(activeOpencodeRuns.keys());
674
+ }