@axhub/genie 0.1.3 → 0.1.4
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/index-C_OkRKiC.js +1249 -0
- package/dist/assets/index-CtRxrKDm.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/database/auth.db +0 -0
- package/server/index.js +1 -17
- package/server/projects.js +3 -72
- package/server/routes/agent.js +9 -54
- package/shared/modelConstants.js +0 -14
- package/dist/assets/index-BYKlB9hp.css +0 -32
- package/dist/assets/index-YzZ559FA.js +0 -1249
- package/dist/icons/opencode-white.svg +0 -4
- package/dist/icons/opencode.svg +0 -10
- package/server/opencode-manager.js +0 -605
- package/server/opencode-sdk.js +0 -474
- package/server/routes/opencode.js +0 -99
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OpenCode">
|
|
2
|
-
<rect width="256" height="256" rx="56" fill="#f8fafc"/>
|
|
3
|
-
<path d="M74 128c0-30.9 23.7-56 53-56h42v28h-39c-14.4 0-26 12.5-26 28s11.6 28 26 28h39v28h-42c-29.3 0-53-25.1-53-56z" fill="#111827"/>
|
|
4
|
-
</svg>
|
package/dist/icons/opencode.svg
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OpenCode">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0%" stop-color="#10b981"/>
|
|
5
|
-
<stop offset="100%" stop-color="#059669"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
</defs>
|
|
8
|
-
<rect width="256" height="256" rx="56" fill="url(#g)"/>
|
|
9
|
-
<path d="M74 128c0-30.9 23.7-56 53-56h42v28h-39c-14.4 0-26 12.5-26 28s11.6 28 26 28h39v28h-42c-29.3 0-53-25.1-53-56z" fill="#ffffff"/>
|
|
10
|
-
</svg>
|
|
@@ -1,605 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { promises as fs } from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import fetch from 'node-fetch';
|
|
6
|
-
import { createOpencodeClient } from '@opencode-ai/sdk/client';
|
|
7
|
-
|
|
8
|
-
const DEFAULT_HOST = process.env.OPENCODE_SERVER_HOST || '127.0.0.1';
|
|
9
|
-
const DEFAULT_PORT = Number(process.env.OPENCODE_SERVER_PORT || 4096);
|
|
10
|
-
const EXTERNAL_BASE_URL = (process.env.OPENCODE_BASE_URL || '').trim();
|
|
11
|
-
const DEFAULT_MODEL = process.env.OPENCODE_DEFAULT_MODEL || 'opencode/gpt-5-nano';
|
|
12
|
-
|
|
13
|
-
let managedServerProcess = null;
|
|
14
|
-
let serverStartPromise = null;
|
|
15
|
-
let resolvedBaseUrl = EXTERNAL_BASE_URL || `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
|
|
16
|
-
|
|
17
|
-
function buildAuthHeader() {
|
|
18
|
-
const password = process.env.OPENCODE_SERVER_PASSWORD;
|
|
19
|
-
if (!password) return null;
|
|
20
|
-
|
|
21
|
-
const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
|
|
22
|
-
const token = Buffer.from(`${username}:${password}`).toString('base64');
|
|
23
|
-
return `Basic ${token}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function buildClientOptions(baseUrl, directory = null) {
|
|
27
|
-
const authHeader = buildAuthHeader();
|
|
28
|
-
const options = { baseUrl };
|
|
29
|
-
|
|
30
|
-
if (directory) {
|
|
31
|
-
options.directory = directory;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (authHeader) {
|
|
35
|
-
options.headers = {
|
|
36
|
-
Authorization: authHeader
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return options;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function checkServerHealth(baseUrl, timeoutMs = 2000) {
|
|
44
|
-
const controller = new AbortController();
|
|
45
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const authHeader = buildAuthHeader();
|
|
49
|
-
const response = await fetch(`${baseUrl}/global/health`, {
|
|
50
|
-
signal: controller.signal,
|
|
51
|
-
headers: authHeader ? { Authorization: authHeader } : undefined
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (!response.ok) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const payload = await response.json();
|
|
59
|
-
return payload?.healthy === true;
|
|
60
|
-
} catch {
|
|
61
|
-
return false;
|
|
62
|
-
} finally {
|
|
63
|
-
clearTimeout(timeout);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function parseServerUrlFromOutput(output) {
|
|
68
|
-
if (!output) return null;
|
|
69
|
-
const match = output.match(/opencode server listening on\s+(https?:\/\/[^\s]+)/i);
|
|
70
|
-
return match ? match[1] : null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function startManagedServer() {
|
|
74
|
-
if (managedServerProcess && !managedServerProcess.killed) {
|
|
75
|
-
return resolvedBaseUrl;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
const args = ['serve', `--hostname=${DEFAULT_HOST}`, `--port=${DEFAULT_PORT}`];
|
|
80
|
-
const child = spawn('opencode', args, {
|
|
81
|
-
env: {
|
|
82
|
-
...process.env
|
|
83
|
-
},
|
|
84
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
managedServerProcess = child;
|
|
88
|
-
let combinedOutput = '';
|
|
89
|
-
let settled = false;
|
|
90
|
-
|
|
91
|
-
const finish = (error, baseUrl) => {
|
|
92
|
-
if (settled) return;
|
|
93
|
-
settled = true;
|
|
94
|
-
clearInterval(pollTimer);
|
|
95
|
-
clearTimeout(startTimeout);
|
|
96
|
-
|
|
97
|
-
if (error) {
|
|
98
|
-
reject(error);
|
|
99
|
-
} else {
|
|
100
|
-
resolve(baseUrl);
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const processOutputChunk = (chunk) => {
|
|
105
|
-
const text = chunk.toString();
|
|
106
|
-
combinedOutput += text;
|
|
107
|
-
const parsedUrl = parseServerUrlFromOutput(text);
|
|
108
|
-
if (parsedUrl) {
|
|
109
|
-
resolvedBaseUrl = parsedUrl;
|
|
110
|
-
finish(null, resolvedBaseUrl);
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
child.stdout?.on('data', processOutputChunk);
|
|
115
|
-
child.stderr?.on('data', processOutputChunk);
|
|
116
|
-
|
|
117
|
-
child.on('error', (error) => {
|
|
118
|
-
if (error?.code === 'ENOENT') {
|
|
119
|
-
finish(new Error('OpenCode CLI not found. Please install `opencode` first.'));
|
|
120
|
-
} else {
|
|
121
|
-
finish(error);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
child.on('exit', (code) => {
|
|
126
|
-
managedServerProcess = null;
|
|
127
|
-
if (!settled) {
|
|
128
|
-
const output = combinedOutput.trim();
|
|
129
|
-
finish(new Error(
|
|
130
|
-
`OpenCode server exited with code ${code}.${output ? `\n${output}` : ''}`
|
|
131
|
-
));
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const pollTimer = setInterval(async () => {
|
|
136
|
-
const healthy = await checkServerHealth(resolvedBaseUrl, 1200);
|
|
137
|
-
if (healthy) {
|
|
138
|
-
finish(null, resolvedBaseUrl);
|
|
139
|
-
}
|
|
140
|
-
}, 350);
|
|
141
|
-
|
|
142
|
-
const startTimeout = setTimeout(() => {
|
|
143
|
-
finish(new Error(`Timeout waiting for OpenCode server startup (${DEFAULT_PORT})`));
|
|
144
|
-
}, 15000);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export async function ensureOpencodeServerReady() {
|
|
149
|
-
const healthy = await checkServerHealth(resolvedBaseUrl);
|
|
150
|
-
if (healthy) {
|
|
151
|
-
return resolvedBaseUrl;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (EXTERNAL_BASE_URL) {
|
|
155
|
-
throw new Error(`OpenCode server is unreachable at ${EXTERNAL_BASE_URL}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (!serverStartPromise) {
|
|
159
|
-
serverStartPromise = startManagedServer().finally(() => {
|
|
160
|
-
serverStartPromise = null;
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return serverStartPromise;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export async function getOpencodeClient(options = {}) {
|
|
168
|
-
const { directory = null } = options;
|
|
169
|
-
const baseUrl = await ensureOpencodeServerReady();
|
|
170
|
-
return createOpencodeClient(buildClientOptions(baseUrl, directory));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function resolveOpencodeModel(modelValue) {
|
|
174
|
-
if (modelValue && typeof modelValue === 'object' && modelValue.providerID && modelValue.modelID) {
|
|
175
|
-
return {
|
|
176
|
-
providerID: modelValue.providerID,
|
|
177
|
-
modelID: modelValue.modelID
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const normalized = typeof modelValue === 'string' && modelValue.trim()
|
|
182
|
-
? modelValue.trim()
|
|
183
|
-
: DEFAULT_MODEL;
|
|
184
|
-
|
|
185
|
-
if (normalized.includes('/')) {
|
|
186
|
-
const [providerID, ...rest] = normalized.split('/');
|
|
187
|
-
return {
|
|
188
|
-
providerID,
|
|
189
|
-
modelID: rest.join('/')
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
providerID: 'opencode',
|
|
195
|
-
modelID: normalized
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function toIsoTime(value, fallback = null) {
|
|
200
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
201
|
-
return new Date(value).toISOString();
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (typeof value === 'string' && value.trim()) {
|
|
205
|
-
const parsed = Date.parse(value);
|
|
206
|
-
if (!Number.isNaN(parsed)) {
|
|
207
|
-
return new Date(parsed).toISOString();
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return fallback;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function normalizeToolName(rawToolName = 'tool') {
|
|
215
|
-
if (rawToolName === 'bash') return 'Bash';
|
|
216
|
-
return rawToolName;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function stringifyToolInput(input) {
|
|
220
|
-
if (input == null) return '';
|
|
221
|
-
if (typeof input === 'string') return input;
|
|
222
|
-
|
|
223
|
-
if (input.command && typeof input.command === 'string') {
|
|
224
|
-
return JSON.stringify({ command: input.command });
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
return JSON.stringify(input);
|
|
229
|
-
} catch {
|
|
230
|
-
return String(input);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function stringifyToolOutput(output) {
|
|
235
|
-
if (output == null) return '';
|
|
236
|
-
if (typeof output === 'string') return output;
|
|
237
|
-
try {
|
|
238
|
-
return JSON.stringify(output);
|
|
239
|
-
} catch {
|
|
240
|
-
return String(output);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export async function listOpencodeSessions(projectPath, options = {}) {
|
|
245
|
-
const { limit = 5 } = options;
|
|
246
|
-
const client = await getOpencodeClient({ directory: projectPath });
|
|
247
|
-
const response = await client.session.list({ query: { directory: projectPath } });
|
|
248
|
-
|
|
249
|
-
const sessions = Array.isArray(response?.data) ? response.data : [];
|
|
250
|
-
|
|
251
|
-
const normalized = sessions
|
|
252
|
-
.map((session) => ({
|
|
253
|
-
id: session.id,
|
|
254
|
-
summary: session.title || 'OpenCode Session',
|
|
255
|
-
name: session.title || 'OpenCode Session',
|
|
256
|
-
messageCount: 0,
|
|
257
|
-
createdAt: toIsoTime(session.time?.created, new Date().toISOString()),
|
|
258
|
-
lastActivity: toIsoTime(session.time?.updated, new Date().toISOString()),
|
|
259
|
-
cwd: session.directory || projectPath,
|
|
260
|
-
provider: 'opencode'
|
|
261
|
-
}))
|
|
262
|
-
.sort((left, right) => new Date(right.lastActivity) - new Date(left.lastActivity));
|
|
263
|
-
|
|
264
|
-
return limit > 0 ? normalized.slice(0, limit) : normalized;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function extractTokenUsageFromMessageInfo(info) {
|
|
268
|
-
const tokens = info?.tokens;
|
|
269
|
-
if (!tokens) return null;
|
|
270
|
-
|
|
271
|
-
const input = Number(tokens.input || 0);
|
|
272
|
-
const output = Number(tokens.output || 0);
|
|
273
|
-
const reasoning = Number(tokens.reasoning || 0);
|
|
274
|
-
const cacheRead = Number(tokens.cache?.read || 0);
|
|
275
|
-
const cacheWrite = Number(tokens.cache?.write || 0);
|
|
276
|
-
|
|
277
|
-
return {
|
|
278
|
-
used: input + output + reasoning + cacheRead + cacheWrite,
|
|
279
|
-
total: 0,
|
|
280
|
-
percentage: null,
|
|
281
|
-
unsupported: true,
|
|
282
|
-
message: 'OpenCode token total is unavailable from current API payload',
|
|
283
|
-
breakdown: {
|
|
284
|
-
input,
|
|
285
|
-
output,
|
|
286
|
-
reasoning,
|
|
287
|
-
cacheRead,
|
|
288
|
-
cacheCreation: cacheWrite
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function convertOpencodeMessageEntries(entries = []) {
|
|
294
|
-
const messages = [];
|
|
295
|
-
let latestTokenUsage = null;
|
|
296
|
-
|
|
297
|
-
for (const entry of entries) {
|
|
298
|
-
const info = entry?.info || {};
|
|
299
|
-
const parts = Array.isArray(entry?.parts) ? entry.parts : [];
|
|
300
|
-
const fallbackTimestamp = new Date().toISOString();
|
|
301
|
-
const createdAt = toIsoTime(info.time?.created, fallbackTimestamp);
|
|
302
|
-
|
|
303
|
-
if (info.role === 'user') {
|
|
304
|
-
const textParts = parts
|
|
305
|
-
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
306
|
-
.map((part) => part.text)
|
|
307
|
-
.filter(Boolean);
|
|
308
|
-
|
|
309
|
-
if (textParts.length > 0) {
|
|
310
|
-
messages.push({
|
|
311
|
-
timestamp: createdAt,
|
|
312
|
-
message: {
|
|
313
|
-
role: 'user',
|
|
314
|
-
content: textParts.join('\n')
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (info.role !== 'assistant') {
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const textParts = parts
|
|
327
|
-
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
328
|
-
.map((part) => part.text)
|
|
329
|
-
.filter(Boolean);
|
|
330
|
-
|
|
331
|
-
if (textParts.length > 0) {
|
|
332
|
-
messages.push({
|
|
333
|
-
timestamp: createdAt,
|
|
334
|
-
message: {
|
|
335
|
-
role: 'assistant',
|
|
336
|
-
content: textParts.join('\n')
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
for (const part of parts) {
|
|
342
|
-
if (part?.type === 'reasoning' && typeof part.text === 'string' && part.text.trim()) {
|
|
343
|
-
messages.push({
|
|
344
|
-
type: 'thinking',
|
|
345
|
-
timestamp: createdAt,
|
|
346
|
-
message: {
|
|
347
|
-
role: 'assistant',
|
|
348
|
-
content: part.text
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (part?.type !== 'tool') {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const toolCallId = part.callID || part.id;
|
|
358
|
-
const toolName = normalizeToolName(part.tool);
|
|
359
|
-
const toolInput = stringifyToolInput(part.state?.input || {});
|
|
360
|
-
const status = part.state?.status;
|
|
361
|
-
|
|
362
|
-
messages.push({
|
|
363
|
-
type: 'tool_use',
|
|
364
|
-
timestamp: createdAt,
|
|
365
|
-
toolName,
|
|
366
|
-
toolInput,
|
|
367
|
-
toolCallId
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
if (status !== 'completed' && status !== 'failed') {
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const output = part.state?.output ?? part.state?.metadata?.output;
|
|
375
|
-
if (output !== undefined && output !== null) {
|
|
376
|
-
const toolTimestamp = toIsoTime(part.state?.time?.end, createdAt);
|
|
377
|
-
messages.push({
|
|
378
|
-
type: 'tool_result',
|
|
379
|
-
timestamp: toolTimestamp,
|
|
380
|
-
toolCallId,
|
|
381
|
-
output: stringifyToolOutput(output)
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (info.error?.data?.message) {
|
|
387
|
-
messages.push({
|
|
388
|
-
timestamp: createdAt,
|
|
389
|
-
type: 'error',
|
|
390
|
-
message: {
|
|
391
|
-
role: 'error',
|
|
392
|
-
content: info.error.data.message
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const tokenUsage = extractTokenUsageFromMessageInfo(info);
|
|
398
|
-
if (tokenUsage) {
|
|
399
|
-
latestTokenUsage = tokenUsage;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
messages.sort((left, right) => new Date(left.timestamp || 0) - new Date(right.timestamp || 0));
|
|
404
|
-
return { messages, tokenUsage: latestTokenUsage };
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
export async function getOpencodeSessionMessages(sessionId, options = {}) {
|
|
408
|
-
const {
|
|
409
|
-
directory = null,
|
|
410
|
-
limit = null,
|
|
411
|
-
offset = 0
|
|
412
|
-
} = options;
|
|
413
|
-
|
|
414
|
-
const client = await getOpencodeClient({ directory });
|
|
415
|
-
const response = await client.session.messages({
|
|
416
|
-
path: { id: sessionId },
|
|
417
|
-
query: directory ? { directory } : undefined
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
const entries = Array.isArray(response?.data) ? response.data : [];
|
|
421
|
-
const { messages, tokenUsage } = convertOpencodeMessageEntries(entries);
|
|
422
|
-
|
|
423
|
-
const total = messages.length;
|
|
424
|
-
|
|
425
|
-
if (limit !== null) {
|
|
426
|
-
const safeLimit = Math.max(0, Number(limit) || 0);
|
|
427
|
-
const safeOffset = Math.max(0, Number(offset) || 0);
|
|
428
|
-
const startIndex = Math.max(0, total - safeOffset - safeLimit);
|
|
429
|
-
const endIndex = Math.max(0, total - safeOffset);
|
|
430
|
-
const page = messages.slice(startIndex, endIndex);
|
|
431
|
-
|
|
432
|
-
return {
|
|
433
|
-
messages: page,
|
|
434
|
-
total,
|
|
435
|
-
hasMore: startIndex > 0,
|
|
436
|
-
offset: safeOffset,
|
|
437
|
-
limit: safeLimit,
|
|
438
|
-
tokenUsage
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return {
|
|
443
|
-
messages,
|
|
444
|
-
total,
|
|
445
|
-
hasMore: false,
|
|
446
|
-
tokenUsage
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
export async function deleteOpencodeSession(sessionId, options = {}) {
|
|
451
|
-
const { directory = null } = options;
|
|
452
|
-
const client = await getOpencodeClient({ directory });
|
|
453
|
-
const response = await client.session.delete({
|
|
454
|
-
path: { id: sessionId },
|
|
455
|
-
query: directory ? { directory } : undefined
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
if (response?.error) {
|
|
459
|
-
throw new Error(response.error.message || 'Failed to delete OpenCode session');
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return response?.data === true;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export async function listOpencodeModels(options = {}) {
|
|
466
|
-
const { directory = null } = options;
|
|
467
|
-
const client = await getOpencodeClient({ directory });
|
|
468
|
-
const response = await client.config.providers({
|
|
469
|
-
query: directory ? { directory } : undefined
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
const payload = response?.data || {};
|
|
473
|
-
const providers = Array.isArray(payload.providers) ? payload.providers : [];
|
|
474
|
-
const modelOptions = [];
|
|
475
|
-
|
|
476
|
-
for (const provider of providers) {
|
|
477
|
-
const providerId = provider?.id;
|
|
478
|
-
if (!providerId || !provider?.models || typeof provider.models !== 'object') {
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const providerName = provider.name || providerId;
|
|
483
|
-
|
|
484
|
-
for (const [modelKey, modelConfig] of Object.entries(provider.models)) {
|
|
485
|
-
const modelId = modelConfig?.id || modelKey;
|
|
486
|
-
const displayName = modelConfig?.name || modelId;
|
|
487
|
-
modelOptions.push({
|
|
488
|
-
value: `${providerId}/${modelId}`,
|
|
489
|
-
label: `${displayName} (${providerName})`
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const uniqueOptions = Array.from(
|
|
495
|
-
new Map(modelOptions.map((option) => [option.value, option])).values()
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
const defaultModel = (() => {
|
|
499
|
-
const defaults = payload.default;
|
|
500
|
-
if (defaults && typeof defaults === 'object') {
|
|
501
|
-
const values = Object.values(defaults).filter((value) => typeof value === 'string' && value.includes('/'));
|
|
502
|
-
if (values.length > 0) {
|
|
503
|
-
return values[0];
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (uniqueOptions.length > 0) {
|
|
508
|
-
return uniqueOptions[0].value;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return DEFAULT_MODEL;
|
|
512
|
-
})();
|
|
513
|
-
|
|
514
|
-
if (uniqueOptions.length === 0) {
|
|
515
|
-
uniqueOptions.push({
|
|
516
|
-
value: DEFAULT_MODEL,
|
|
517
|
-
label: 'GPT-5 Nano (OpenCode)'
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return {
|
|
522
|
-
options: uniqueOptions,
|
|
523
|
-
defaultModel
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
export async function getOpencodeStatus() {
|
|
528
|
-
let cliVersion = null;
|
|
529
|
-
let cliAvailable = false;
|
|
530
|
-
|
|
531
|
-
try {
|
|
532
|
-
const version = await new Promise((resolve, reject) => {
|
|
533
|
-
const child = spawn('opencode', ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
534
|
-
let stdout = '';
|
|
535
|
-
let stderr = '';
|
|
536
|
-
|
|
537
|
-
child.stdout?.on('data', (chunk) => {
|
|
538
|
-
stdout += chunk.toString();
|
|
539
|
-
});
|
|
540
|
-
child.stderr?.on('data', (chunk) => {
|
|
541
|
-
stderr += chunk.toString();
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
child.on('error', reject);
|
|
545
|
-
child.on('close', (code) => {
|
|
546
|
-
if (code === 0) {
|
|
547
|
-
resolve(stdout.trim() || stderr.trim());
|
|
548
|
-
} else {
|
|
549
|
-
reject(new Error(stderr.trim() || `Exited with ${code}`));
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
cliVersion = version;
|
|
555
|
-
cliAvailable = true;
|
|
556
|
-
} catch {
|
|
557
|
-
cliVersion = null;
|
|
558
|
-
cliAvailable = false;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
let serverHealthy = false;
|
|
562
|
-
let serverUrl = resolvedBaseUrl;
|
|
563
|
-
let serverError = null;
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
serverUrl = await ensureOpencodeServerReady();
|
|
567
|
-
serverHealthy = true;
|
|
568
|
-
} catch (error) {
|
|
569
|
-
serverHealthy = false;
|
|
570
|
-
serverError = error.message;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const authCandidates = [
|
|
574
|
-
path.join(os.homedir(), '.local', 'share', 'opencode', 'auth.json'),
|
|
575
|
-
path.join(os.homedir(), '.config', 'opencode', 'auth.json')
|
|
576
|
-
];
|
|
577
|
-
|
|
578
|
-
let hasAuthFile = false;
|
|
579
|
-
for (const filePath of authCandidates) {
|
|
580
|
-
try {
|
|
581
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
582
|
-
const parsed = JSON.parse(content);
|
|
583
|
-
if (parsed && typeof parsed === 'object' && Object.keys(parsed).length > 0) {
|
|
584
|
-
hasAuthFile = true;
|
|
585
|
-
break;
|
|
586
|
-
}
|
|
587
|
-
} catch {
|
|
588
|
-
// Continue checking next location
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
const hasEnvKey = Boolean(process.env.OPENCODE_API_KEY || process.env.OPENROUTER_API_KEY);
|
|
593
|
-
const authenticated = cliAvailable && (hasAuthFile || hasEnvKey);
|
|
594
|
-
|
|
595
|
-
return {
|
|
596
|
-
authenticated,
|
|
597
|
-
email: authenticated ? 'Configured' : null,
|
|
598
|
-
cliAvailable,
|
|
599
|
-
cliVersion,
|
|
600
|
-
serverHealthy,
|
|
601
|
-
serverUrl,
|
|
602
|
-
error: serverError
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
|