@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.
Files changed (102) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-CTKZtqB1.js +460 -0
  3. package/dist/assets/{ReviewApp-BEicSBzW.js → ReviewApp-DM6BNAzR.js} +1 -1
  4. package/dist/assets/{_basePickBy-DkiHsp3X.js → _basePickBy-CqJbRZ9y.js} +1 -1
  5. package/dist/assets/{_baseUniq-7ElXb2sX.js → _baseUniq-BS8YH8jO.js} +1 -1
  6. package/dist/assets/{arc-CEsS3MdK.js → arc-BBmKEN-S.js} +1 -1
  7. package/dist/assets/{architectureDiagram-2XIMDMQ5-BubZ7T3U.js → architectureDiagram-2XIMDMQ5-N5lcb82R.js} +1 -1
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-Cza6M6Ht.js → blockDiagram-WCTKOSBZ-DTMwHuLn.js} +1 -1
  9. package/dist/assets/{c4Diagram-IC4MRINW-jhjtOQ12.js → c4Diagram-IC4MRINW-BTKlkXI9.js} +1 -1
  10. package/dist/assets/channel-1oJBvF-0.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB--HkodwbY.js → chunk-4BX2VUAB-DUdoTxAc.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-CyBuez4e.js → chunk-55IACEB6-Bm_92xe4.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-CuzG4iAl.js → chunk-FMBD7UC4-CGW0g62g.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-BNi8S861.js → chunk-JSJVCQXG-DYkTH3w1.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-D817O-GT.js → chunk-KX2RTZJC-C9oTlISU.js} +1 -1
  16. package/dist/assets/{chunk-NQ4KR5QH-DyujyOvx.js → chunk-NQ4KR5QH-CM50ygWP.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-VMEn-zxh.js → chunk-QZHKN3VN-7dzpYeNJ.js} +1 -1
  18. package/dist/assets/{chunk-WL4C6EOR-CQHHFLvx.js → chunk-WL4C6EOR-Cm9nQrsr.js} +1 -1
  19. package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +1 -0
  21. package/dist/assets/clone-CinxIlEu.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-qykDd54p.js → cose-bilkent-S5V4N54A-Ccp_p0JZ.js} +1 -1
  23. package/dist/assets/{dagre-KLK3FWXG-Bqp7DjEa.js → dagre-KLK3FWXG-fBwTLUp9.js} +1 -1
  24. package/dist/assets/{diagram-E7M64L7V-BKtx468K.js → diagram-E7M64L7V-CeNVmFUp.js} +1 -1
  25. package/dist/assets/{diagram-IFDJBPK2--fHfW6V2.js → diagram-IFDJBPK2-CtavyLGa.js} +1 -1
  26. package/dist/assets/{diagram-P4PSJMXO-D1kQI5RB.js → diagram-P4PSJMXO-CpQTjQwc.js} +1 -1
  27. package/dist/assets/{erDiagram-INFDFZHY-DT9YzdNw.js → erDiagram-INFDFZHY-B8R5vwhd.js} +1 -1
  28. package/dist/assets/{flowDiagram-PKNHOUZH-DWeNr4yg.js → flowDiagram-PKNHOUZH-BvkVVwIQ.js} +1 -1
  29. package/dist/assets/{ganttDiagram-A5KZAMGK--IgwcUhI.js → ganttDiagram-A5KZAMGK-DOu3hSNa.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-B5a8UWjN.js → gitGraphDiagram-K3NZZRJ6-C7zT67YE.js} +1 -1
  31. package/dist/assets/{graph-Cw1rYoD9.js → graph-D11wiwHo.js} +1 -1
  32. package/dist/assets/{highlighted-body-TPN3WLV5-BCxJHuqY.js → highlighted-body-TPN3WLV5-Babpthg-.js} +1 -1
  33. package/dist/assets/index-DFxzgWoO.js +2 -0
  34. package/dist/assets/index-YCFGDVKw.css +1 -0
  35. package/dist/assets/{infoDiagram-LFFYTUFH-D2u70rhN.js → infoDiagram-LFFYTUFH-BmA7IpQG.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-Cl8yrezU.js → ishikawaDiagram-PHBUUO56-BEquZd3E.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-ddP0AMU9.js → journeyDiagram-4ABVD52K-BfemGz7f.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-DbVt0v29.js → kanban-definition-K7BYSVSG-CWja3mln.js} +1 -1
  39. package/dist/assets/{layout-W_tRx4UV.js → layout-BLUNf-PJ.js} +1 -1
  40. package/dist/assets/{linear-CcMb2ay-.js → linear-DukIV_Xv.js} +1 -1
  41. package/dist/assets/{mermaid-O7DHMXV3-BBJqt8pT.js → mermaid-O7DHMXV3-SgtM28qI.js} +265 -215
  42. package/dist/assets/{mindmap-definition-YRQLILUH-BGhZa7Na.js → mindmap-definition-YRQLILUH-4UjqXITU.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CDyJaACv.js → pieDiagram-SKSYHLDU-8AxqJd0M.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-BSYuqf0Q.js → quadrantDiagram-337W2JSQ-D60m8V8r.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-Cfi9YX9H.js → requirementDiagram-Z7DCOOCP-zqh9jBVf.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-Di1ShaMF.js → sankeyDiagram-WA2Y5GQK-CDZILTLI.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-CYTTG38e.js → sequenceDiagram-2WXFIKYE-7BReFd0L.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-CVZYMqyW.js → stateDiagram-RAJIS63D-HPTVdIG4.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-B1sdb5mK.js → timeline-definition-YZTLITO2-CTVllFgr.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-CGG4gx3C.js → treemap-KZPCXAKY-BtyxboJZ.js} +1 -1
  52. package/dist/assets/{vennDiagram-LZ73GAT5-Dds37L2k.js → vennDiagram-LZ73GAT5-D96ZI6Mg.js} +1 -1
  53. package/dist/assets/{xychartDiagram-JWTSCODW-C8QKSyRR.js → xychartDiagram-JWTSCODW-eRk-39YO.js} +1 -1
  54. package/dist/index.html +2 -2
  55. package/package.json +35 -33
  56. package/server/_legacy-providers/README.md +30 -0
  57. package/server/_legacy-providers/claude-sdk.js +956 -0
  58. package/server/_legacy-providers/gemini-cli.js +368 -0
  59. package/server/_legacy-providers/openai-codex.js +705 -0
  60. package/server/_legacy-providers/opencode-cli.js +674 -0
  61. package/server/acp-runtime/client.js +1872 -0
  62. package/server/acp-runtime/index.js +408 -0
  63. package/server/acp-runtime/registry.js +45 -0
  64. package/server/acp-runtime/session-store.js +254 -0
  65. package/server/channels/runtime/AgentRuntimeAdapter.js +22 -80
  66. package/server/claude-sdk.js +24 -946
  67. package/server/cli.js +140 -2
  68. package/server/external-agent/service.js +52 -63
  69. package/server/gemini-cli.js +21 -360
  70. package/server/index.js +133 -58
  71. package/server/openai-codex.js +19 -695
  72. package/server/opencode-cli.js +68 -640
  73. package/server/projects.js +128 -85
  74. package/server/routes/agent.js +2 -0
  75. package/server/routes/cc-connect.js +1131 -0
  76. package/server/routes/cli-auth.js +1 -73
  77. package/server/routes/commands.js +4 -9
  78. package/server/routes/git.js +3 -20
  79. package/server/routes/projects.js +45 -24
  80. package/server/routes/session-core.js +44 -10
  81. package/server/session-core/abortSession.js +2 -18
  82. package/server/session-core/eventStore.js +5 -1
  83. package/server/session-core/providerAdapters.js +98 -10
  84. package/server/session-core/providerDiscovery.js +8 -3
  85. package/server/session-core/runtimeState.js +16 -17
  86. package/server/session-core/runtimeWriter.js +19 -12
  87. package/server/utils/ccConnectManager.js +390 -0
  88. package/server/utils/ccConnectState.js +575 -0
  89. package/server/utils/resolveCommandPath.js +71 -0
  90. package/server/utils/workspaceRoots.js +154 -0
  91. package/shared/conversationEvents.js +347 -10
  92. package/dist/assets/App-CYTE30Cf.js +0 -484
  93. package/dist/assets/channel-RmqTALN0.js +0 -1
  94. package/dist/assets/classDiagram-VBA2DB6C-wvVV1ggz.js +0 -1
  95. package/dist/assets/classDiagram-v2-RAHNMMFH-wvVV1ggz.js +0 -1
  96. package/dist/assets/clone-oT5aWXpf.js +0 -1
  97. package/dist/assets/index-CBuAXA5S.js +0 -2
  98. package/dist/assets/index-CyLWKyxy.css +0 -1
  99. package/dist/assets/stateDiagram-v2-FVOUBMTO-Bbl0b4-i.js +0 -1
  100. package/server/cli.test.js +0 -76
  101. package/server/external-agent/service.test.js +0 -53
  102. package/server/external-agent/ws.test.js +0 -289
