@axhub/genie 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/App-BWSqiXAT.js +220 -0
- package/dist/assets/App-DrlLKa8f.css +1 -0
- package/dist/assets/ReviewApp-nz3mbArg.js +1 -0
- package/dist/assets/{_basePickBy-DkiHsp3X.js → _basePickBy-C19AekOu.js} +1 -1
- package/dist/assets/{_baseUniq-7ElXb2sX.js → _baseUniq-JsnevLw_.js} +1 -1
- package/dist/assets/{arc-CEsS3MdK.js → arc-BLpcuBlf.js} +1 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +36 -0
- package/dist/assets/{blockDiagram-WCTKOSBZ-Cza6M6Ht.js → blockDiagram-WCTKOSBZ-DQBLwsUS.js} +3 -3
- package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +10 -0
- package/dist/assets/channel-DkFNxV_H.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB--HkodwbY.js → chunk-4BX2VUAB-De63kbgc.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CyBuez4e.js → chunk-55IACEB6-DtTDDdM9.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CuzG4iAl.js → chunk-FMBD7UC4-DHuwd8tw.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-BNi8S861.js → chunk-JSJVCQXG-BgytFtmO.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-D817O-GT.js → chunk-KX2RTZJC-nZdp86aN.js} +1 -1
- package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +220 -0
- package/dist/assets/{chunk-QZHKN3VN-VMEn-zxh.js → chunk-QZHKN3VN-DvUQ3mnO.js} +1 -1
- package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +189 -0
- package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +1 -0
- package/dist/assets/clone-C0lCEIEO.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/{dagre-KLK3FWXG-Bqp7DjEa.js → dagre-KLK3FWXG-CHYIvW47.js} +1 -1
- package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +24 -0
- package/dist/assets/{diagram-IFDJBPK2--fHfW6V2.js → diagram-IFDJBPK2-Dzsiln_C.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-D1kQI5RB.js → diagram-P4PSJMXO-DKnGbUpE.js} +1 -1
- package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +70 -0
- package/dist/assets/{flowDiagram-PKNHOUZH-DWeNr4yg.js → flowDiagram-PKNHOUZH-BAZ2-jKp.js} +4 -4
- package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +292 -0
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-B5a8UWjN.js → gitGraphDiagram-K3NZZRJ6-BflpyjGy.js} +1 -1
- package/dist/assets/{graph-Cw1rYoD9.js → graph-suelaXFh.js} +1 -1
- package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +1 -0
- package/dist/assets/index-B01NxbUv.css +1 -0
- package/dist/assets/index-DW5pGgQ_.js +2 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-D2u70rhN.js → infoDiagram-LFFYTUFH-pfD1FA3p.js} +1 -1
- package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +70 -0
- package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +139 -0
- package/dist/assets/{kanban-definition-K7BYSVSG-DbVt0v29.js → kanban-definition-K7BYSVSG-FWinmur1.js} +5 -5
- package/dist/assets/{layout-W_tRx4UV.js → layout-vcz43XvZ.js} +1 -1
- package/dist/assets/{linear-CcMb2ay-.js → linear-le4gc0vx.js} +1 -1
- package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +870 -0
- package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +68 -0
- package/dist/assets/{pieDiagram-SKSYHLDU-CDyJaACv.js → pieDiagram-SKSYHLDU-C7PKDh3b.js} +2 -2
- package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +7 -0
- package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +73 -0
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-Di1ShaMF.js → sankeyDiagram-WA2Y5GQK-4gulcOP4.js} +3 -3
- package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +145 -0
- package/dist/assets/{stateDiagram-RAJIS63D-CVZYMqyW.js → stateDiagram-RAJIS63D-CB4Vl7qM.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +1 -0
- package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +61 -0
- package/dist/assets/{treemap-KZPCXAKY-CGG4gx3C.js → treemap-KZPCXAKY-DZSEE6Hz.js} +58 -58
- package/dist/assets/vendor-codemirror-CyOKkaQZ.js +31 -0
- package/dist/assets/vendor-react-CP4yFTs7.js +8 -0
- package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
- package/dist/assets/{vennDiagram-LZ73GAT5-Dds37L2k.js → vennDiagram-LZ73GAT5-8E_G06fI.js} +4 -4
- package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +7 -0
- package/dist/index.html +4 -4
- package/package.json +2 -1
- package/server/_legacy-providers/README.md +30 -0
- package/server/_legacy-providers/claude-sdk.js +956 -0
- package/server/_legacy-providers/gemini-cli.js +368 -0
- package/server/_legacy-providers/openai-codex.js +705 -0
- package/server/_legacy-providers/opencode-cli.js +674 -0
- package/server/acp-runtime/client.js +1805 -0
- package/server/acp-runtime/client.test.js +688 -0
- package/server/acp-runtime/index.js +419 -0
- package/server/acp-runtime/registry.js +45 -0
- package/server/acp-runtime/session-store.js +254 -0
- package/server/acp-runtime/session-store.test.js +89 -0
- package/server/channels/runtime/AgentRuntimeAdapter.js +21 -70
- package/server/claude-sdk.js +24 -944
- package/server/cli.js +4 -2
- package/server/external-agent/service.js +52 -63
- package/server/gemini-cli.js +23 -360
- package/server/index.js +47 -44
- package/server/openai-codex.js +24 -698
- package/server/opencode-cli.js +70 -640
- package/server/routes/agent.js +2 -0
- package/server/routes/git.js +3 -20
- package/server/routes/session-core.js +44 -10
- package/server/session-core/abortSession.js +2 -18
- package/server/session-core/eventStore.js +5 -1
- package/server/session-core/providerAdapters.js +98 -10
- package/server/session-core/runtimeState.js +16 -17
- package/server/session-core/runtimeWriter.js +19 -12
- package/shared/conversationEvents.js +347 -10
- package/shared/conversationEvents.test.js +403 -0
- package/dist/assets/App-CYTE30Cf.js +0 -484
- package/dist/assets/App-qxJ8_QYu.css +0 -32
- package/dist/assets/ReviewApp-BEicSBzW.js +0 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-BubZ7T3U.js +0 -36
- package/dist/assets/c4Diagram-IC4MRINW-jhjtOQ12.js +0 -10
- package/dist/assets/channel-RmqTALN0.js +0 -1
- package/dist/assets/chunk-NQ4KR5QH-DyujyOvx.js +0 -220
- package/dist/assets/chunk-WL4C6EOR-CQHHFLvx.js +0 -189
- package/dist/assets/classDiagram-VBA2DB6C-wvVV1ggz.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-wvVV1ggz.js +0 -1
- package/dist/assets/clone-oT5aWXpf.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-qykDd54p.js +0 -1
- package/dist/assets/cytoscape.esm-2ZfV8NB5.js +0 -331
- package/dist/assets/diagram-E7M64L7V-BKtx468K.js +0 -24
- package/dist/assets/erDiagram-INFDFZHY-DT9YzdNw.js +0 -70
- package/dist/assets/ganttDiagram-A5KZAMGK--IgwcUhI.js +0 -292
- package/dist/assets/highlighted-body-TPN3WLV5-BCxJHuqY.js +0 -1
- package/dist/assets/index-CBuAXA5S.js +0 -2
- package/dist/assets/index-CyLWKyxy.css +0 -1
- package/dist/assets/ishikawaDiagram-PHBUUO56-Cl8yrezU.js +0 -70
- package/dist/assets/journeyDiagram-4ABVD52K-ddP0AMU9.js +0 -139
- package/dist/assets/mermaid-O7DHMXV3-BBJqt8pT.js +0 -988
- package/dist/assets/mindmap-definition-YRQLILUH-BGhZa7Na.js +0 -68
- package/dist/assets/quadrantDiagram-337W2JSQ-BSYuqf0Q.js +0 -7
- package/dist/assets/requirementDiagram-Z7DCOOCP-Cfi9YX9H.js +0 -73
- package/dist/assets/sequenceDiagram-2WXFIKYE-CYTTG38e.js +0 -145
- package/dist/assets/stateDiagram-v2-FVOUBMTO-Bbl0b4-i.js +0 -1
- package/dist/assets/timeline-definition-YZTLITO2-B1sdb5mK.js +0 -61
- package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
- package/dist/assets/vendor-react-Cpt6D04s.js +0 -59
- package/dist/assets/vendor-xterm-DfaPXD3y.js +0 -66
- package/dist/assets/xychartDiagram-JWTSCODW-C8QKSyRR.js +0 -7
|
@@ -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
|
+
}
|