@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
@@ -1,368 +1,29 @@
1
- import { spawn } from 'child_process';
2
- import crossSpawn from 'cross-spawn';
3
- import { resolveWorkingDirectory } from './utils/defaultWorkingDirectory.js';
4
-
5
- const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
6
-
7
- const activeGeminiProcesses = new Map();
8
-
9
- function collectTextChunks(payload) {
10
- if (!payload) return [];
11
- if (typeof payload === 'string') return payload.trim() ? [payload] : [];
12
-
13
- const chunks = [];
14
-
15
- if (Array.isArray(payload)) {
16
- payload.forEach(item => {
17
- chunks.push(...collectTextChunks(item));
18
- });
19
- return chunks;
20
- }
21
-
22
- if (typeof payload !== 'object') {
23
- return chunks;
24
- }
25
-
26
- const directKeys = ['text', 'response', 'content', 'message', 'delta'];
27
- for (const key of directKeys) {
28
- if (payload[key] !== undefined) {
29
- chunks.push(...collectTextChunks(payload[key]));
30
- }
31
- }
32
-
33
- if (Array.isArray(payload.parts)) {
34
- payload.parts.forEach(part => {
35
- chunks.push(...collectTextChunks(part));
36
- });
37
- }
38
-
39
- if (payload.data && typeof payload.data === 'object') {
40
- chunks.push(...collectTextChunks(payload.data));
41
- }
42
-
43
- return chunks;
44
- }
45
-
46
- function getEventRole(event) {
47
- return (
48
- event?.role ||
49
- event?.author ||
50
- event?.sender ||
51
- event?.message?.role ||
52
- event?.data?.role ||
53
- event?.content?.role ||
54
- null
55
- );
56
- }
57
-
58
- function extractAssistantTextChunks(event, command) {
59
- const role = String(getEventRole(event) || '').toLowerCase();
60
- if (role === 'user') return [];
61
-
62
- const eventType = String(event?.type || event?.event || event?.kind || '').toLowerCase();
63
- const hasAssistantPayload = !!(event?.response || event?.candidates || event?.delta || event?.text || event?.content || event?.message);
64
- if (eventType.includes('prompt') && !hasAssistantPayload) {
65
- return [];
66
- }
67
-
68
- const normalizedPrompt = String(command || '').trim();
69
- return collectTextChunks(event).filter((text) => {
70
- const trimmed = String(text || '').trim();
71
- if (!trimmed) return false;
72
- if (normalizedPrompt && trimmed === normalizedPrompt) return false;
73
- return true;
74
- });
75
- }
76
-
77
- function parseGeminiJsonLine(line) {
78
- const trimmed = line.trim();
79
- if (!trimmed) return null;
80
-
81
- const payload = trimmed.startsWith('data:') ? trimmed.slice(5).trim() : trimmed;
82
- if (!payload) return null;
83
-
84
- return JSON.parse(payload);
85
- }
86
-
87
- function emitTextChunks(ws, sessionId, chunks) {
88
- chunks.forEach((text) => {
89
- if (!text) return;
90
- ws.send({
91
- type: 'claude-response',
92
- data: {
93
- type: 'content_block_delta',
94
- delta: {
95
- type: 'text_delta',
96
- text
97
- }
98
- },
99
- provider: 'gemini',
100
- sessionId
101
- });
102
- });
103
- }
104
-
105
- function formatGeminiProcessError(code, stderrOutput) {
106
- const normalizedStderr = String(stderrOutput || '').trim();
107
- if (normalizedStderr) {
108
- const apiMessageMatch =
109
- normalizedStderr.match(/"message":"([^"]+)"/) ||
110
- normalizedStderr.match(/message:\s*"([^"]+)"/i) ||
111
- normalizedStderr.match(/ApiError:\s*\{.*?"message":"([^"]+)"/i) ||
112
- normalizedStderr.match(/Error when talking to Gemini API[\s\S]*?message":"([^"]+)"/i);
113
-
114
- if (apiMessageMatch?.[1]) {
115
- return apiMessageMatch[1];
116
- }
117
-
118
- const firstMeaningfulLine = normalizedStderr
119
- .split('\n')
120
- .map((line) => line.trim())
121
- .find((line) => line && !line.startsWith('at '));
122
-
123
- if (firstMeaningfulLine) {
124
- return firstMeaningfulLine;
125
- }
126
-
127
- return normalizedStderr;
128
- }
129
-
130
- return `Gemini CLI exited with code ${code}`;
131
- }
132
-
133
- function extractGeminiEventError(event) {
134
- if (!event || typeof event !== 'object') {
135
- return null;
136
- }
137
-
138
- const candidates = [
139
- event?.error?.message,
140
- event?.error?.details,
141
- event?.result?.error?.message,
142
- event?.result?.error,
143
- event?.data?.error?.message,
144
- event?.data?.error,
145
- event?.message
146
- ];
147
-
148
- for (const candidate of candidates) {
149
- const normalized = String(candidate || '').trim();
150
- if (normalized) {
151
- return normalized;
152
- }
153
- }
154
-
155
- return null;
156
- }
157
-
158
- async function queryGemini(command, options = {}, ws) {
159
- return new Promise((resolve, reject) => {
160
- const { sessionId, cwd, projectPath, resume, model, permissionMode } = options;
161
- let capturedSessionId = sessionId;
162
- let sentSessionCreated = false;
163
- let stderrBuffer = '';
164
- let fatalErrorSent = false;
165
- let sawSuccessfulTerminalEvent = false;
166
- let sawErroredTerminalEvent = false;
167
-
168
- const args = ['-y', '@google/gemini-cli'];
169
-
170
- if (sessionId && (resume || !command || !command.trim())) {
171
- args.push('--resume', sessionId);
172
- }
173
-
174
- if (command && command.trim()) {
175
- args.push('--prompt', command);
176
- args.push('--output-format', 'stream-json');
177
- }
178
-
179
- if (model) {
180
- args.push('--model', model);
181
- }
182
-
183
- if (permissionMode === 'bypassPermissions' || permissionMode === 'acceptEdits') {
184
- args.push('--yolo');
185
- }
186
-
187
- const workingDir = resolveWorkingDirectory({ cwd, projectPath });
188
-
189
- const geminiProcess = spawnFunction('npx', args, {
190
- cwd: workingDir,
191
- stdio: ['pipe', 'pipe', 'pipe'],
192
- env: {
193
- ...process.env,
194
- GEMINI_NONINTERACTIVE: '1'
195
- }
196
- });
197
-
198
- const processKey = capturedSessionId || Date.now().toString();
199
- let processRegistryKey = processKey;
200
- activeGeminiProcesses.set(processRegistryKey, geminiProcess);
201
-
202
- const finalizeSessionId = () => capturedSessionId || sessionId || null;
203
- const emitFatalError = (errorMessage) => {
204
- if (fatalErrorSent || !errorMessage) {
205
- return;
206
- }
207
-
208
- fatalErrorSent = true;
209
- ws.send({
210
- type: 'claude-error',
211
- error: errorMessage,
212
- provider: 'gemini',
213
- sessionId: finalizeSessionId()
214
- });
215
- };
216
-
217
- const handleJsonEvent = (event) => {
218
- const incomingSessionId = event?.session_id || event?.sessionId || event?.data?.session_id || event?.data?.sessionId;
219
- if (incomingSessionId && !capturedSessionId) {
220
- capturedSessionId = incomingSessionId;
221
- if (ws.setSessionId && typeof ws.setSessionId === 'function') {
222
- ws.setSessionId(capturedSessionId);
223
- }
224
-
225
- if (processRegistryKey !== capturedSessionId) {
226
- activeGeminiProcesses.delete(processRegistryKey);
227
- activeGeminiProcesses.set(capturedSessionId, geminiProcess);
228
- processRegistryKey = capturedSessionId;
229
- }
230
-
231
- if (!sessionId && !sentSessionCreated) {
232
- sentSessionCreated = true;
233
- ws.send({
234
- type: 'session-created',
235
- sessionId: capturedSessionId,
236
- provider: 'gemini'
237
- });
238
- }
239
- }
240
-
241
- const type = event?.type || event?.event || event?.kind;
242
- if (type === 'result' || type === 'done' || type === 'complete') {
243
- const eventStatus = String(event?.status || event?.result?.status || '').trim().toLowerCase();
244
- const eventError = extractGeminiEventError(event);
245
- if (eventStatus === 'error' || eventError) {
246
- sawErroredTerminalEvent = true;
247
- emitFatalError(eventError || 'Gemini CLI request failed');
248
- return;
249
- }
250
-
251
- sawSuccessfulTerminalEvent = true;
252
- ws.send({
253
- type: 'claude-response',
254
- data: { type: 'content_block_stop' },
255
- provider: 'gemini',
256
- sessionId: finalizeSessionId()
257
- });
258
-
259
- ws.send({
260
- type: 'gemini-result',
261
- data: event,
262
- sessionId: finalizeSessionId()
263
- });
264
- return;
265
- }
266
-
267
- const textChunks = extractAssistantTextChunks(event, command);
268
- emitTextChunks(ws, finalizeSessionId(), textChunks);
269
- };
270
-
271
- let stdoutBuffer = '';
272
- geminiProcess.stdout.on('data', (data) => {
273
- stdoutBuffer += data.toString();
274
- const lines = stdoutBuffer.split('\n');
275
- stdoutBuffer = lines.pop() || '';
276
-
277
- for (const line of lines) {
278
- try {
279
- const event = parseGeminiJsonLine(line);
280
- if (!event) continue;
281
- handleJsonEvent(event);
282
- } catch {}
283
- }
284
- });
285
-
286
- geminiProcess.stderr.on('data', (data) => {
287
- const chunk = data.toString();
288
- stderrBuffer += chunk;
289
-
290
- const normalizedChunk = chunk.trim();
291
- if (normalizedChunk) {
292
- console.warn('Gemini CLI stderr:', normalizedChunk);
293
- }
294
- });
295
-
296
- geminiProcess.on('close', (code) => {
297
- if (stdoutBuffer.trim()) {
298
- try {
299
- const finalEvent = parseGeminiJsonLine(stdoutBuffer);
300
- if (finalEvent) {
301
- handleJsonEvent(finalEvent);
302
- }
303
- } catch {}
304
- }
305
-
306
- activeGeminiProcesses.delete(processRegistryKey);
307
-
308
- if (code === 0 && !sawErroredTerminalEvent) {
309
- if (!sawSuccessfulTerminalEvent) {
310
- ws.send({
311
- type: 'claude-response',
312
- data: { type: 'content_block_stop' },
313
- provider: 'gemini',
314
- sessionId: finalizeSessionId()
315
- });
316
- }
317
-
318
- ws.send({
319
- type: 'claude-complete',
320
- sessionId: finalizeSessionId(),
321
- provider: 'gemini',
322
- exitCode: code,
323
- isNewSession: !sessionId && !!command
324
- });
325
- resolve();
326
- } else {
327
- const errorMessage = formatGeminiProcessError(code, stderrBuffer);
328
- emitFatalError(errorMessage);
329
- reject(new Error(errorMessage));
330
- }
331
- });
332
-
333
- geminiProcess.on('error', (error) => {
334
- activeGeminiProcesses.delete(processRegistryKey);
335
-
336
- emitFatalError(error.message);
337
-
338
- reject(error);
339
- });
340
-
341
- geminiProcess.stdin.end();
1
+ import {
2
+ abortAgentSession,
3
+ executeAgentPrompt,
4
+ getActiveAgentSessions,
5
+ isAgentSessionActive
6
+ } from './acp-runtime/index.js';
7
+
8
+ export async function queryGemini(command, options = {}, writer) {
9
+ return executeAgentPrompt({
10
+ agentKey: 'gemini',
11
+ command,
12
+ options: {
13
+ ...options
14
+ },
15
+ writer
342
16
  });
343
17
  }
344
18
 
345
- function abortGeminiSession(sessionId) {
346
- const process = activeGeminiProcesses.get(sessionId);
347
- if (process) {
348
- process.kill('SIGTERM');
349
- activeGeminiProcesses.delete(sessionId);
350
- return true;
351
- }
352
- return false;
19
+ export async function abortGeminiSession(sessionId) {
20
+ return abortAgentSession('gemini', sessionId);
353
21
  }
354
22
 
355
- function isGeminiSessionActive(sessionId) {
356
- return activeGeminiProcesses.has(sessionId);
23
+ export function isGeminiSessionActive(sessionId) {
24
+ return isAgentSessionActive('gemini', sessionId);
357
25
  }
358
26
 
359
- function getActiveGeminiSessions() {
360
- return Array.from(activeGeminiProcesses.keys());
27
+ export function getActiveGeminiSessions() {
28
+ return getActiveAgentSessions('gemini');
361
29
  }
362
-
363
- export {
364
- queryGemini,
365
- abortGeminiSession,
366
- isGeminiSessionActive,
367
- getActiveGeminiSessions
368
- };
package/server/index.js CHANGED
@@ -97,10 +97,13 @@ import fetch from 'node-fetch';
97
97
  import mime from 'mime-types';
98
98
 
99
99
  import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, clearProviderSessionLookupCaches, getGeminiSessionMessages } from './projects.js';
100
- import { queryClaudeSDK, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js';
101
- import { queryCodex, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
102
- import { queryGemini, isGeminiSessionActive, getActiveGeminiSessions } from './gemini-cli.js';
103
- import { queryOpencode, isOpencodeSessionActive, getActiveOpencodeSessions } from './opencode-cli.js';
100
+ import {
101
+ executeAgentPrompt,
102
+ getActiveAgentSessions,
103
+ isAgentSessionActive,
104
+ resolveAgentPermission,
105
+ setAgentSessionMode
106
+ } from './acp-runtime/index.js';
104
107
  import gitRoutes from './routes/git.js';
105
108
  import authRoutes from './routes/auth.js';
106
109
  import mcpRoutes from './routes/mcp.js';
@@ -110,8 +113,20 @@ import commandsRoutes from './routes/commands.js';
110
113
  import settingsRoutes from './routes/settings.js';
111
114
  import channelsRoutes from './routes/channels.js';
112
115
  import agentRoutes from './routes/agent.js';
113
- import projectsRoutes, { WORKSPACES_ROOT, validateWorkspacePath } from './routes/projects.js';
116
+ import projectsRoutes, {
117
+ DEFAULT_WORKSPACES_ROOT,
118
+ HAS_WORKSPACES_ROOT_RESTRICTION,
119
+ WORKSPACES_ROOTS,
120
+ validateWorkspacePath
121
+ } from './routes/projects.js';
122
+ import {
123
+ UNIX_WORKSPACE_ROOTS,
124
+ buildWorkspaceRootSuggestions,
125
+ expandWorkspacePath,
126
+ shouldUseVirtualWorkspaceRoots
127
+ } from './utils/workspaceRoots.js';
114
128
  import cliAuthRoutes, { detectProviderInstallationStatus } from './routes/cli-auth.js';
129
+ import ccConnectRoutes from './routes/cc-connect.js';
115
130
  import userRoutes from './routes/user.js';
116
131
  import codexRoutes from './routes/codex.js';
117
132
  import opencodeRoutes from './routes/opencode.js';
@@ -135,6 +150,7 @@ const UPDATE_PACKAGE_NAME = process.env.UPDATE_PACKAGE_NAME || packageInfo.id ||
135
150
  const VERSION_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
136
151
  let cachedSystemVersion = null;
137
152
  let cachedSystemVersionExpiresAt = 0;
153
+ const WINDOWS_DRIVES_ROOT = '__windows_drives__';
138
154
  const RUNTIME_STATUS_PATH = (() => {
139
155
  if (process.env.AXHUB_GENIE_STATUS_PATH) {
140
156
  return process.env.AXHUB_GENIE_STATUS_PATH;
@@ -501,6 +517,7 @@ app.use('/api/channels', authenticateToken, channelsRoutes);
501
517
 
502
518
  // CLI Authentication API Routes (protected)
503
519
  app.use('/api/cli', authenticateToken, cliAuthRoutes);
520
+ app.use('/api/cc-connect', authenticateToken, ccConnectRoutes);
504
521
 
505
522
  // User API Routes (protected)
506
523
  app.use('/api/user', authenticateToken, userRoutes);
@@ -740,16 +757,31 @@ app.post('/api/projects/create', authenticateToken, async (req, res) => {
740
757
  }
741
758
  });
742
759
 
743
- const expandWorkspacePath = (inputPath) => {
744
- if (!inputPath) return inputPath;
745
- if (inputPath === '~') {
746
- return WORKSPACES_ROOT;
747
- }
748
- if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) {
749
- return path.join(WORKSPACES_ROOT, inputPath.slice(2));
750
- }
751
- return inputPath;
752
- };
760
+ async function listWindowsDrives() {
761
+ const checks = await Promise.all(
762
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(async (letter) => {
763
+ const drivePath = `${letter}:\\`;
764
+ try {
765
+ await fsPromises.access(drivePath, fs.constants.R_OK);
766
+ const stats = await fsPromises.stat(drivePath);
767
+ if (!stats.isDirectory()) {
768
+ return null;
769
+ }
770
+
771
+ return {
772
+ path: drivePath,
773
+ name: drivePath,
774
+ type: 'directory',
775
+ isDrive: true
776
+ };
777
+ } catch (error) {
778
+ return null;
779
+ }
780
+ })
781
+ );
782
+
783
+ return checks.filter(Boolean);
784
+ }
753
785
 
754
786
  // Browse filesystem endpoint for project suggestions - uses existing getFileTree
755
787
  app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
@@ -757,9 +789,47 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
757
789
  const { path: dirPath } = req.query;
758
790
 
759
791
  console.log('[API] Browse filesystem request for path:', dirPath);
760
- console.log('[API] WORKSPACES_ROOT is:', WORKSPACES_ROOT);
792
+ console.log('[API] WORKSPACES_ROOTS are:', WORKSPACES_ROOTS.length > 0 ? WORKSPACES_ROOTS : '(unrestricted)');
793
+
794
+ if (process.platform === 'win32' && dirPath === WINDOWS_DRIVES_ROOT) {
795
+ const suggestions = await listWindowsDrives();
796
+ return res.json({
797
+ path: WINDOWS_DRIVES_ROOT,
798
+ displayPath: 'This PC',
799
+ suggestions,
800
+ platform: process.platform,
801
+ virtualRoot: 'windows-drives',
802
+ hasWorkspaceRootRestriction: HAS_WORKSPACES_ROOT_RESTRICTION,
803
+ allowedRoots: WORKSPACES_ROOTS
804
+ });
805
+ }
806
+
807
+ if (shouldUseVirtualWorkspaceRoots({ dirPath })) {
808
+ const suggestions = buildWorkspaceRootSuggestions({
809
+ allowedWorkspaceRoots: WORKSPACES_ROOTS,
810
+ homeDir: DEFAULT_WORKSPACES_ROOT,
811
+ platform: process.platform
812
+ }).filter((entry) => {
813
+ try {
814
+ return fs.existsSync(entry.path);
815
+ } catch (error) {
816
+ return false;
817
+ }
818
+ });
819
+
820
+ return res.json({
821
+ path: UNIX_WORKSPACE_ROOTS,
822
+ displayPath: 'Allowed workspace roots',
823
+ suggestions,
824
+ platform: process.platform,
825
+ virtualRoot: 'workspace-roots',
826
+ hasWorkspaceRootRestriction: HAS_WORKSPACES_ROOT_RESTRICTION,
827
+ allowedRoots: WORKSPACES_ROOTS
828
+ });
829
+ }
830
+
761
831
  // Default to home directory if no path provided
762
- const defaultRoot = WORKSPACES_ROOT;
832
+ const defaultRoot = DEFAULT_WORKSPACES_ROOT;
763
833
  let targetPath = dirPath ? expandWorkspacePath(dirPath) : defaultRoot;
764
834
 
765
835
  // Resolve and normalize the path
@@ -823,7 +893,12 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
823
893
 
824
894
  res.json({
825
895
  path: resolvedPath,
826
- suggestions: suggestions
896
+ displayPath: resolvedPath,
897
+ suggestions: suggestions,
898
+ platform: process.platform,
899
+ virtualRoot: null,
900
+ hasWorkspaceRootRestriction: HAS_WORKSPACES_ROOT_RESTRICTION,
901
+ allowedRoots: WORKSPACES_ROOTS
827
902
  });
828
903
 
829
904
  } catch (error) {
@@ -1112,31 +1187,24 @@ function handleChatConnection(ws) {
1112
1187
  try {
1113
1188
  data = JSON.parse(message);
1114
1189
 
1115
- if (data.type === 'claude-command') {
1116
- console.log('[DEBUG] User message:', data.command || '[Continue/Resume]');
1117
- console.log('📁 Project:', data.options?.projectPath || 'Unknown');
1118
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1190
+ const agentCommandTypes = ['claude-command', 'codex-command', 'gemini-command', 'opencode-command'];
1119
1191
 
1120
- // Use Claude Agents SDK
1121
- await queryClaudeSDK(data.command, data.options, new SessionEventMirrorWriter(baseWriter, 'claude'));
1122
- } else if (data.type === 'codex-command') {
1123
- console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
1124
- console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1125
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1126
- console.log('🤖 Model:', data.options?.model || 'default');
1127
- await queryCodex(data.command, data.options, new SessionEventMirrorWriter(baseWriter, 'codex'));
1128
- } else if (data.type === 'gemini-command') {
1129
- console.log('[DEBUG] Gemini message:', data.command || '[Continue/Resume]');
1192
+ if (agentCommandTypes.includes(data.type)) {
1193
+ const agentKey = data.type.replace(/-command$/, '');
1194
+ console.log(`[DEBUG] ${agentKey} message:`, data.command || '[Continue/Resume]');
1130
1195
  console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1131
1196
  console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1132
1197
  console.log('🤖 Model:', data.options?.model || 'default');
1133
- await queryGemini(data.command, data.options, new SessionEventMirrorWriter(baseWriter, 'gemini'));
1134
- } else if (data.type === 'opencode-command') {
1135
- console.log('[DEBUG] OpenCode message:', data.command || '[Continue/Resume]');
1136
- console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1137
- console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1138
- console.log('🤖 Model:', data.options?.model || 'default');
1139
- await queryOpencode(data.command, data.options, new SessionEventMirrorWriter(baseWriter, 'opencode'));
1198
+
1199
+ await executeAgentPrompt({
1200
+ agentKey,
1201
+ command: data.command,
1202
+ options: {
1203
+ ...(data.options || {}),
1204
+ clientRequestId: data.clientRequestId || data.options?.clientRequestId || null
1205
+ },
1206
+ writer: new SessionEventMirrorWriter(baseWriter, agentKey)
1207
+ });
1140
1208
  } else if (data.type === 'abort-session') {
1141
1209
  console.log('[DEBUG] Abort session request:', data.sessionId);
1142
1210
  const provider = typeof data.provider === 'string' && data.provider.trim()
@@ -1147,13 +1215,29 @@ function handleChatConnection(ws) {
1147
1215
  sessionId: data.sessionId,
1148
1216
  writer: getSessionWriter(provider)
1149
1217
  });
1150
- } else if (data.type === 'claude-permission-response') {
1151
- // Relay UI approval decisions back into the SDK control flow.
1152
- // This does not persist permissions; it only resolves the in-flight request,
1153
- // introduced so the SDK can resume once the user clicks Allow/Deny.
1218
+ } else if (data.type === 'set-session-mode') {
1219
+ const provider = typeof data.provider === 'string' && data.provider.trim()
1220
+ ? data.provider.trim().toLowerCase()
1221
+ : 'claude';
1222
+ const modeId = String(data.modeId || '').trim();
1223
+ if (!data.sessionId || !modeId) {
1224
+ throw new Error('set-session-mode requires sessionId and modeId');
1225
+ }
1226
+
1227
+ const changed = await setAgentSessionMode(provider, data.sessionId, modeId, {
1228
+ writer: getSessionWriter(provider)
1229
+ });
1230
+
1231
+ if (!changed) {
1232
+ throw new Error(`Unable to set session mode for ${provider}:${data.sessionId}`);
1233
+ }
1234
+ } else if (data.type === 'permission-response' || data.type === 'claude-permission-response') {
1154
1235
  if (data.requestId) {
1155
- resolveToolApproval(data.requestId, {
1236
+ resolveAgentPermission(data.requestId, {
1156
1237
  allow: Boolean(data.allow),
1238
+ optionId: data.optionId,
1239
+ outcome: data.outcome,
1240
+ cancelled: data.outcome?.outcome === 'cancelled' || Boolean(data.cancelled),
1157
1241
  updatedInput: data.updatedInput,
1158
1242
  message: data.message,
1159
1243
  rememberEntry: data.rememberEntry
@@ -1164,17 +1248,7 @@ function handleChatConnection(ws) {
1164
1248
  const provider = data.provider || 'claude';
1165
1249
  const sessionId = data.sessionId;
1166
1250
  const sessionWriter = getSessionWriter(provider);
1167
- let isActive;
1168
- if (provider === 'codex') {
1169
- isActive = isCodexSessionActive(sessionId);
1170
- } else if (provider === 'gemini') {
1171
- isActive = isGeminiSessionActive(sessionId);
1172
- } else if (provider === 'opencode') {
1173
- isActive = isOpencodeSessionActive(sessionId);
1174
- } else {
1175
- // Use Claude Agents SDK
1176
- isActive = isClaudeSDKSessionActive(sessionId);
1177
- }
1251
+ const isActive = isAgentSessionActive(provider, sessionId);
1178
1252
 
1179
1253
  sessionWriter.send({
1180
1254
  type: 'session-status',
@@ -1185,9 +1259,10 @@ function handleChatConnection(ws) {
1185
1259
  } else if (data.type === 'get-active-sessions') {
1186
1260
  // Get all currently active sessions
1187
1261
  const activeSessions = {
1188
- claude: getActiveClaudeSDKSessions(), codex: getActiveCodexSessions(),
1189
- gemini: getActiveGeminiSessions(),
1190
- opencode: getActiveOpencodeSessions()
1262
+ claude: getActiveAgentSessions('claude'),
1263
+ codex: getActiveAgentSessions('codex'),
1264
+ gemini: getActiveAgentSessions('gemini'),
1265
+ opencode: getActiveAgentSessions('opencode')
1191
1266
  };
1192
1267
  baseWriter.send({
1193
1268
  type: 'active-sessions',