@@ -0,0 +1,408 @@
1
+ import { resolveWorkingDirectory } from '../utils/defaultWorkingDirectory.js';
2
+ import { AcpClient } from './client.js';
3
+ import {
4
+ findAcpSessionRecord,
5
+ touchAcpSessionRecord
6
+ } from './session-store.js';
7
+
8
+ const activeSessionClients = new Map();
9
+ const pendingSessionClientLoads = new Map();
10
+ const DEFAULT_ACTIVE_SESSION_IDLE_TIMEOUT_MS = parseInt(process.env.ACP_SESSION_IDLE_TIMEOUT_MS, 10) || (5 * 60 * 1000);
11
+
12
+ function normalizeProvider(value) {
13
+ return String(value || '').trim().toLowerCase();
14
+ }
15
+
16
+ function createActiveSessionKey(provider, sessionId) {
17
+ const normalizedProvider = normalizeProvider(provider);
18
+ const normalizedSessionId = String(sessionId || '').trim();
19
+ if (!normalizedProvider || !normalizedSessionId) {
20
+ return null;
21
+ }
22
+ return `${normalizedProvider}:${normalizedSessionId}`;
23
+ }
24
+
25
+ function registerActiveClient(provider, sessionId, client) {
26
+ const key = createActiveSessionKey(provider, sessionId);
27
+ if (!key) {
28
+ return null;
29
+ }
30
+
31
+ const existing = activeSessionClients.get(key);
32
+ if (existing?.idleTimer) {
33
+ clearTimeout(existing.idleTimer);
34
+ }
35
+
36
+ const entry = existing && existing.client === client
37
+ ? existing
38
+ : {
39
+ client,
40
+ idleTimer: null,
41
+ lastUsedAt: null
42
+ };
43
+
44
+ entry.client = client;
45
+ entry.lastUsedAt = Date.now();
46
+ entry.idleTimer = null;
47
+ activeSessionClients.set(key, entry);
48
+ return entry;
49
+ }
50
+
51
+ export function resolveDefaultModel(agentKey, requestedModel) {
52
+ const normalizedRequestedModel = typeof requestedModel === 'string'
53
+ ? requestedModel.trim()
54
+ : '';
55
+ return normalizedRequestedModel || null;
56
+ }
57
+
58
+ function unregisterActiveClient(provider, sessionId, client = null) {
59
+ const key = createActiveSessionKey(provider, sessionId);
60
+ if (!key) {
61
+ return null;
62
+ }
63
+
64
+ const entry = activeSessionClients.get(key);
65
+ if (!entry) {
66
+ return null;
67
+ }
68
+
69
+ if (client && entry.client !== client) {
70
+ return null;
71
+ }
72
+
73
+ if (entry.idleTimer) {
74
+ clearTimeout(entry.idleTimer);
75
+ }
76
+
77
+ activeSessionClients.delete(key);
78
+ return entry.client;
79
+ }
80
+
81
+ function getActiveClient(provider, sessionId) {
82
+ const key = createActiveSessionKey(provider, sessionId);
83
+ return key ? activeSessionClients.get(key)?.client || null : null;
84
+ }
85
+
86
+ async function disposeActiveClient(provider, sessionId, client = null) {
87
+ const resolvedClient = unregisterActiveClient(provider, sessionId, client) || client;
88
+ if (resolvedClient) {
89
+ await resolvedClient.close().catch(() => {});
90
+ }
91
+ }
92
+
93
+ function scheduleActiveClientCleanup(provider, sessionId, metadata = {}) {
94
+ if (DEFAULT_ACTIVE_SESSION_IDLE_TIMEOUT_MS <= 0) {
95
+ return;
96
+ }
97
+
98
+ const key = createActiveSessionKey(provider, sessionId);
99
+ const entry = key ? activeSessionClients.get(key) : null;
100
+ if (!entry) {
101
+ return;
102
+ }
103
+
104
+ if (entry.idleTimer) {
105
+ clearTimeout(entry.idleTimer);
106
+ }
107
+
108
+ entry.lastUsedAt = Date.now();
109
+ entry.idleTimer = setTimeout(() => {
110
+ void (async () => {
111
+ const activeClient = unregisterActiveClient(provider, sessionId, entry.client);
112
+ if (!activeClient) {
113
+ return;
114
+ }
115
+
116
+ await touchAcpSessionRecord({
117
+ ...metadata,
118
+ provider,
119
+ sessionId,
120
+ lastActivity: new Date().toISOString(),
121
+ isClosed: false
122
+ }).catch(() => {});
123
+
124
+ await activeClient.close().catch(() => {});
125
+ })();
126
+ }, DEFAULT_ACTIVE_SESSION_IDLE_TIMEOUT_MS);
127
+
128
+ entry.idleTimer.unref?.();
129
+ }
130
+
131
+ async function resolveProjectPath(options = {}) {
132
+ const explicitProjectPath = resolveWorkingDirectory({
133
+ cwd: options.cwd,
134
+ projectPath: options.projectPath
135
+ });
136
+
137
+ if (explicitProjectPath) {
138
+ return explicitProjectPath;
139
+ }
140
+
141
+ if (options.sessionId) {
142
+ const record = await findAcpSessionRecord(options.sessionId, options.provider || options.agentKey || null);
143
+ if (record?.projectPath) {
144
+ return record.projectPath;
145
+ }
146
+ }
147
+
148
+ return process.cwd();
149
+ }
150
+
151
+ function createClient({ normalizedAgent, writer, projectPath, options = {} }) {
152
+ return new AcpClient({
153
+ agentKey: normalizedAgent,
154
+ writer,
155
+ projectPath,
156
+ agentCommandOverrides: options.agentCommandOverrides,
157
+ model: resolveDefaultModel(normalizedAgent, options.model),
158
+ permissionMode: options.permissionMode || 'default'
159
+ });
160
+ }
161
+
162
+ async function createOrResumeClient({ normalizedAgent, options = {}, projectPath, writer }) {
163
+ const requestedSessionId = typeof options.sessionId === 'string' ? options.sessionId.trim() : '';
164
+ const activeClient = requestedSessionId ? getActiveClient(normalizedAgent, requestedSessionId) : null;
165
+ if (activeClient) {
166
+ registerActiveClient(normalizedAgent, requestedSessionId, activeClient);
167
+ return activeClient;
168
+ }
169
+
170
+ const pendingKey = requestedSessionId ? createActiveSessionKey(normalizedAgent, requestedSessionId) : null;
171
+ if (pendingKey && pendingSessionClientLoads.has(pendingKey)) {
172
+ return pendingSessionClientLoads.get(pendingKey);
173
+ }
174
+
175
+ const clientPromise = (async () => {
176
+ const client = createClient({
177
+ normalizedAgent,
178
+ writer,
179
+ projectPath,
180
+ options
181
+ });
182
+
183
+ try {
184
+ await client.start();
185
+
186
+ if (requestedSessionId) {
187
+ try {
188
+ await client.loadSession(requestedSessionId);
189
+ } catch (error) {
190
+ console.warn(`[ACP:${normalizedAgent}] Unable to resume session ${requestedSessionId}: ${error.message}`);
191
+ await client.createSession();
192
+ client.emitSystemNotice(`Unable to resume session ${requestedSessionId}; started a new session instead.`);
193
+ }
194
+ } else {
195
+ await client.createSession();
196
+ }
197
+
198
+ registerActiveClient(normalizedAgent, client.sessionId, client);
199
+ writer?.setSessionId?.(client.sessionId);
200
+ return client;
201
+ } catch (error) {
202
+ await client.close().catch(() => {});
203
+ throw error;
204
+ }
205
+ })();
206
+
207
+ if (pendingKey) {
208
+ pendingSessionClientLoads.set(pendingKey, clientPromise);
209
+ }
210
+
211
+ try {
212
+ return await clientPromise;
213
+ } finally {
214
+ if (pendingKey) {
215
+ pendingSessionClientLoads.delete(pendingKey);
216
+ }
217
+ }
218
+ }
219
+
220
+ export async function executeAgentPrompt({ agentKey, command, options = {}, writer }) {
221
+ const normalizedAgent = normalizeProvider(agentKey || options.provider);
222
+ const projectPath = await resolveProjectPath({
223
+ ...options,
224
+ agentKey: normalizedAgent,
225
+ provider: normalizedAgent
226
+ });
227
+ const resolvedModel = resolveDefaultModel(normalizedAgent, options.model);
228
+ const agentCommand = options.agentCommand || null;
229
+ let sessionRecord = null;
230
+ let client = null;
231
+
232
+ try {
233
+ client = await createOrResumeClient({
234
+ normalizedAgent,
235
+ options,
236
+ projectPath,
237
+ writer
238
+ });
239
+
240
+ const effectiveProjectPath = client.projectPath || projectPath;
241
+ const response = await client.runExclusive(async () => {
242
+ registerActiveClient(normalizedAgent, client.sessionId, client);
243
+ await client.configureForTurn({
244
+ writer,
245
+ permissionMode: options.permissionMode || 'default',
246
+ model: resolvedModel
247
+ });
248
+ writer?.setSessionId?.(client.sessionId);
249
+
250
+ sessionRecord = await touchAcpSessionRecord({
251
+ provider: normalizedAgent,
252
+ sessionId: client.sessionId,
253
+ projectPath: effectiveProjectPath,
254
+ agentCommand,
255
+ model: resolvedModel,
256
+ lastActivity: new Date().toISOString(),
257
+ lastPromptAt: new Date().toISOString(),
258
+ isClosed: false
259
+ });
260
+
261
+ return client.sendPrompt(command, {
262
+ images: options.images || [],
263
+ resourceLinks: options.resourceLinks || [],
264
+ clientRequestId: options.clientRequestId || null
265
+ });
266
+ });
267
+
268
+ await touchAcpSessionRecord({
269
+ provider: normalizedAgent,
270
+ sessionId: client.sessionId,
271
+ projectPath: effectiveProjectPath,
272
+ agentCommand,
273
+ model: resolvedModel,
274
+ lastActivity: new Date().toISOString(),
275
+ lastPromptAt: new Date().toISOString(),
276
+ isClosed: false
277
+ });
278
+
279
+ scheduleActiveClientCleanup(normalizedAgent, client.sessionId, {
280
+ projectPath: effectiveProjectPath,
281
+ agentCommand,
282
+ model: resolvedModel
283
+ });
284
+
285
+ return {
286
+ sessionId: client.sessionId,
287
+ provider: normalizedAgent,
288
+ runtime: 'acp',
289
+ stopReason: response?.stopReason || null,
290
+ source: sessionRecord?.source || 'acp'
291
+ };
292
+ } catch (error) {
293
+ if (client?.sessionId && !client.hasReportedPromptFatalError) {
294
+ writer?.send?.({
295
+ type: 'error',
296
+ provider: normalizedAgent,
297
+ sessionId: client.sessionId,
298
+ runtime: 'acp',
299
+ error: error.message
300
+ });
301
+ }
302
+
303
+ if (client?.sessionId) {
304
+ await disposeActiveClient(normalizedAgent, client.sessionId, client);
305
+ } else if (client) {
306
+ await client.close().catch(() => {});
307
+ }
308
+
309
+ throw error;
310
+ }
311
+ }
312
+
313
+ export async function abortAgentSession(agentKey, sessionId) {
314
+ const normalizedAgent = normalizeProvider(agentKey);
315
+ const client = getActiveClient(normalizedAgent, sessionId);
316
+
317
+ if (!client) {
318
+ return false;
319
+ }
320
+
321
+ const cancelled = await client.cancel(sessionId);
322
+ if (cancelled) {
323
+ await touchAcpSessionRecord({
324
+ provider: normalizedAgent,
325
+ sessionId,
326
+ lastActivity: new Date().toISOString()
327
+ }).catch(() => {});
328
+ }
329
+ return cancelled;
330
+ }
331
+
332
+ export async function setAgentSessionMode(agentKey, sessionId, modeId, { writer = null } = {}) {
333
+ const normalizedAgent = normalizeProvider(agentKey);
334
+ const normalizedSessionId = String(sessionId || '').trim();
335
+ const normalizedModeId = String(modeId || '').trim();
336
+
337
+ if (!normalizedAgent || !normalizedSessionId || !normalizedModeId) {
338
+ return false;
339
+ }
340
+
341
+ let client = getActiveClient(normalizedAgent, normalizedSessionId);
342
+ if (!client) {
343
+ const projectPath = await resolveProjectPath({
344
+ provider: normalizedAgent,
345
+ sessionId: normalizedSessionId
346
+ });
347
+
348
+ client = await createOrResumeClient({
349
+ normalizedAgent,
350
+ options: {
351
+ sessionId: normalizedSessionId
352
+ },
353
+ projectPath,
354
+ writer
355
+ });
356
+ }
357
+
358
+ const changed = await client.runExclusive(async () => {
359
+ registerActiveClient(normalizedAgent, normalizedSessionId, client);
360
+ if (writer) {
361
+ client.attachWriter(writer);
362
+ writer.setSessionId?.(normalizedSessionId);
363
+ }
364
+ return client.setMode(normalizedModeId);
365
+ });
366
+
367
+ if (changed) {
368
+ await touchAcpSessionRecord({
369
+ provider: normalizedAgent,
370
+ sessionId: normalizedSessionId,
371
+ projectPath: client.projectPath || null,
372
+ model: client.model || null,
373
+ lastActivity: new Date().toISOString(),
374
+ isClosed: false
375
+ }).catch(() => {});
376
+
377
+ scheduleActiveClientCleanup(normalizedAgent, normalizedSessionId, {
378
+ projectPath: client.projectPath || null,
379
+ model: client.model || null
380
+ });
381
+ }
382
+
383
+ return changed;
384
+ }
385
+
386
+ export function isAgentSessionActive(agentKey, sessionId) {
387
+ const key = createActiveSessionKey(agentKey, sessionId);
388
+ return key ? activeSessionClients.has(key) : false;
389
+ }
390
+
391
+ export function getActiveAgentSessions(agentKey = null) {
392
+ const normalizedAgent = agentKey ? normalizeProvider(agentKey) : null;
393
+ const sessions = [];
394
+
395
+ for (const key of activeSessionClients.keys()) {
396
+ const [provider, sessionId] = key.split(':');
397
+ if (normalizedAgent && provider !== normalizedAgent) {
398
+ continue;
399
+ }
400
+ sessions.push(sessionId);
401
+ }
402
+
403
+ return sessions;
404
+ }
405
+
406
+ export function resolveAgentPermission(requestId, decision = {}) {
407
+ return AcpClient.resolvePermissionRequest(requestId, decision);
408
+ }
@@ -0,0 +1,45 @@
1
+ const ACP_ADAPTER_PACKAGE_RANGES = {
2
+ claude: '^0.24.2',
3
+ codex: '^0.10.0'
4
+ };
5
+
6
+ export const AGENT_REGISTRY = {
7
+ claude: `npx -y @agentclientprotocol/claude-agent-acp@${ACP_ADAPTER_PACKAGE_RANGES.claude}`,
8
+ codex: `npx @zed-industries/codex-acp@${ACP_ADAPTER_PACKAGE_RANGES.codex}`,
9
+ gemini: 'gemini --acp',
10
+ opencode: 'npx -y opencode-ai acp'
11
+ };
12
+
13
+ const AGENT_ALIASES = {
14
+ openai: 'codex'
15
+ };
16
+
17
+ export function normalizeAgentName(value) {
18
+ const normalized = String(value || '').trim().toLowerCase();
19
+ return AGENT_ALIASES[normalized] || normalized;
20
+ }
21
+
22
+ export function mergeAgentRegistry(overrides = {}) {
23
+ const merged = { ...AGENT_REGISTRY };
24
+
25
+ Object.entries(overrides || {}).forEach(([agentName, command]) => {
26
+ const normalizedAgent = normalizeAgentName(agentName);
27
+ const normalizedCommand = typeof command === 'string' ? command.trim() : '';
28
+ if (!normalizedAgent || !normalizedCommand) {
29
+ return;
30
+ }
31
+ merged[normalizedAgent] = normalizedCommand;
32
+ });
33
+
34
+ return merged;
35
+ }
36
+
37
+ export function resolveAgentCommand(agentKey, overrides = {}) {
38
+ const normalizedAgent = normalizeAgentName(agentKey);
39
+ const registry = mergeAgentRegistry(overrides);
40
+ return registry[normalizedAgent] || registry[AGENT_ALIASES[normalizedAgent] || normalizedAgent] || '';
41
+ }
42
+
43
+ export function listBuiltInAgents(overrides = {}) {
44
+ return Object.keys(mergeAgentRegistry(overrides)).sort();
45
+ }
@@ -0,0 +1,254 @@
1
+ import fs from 'fs/promises';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ function parsePositiveInteger(value, fallback) {
6
+ const parsed = parseInt(value, 10);
7
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
8
+ }
9
+
10
+ const ACP_SESSION_ROOT = process.env.AXHUB_GENIE_ACP_SESSION_ROOT
11
+ ? path.resolve(process.env.AXHUB_GENIE_ACP_SESSION_ROOT)
12
+ : path.join(os.homedir(), '.axhub-genie', 'acp-sessions');
13
+ const DEFAULT_SESSION_GC_MAX_AGE_DAYS = parsePositiveInteger(process.env.ACP_SESSION_GC_MAX_AGE_DAYS, 30);
14
+
15
+ function normalizeProvider(value) {
16
+ return String(value || '').trim().toLowerCase();
17
+ }
18
+
19
+ function normalizeSessionId(value) {
20
+ const normalized = String(value || '').trim();
21
+ return normalized || null;
22
+ }
23
+
24
+ function getProviderDir(provider) {
25
+ return path.join(ACP_SESSION_ROOT, normalizeProvider(provider));
26
+ }
27
+
28
+ function getSessionFilePath(provider, sessionId) {
29
+ return path.join(getProviderDir(provider), `${encodeURIComponent(sessionId)}.json`);
30
+ }
31
+
32
+ function sortSessionsByActivity(sessions = []) {
33
+ return sessions.sort((left, right) => {
34
+ const leftTime = new Date(left.lastActivity || left.updatedAt || left.createdAt || 0).getTime();
35
+ const rightTime = new Date(right.lastActivity || right.updatedAt || right.createdAt || 0).getTime();
36
+ return rightTime - leftTime;
37
+ });
38
+ }
39
+
40
+ async function ensureProviderDir(provider) {
41
+ await fs.mkdir(getProviderDir(provider), { recursive: true });
42
+ }
43
+
44
+ async function listProviderSessionFiles(provider) {
45
+ try {
46
+ const entries = await fs.readdir(getProviderDir(provider), { withFileTypes: true });
47
+ return entries
48
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
49
+ .map((entry) => path.join(getProviderDir(provider), entry.name));
50
+ } catch (error) {
51
+ if (error?.code === 'ENOENT') {
52
+ return [];
53
+ }
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ async function readJsonFile(filePath) {
59
+ try {
60
+ const content = await fs.readFile(filePath, 'utf8');
61
+ return JSON.parse(content);
62
+ } catch (error) {
63
+ if (error?.code === 'ENOENT') {
64
+ return null;
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ function createSessionSummary(record) {
71
+ if (!record || typeof record !== 'object') {
72
+ return null;
73
+ }
74
+
75
+ return {
76
+ id: record.sessionId,
77
+ sessionId: record.sessionId,
78
+ provider: record.provider,
79
+ source: 'acp',
80
+ runtime: 'acp',
81
+ projectPath: record.projectPath || null,
82
+ cwd: record.projectPath || null,
83
+ title: record.title || record.sessionId,
84
+ summary: record.summary || null,
85
+ model: record.model || null,
86
+ createdAt: record.createdAt || null,
87
+ updatedAt: record.updatedAt || null,
88
+ lastActivity: record.lastActivity || record.updatedAt || record.createdAt || null,
89
+ lastPromptAt: record.lastPromptAt || null,
90
+ closedAt: record.closedAt || null,
91
+ isClosed: Boolean(record.isClosed)
92
+ };
93
+ }
94
+
95
+ function getSessionActivityTime(record) {
96
+ return new Date(
97
+ record?.closedAt
98
+ || record?.lastActivity
99
+ || record?.updatedAt
100
+ || record?.createdAt
101
+ || 0
102
+ ).getTime();
103
+ }
104
+
105
+ export async function readAcpSessionRecord(provider, sessionId) {
106
+ const normalizedProvider = normalizeProvider(provider);
107
+ const normalizedSessionId = normalizeSessionId(sessionId);
108
+ if (!normalizedProvider || !normalizedSessionId) {
109
+ return null;
110
+ }
111
+
112
+ const record = await readJsonFile(getSessionFilePath(normalizedProvider, normalizedSessionId));
113
+ return record && typeof record === 'object' ? record : null;
114
+ }
115
+
116
+ export async function writeAcpSessionRecord(record) {
117
+ const normalizedProvider = normalizeProvider(record?.provider);
118
+ const normalizedSessionId = normalizeSessionId(record?.sessionId);
119
+
120
+ if (!normalizedProvider || !normalizedSessionId) {
121
+ throw new Error('provider and sessionId are required to persist an ACP session');
122
+ }
123
+
124
+ const now = new Date().toISOString();
125
+ const existing = await readAcpSessionRecord(normalizedProvider, normalizedSessionId);
126
+ const nextRecord = {
127
+ source: 'acp',
128
+ runtime: 'acp',
129
+ provider: normalizedProvider,
130
+ sessionId: normalizedSessionId,
131
+ createdAt: existing?.createdAt || record.createdAt || now,
132
+ updatedAt: now,
133
+ lastActivity: record.lastActivity || now,
134
+ projectPath: record.projectPath || existing?.projectPath || null,
135
+ agentCommand: record.agentCommand || existing?.agentCommand || null,
136
+ model: record.model || existing?.model || null,
137
+ summary: record.summary || existing?.summary || null,
138
+ title: record.title || existing?.title || null,
139
+ lastPromptAt: record.lastPromptAt || existing?.lastPromptAt || null,
140
+ closedAt: record.closedAt || existing?.closedAt || null,
141
+ isClosed: record.isClosed ?? existing?.isClosed ?? false
142
+ };
143
+
144
+ await ensureProviderDir(normalizedProvider);
145
+ const filePath = getSessionFilePath(normalizedProvider, normalizedSessionId);
146
+ await fs.writeFile(filePath, `${JSON.stringify(nextRecord, null, 2)}\n`, 'utf8');
147
+
148
+ return nextRecord;
149
+ }
150
+
151
+ export async function touchAcpSessionRecord(record) {
152
+ return writeAcpSessionRecord(record);
153
+ }
154
+
155
+ export async function closeAcpSessionRecord(provider, sessionId, updates = {}) {
156
+ const now = new Date().toISOString();
157
+ return writeAcpSessionRecord({
158
+ ...updates,
159
+ provider,
160
+ sessionId,
161
+ isClosed: true,
162
+ closedAt: updates.closedAt || now,
163
+ lastActivity: updates.lastActivity || now
164
+ });
165
+ }
166
+
167
+ export async function gcOldAcpSessions({ provider = null, maxAgeDays = DEFAULT_SESSION_GC_MAX_AGE_DAYS, includeOpen = false } = {}) {
168
+ const normalizedMaxAgeDays = Number(maxAgeDays);
169
+ if (!Number.isFinite(normalizedMaxAgeDays) || normalizedMaxAgeDays <= 0) {
170
+ return { removed: 0 };
171
+ }
172
+
173
+ const providers = provider ? [normalizeProvider(provider)] : ['claude', 'codex', 'gemini', 'opencode'];
174
+ const cutoffTime = Date.now() - (normalizedMaxAgeDays * 24 * 60 * 60 * 1000);
175
+ let removed = 0;
176
+
177
+ for (const currentProvider of providers) {
178
+ if (!currentProvider) {
179
+ continue;
180
+ }
181
+
182
+ const filePaths = await listProviderSessionFiles(currentProvider);
183
+ for (const filePath of filePaths) {
184
+ const record = await readJsonFile(filePath);
185
+ if (!record || typeof record !== 'object') {
186
+ continue;
187
+ }
188
+
189
+ if (!includeOpen && !record.isClosed) {
190
+ continue;
191
+ }
192
+
193
+ const activityTime = getSessionActivityTime(record);
194
+ if (!Number.isFinite(activityTime) || activityTime >= cutoffTime) {
195
+ continue;
196
+ }
197
+
198
+ await fs.rm(filePath, { force: true });
199
+ removed += 1;
200
+ }
201
+ }
202
+
203
+ return { removed };
204
+ }
205
+
206
+ export async function listAcpSessions({ provider = null, projectPath = null } = {}) {
207
+ await gcOldAcpSessions({ provider }).catch(() => {});
208
+ const providers = provider ? [normalizeProvider(provider)] : ['claude', 'codex', 'gemini', 'opencode'];
209
+ const sessions = [];
210
+
211
+ for (const currentProvider of providers) {
212
+ if (!currentProvider) {
213
+ continue;
214
+ }
215
+
216
+ const filePaths = await listProviderSessionFiles(currentProvider);
217
+ for (const filePath of filePaths) {
218
+ const record = await readJsonFile(filePath);
219
+ const summary = createSessionSummary(record);
220
+ if (!summary) {
221
+ continue;
222
+ }
223
+
224
+ if (projectPath && summary.projectPath !== projectPath) {
225
+ continue;
226
+ }
227
+
228
+ sessions.push(summary);
229
+ }
230
+ }
231
+
232
+ return sortSessionsByActivity(sessions);
233
+ }
234
+
235
+ export async function findAcpSessionRecord(sessionId, provider = null) {
236
+ const normalizedSessionId = normalizeSessionId(sessionId);
237
+ if (!normalizedSessionId) {
238
+ return null;
239
+ }
240
+
241
+ if (provider) {
242
+ return readAcpSessionRecord(provider, normalizedSessionId);
243
+ }
244
+
245
+ const providers = ['claude', 'codex', 'gemini', 'opencode'];
246
+ for (const currentProvider of providers) {
247
+ const record = await readAcpSessionRecord(currentProvider, normalizedSessionId);
248
+ if (record) {
249
+ return record;
250
+ }
251
+ }
252
+
253
+ return null;
254
+ }