@axhub/genie 0.2.5 → 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-CFRQvihx.js → _basePickBy-C19AekOu.js} +1 -1
- package/dist/assets/{_baseUniq-Dhh8nCvs.js → _baseUniq-JsnevLw_.js} +1 -1
- package/dist/assets/{arc-DQ0v3dU4.js → arc-BLpcuBlf.js} +1 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +36 -0
- package/dist/assets/{blockDiagram-WCTKOSBZ-Bbxhj5KC.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-DlvtrM0q.js → chunk-4BX2VUAB-De63kbgc.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DJUSHyTa.js → chunk-55IACEB6-DtTDDdM9.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C6Ch-htf.js → chunk-FMBD7UC4-DHuwd8tw.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-DzQIht58.js → chunk-JSJVCQXG-BgytFtmO.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-C05jARMH.js → chunk-KX2RTZJC-nZdp86aN.js} +1 -1
- package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +220 -0
- package/dist/assets/{chunk-QZHKN3VN-jxti9HTX.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-DJ3dNSYk.js → dagre-KLK3FWXG-CHYIvW47.js} +1 -1
- package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +24 -0
- package/dist/assets/{diagram-IFDJBPK2-Da6K4aP-.js → diagram-IFDJBPK2-Dzsiln_C.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-vZZKB92A.js → diagram-P4PSJMXO-DKnGbUpE.js} +1 -1
- package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +70 -0
- package/dist/assets/{flowDiagram-PKNHOUZH-DUV13pHi.js → flowDiagram-PKNHOUZH-BAZ2-jKp.js} +4 -4
- package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +292 -0
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BZ5gW69I.js → gitGraphDiagram-K3NZZRJ6-BflpyjGy.js} +1 -1
- package/dist/assets/{graph-BbvHswRd.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-8auUIPKW.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-Bappd2YO.js → kanban-definition-K7BYSVSG-FWinmur1.js} +5 -5
- package/dist/assets/{layout-BmbfFZKy.js → layout-vcz43XvZ.js} +1 -1
- package/dist/assets/{linear-WZnF-PT6.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-uxjlAy1t.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-2-FHHM-R.js → sankeyDiagram-WA2Y5GQK-4gulcOP4.js} +3 -3
- package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +145 -0
- package/dist/assets/{stateDiagram-RAJIS63D-DoW8U53H.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-ajdAP-72.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-C9If0AT0.js → vennDiagram-LZ73GAT5-8E_G06fI.js} +4 -4
- package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +7 -0
- package/dist/favicon.png +0 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/index.html +4 -5
- package/dist/logo-128.png +0 -0
- package/dist/logo-256.png +0 -0
- package/dist/logo-32.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-64.png +0 -0
- 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 +11 -5
- package/server/external-agent/service.js +77 -63
- package/server/gemini-cli.js +23 -360
- package/server/index.js +54 -46
- package/server/openai-codex.js +24 -698
- package/server/opencode-cli.js +70 -640
- package/server/routes/agent.js +2 -0
- package/server/routes/codex.js +5 -5
- package/server/routes/git.js +3 -20
- package/server/routes/mcp.js +18 -34
- 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/providerDiscovery.js +2 -2
- package/server/session-core/runtimeState.js +16 -17
- package/server/session-core/runtimeWriter.js +19 -12
- package/server/utils/codexPath.js +3 -1
- package/server/utils/spawnCommand.js +7 -0
- package/shared/conversationEvents.js +347 -10
- package/shared/conversationEvents.test.js +403 -0
- package/dist/assets/App-BxazfNJn.js +0 -484
- package/dist/assets/App-qxJ8_QYu.css +0 -32
- package/dist/assets/ReviewApp-CsqTAlGU.js +0 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-DmUHdvQH.js +0 -36
- package/dist/assets/c4Diagram-IC4MRINW-BOivDlQU.js +0 -10
- package/dist/assets/channel-Cj8xVD0X.js +0 -1
- package/dist/assets/chunk-NQ4KR5QH-Ci-n7jfu.js +0 -220
- package/dist/assets/chunk-WL4C6EOR-C559Mk71.js +0 -189
- package/dist/assets/classDiagram-VBA2DB6C-CI2zklxw.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-CI2zklxw.js +0 -1
- package/dist/assets/clone-BEVqubrI.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-DNO9ncXL.js +0 -1
- package/dist/assets/cytoscape.esm-2ZfV8NB5.js +0 -331
- package/dist/assets/diagram-E7M64L7V-Ba_LGLun.js +0 -24
- package/dist/assets/erDiagram-INFDFZHY-Csb8dFdP.js +0 -70
- package/dist/assets/ganttDiagram-A5KZAMGK-B5Kv9Wfz.js +0 -292
- package/dist/assets/highlighted-body-TPN3WLV5-DZJajMGm.js +0 -1
- package/dist/assets/index-BFX9lxRB.css +0 -1
- package/dist/assets/index-BiErUGrv.js +0 -2
- package/dist/assets/ishikawaDiagram-PHBUUO56-JmsNlo2I.js +0 -70
- package/dist/assets/journeyDiagram-4ABVD52K-Cuudv7Vv.js +0 -139
- package/dist/assets/mermaid-O7DHMXV3-D-2fQRvw.js +0 -988
- package/dist/assets/mindmap-definition-YRQLILUH-BQHnzzud.js +0 -68
- package/dist/assets/quadrantDiagram-337W2JSQ-DpwZU-f_.js +0 -7
- package/dist/assets/requirementDiagram-Z7DCOOCP-C_9ClOWm.js +0 -73
- package/dist/assets/sequenceDiagram-2WXFIKYE-egns-0XI.js +0 -145
- package/dist/assets/stateDiagram-v2-FVOUBMTO-BoFZZ4Ds.js +0 -1
- package/dist/assets/timeline-definition-YZTLITO2-chPa8ppH.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-DD42U6Or.js +0 -7
package/server/opencode-cli.js
CHANGED
|
@@ -1,674 +1,104 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
abortAgentSession,
|
|
3
|
+
executeAgentPrompt,
|
|
4
|
+
getActiveAgentSessions,
|
|
5
|
+
isAgentSessionActive
|
|
6
|
+
} from './acp-runtime/index.js';
|
|
7
|
+
import { OPENCODE_MODELS } from '../shared/modelConstants.js';
|
|
8
|
+
import { spawnCommand } from './utils/spawnCommand.js';
|
|
9
9
|
import { resolveWorkingDirectory } from './utils/defaultWorkingDirectory.js';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
]
|
|
11
|
+
function parseOpencodeModelList(stdout = '') {
|
|
12
|
+
return String(stdout)
|
|
13
|
+
.split(/\r?\n/)
|
|
14
|
+
.map((line) => line.trim())
|
|
104
15
|
.filter(Boolean)
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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`);
|
|
16
|
+
.filter((line) => line.includes('/'))
|
|
17
|
+
.filter((line) => !line.endsWith(':'))
|
|
18
|
+
.map((line) => line.split(/\s+/)[0])
|
|
19
|
+
.filter((line, index, list) => list.indexOf(line) === index);
|
|
150
20
|
}
|
|
151
21
|
|
|
152
|
-
async function
|
|
153
|
-
|
|
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
|
-
}
|
|
22
|
+
export async function listOpencodeModels(options = {}) {
|
|
23
|
+
const workingDir = resolveWorkingDirectory(options);
|
|
161
24
|
|
|
162
|
-
|
|
163
|
-
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
let stdout = '';
|
|
27
|
+
let stderr = '';
|
|
164
28
|
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
29
|
|
|
177
|
-
const
|
|
178
|
-
if (settled)
|
|
179
|
-
|
|
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));
|
|
30
|
+
const finalize = (result) => {
|
|
31
|
+
if (settled) {
|
|
32
|
+
return;
|
|
207
33
|
}
|
|
34
|
+
settled = true;
|
|
35
|
+
resolve(result);
|
|
208
36
|
};
|
|
209
37
|
|
|
210
|
-
|
|
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;
|
|
38
|
+
let childProcess = null;
|
|
438
39
|
try {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
40
|
+
childProcess = spawnCommand('opencode', ['models'], {
|
|
41
|
+
cwd: workingDir,
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
env: process.env
|
|
565
44
|
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
finalize({ models: [], defaults: {}, connected: [], error: error.message });
|
|
47
|
+
return;
|
|
566
48
|
}
|
|
567
49
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
});
|
|
50
|
+
const timeoutId = setTimeout(() => {
|
|
51
|
+
try {
|
|
52
|
+
childProcess.kill('SIGTERM');
|
|
53
|
+
} catch {}
|
|
54
|
+
finalize({ models: [], defaults: {}, connected: [], error: 'Command timeout' });
|
|
55
|
+
}, 12000);
|
|
575
56
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
server,
|
|
579
|
-
workingDir,
|
|
580
|
-
sessionId: resolvedSessionId
|
|
57
|
+
childProcess.stdout.on('data', (chunk) => {
|
|
58
|
+
stdout += chunk.toString();
|
|
581
59
|
});
|
|
582
60
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
headers,
|
|
586
|
-
workingDir,
|
|
587
|
-
sessionId: resolvedSessionId,
|
|
588
|
-
ws,
|
|
589
|
-
abortSignal: abortController.signal,
|
|
590
|
-
promptText: command || ''
|
|
61
|
+
childProcess.stderr.on('data', (chunk) => {
|
|
62
|
+
stderr += chunk.toString();
|
|
591
63
|
});
|
|
592
64
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
65
|
+
childProcess.on('close', () => {
|
|
66
|
+
clearTimeout(timeoutId);
|
|
67
|
+
finalize({
|
|
68
|
+
models: parseOpencodeModelList(stdout),
|
|
69
|
+
defaults: {},
|
|
70
|
+
connected: [],
|
|
71
|
+
error: stderr.trim() || null
|
|
617
72
|
});
|
|
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
73
|
});
|
|
628
|
-
} catch (error) {
|
|
629
|
-
if (abortController.signal.aborted) {
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
74
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
sessionId: resolvedSessionId,
|
|
637
|
-
error: error.message || String(error)
|
|
75
|
+
childProcess.on('error', (error) => {
|
|
76
|
+
clearTimeout(timeoutId);
|
|
77
|
+
finalize({ models: [], defaults: {}, connected: [], error: error.message });
|
|
638
78
|
});
|
|
639
|
-
|
|
640
|
-
} finally {
|
|
641
|
-
if (resolvedSessionId) {
|
|
642
|
-
activeOpencodeRuns.delete(resolvedSessionId);
|
|
643
|
-
}
|
|
644
|
-
abortController.abort();
|
|
645
|
-
if (server?.child) {
|
|
646
|
-
terminateProcess(server.child);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
79
|
+
});
|
|
649
80
|
}
|
|
650
81
|
|
|
651
|
-
export function
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
82
|
+
export async function queryOpencode(command, options = {}, writer) {
|
|
83
|
+
return executeAgentPrompt({
|
|
84
|
+
agentKey: 'opencode',
|
|
85
|
+
command,
|
|
86
|
+
options: {
|
|
87
|
+
...options,
|
|
88
|
+
model: options.model || OPENCODE_MODELS.DEFAULT
|
|
89
|
+
},
|
|
90
|
+
writer
|
|
91
|
+
});
|
|
92
|
+
}
|
|
662
93
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return true;
|
|
94
|
+
export async function abortOpencodeSession(sessionId) {
|
|
95
|
+
return abortAgentSession('opencode', sessionId);
|
|
666
96
|
}
|
|
667
97
|
|
|
668
98
|
export function isOpencodeSessionActive(sessionId) {
|
|
669
|
-
return
|
|
99
|
+
return isAgentSessionActive('opencode', sessionId);
|
|
670
100
|
}
|
|
671
101
|
|
|
672
102
|
export function getActiveOpencodeSessions() {
|
|
673
|
-
return
|
|
103
|
+
return getActiveAgentSessions('opencode');
|
|
674
104
|
}
|