@axhub/genie 0.1.2 → 0.1.3
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-BYKlB9hp.css +32 -0
- package/dist/assets/index-YzZ559FA.js +1249 -0
- package/dist/icons/opencode-white.svg +4 -0
- package/dist/icons/opencode.svg +10 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/cli.js +93 -13
- package/server/index.js +18 -2
- package/server/opencode-manager.js +605 -0
- package/server/opencode-sdk.js +474 -0
- package/server/projects.js +72 -3
- package/server/routes/agent.js +54 -9
- package/server/routes/opencode.js +99 -0
- package/shared/modelConstants.js +14 -0
- package/dist/assets/index-CtRxrKDm.css +0 -32
- package/dist/assets/index-DfLTVRPO.js +0 -1249
|
@@ -0,0 +1,474 @@
|
|
|
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
|
+
|
package/server/projects.js
CHANGED
|
@@ -66,6 +66,7 @@ import sqlite3 from 'sqlite3';
|
|
|
66
66
|
import { open } from 'sqlite';
|
|
67
67
|
import os from 'os';
|
|
68
68
|
import { parseCodexTokenCountInfo } from './utils/codexTokenUsage.js';
|
|
69
|
+
import { listOpencodeSessions, getOpencodeSessionMessages, deleteOpencodeSession } from './opencode-manager.js';
|
|
69
70
|
|
|
70
71
|
// Import TaskMaster detection functions
|
|
71
72
|
async function detectTaskMasterFolder(projectPath) {
|
|
@@ -467,6 +468,14 @@ async function getProjects(progressCallback = null) {
|
|
|
467
468
|
project.codexSessions = [];
|
|
468
469
|
}
|
|
469
470
|
|
|
471
|
+
// Also fetch OpenCode sessions for this project
|
|
472
|
+
try {
|
|
473
|
+
project.opencodeSessions = await getOpencodeSessions(actualProjectDir);
|
|
474
|
+
} catch (e) {
|
|
475
|
+
console.warn(`Could not load OpenCode sessions for project ${entry.name}:`, e.message);
|
|
476
|
+
project.opencodeSessions = [];
|
|
477
|
+
}
|
|
478
|
+
|
|
470
479
|
// Add TaskMaster detection
|
|
471
480
|
try {
|
|
472
481
|
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
|
|
@@ -535,7 +544,8 @@ async function getProjects(progressCallback = null) {
|
|
|
535
544
|
isManuallyAdded: true,
|
|
536
545
|
sessions: [],
|
|
537
546
|
cursorSessions: [],
|
|
538
|
-
codexSessions: []
|
|
547
|
+
codexSessions: [],
|
|
548
|
+
opencodeSessions: []
|
|
539
549
|
};
|
|
540
550
|
|
|
541
551
|
// Try to fetch Cursor sessions for manual projects too
|
|
@@ -552,6 +562,13 @@ async function getProjects(progressCallback = null) {
|
|
|
552
562
|
console.warn(`Could not load Codex sessions for manual project ${projectName}:`, e.message);
|
|
553
563
|
}
|
|
554
564
|
|
|
565
|
+
// Try to fetch OpenCode sessions for manual projects too
|
|
566
|
+
try {
|
|
567
|
+
project.opencodeSessions = await getOpencodeSessions(actualProjectDir);
|
|
568
|
+
} catch (e) {
|
|
569
|
+
console.warn(`Could not load OpenCode sessions for manual project ${projectName}:`, e.message);
|
|
570
|
+
}
|
|
571
|
+
|
|
555
572
|
// Add TaskMaster detection for manual projects
|
|
556
573
|
try {
|
|
557
574
|
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
|
|
@@ -1063,6 +1080,20 @@ async function deleteProject(projectName, force = false) {
|
|
|
1063
1080
|
console.warn('Failed to delete Codex sessions:', err.message);
|
|
1064
1081
|
}
|
|
1065
1082
|
|
|
1083
|
+
// Delete all OpenCode sessions associated with this project
|
|
1084
|
+
try {
|
|
1085
|
+
const opencodeSessions = await getOpencodeSessions(projectPath, { limit: 0 });
|
|
1086
|
+
for (const session of opencodeSessions) {
|
|
1087
|
+
try {
|
|
1088
|
+
await deleteOpencodeSession(session.id, { directory: projectPath });
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
console.warn(`Failed to delete OpenCode session ${session.id}:`, err.message);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
console.warn('Failed to delete OpenCode sessions:', err.message);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1066
1097
|
// Delete Cursor sessions directory if it exists
|
|
1067
1098
|
try {
|
|
1068
1099
|
const hash = crypto.createHash('md5').update(projectPath).digest('hex');
|
|
@@ -1129,7 +1160,9 @@ async function addProjectManually(projectPath, displayName = null) {
|
|
|
1129
1160
|
displayName: displayName || await generateDisplayName(projectName, absolutePath),
|
|
1130
1161
|
isManuallyAdded: true,
|
|
1131
1162
|
sessions: [],
|
|
1132
|
-
cursorSessions: []
|
|
1163
|
+
cursorSessions: [],
|
|
1164
|
+
codexSessions: [],
|
|
1165
|
+
opencodeSessions: []
|
|
1133
1166
|
};
|
|
1134
1167
|
}
|
|
1135
1168
|
|
|
@@ -1321,6 +1354,28 @@ async function getCodexSessions(projectPath, options = {}) {
|
|
|
1321
1354
|
}
|
|
1322
1355
|
}
|
|
1323
1356
|
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
let opencodeDiscoveryUnavailable = false;
|
|
1360
|
+
|
|
1361
|
+
async function getOpencodeSessions(projectPath, options = {}) {
|
|
1362
|
+
const { limit = 5 } = options;
|
|
1363
|
+
|
|
1364
|
+
if (opencodeDiscoveryUnavailable) {
|
|
1365
|
+
return [];
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
try {
|
|
1369
|
+
const sessions = await listOpencodeSessions(projectPath, { limit });
|
|
1370
|
+
opencodeDiscoveryUnavailable = false;
|
|
1371
|
+
return sessions;
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
opencodeDiscoveryUnavailable = true;
|
|
1374
|
+
console.error('Error fetching OpenCode sessions:', error.message);
|
|
1375
|
+
return [];
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1324
1379
|
// Parse a Codex session JSONL file to extract metadata
|
|
1325
1380
|
async function parseCodexSessionFile(filePath) {
|
|
1326
1381
|
try {
|
|
@@ -1622,6 +1677,17 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
|
1622
1677
|
}
|
|
1623
1678
|
}
|
|
1624
1679
|
|
|
1680
|
+
|
|
1681
|
+
|
|
1682
|
+
async function getOpencodeSessionMessagesFromManager(sessionId, options = {}) {
|
|
1683
|
+
try {
|
|
1684
|
+
return await getOpencodeSessionMessages(sessionId, options);
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
console.error(`Error reading OpenCode session messages for ${sessionId}:`, error);
|
|
1687
|
+
return { messages: [], total: 0, hasMore: false };
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1625
1691
|
async function deleteCodexSession(sessionId) {
|
|
1626
1692
|
try {
|
|
1627
1693
|
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
@@ -1675,5 +1741,8 @@ export {
|
|
|
1675
1741
|
clearProjectDirectoryCache,
|
|
1676
1742
|
getCodexSessions,
|
|
1677
1743
|
getCodexSessionMessages,
|
|
1678
|
-
deleteCodexSession
|
|
1744
|
+
deleteCodexSession,
|
|
1745
|
+
getOpencodeSessions,
|
|
1746
|
+
getOpencodeSessionMessagesFromManager,
|
|
1747
|
+
deleteOpencodeSession
|
|
1679
1748
|
};
|