@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.
- package/dist/api-docs.html +2 -2
- package/dist/assets/App-CTKZtqB1.js +460 -0
- package/dist/assets/{ReviewApp-BEicSBzW.js → ReviewApp-DM6BNAzR.js} +1 -1
- package/dist/assets/{_basePickBy-DkiHsp3X.js → _basePickBy-CqJbRZ9y.js} +1 -1
- package/dist/assets/{_baseUniq-7ElXb2sX.js → _baseUniq-BS8YH8jO.js} +1 -1
- package/dist/assets/{arc-CEsS3MdK.js → arc-BBmKEN-S.js} +1 -1
- package/dist/assets/{architectureDiagram-2XIMDMQ5-BubZ7T3U.js → architectureDiagram-2XIMDMQ5-N5lcb82R.js} +1 -1
- package/dist/assets/{blockDiagram-WCTKOSBZ-Cza6M6Ht.js → blockDiagram-WCTKOSBZ-DTMwHuLn.js} +1 -1
- package/dist/assets/{c4Diagram-IC4MRINW-jhjtOQ12.js → c4Diagram-IC4MRINW-BTKlkXI9.js} +1 -1
- package/dist/assets/channel-1oJBvF-0.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB--HkodwbY.js → chunk-4BX2VUAB-DUdoTxAc.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CyBuez4e.js → chunk-55IACEB6-Bm_92xe4.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CuzG4iAl.js → chunk-FMBD7UC4-CGW0g62g.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-BNi8S861.js → chunk-JSJVCQXG-DYkTH3w1.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-D817O-GT.js → chunk-KX2RTZJC-C9oTlISU.js} +1 -1
- package/dist/assets/{chunk-NQ4KR5QH-DyujyOvx.js → chunk-NQ4KR5QH-CM50ygWP.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-VMEn-zxh.js → chunk-QZHKN3VN-7dzpYeNJ.js} +1 -1
- package/dist/assets/{chunk-WL4C6EOR-CQHHFLvx.js → chunk-WL4C6EOR-Cm9nQrsr.js} +1 -1
- package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +1 -0
- package/dist/assets/clone-CinxIlEu.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-qykDd54p.js → cose-bilkent-S5V4N54A-Ccp_p0JZ.js} +1 -1
- package/dist/assets/{dagre-KLK3FWXG-Bqp7DjEa.js → dagre-KLK3FWXG-fBwTLUp9.js} +1 -1
- package/dist/assets/{diagram-E7M64L7V-BKtx468K.js → diagram-E7M64L7V-CeNVmFUp.js} +1 -1
- package/dist/assets/{diagram-IFDJBPK2--fHfW6V2.js → diagram-IFDJBPK2-CtavyLGa.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-D1kQI5RB.js → diagram-P4PSJMXO-CpQTjQwc.js} +1 -1
- package/dist/assets/{erDiagram-INFDFZHY-DT9YzdNw.js → erDiagram-INFDFZHY-B8R5vwhd.js} +1 -1
- package/dist/assets/{flowDiagram-PKNHOUZH-DWeNr4yg.js → flowDiagram-PKNHOUZH-BvkVVwIQ.js} +1 -1
- package/dist/assets/{ganttDiagram-A5KZAMGK--IgwcUhI.js → ganttDiagram-A5KZAMGK-DOu3hSNa.js} +1 -1
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-B5a8UWjN.js → gitGraphDiagram-K3NZZRJ6-C7zT67YE.js} +1 -1
- package/dist/assets/{graph-Cw1rYoD9.js → graph-D11wiwHo.js} +1 -1
- package/dist/assets/{highlighted-body-TPN3WLV5-BCxJHuqY.js → highlighted-body-TPN3WLV5-Babpthg-.js} +1 -1
- package/dist/assets/index-DFxzgWoO.js +2 -0
- package/dist/assets/index-YCFGDVKw.css +1 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-D2u70rhN.js → infoDiagram-LFFYTUFH-BmA7IpQG.js} +1 -1
- package/dist/assets/{ishikawaDiagram-PHBUUO56-Cl8yrezU.js → ishikawaDiagram-PHBUUO56-BEquZd3E.js} +1 -1
- package/dist/assets/{journeyDiagram-4ABVD52K-ddP0AMU9.js → journeyDiagram-4ABVD52K-BfemGz7f.js} +1 -1
- package/dist/assets/{kanban-definition-K7BYSVSG-DbVt0v29.js → kanban-definition-K7BYSVSG-CWja3mln.js} +1 -1
- package/dist/assets/{layout-W_tRx4UV.js → layout-BLUNf-PJ.js} +1 -1
- package/dist/assets/{linear-CcMb2ay-.js → linear-DukIV_Xv.js} +1 -1
- package/dist/assets/{mermaid-O7DHMXV3-BBJqt8pT.js → mermaid-O7DHMXV3-SgtM28qI.js} +265 -215
- package/dist/assets/{mindmap-definition-YRQLILUH-BGhZa7Na.js → mindmap-definition-YRQLILUH-4UjqXITU.js} +1 -1
- package/dist/assets/{pieDiagram-SKSYHLDU-CDyJaACv.js → pieDiagram-SKSYHLDU-8AxqJd0M.js} +1 -1
- package/dist/assets/{quadrantDiagram-337W2JSQ-BSYuqf0Q.js → quadrantDiagram-337W2JSQ-D60m8V8r.js} +1 -1
- package/dist/assets/{requirementDiagram-Z7DCOOCP-Cfi9YX9H.js → requirementDiagram-Z7DCOOCP-zqh9jBVf.js} +1 -1
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-Di1ShaMF.js → sankeyDiagram-WA2Y5GQK-CDZILTLI.js} +1 -1
- package/dist/assets/{sequenceDiagram-2WXFIKYE-CYTTG38e.js → sequenceDiagram-2WXFIKYE-7BReFd0L.js} +1 -1
- package/dist/assets/{stateDiagram-RAJIS63D-CVZYMqyW.js → stateDiagram-RAJIS63D-HPTVdIG4.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +1 -0
- package/dist/assets/{timeline-definition-YZTLITO2-B1sdb5mK.js → timeline-definition-YZTLITO2-CTVllFgr.js} +1 -1
- package/dist/assets/{treemap-KZPCXAKY-CGG4gx3C.js → treemap-KZPCXAKY-BtyxboJZ.js} +1 -1
- package/dist/assets/{vennDiagram-LZ73GAT5-Dds37L2k.js → vennDiagram-LZ73GAT5-D96ZI6Mg.js} +1 -1
- package/dist/assets/{xychartDiagram-JWTSCODW-C8QKSyRR.js → xychartDiagram-JWTSCODW-eRk-39YO.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +35 -33
- 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 +1872 -0
- package/server/acp-runtime/index.js +408 -0
- package/server/acp-runtime/registry.js +45 -0
- package/server/acp-runtime/session-store.js +254 -0
- package/server/channels/runtime/AgentRuntimeAdapter.js +22 -80
- package/server/claude-sdk.js +24 -946
- package/server/cli.js +140 -2
- package/server/external-agent/service.js +52 -63
- package/server/gemini-cli.js +21 -360
- package/server/index.js +133 -58
- package/server/openai-codex.js +19 -695
- package/server/opencode-cli.js +68 -640
- package/server/projects.js +128 -85
- package/server/routes/agent.js +2 -0
- package/server/routes/cc-connect.js +1131 -0
- package/server/routes/cli-auth.js +1 -73
- package/server/routes/commands.js +4 -9
- package/server/routes/git.js +3 -20
- package/server/routes/projects.js +45 -24
- 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 +8 -3
- package/server/session-core/runtimeState.js +16 -17
- package/server/session-core/runtimeWriter.js +19 -12
- package/server/utils/ccConnectManager.js +390 -0
- package/server/utils/ccConnectState.js +575 -0
- package/server/utils/resolveCommandPath.js +71 -0
- package/server/utils/workspaceRoots.js +154 -0
- package/shared/conversationEvents.js +347 -10
- package/dist/assets/App-CYTE30Cf.js +0 -484
- package/dist/assets/channel-RmqTALN0.js +0 -1
- 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/index-CBuAXA5S.js +0 -2
- package/dist/assets/index-CyLWKyxy.css +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-Bbl0b4-i.js +0 -1
- package/server/cli.test.js +0 -76
- package/server/external-agent/service.test.js +0 -53
- package/server/external-agent/ws.test.js +0 -289
package/server/opencode-cli.js
CHANGED
|
@@ -1,674 +1,102 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import os from 'os';
|
|
1
|
+
import {
|
|
2
|
+
abortAgentSession,
|
|
3
|
+
executeAgentPrompt,
|
|
4
|
+
getActiveAgentSessions,
|
|
5
|
+
isAgentSessionActive
|
|
6
|
+
} from './acp-runtime/index.js';
|
|
7
|
+
import { spawnCommand } from './utils/spawnCommand.js';
|
|
9
8
|
import { resolveWorkingDirectory } from './utils/defaultWorkingDirectory.js';
|
|
10
9
|
|
|
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
|
-
]
|
|
10
|
+
function parseOpencodeModelList(stdout = '') {
|
|
11
|
+
return String(stdout)
|
|
12
|
+
.split(/\r?\n/)
|
|
13
|
+
.map((line) => line.trim())
|
|
104
14
|
.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`);
|
|
15
|
+
.filter((line) => line.includes('/'))
|
|
16
|
+
.filter((line) => !line.endsWith(':'))
|
|
17
|
+
.map((line) => line.split(/\s+/)[0])
|
|
18
|
+
.filter((line, index, list) => list.indexOf(line) === index);
|
|
150
19
|
}
|
|
151
20
|
|
|
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
|
-
}
|
|
21
|
+
export async function listOpencodeModels(options = {}) {
|
|
22
|
+
const workingDir = resolveWorkingDirectory(options);
|
|
161
23
|
|
|
162
|
-
|
|
163
|
-
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let stdout = '';
|
|
26
|
+
let stderr = '';
|
|
164
27
|
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
28
|
|
|
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));
|
|
29
|
+
const finalize = (result) => {
|
|
30
|
+
if (settled) {
|
|
31
|
+
return;
|
|
207
32
|
}
|
|
33
|
+
settled = true;
|
|
34
|
+
resolve(result);
|
|
208
35
|
};
|
|
209
36
|
|
|
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;
|
|
37
|
+
let childProcess = null;
|
|
438
38
|
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
|
|
39
|
+
childProcess = spawnCommand('opencode', ['models'], {
|
|
40
|
+
cwd: workingDir,
|
|
41
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
42
|
+
env: process.env
|
|
565
43
|
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
finalize({ models: [], defaults: {}, connected: [], error: error.message });
|
|
46
|
+
return;
|
|
566
47
|
}
|
|
567
48
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
});
|
|
49
|
+
const timeoutId = setTimeout(() => {
|
|
50
|
+
try {
|
|
51
|
+
childProcess.kill('SIGTERM');
|
|
52
|
+
} catch {}
|
|
53
|
+
finalize({ models: [], defaults: {}, connected: [], error: 'Command timeout' });
|
|
54
|
+
}, 12000);
|
|
575
55
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
server,
|
|
579
|
-
workingDir,
|
|
580
|
-
sessionId: resolvedSessionId
|
|
56
|
+
childProcess.stdout.on('data', (chunk) => {
|
|
57
|
+
stdout += chunk.toString();
|
|
581
58
|
});
|
|
582
59
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
headers,
|
|
586
|
-
workingDir,
|
|
587
|
-
sessionId: resolvedSessionId,
|
|
588
|
-
ws,
|
|
589
|
-
abortSignal: abortController.signal,
|
|
590
|
-
promptText: command || ''
|
|
60
|
+
childProcess.stderr.on('data', (chunk) => {
|
|
61
|
+
stderr += chunk.toString();
|
|
591
62
|
});
|
|
592
63
|
|
|
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
|
|
64
|
+
childProcess.on('close', () => {
|
|
65
|
+
clearTimeout(timeoutId);
|
|
66
|
+
finalize({
|
|
67
|
+
models: parseOpencodeModelList(stdout),
|
|
68
|
+
defaults: {},
|
|
69
|
+
connected: [],
|
|
70
|
+
error: stderr.trim() || null
|
|
617
71
|
});
|
|
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
72
|
});
|
|
628
|
-
} catch (error) {
|
|
629
|
-
if (abortController.signal.aborted) {
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
73
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
sessionId: resolvedSessionId,
|
|
637
|
-
error: error.message || String(error)
|
|
74
|
+
childProcess.on('error', (error) => {
|
|
75
|
+
clearTimeout(timeoutId);
|
|
76
|
+
finalize({ models: [], defaults: {}, connected: [], error: error.message });
|
|
638
77
|
});
|
|
639
|
-
|
|
640
|
-
} finally {
|
|
641
|
-
if (resolvedSessionId) {
|
|
642
|
-
activeOpencodeRuns.delete(resolvedSessionId);
|
|
643
|
-
}
|
|
644
|
-
abortController.abort();
|
|
645
|
-
if (server?.child) {
|
|
646
|
-
terminateProcess(server.child);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
78
|
+
});
|
|
649
79
|
}
|
|
650
80
|
|
|
651
|
-
export function
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}).catch(() => {});
|
|
81
|
+
export async function queryOpencode(command, options = {}, writer) {
|
|
82
|
+
return executeAgentPrompt({
|
|
83
|
+
agentKey: 'opencode',
|
|
84
|
+
command,
|
|
85
|
+
options: {
|
|
86
|
+
...options
|
|
87
|
+
},
|
|
88
|
+
writer
|
|
89
|
+
});
|
|
90
|
+
}
|
|
662
91
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return true;
|
|
92
|
+
export async function abortOpencodeSession(sessionId) {
|
|
93
|
+
return abortAgentSession('opencode', sessionId);
|
|
666
94
|
}
|
|
667
95
|
|
|
668
96
|
export function isOpencodeSessionActive(sessionId) {
|
|
669
|
-
return
|
|
97
|
+
return isAgentSessionActive('opencode', sessionId);
|
|
670
98
|
}
|
|
671
99
|
|
|
672
100
|
export function getActiveOpencodeSessions() {
|
|
673
|
-
return
|
|
101
|
+
return getActiveAgentSessions('opencode');
|
|
674
102
|
}
|