@axhub/genie 0.1.5 → 0.1.7

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.
@@ -0,0 +1,280 @@
1
+ import { spawn } from 'child_process';
2
+ import crossSpawn from 'cross-spawn';
3
+
4
+ const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
5
+
6
+ const activeGeminiProcesses = new Map();
7
+
8
+ function collectTextChunks(payload) {
9
+ if (!payload) return [];
10
+ if (typeof payload === 'string') return payload.trim() ? [payload] : [];
11
+
12
+ const chunks = [];
13
+
14
+ if (Array.isArray(payload)) {
15
+ payload.forEach(item => {
16
+ chunks.push(...collectTextChunks(item));
17
+ });
18
+ return chunks;
19
+ }
20
+
21
+ if (typeof payload !== 'object') {
22
+ return chunks;
23
+ }
24
+
25
+ const directKeys = ['text', 'response', 'content', 'message', 'delta'];
26
+ for (const key of directKeys) {
27
+ if (payload[key] !== undefined) {
28
+ chunks.push(...collectTextChunks(payload[key]));
29
+ }
30
+ }
31
+
32
+ if (Array.isArray(payload.parts)) {
33
+ payload.parts.forEach(part => {
34
+ chunks.push(...collectTextChunks(part));
35
+ });
36
+ }
37
+
38
+ if (payload.data && typeof payload.data === 'object') {
39
+ chunks.push(...collectTextChunks(payload.data));
40
+ }
41
+
42
+ return chunks;
43
+ }
44
+
45
+ function getEventRole(event) {
46
+ return (
47
+ event?.role ||
48
+ event?.author ||
49
+ event?.sender ||
50
+ event?.message?.role ||
51
+ event?.data?.role ||
52
+ event?.content?.role ||
53
+ null
54
+ );
55
+ }
56
+
57
+ function extractAssistantTextChunks(event, command) {
58
+ const role = String(getEventRole(event) || '').toLowerCase();
59
+ if (role === 'user') return [];
60
+
61
+ const eventType = String(event?.type || event?.event || event?.kind || '').toLowerCase();
62
+ const hasAssistantPayload = !!(event?.response || event?.candidates || event?.delta || event?.text || event?.content || event?.message);
63
+ if (eventType.includes('prompt') && !hasAssistantPayload) {
64
+ return [];
65
+ }
66
+
67
+ const normalizedPrompt = String(command || '').trim();
68
+ return collectTextChunks(event).filter((text) => {
69
+ const trimmed = String(text || '').trim();
70
+ if (!trimmed) return false;
71
+ if (normalizedPrompt && trimmed === normalizedPrompt) return false;
72
+ return true;
73
+ });
74
+ }
75
+
76
+ function parseGeminiJsonLine(line) {
77
+ const trimmed = line.trim();
78
+ if (!trimmed) return null;
79
+
80
+ const payload = trimmed.startsWith('data:') ? trimmed.slice(5).trim() : trimmed;
81
+ if (!payload) return null;
82
+
83
+ return JSON.parse(payload);
84
+ }
85
+
86
+ function emitTextChunks(ws, sessionId, chunks) {
87
+ chunks.forEach((text) => {
88
+ if (!text) return;
89
+ ws.send({
90
+ type: 'claude-response',
91
+ data: {
92
+ type: 'content_block_delta',
93
+ delta: {
94
+ type: 'text_delta',
95
+ text
96
+ }
97
+ },
98
+ provider: 'gemini',
99
+ sessionId
100
+ });
101
+ });
102
+ }
103
+
104
+ async function queryGemini(command, options = {}, ws) {
105
+ return new Promise((resolve, reject) => {
106
+ const { sessionId, cwd, projectPath, resume, model, permissionMode } = options;
107
+ let capturedSessionId = sessionId;
108
+ let sentSessionCreated = false;
109
+
110
+ const args = ['-y', '@google/gemini-cli'];
111
+
112
+ if (sessionId && (resume || !command || !command.trim())) {
113
+ args.push('--resume', sessionId);
114
+ }
115
+
116
+ if (command && command.trim()) {
117
+ args.push('--prompt', command);
118
+ args.push('--output-format', 'stream-json');
119
+ }
120
+
121
+ if (model) {
122
+ args.push('--model', model);
123
+ }
124
+
125
+ if (permissionMode === 'bypassPermissions' || permissionMode === 'acceptEdits') {
126
+ args.push('--yolo');
127
+ }
128
+
129
+ const workingDir = cwd || projectPath || process.cwd();
130
+
131
+ const geminiProcess = spawnFunction('npx', args, {
132
+ cwd: workingDir,
133
+ stdio: ['pipe', 'pipe', 'pipe'],
134
+ env: {
135
+ ...process.env,
136
+ GEMINI_NONINTERACTIVE: '1'
137
+ }
138
+ });
139
+
140
+ const processKey = capturedSessionId || Date.now().toString();
141
+ let processRegistryKey = processKey;
142
+ activeGeminiProcesses.set(processRegistryKey, geminiProcess);
143
+
144
+ const finalizeSessionId = () => capturedSessionId || sessionId || null;
145
+
146
+ const handleJsonEvent = (event) => {
147
+ const incomingSessionId = event?.session_id || event?.sessionId || event?.data?.session_id || event?.data?.sessionId;
148
+ if (incomingSessionId && !capturedSessionId) {
149
+ capturedSessionId = incomingSessionId;
150
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
151
+ ws.setSessionId(capturedSessionId);
152
+ }
153
+
154
+ if (processRegistryKey !== capturedSessionId) {
155
+ activeGeminiProcesses.delete(processRegistryKey);
156
+ activeGeminiProcesses.set(capturedSessionId, geminiProcess);
157
+ processRegistryKey = capturedSessionId;
158
+ }
159
+
160
+ if (!sessionId && !sentSessionCreated) {
161
+ sentSessionCreated = true;
162
+ ws.send({
163
+ type: 'session-created',
164
+ sessionId: capturedSessionId,
165
+ provider: 'gemini'
166
+ });
167
+ }
168
+ }
169
+
170
+ const type = event?.type || event?.event || event?.kind;
171
+ if (type === 'result' || type === 'done' || type === 'complete') {
172
+ ws.send({
173
+ type: 'claude-response',
174
+ data: { type: 'content_block_stop' },
175
+ provider: 'gemini',
176
+ sessionId: finalizeSessionId()
177
+ });
178
+
179
+ ws.send({
180
+ type: 'gemini-result',
181
+ data: event,
182
+ sessionId: finalizeSessionId()
183
+ });
184
+ return;
185
+ }
186
+
187
+ const textChunks = extractAssistantTextChunks(event, command);
188
+ emitTextChunks(ws, finalizeSessionId(), textChunks);
189
+ };
190
+
191
+ let stdoutBuffer = '';
192
+ geminiProcess.stdout.on('data', (data) => {
193
+ stdoutBuffer += data.toString();
194
+ const lines = stdoutBuffer.split('\n');
195
+ stdoutBuffer = lines.pop() || '';
196
+
197
+ for (const line of lines) {
198
+ try {
199
+ const event = parseGeminiJsonLine(line);
200
+ if (!event) continue;
201
+ handleJsonEvent(event);
202
+ } catch {}
203
+ }
204
+ });
205
+
206
+ geminiProcess.stderr.on('data', (data) => {
207
+ ws.send({
208
+ type: 'claude-error',
209
+ error: data.toString(),
210
+ sessionId: finalizeSessionId()
211
+ });
212
+ });
213
+
214
+ geminiProcess.on('close', (code) => {
215
+ if (stdoutBuffer.trim()) {
216
+ try {
217
+ const finalEvent = parseGeminiJsonLine(stdoutBuffer);
218
+ if (finalEvent) {
219
+ handleJsonEvent(finalEvent);
220
+ }
221
+ } catch {}
222
+ }
223
+
224
+ activeGeminiProcesses.delete(processRegistryKey);
225
+
226
+ ws.send({
227
+ type: 'claude-complete',
228
+ sessionId: finalizeSessionId(),
229
+ provider: 'gemini',
230
+ exitCode: code,
231
+ isNewSession: !sessionId && !!command
232
+ });
233
+
234
+ if (code === 0) {
235
+ resolve();
236
+ } else {
237
+ reject(new Error(`Gemini CLI exited with code ${code}`));
238
+ }
239
+ });
240
+
241
+ geminiProcess.on('error', (error) => {
242
+ activeGeminiProcesses.delete(processRegistryKey);
243
+
244
+ ws.send({
245
+ type: 'claude-error',
246
+ error: error.message,
247
+ sessionId: finalizeSessionId()
248
+ });
249
+
250
+ reject(error);
251
+ });
252
+
253
+ geminiProcess.stdin.end();
254
+ });
255
+ }
256
+
257
+ function abortGeminiSession(sessionId) {
258
+ const process = activeGeminiProcesses.get(sessionId);
259
+ if (process) {
260
+ process.kill('SIGTERM');
261
+ activeGeminiProcesses.delete(sessionId);
262
+ return true;
263
+ }
264
+ return false;
265
+ }
266
+
267
+ function isGeminiSessionActive(sessionId) {
268
+ return activeGeminiProcesses.has(sessionId);
269
+ }
270
+
271
+ function getActiveGeminiSessions() {
272
+ return Array.from(activeGeminiProcesses.keys());
273
+ }
274
+
275
+ export {
276
+ queryGemini,
277
+ abortGeminiSession,
278
+ isGeminiSessionActive,
279
+ getActiveGeminiSessions
280
+ };
package/server/index.js CHANGED
@@ -73,10 +73,12 @@ import pty from 'node-pty';
73
73
  import fetch from 'node-fetch';
