@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.
- package/dist/api-docs.html +351 -909
- package/dist/assets/index-C2r-Jzfw.js +897 -0
- package/dist/assets/index-COkoBQi5.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +6 -2
- package/server/gemini-cli.js +280 -0
- package/server/index.js +206 -8
- package/server/openai-codex.js +102 -7
- package/server/opencode-cli.js +673 -0
- package/server/projects.js +645 -5
- package/server/routes/agent.js +35 -11
- package/server/routes/cli-auth.js +271 -0
- package/server/routes/commands.js +29 -3
- package/server/routes/git.js +15 -5
- package/server/routes/opencode.js +72 -0
- package/shared/modelConstants.js +62 -17
- package/dist/assets/index-Bue8nA1L.js +0 -1249
- package/dist/assets/index-CtRxrKDm.css +0 -32
- package/server/database/auth.db +0 -0
|
@@ -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
|
-
|
|
372
|
-
const
|
|
514
|
+
const npmCommand = getNpmCommand();
|
|
515
|
+
const updateArgs = ['update', '-g', UPDATE_PACKAGE_NAME];
|
|
373
516
|
|
|
374
|
-
|
|
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';
|