@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,474 +0,0 @@
1
- import { getOpencodeClient, resolveOpencodeModel } from './opencode-manager.js';
2
-
3
- const activeOpencodeSessions = new Map();
4
-
5
- function getEventSessionId(event) {
6
- if (!event || typeof event !== 'object') return null;
7
-
8
- const direct = event.sessionID || event.sessionId;
9
- if (typeof direct === 'string' && direct.trim()) {
10
- return direct.trim();
11
- }
12
-
13
- const props = event.properties || {};
14
- const propId = props.sessionID || props.sessionId;
15
- if (typeof propId === 'string' && propId.trim()) {
16
- return propId.trim();
17
- }
18
-
19
- if (props.info && typeof props.info === 'object') {
20
- const infoId = props.info.sessionID || props.info.sessionId;
21
- if (typeof infoId === 'string' && infoId.trim()) {
22
- return infoId.trim();
23
- }
24
- }
25
-
26
- if (props.part && typeof props.part === 'object') {
27
- const partId = props.part.sessionID || props.part.sessionId;
28
- if (typeof partId === 'string' && partId.trim()) {
29
- return partId.trim();
30
- }
31
- }
32
-
33
- return null;
34
- }
35
-
36
- function toIso(value, fallback = null) {
37
- if (typeof value === 'number' && Number.isFinite(value)) {
38
- return new Date(value).toISOString();
39
- }
40
-
41
- if (typeof value === 'string' && value.trim()) {
42
- const parsed = Date.parse(value);
43
- if (!Number.isNaN(parsed)) {
44
- return new Date(parsed).toISOString();
45
- }
46
- }
47
-
48
- return fallback;
49
- }
50
-
51
- function parsePermissionMode(permissionMode) {
52
- switch (permissionMode) {
53
- case 'acceptEdits':
54
- return {
55
- edit: 'allow',
56
- bash: 'allow',
57
- webfetch: 'allow'
58
- };
59
- case 'bypassPermissions':
60
- return {
61
- edit: 'allow',
62
- bash: 'allow',
63
- webfetch: 'allow',
64
- external_directory: 'allow'
65
- };
66
- case 'default':
67
- default:
68
- return null;
69
- }
70
- }
71
-
72
- function normalizeToolName(toolName = 'tool') {
73
- if (toolName === 'bash') return 'Bash';
74
- return toolName;
75
- }
76
-
77
- function mapPartToDelta(part, context) {
78
- if (!part || typeof part !== 'object') return null;
79
-
80
- const partId = part.id;
81
- const messageId = part.messageID;
82
-
83
- if (part.type === 'text') {
84
- const currentText = typeof part.text === 'string' ? part.text : '';
85
- const previousText = context.textPartCache.get(partId) || '';
86
- let delta = currentText;
87
-
88
- if (currentText.startsWith(previousText)) {
89
- delta = currentText.slice(previousText.length);
90
- }
91
-
92
- context.textPartCache.set(partId, currentText);
93
-
94
- if (!delta) {
95
- return null;
96
- }
97
-
98
- return {
99
- type: 'text_delta',
100
- messageId,
101
- partId,
102
- delta,
103
- timestamp: toIso(part.time?.start, new Date().toISOString())
104
- };
105
- }
106
-
107
- if (part.type === 'reasoning') {
108
- const currentText = typeof part.text === 'string' ? part.text : '';
109
- const previousText = context.reasoningPartCache.get(partId) || '';
110
- let delta = currentText;
111
-
112
- if (currentText.startsWith(previousText)) {
113
- delta = currentText.slice(previousText.length);
114
- }
115
-
116
- context.reasoningPartCache.set(partId, currentText);
117
-
118
- if (!delta) {
119
- return null;
120
- }
121
-
122
- return {
123
- type: 'reasoning_delta',
124
- messageId,
125
- partId,
126
- delta,
127
- timestamp: toIso(part.time?.start, new Date().toISOString())
128
- };
129
- }
130
-
131
- if (part.type === 'tool') {
132
- const callId = part.callID || part.id;
133
- const toolName = normalizeToolName(part.tool);
134
- const state = part.state || {};
135
-
136
- let toolInput = '';
137
- if (state.input && typeof state.input === 'object') {
138
- if (typeof state.input.command === 'string') {
139
- toolInput = state.input.command;
140
- } else {
141
- try {
142
- toolInput = JSON.stringify(state.input);
143
- } catch {
144
- toolInput = String(state.input);
145
- }
146
- }
147
- }
148
-
149
- let toolOutput = null;
150
- if (state.output !== undefined && state.output !== null) {
151
- toolOutput = typeof state.output === 'string' ? state.output : JSON.stringify(state.output);
152
- } else if (state.metadata?.output !== undefined && state.metadata?.output !== null) {
153
- toolOutput = typeof state.metadata.output === 'string'
154
- ? state.metadata.output
155
- : JSON.stringify(state.metadata.output);
156
- }
157
-
158
- return {
159
- type: 'tool_update',
160
- messageId,
161
- partId,
162
- toolCallId: callId,
163
- toolName,
164
- status: state.status || 'pending',
165
- toolInput,
166
- toolOutput,
167
- exitCode: typeof state.metadata?.exit === 'number' ? state.metadata.exit : undefined,
168
- timestamp: toIso(state.time?.start || part.time?.start, new Date().toISOString())
169
- };
170
- }
171
-
172
- if (part.type === 'step-finish' && part.tokens) {
173
- const input = Number(part.tokens.input || 0);
174
- const output = Number(part.tokens.output || 0);
175
- const reasoning = Number(part.tokens.reasoning || 0);
176
- const cacheRead = Number(part.tokens.cache?.read || 0);
177
- const cacheWrite = Number(part.tokens.cache?.write || 0);
178
-
179
- return {
180
- type: 'token_usage',
181
- usage: {
182
- used: input + output + reasoning + cacheRead + cacheWrite,
183
- total: 0,
184
- percentage: null,
185
- unsupported: true,
186
- message: 'OpenCode token total is unavailable from current event payload',
187
- breakdown: {
188
- input,
189
- output,
190
- reasoning,
191
- cacheRead,
192
- cacheCreation: cacheWrite
193
- }
194
- }
195
- };
196
- }
197
-
198
- return null;
199
- }
200
-
201
- function sendMessage(ws, data) {
202
- try {
203
- if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
204
- ws.send(data);
205
- } else if (typeof ws.send === 'function') {
206
- ws.send(JSON.stringify(data));
207
- }
208
- } catch (error) {
209
- console.error('[OpenCode] Error sending message:', error);
210
- }
211
- }
212
-
213
- function updateSessionId(session, ws, nextSessionId, broadcastUpdate = false) {
214
- if (!nextSessionId || nextSessionId === session.sessionId) return;
215
-
216
- const previousSessionId = session.sessionId;
217
- session.sessionId = nextSessionId;
218
-
219
- if (previousSessionId && activeOpencodeSessions.has(previousSessionId)) {
220
- activeOpencodeSessions.delete(previousSessionId);
221
- }
222
-
223
- activeOpencodeSessions.set(nextSessionId, session);
224
-
225
- if (ws?.setSessionId && typeof ws.setSessionId === 'function') {
226
- ws.setSessionId(nextSessionId);
227
- }
228
-
229
- if (broadcastUpdate) {
230
- sendMessage(ws, {
231
- type: 'session-created',
232
- sessionId: nextSessionId,
233
- provider: 'opencode'
234
- });
235
- }
236
- }
237
-
238
- export async function queryOpencode(command, options = {}, ws) {
239
- const {
240
- sessionId,
241
- cwd,
242
- projectPath,
243
- model,
244
- permissionMode = 'default'
245
- } = options;
246
-
247
- const workingDirectory = cwd || projectPath || process.cwd();
248
-
249
- let client;
250
- let currentSessionId = sessionId;
251
-
252
- const sessionState = {
253
- sessionId: currentSessionId || null,
254
- status: 'running',
255
- startedAt: new Date().toISOString(),
256
- abortController: new AbortController(),
257
- textPartCache: new Map(),
258
- reasoningPartCache: new Map()
259
- };
260
-
261
- try {
262
- client = await getOpencodeClient({ directory: workingDirectory });
263
-
264
- if (!currentSessionId) {
265
- const created = await client.session.create({
266
- query: { directory: workingDirectory },
267
- body: {
268
- title: 'OpenCode Session'
269
- }
270
- });
271
-
272
- currentSessionId = created?.data?.id;
273
- if (!currentSessionId) {
274
- throw new Error('Failed to create OpenCode session');
275
- }
276
- }
277
-
278
- sessionState.sessionId = currentSessionId;
279
- activeOpencodeSessions.set(currentSessionId, sessionState);
280
-
281
- if (ws?.setSessionId && typeof ws.setSessionId === 'function') {
282
- ws.setSessionId(currentSessionId);
283
- }
284
-
285
- sendMessage(ws, {
286
- type: 'session-created',
287
- sessionId: currentSessionId,
288
- provider: 'opencode'
289
- });
290
-
291
- const modelSelection = resolveOpencodeModel(model);
292
- const permission = parsePermissionMode(permissionMode);
293
-
294
- const events = await client.event.subscribe({
295
- query: { directory: workingDirectory },
296
- signal: sessionState.abortController.signal
297
- });
298
-
299
- await client.session.promptAsync({
300
- path: { id: currentSessionId },
301
- query: { directory: workingDirectory },
302
- body: {
303
- model: modelSelection,
304
- permission: permission || undefined,
305
- parts: [{
306
- type: 'text',
307
- text: command
308
- }]
309
- }
310
- });
311
-
312
- for await (const event of events.stream) {
313
- if (!event || sessionState.status !== 'running') {
314
- break;
315
- }
316
-
317
- const eventSessionId = getEventSessionId(event);
318
- if (eventSessionId) {
319
- updateSessionId(sessionState, ws, eventSessionId, true);
320
- currentSessionId = sessionState.sessionId;
321
- }
322
-
323
- if (eventSessionId && currentSessionId && eventSessionId !== currentSessionId) {
324
- continue;
325
- }
326
-
327
- if (event.type === 'server.connected') {
328
- continue;
329
- }
330
-
331
- if (event.type === 'message.part.updated') {
332
- const part = event.properties?.part;
333
- const deltaEvent = mapPartToDelta(part, sessionState);
334
- if (deltaEvent) {
335
- sendMessage(ws, {
336
- type: 'opencode-response',
337
- data: deltaEvent,
338
- sessionId: currentSessionId
339
- });
340
-
341
- if (deltaEvent.type === 'token_usage') {
342
- sendMessage(ws, {
343
- type: 'token-budget',
344
- data: deltaEvent.usage,
345
- sessionId: currentSessionId
346
- });
347
- }
348
- }
349
-
350
- continue;
351
- }
352
-
353
- if (event.type === 'message.updated') {
354
- const info = event.properties?.info;
355
- if (info?.error?.data?.message) {
356
- sendMessage(ws, {
357
- type: 'opencode-response',
358
- data: {
359
- type: 'error',
360
- message: info.error.data.message
361
- },
362
- sessionId: currentSessionId
363
- });
364
- }
365
- continue;
366
- }
367
-
368
- if (event.type === 'session.status') {
369
- sendMessage(ws, {
370
- type: 'opencode-response',
371
- data: {
372
- type: 'session_status',
373
- status: event.properties?.status?.type || 'unknown'
374
- },
375
- sessionId: currentSessionId
376
- });
377
- continue;
378
- }
379
-
380
- if (event.type === 'session.error') {
381
- sendMessage(ws, {
382
- type: 'opencode-response',
383
- data: {
384
- type: 'error',
385
- message: event.properties?.error?.data?.message || 'OpenCode session failed'
386
- },
387
- sessionId: currentSessionId
388
- });
389
- continue;
390
- }
391
-
392
- if (event.type === 'session.idle') {
393
- break;
394
- }
395
- }
396
-
397
- if (sessionState.status === 'running') {
398
- sendMessage(ws, {
399
- type: 'opencode-complete',
400
- sessionId: currentSessionId,
401
- actualSessionId: currentSessionId
402
- });
403
- sessionState.status = 'completed';
404
- }
405
- } catch (error) {
406
- if (sessionState.status === 'aborted') {
407
- sendMessage(ws, {
408
- type: 'opencode-complete',
409
- sessionId: currentSessionId,
410
- actualSessionId: currentSessionId,
411
- aborted: true
412
- });
413
- } else {
414
- sendMessage(ws, {
415
- type: 'opencode-error',
416
- error: error.message,
417
- sessionId: currentSessionId
418
- });
419
- sessionState.status = 'failed';
420
- }
421
- }
422
- }
423
-
424
- export function abortOpencodeSession(sessionId) {
425
- const session = activeOpencodeSessions.get(sessionId);
426
- if (!session) {
427
- return false;
428
- }
429
-
430
- session.status = 'aborted';
431
- try {
432
- session.abortController.abort();
433
- } catch {
434
- // no-op
435
- }
436
-
437
- return true;
438
- }
439
-
440
- export function isOpencodeSessionActive(sessionId) {
441
- const session = activeOpencodeSessions.get(sessionId);
442
- return session?.status === 'running';
443
- }
444
-
445
- export function getActiveOpencodeSessions() {
446
- const sessions = [];
447
-
448
- for (const [id, session] of activeOpencodeSessions.entries()) {
449
- if (session.status === 'running') {
450
- sessions.push({
451
- id,
452
- status: session.status,
453
- startedAt: session.startedAt
454
- });
455
- }
456
- }
457
-
458
- return sessions;
459
- }
460
-
461
- setInterval(() => {
462
- const now = Date.now();
463
- const maxAge = 30 * 60 * 1000;
464
-
465
- for (const [id, session] of activeOpencodeSessions.entries()) {
466
- if (session.status === 'running') continue;
467
-
468
- const startedAt = new Date(session.startedAt).getTime();
469
- if (now - startedAt > maxAge) {
470
- activeOpencodeSessions.delete(id);
471
- }
472
- }
473
- }, 5 * 60 * 1000);
474
-
@@ -1,99 +0,0 @@
1
- import express from 'express';
2
- import {
3
- listOpencodeSessions,
4
- getOpencodeSessionMessages,
5
- deleteOpencodeSession,
6
- listOpencodeModels,
7
- getOpencodeStatus
8
- } from '../opencode-manager.js';
9
-
10
- const router = express.Router();
11
-
12
- router.get('/sessions', async (req, res) => {
13
- try {
14
- const { projectPath } = req.query;
15
-
16
- if (!projectPath || typeof projectPath !== 'string') {
17
- return res.status(400).json({
18
- success: false,
19
- error: 'projectPath query parameter required'
20
- });
21
- }
22
-
23
- const sessions = await listOpencodeSessions(projectPath);
24
- res.json({ success: true, sessions });
25
- } catch (error) {
26
- console.error('Error fetching OpenCode sessions:', error);
27
- res.status(500).json({ success: false, error: error.message });
28
- }
29
- });
30
-
31
- router.get('/sessions/:sessionId/messages', async (req, res) => {
32
- try {
33
- const { sessionId } = req.params;
34
- const { limit, offset, projectPath } = req.query;
35
-
36
- const result = await getOpencodeSessionMessages(sessionId, {
37
- directory: typeof projectPath === 'string' ? projectPath : null,
38
- limit: limit ? parseInt(limit, 10) : null,
39
- offset: offset ? parseInt(offset, 10) : 0
40
- });
41
-
42
- res.json({ success: true, ...result });
43
- } catch (error) {
44
- console.error('Error fetching OpenCode session messages:', error);
45
- res.status(500).json({ success: false, error: error.message });
46
- }
47
- });
48
-
49
- router.delete('/sessions/:sessionId', async (req, res) => {
50
- try {
51
- const { sessionId } = req.params;
52
- const { projectPath } = req.query;
53
-
54
- const deleted = await deleteOpencodeSession(sessionId, {
55
- directory: typeof projectPath === 'string' ? projectPath : null
56
- });
57
-
58
- res.json({ success: deleted });
59
- } catch (error) {
60
- console.error(`Error deleting OpenCode session ${req.params.sessionId}:`, error);
61
- res.status(500).json({ success: false, error: error.message });
62
- }
63
- });
64
-
65
- router.get('/models', async (req, res) => {
66
- try {
67
- const { projectPath } = req.query;
68
-
69
- const models = await listOpencodeModels({
70
- directory: typeof projectPath === 'string' ? projectPath : null
71
- });
72
-
73
- res.json({
74
- success: true,
75
- models: models.options,
76
- defaultModel: models.defaultModel
77
- });
78
- } catch (error) {
79
- console.error('Error loading OpenCode models:', error);
80
- res.status(500).json({ success: false, error: error.message });
81
- }
82
- });
83
-
84
- router.get('/status', async (req, res) => {
85
- try {
86
- const status = await getOpencodeStatus();
87
- res.json(status);
88
- } catch (error) {
89
- console.error('Error checking OpenCode status:', error);
90
- res.status(500).json({
91
- authenticated: false,
92
- email: null,
93
- error: error.message
94
- });
95
- }
96
- });
97
-
98
- export default router;
99
-