74
74
  import mime from 'mime-types';
75
75
 
76
- import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
76
+ import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, getGeminiSessionMessages } from './projects.js';
77
77
  import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js';
78
78
  import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
79
79
  import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
80
+ import { queryGemini, abortGeminiSession, isGeminiSessionActive, getActiveGeminiSessions } from './gemini-cli.js';
81
+ import { queryOpencode, abortOpencodeSession, isOpencodeSessionActive, getActiveOpencodeSessions } from './opencode-cli.js';
80
82
  import gitRoutes from './routes/git.js';
81
83
  import authRoutes from './routes/auth.js';
82
84
  import mcpRoutes from './routes/mcp.js';
@@ -90,6 +92,7 @@ import projectsRoutes, { WORKSPACES_ROOT, validateWorkspacePath } from './routes
90
92
  import cliAuthRoutes from './routes/cli-auth.js';
91
93
  import userRoutes from './routes/user.js';
92
94
  import codexRoutes from './routes/codex.js';
95
+ import opencodeRoutes from './routes/opencode.js';
93
96
  import { parseCodexTokenCountInfo } from './utils/codexTokenUsage.js';
94
97
  import { initializeDatabase } from './database/db.js';
95
98
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
@@ -99,6 +102,10 @@ import { IS_PLATFORM } from './constants/config.js';
99
102
  let projectsWatcher = null;
