@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.
@@ -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>
@@ -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
-