100
103
  const connectedClients = new Set();
101
104
  let isGetProjectsRunning = false; // Flag to prevent reentrant calls
105
+ const UPDATE_PACKAGE_NAME = process.env.UPDATE_PACKAGE_NAME || packageInfo.id || '@axhub/genie';
106
+ const VERSION_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
107
+ let cachedSystemVersion = null;
108
+ let cachedSystemVersionExpiresAt = 0;
102
109
  const RUNTIME_STATUS_PATH = (() => {
103
110
  if (process.env.AXHUB_GENIE_STATUS_PATH) {
104
111
  return process.env.AXHUB_GENIE_STATUS_PATH;
@@ -107,6 +114,120 @@ const RUNTIME_STATUS_PATH = (() => {
107
114
  return path.join(os.homedir(), '.axhub-genie', 'runtime-status.json');
108
115
  })();
109
116
 
117
+ function getNpmCommand() {
118
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
119
+ }
120
+
121
+ function compareSemver(v1, v2) {
122
+ const normalizedV1 = String(v1 || '').replace(/^v/i, '').split('-')[0];
123
+ const normalizedV2 = String(v2 || '').replace(/^v/i, '').split('-')[0];
124
+ const parts1 = normalizedV1.split('.').map((part) => Number(part));
125
+ const parts2 = normalizedV2.split('.').map((part) => Number(part));
126
+
127
+ for (let i = 0; i < 3; i += 1) {
128
+ const a = Number.isFinite(parts1[i]) ? parts1[i] : 0;
129
+ const b = Number.isFinite(parts2[i]) ? parts2[i] : 0;
130
+ if (a > b) return 1;
131
+ if (a < b) return -1;
132
+ }
133
+
134
+ return 0;
135
+ }
136
+
137
+ function readLatestVersionFromNpmRegistry() {
138
+ return new Promise((resolve, reject) => {
139
+ const npmCommand = getNpmCommand();
140
+ const child = spawn(npmCommand, ['view', UPDATE_PACKAGE_NAME, 'version', 'time', '--json'], {
141
+ cwd: path.join(__dirname, '..'),
142
+ env: process.env
143
+ });
144
+
145
+ let output = '';
146
+ let errorOutput = '';
147
+
148
+ child.stdout.on('data', (data) => {
149
+ output += data.toString();
150
+ });
151
+
152
+ child.stderr.on('data', (data) => {
153
+ errorOutput += data.toString();
154
+ });
155
+
156
+ child.on('close', (code) => {
157
+ if (code !== 0) {
158
+ reject(new Error(errorOutput || `npm view exited with code ${code}`));
159
+ return;
160
+ }
161
+
162
+ try {
163
+ const parsed = JSON.parse(output.trim());
164
+ const latestVersion = parsed?.version;
165
+ if (!latestVersion) {
166
+ reject(new Error('npm view returned no version'));
167
+ return;
168
+ }
169
+
170
+ const publishedAt = parsed?.time?.[latestVersion] || parsed?.time?.modified || null;
171
+ resolve({
172
+ latestVersion,
173
+ publishedAt
174
+ });
175
+ } catch (error) {
176
+ reject(new Error(`failed to parse npm view output: ${error.message}`));
177
+ }
178
+ });
179
+
180
+ child.on('error', (error) => {
181
+ reject(error);
182
+ });
183
+ });
184
+ }
185
+
186
+ async function getSystemVersionInfo(forceRefresh = false) {
187
+ const now = Date.now();
188
+ if (!forceRefresh && cachedSystemVersion && now < cachedSystemVersionExpiresAt) {
189
+ return { ...cachedSystemVersion, cacheHit: true, stale: false };
190
+ }
191
+
192
+ try {
193
+ const npmInfo = await readLatestVersionFromNpmRegistry();
194
+ const currentVersion = packageInfo.version;
195
+ const latestVersion = npmInfo.latestVersion;
196
+ const updateAvailable = compareSemver(latestVersion, currentVersion) > 0;
197
+ const fetchedAt = new Date().toISOString();
198
+ const expiresAt = now + VERSION_CACHE_TTL_MS;
199
+
200
+ cachedSystemVersion = {
201
+ packageName: UPDATE_PACKAGE_NAME,
202
+ currentVersion,
203
+ latestVersion,
204
+ updateAvailable,
205
+ releaseInfo: {
206
+ title: `v${latestVersion}`,
207
+ body: '',
208
+ htmlUrl: `https://www.npmjs.com/package/${UPDATE_PACKAGE_NAME}`,
209
+ publishedAt: npmInfo.publishedAt
210
+ },
211
+ fetchedAt,
212
+ expiresAt: new Date(expiresAt).toISOString()
213
+ };
214
+ cachedSystemVersionExpiresAt = expiresAt;
215
+
216
+ return { ...cachedSystemVersion, cacheHit: false, stale: false };
217
+ } catch (error) {
218
+ // Return stale cache when npm registry is temporarily unavailable.
219
+ if (cachedSystemVersion) {
220
+ return {
221
+ ...cachedSystemVersion,
222
+ cacheHit: true,
223
+ stale: true,
224
+ warning: `Using stale cached version data: ${error.message}`
225
+ };
226
+ }
227
+ throw error;
228
+ }
229
+ }
230
+
110
231
  // Broadcast progress to all connected WebSocket clients
111
232
  function broadcastProgress(progress) {
112
233
  const message = JSON.stringify({
@@ -333,6 +454,7 @@ app.use('/api/user', authenticateToken, userRoutes);
333
454
 
334
455
  // Codex API Routes (protected)
335
456
  app.use('/api/codex', authenticateToken, codexRoutes);
457
+ app.use('/api/opencode', authenticateToken, opencodeRoutes);
336
458
 
337
459
  // Agent API Routes (uses API key authentication)
338
460
  app.use('/api/agent', agentRoutes);
@@ -360,6 +482,27 @@ app.use(express.static(path.join(__dirname, '../dist'), {
360
482
  // /api/config endpoint removed - no longer needed
361
483
  // Frontend now uses window.location for WebSocket URLs
362
484
 
485
+ // System version check endpoint (npm registry with server-side cache)
486
+ app.get('/api/system/version', authenticateToken, async (req, res) => {
487
+ try {
488
+ const forceRefresh = String(req.query.force || '').toLowerCase() === 'true';
489
+ const versionInfo = await getSystemVersionInfo(forceRefresh);
490
+
491
+ res.setHeader('Cache-Control', 'private, max-age=3600, stale-while-revalidate=300');
492
+ res.json({
493
+ success: true,
494
+ ...versionInfo,
495
+ cacheTtlMs: VERSION_CACHE_TTL_MS
496
+ });
497
+ } catch (error) {
498
+ console.error('System version check error:', error);
499
+ res.status(500).json({
500
+ success: false,
501
+ error: error.message
502
+ });
503
+ }
504
+ });
505
+
363
506
  // System update endpoint
364
507
  app.post('/api/system/update', authenticateToken, async (req, res) => {
365
508
  try {
@@ -368,10 +511,11 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
368
511
 
369
512
  console.log('Starting system update from directory:', projectRoot);
370
513
 
371
- // Run the update command
372
- const updateCommand = 'git checkout main && git pull && npm install';
514
+ const npmCommand = getNpmCommand();
515
+ const updateArgs = ['update', '-g', UPDATE_PACKAGE_NAME];
373
516
 
374
- const child = spawn('sh', ['-c', updateCommand], {
517
+ // Run npm global update command
518
+ const child = spawn(npmCommand, updateArgs, {
375
519
  cwd: projectRoot,
376
520
  env: process.env
377
521
  });
@@ -393,17 +537,21 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
393
537
 
394
538
  child.on('close', (code) => {
395
539
  if (code === 0) {
540
+ cachedSystemVersion = null;
541
+ cachedSystemVersionExpiresAt = 0;
396
542
  res.json({
397
543
  success: true,
398
544
  output: output || 'Update completed successfully',
399
- message: 'Update completed. Please restart the server to apply changes.'
545
+ message: 'Update completed. Please restart the server to apply changes.',
546
+ command: `${npmCommand} ${updateArgs.join(' ')}`
400
547
  });
401
548
  } else {
402
549
  res.status(500).json({
403
550
  success: false,
404
551
  error: 'Update command failed',
405
552
  output: output,
406
- errorOutput: errorOutput
553
+ errorOutput: errorOutput,
554
+ command: `${npmCommand} ${updateArgs.join(' ')}`
407
555
  });
408
556
  }
409
557
  });
@@ -469,6 +617,20 @@ app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateT
469
617
  }
470
618
  });
471
619
 
620
+ app.get('/api/gemini/sessions/:sessionId/messages', authenticateToken, async (req, res) => {
621
+ try {
622
+ const { sessionId } = req.params;
623
+ const { limit, offset } = req.query;
624
+
625
+ const parsedLimit = limit ? parseInt(limit, 10) : null;
626
+ const parsedOffset = offset ? parseInt(offset, 10) : 0;
627
+ const result = await getGeminiSessionMessages(sessionId, parsedLimit, parsedOffset);
628
+ res.json(result);
629
+ } catch (error) {
630
+ res.status(500).json({ error: error.message });
631
+ }
632
+ });
633
+
472
634
  // Rename project endpoint
473
635
  app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
474
636
  try {
@@ -910,6 +1072,18 @@ function handleChatConnection(ws) {
910
1072
  console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
911
1073
  console.log('🤖 Model:', data.options?.model || 'default');
912
1074
  await queryCodex(data.command, data.options, writer);
1075
+ } else if (data.type === 'gemini-command') {
1076
+ console.log('[DEBUG] Gemini message:', data.command || '[Continue/Resume]');
1077
+ console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1078
+ console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1079
+ console.log('🤖 Model:', data.options?.model || 'default');
1080
+ await queryGemini(data.command, data.options, writer);
1081
+ } else if (data.type === 'opencode-command') {
1082
+ console.log('[DEBUG] OpenCode message:', data.command || '[Continue/Resume]');
1083
+ console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
1084
+ console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
1085
+ console.log('🤖 Model:', data.options?.model || 'default');
1086
+ await queryOpencode(data.command, data.options, writer);
913
1087
  } else if (data.type === 'cursor-resume') {
914
1088
  // Backward compatibility: treat as cursor-command with resume and no prompt
915
1089
  console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
@@ -927,6 +1101,10 @@ function handleChatConnection(ws) {
927
1101
  success = abortCursorSession(data.sessionId);
928
1102
  } else if (provider === 'codex') {
929
1103
  success = abortCodexSession(data.sessionId);
1104
+ } else if (provider === 'gemini') {
1105
+ success = abortGeminiSession(data.sessionId);
1106
+ } else if (provider === 'opencode') {
1107
+ success = abortOpencodeSession(data.sessionId);
930
1108
  } else {
931
1109
  // Use Claude Agents SDK
932
1110
  success = await abortClaudeSDKSession(data.sessionId);
@@ -969,6 +1147,10 @@ function handleChatConnection(ws) {
969
1147
  isActive = isCursorSessionActive(sessionId);
970
1148
  } else if (provider === 'codex') {
971
1149
  isActive = isCodexSessionActive(sessionId);
1150
+ } else if (provider === 'gemini') {
1151
+ isActive = isGeminiSessionActive(sessionId);
1152
+ } else if (provider === 'opencode') {
1153
+ isActive = isOpencodeSessionActive(sessionId);
972
1154
  } else {
973
1155
  // Use Claude Agents SDK
974
1156
  isActive = isClaudeSDKSessionActive(sessionId);
@@ -985,7 +1167,9 @@ function handleChatConnection(ws) {
985
1167
  const activeSessions = {
986
1168
  claude: getActiveClaudeSDKSessions(),
987
1169
  cursor: getActiveCursorSessions(),
988
- codex: getActiveCodexSessions()
1170
+ codex: getActiveCodexSessions(),
1171
+ gemini: getActiveGeminiSessions(),
1172
+ opencode: getActiveOpencodeSessions()
989
1173
  };
990
1174
  writer.send({
991
1175
  type: 'active-sessions',
@@ -1091,7 +1275,7 @@ function handleShellConnection(ws) {
1091
1275
  if (isPlainShell) {
1092
1276
  welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
1093
1277
  } else {
1094
- const providerName = provider === 'cursor' ? 'Cursor' : 'Claude';
1278
+ const providerName = provider === 'cursor' ? 'Cursor' : provider === 'gemini' ? 'Gemini' : 'Claude';
1095
1279
  welcomeMsg = hasSession ?
1096
1280
  `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
1097
1281
  `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
@@ -1127,6 +1311,20 @@ function handleShellConnection(ws) {
1127
1311
  shellCommand = `cd "${projectPath}" && cursor-agent`;
1128
1312
  }
1129
1313
  }
1314
+ } else if (provider === 'gemini') {
1315
+ if (os.platform() === 'win32') {
1316
+ if (hasSession && sessionId) {
1317
+ shellCommand = `Set-Location -Path "${projectPath}"; npx -y @google/gemini-cli --resume ${sessionId}`;
1318
+ } else {
1319
+ shellCommand = `Set-Location -Path "${projectPath}"; npx -y @google/gemini-cli`;
1320
+ }
1321
+ } else {
1322
+ if (hasSession && sessionId) {
1323
+ shellCommand = `cd "${projectPath}" && npx -y @google/gemini-cli --resume ${sessionId}`;
1324
+ } else {
1325
+ shellCommand = `cd "${projectPath}" && npx -y @google/gemini-cli`;
1326
+ }
1327
+ }
1130
1328
  } else {
1131
1329
  // Use claude command (default) or initialCommand if provided
1132
1330
  const command = initialCommand || 'claude';