@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
package/server/routes/agent.js
CHANGED
|
@@ -9,8 +9,10 @@ import { addProjectManually } from '../projects.js';
|
|
|
9
9
|
import { queryClaudeSDK } from '../claude-sdk.js';
|
|
10
10
|
import { spawnCursor } from '../cursor-cli.js';
|
|
11
11
|
import { queryCodex } from '../openai-codex.js';
|
|
12
|
+
import { queryGemini } from '../gemini-cli.js';
|
|
13
|
+
import { queryOpencode } from '../opencode-cli.js';
|
|
12
14
|
import { Octokit } from '@octokit/rest';
|
|
13
|
-
import {
|
|
15
|
+
import { CODEX_MODELS, GEMINI_MODELS, OPENCODE_MODELS } from '../../shared/modelConstants.js';
|
|
14
16
|
import { IS_PLATFORM } from '../constants/config.js';
|
|
15
17
|
|
|
16
18
|
const router = express.Router();
|
|
@@ -633,7 +635,7 @@ class ResponseCollector {
|
|
|
633
635
|
/**
|
|
634
636
|
* POST /api/agent
|
|
635
637
|
*
|
|
636
|
-
* Trigger an AI agent (Claude or
|
|
638
|
+
* Trigger an AI agent (Claude, Cursor, Codex, or Gemini) to work on a project.
|
|
637
639
|
* Supports automatic GitHub branch and pull request creation after successful completion.
|
|
638
640
|
*
|
|
639
641
|
* ================================================================================================
|
|
@@ -661,7 +663,7 @@ class ResponseCollector {
|
|
|
661
663
|
* @param {string} sessionId - (Optional) Existing session ID to resume.
|
|
662
664
|
* If provided, the request continues that session instead of creating a new one.
|
|
663
665
|
*
|
|
664
|
-
* @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor'
|
|
666
|
+
* @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'gemini'
|
|
665
667
|
* Default: 'claude'
|
|
666
668
|
*
|
|
667
669
|
* @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates.
|
|
@@ -677,6 +679,7 @@ class ResponseCollector {
|
|
|
677
679
|
* 'gpt-5.1-codex', 'gpt-5.1-codex-high', 'gpt-5.1-codex-max',
|
|
678
680
|
* 'gpt-5.1-codex-max-high', 'opus-4.1', 'grok', and thinking variants
|
|
679
681
|
* Codex models: 'gpt-5.2' (default), 'gpt-5.1-codex-max', 'o3', 'o4-mini'
|
|
682
|
+
* Gemini models: 'gemini-2.5-pro' (default), 'gemini-2.5-flash', 'gemini-2.5-flash-lite'
|
|
680
683
|
*
|
|
681
684
|
* @param {boolean} cleanup - (Optional) Auto-cleanup project directory after completion.
|
|
682
685
|
* Default: true
|
|
@@ -779,7 +782,7 @@ class ResponseCollector {
|
|
|
779
782
|
* Input Validations (400 Bad Request):
|
|
780
783
|
* - Either githubUrl OR projectPath must be provided (not neither)
|
|
781
784
|
* - message must be non-empty string
|
|
782
|
-
* - provider must be 'claude' or '
|
|
785
|
+
* - provider must be 'claude', 'cursor', 'codex', or 'gemini'
|
|
783
786
|
* - createBranch/createPR requires githubUrl OR projectPath (not neither)
|
|
784
787
|
* - branchName must pass Git naming rules (if provided)
|
|
785
788
|
*
|
|
@@ -872,7 +875,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
872
875
|
|
|
873
876
|
// Parse stream and cleanup as booleans (handle string "true"/"false" from curl)
|
|
874
877
|
const stream = req.body.stream === undefined ? true : (req.body.stream === true || req.body.stream === 'true');
|
|
875
|
-
const
|
|
878
|
+
const requestedCleanup = req.body.cleanup === undefined ? true : (req.body.cleanup === true || req.body.cleanup === 'true');
|
|
876
879
|
|
|
877
880
|
// If branchName is provided, automatically enable createBranch
|
|
878
881
|
const createBranch = branchName ? true : (req.body.createBranch === true || req.body.createBranch === 'true');
|
|
@@ -881,10 +884,13 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
881
884
|
const normalizedMessage = typeof message === 'string' ? message.trim() : '';
|
|
882
885
|
const requestedOpenOnly = openOnly === true || openOnly === 'true';
|
|
883
886
|
const isOpenOnly = requestedOpenOnly || (!normalizedMessage && !!normalizedSessionId);
|
|
887
|
+
// Keep openOnly requests backward-compatible with existing docs/examples:
|
|
888
|
+
// cleanup is irrelevant when we only generate session navigation.
|
|
889
|
+
const cleanup = isOpenOnly ? false : requestedCleanup;
|
|
884
890
|
|
|
885
891
|
// Validate inputs
|
|
886
|
-
if (!['claude', 'cursor', 'codex'].includes(provider)) {
|
|
887
|
-
return res.status(400).json({ error: 'provider must be "claude", "cursor", or "
|
|
892
|
+
if (!['claude', 'cursor', 'codex', 'gemini', 'opencode'].includes(provider)) {
|
|
893
|
+
return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", "gemini", or "opencode"' });
|
|
888
894
|
}
|
|
889
895
|
|
|
890
896
|
if (sessionId !== undefined && (typeof sessionId !== 'string' || !sessionId.trim())) {
|
|
@@ -911,10 +917,6 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
911
917
|
return res.status(400).json({ error: 'createBranch and createPR are not supported when openOnly=true' });
|
|
912
918
|
}
|
|
913
919
|
|
|
914
|
-
if (isOpenOnly && cleanup) {
|
|
915
|
-
return res.status(400).json({ error: 'cleanup is not supported when openOnly=true' });
|
|
916
|
-
}
|
|
917
|
-
|
|
918
920
|
// Validate GitHub branch/PR creation requirements
|
|
919
921
|
// Allow branch/PR creation with projectPath as long as it has a GitHub remote
|
|
920
922
|
if ((createBranch || createPR) && !githubUrl && !projectPath) {
|
|
@@ -1062,6 +1064,28 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
1062
1064
|
model: model || CODEX_MODELS.DEFAULT,
|
|
1063
1065
|
permissionMode: 'bypassPermissions'
|
|
1064
1066
|
}, writer);
|
|
1067
|
+
} else if (provider === 'gemini') {
|
|
1068
|
+
console.log('💎 Starting Gemini CLI session');
|
|
1069
|
+
|
|
1070
|
+
await queryGemini(normalizedMessage, {
|
|
1071
|
+
projectPath: finalProjectPath,
|
|
1072
|
+
cwd: finalProjectPath,
|
|
1073
|
+
sessionId: normalizedSessionId,
|
|
1074
|
+
resume: !!normalizedSessionId,
|
|
1075
|
+
model: model || GEMINI_MODELS.DEFAULT,
|
|
1076
|
+
permissionMode: 'bypassPermissions'
|
|
1077
|
+
}, writer);
|
|
1078
|
+
} else if (provider === 'opencode') {
|
|
1079
|
+
console.log('🧠 Starting OpenCode CLI session');
|
|
1080
|
+
|
|
1081
|
+
await queryOpencode(normalizedMessage, {
|
|
1082
|
+
projectPath: finalProjectPath,
|
|
1083
|
+
cwd: finalProjectPath,
|
|
1084
|
+
sessionId: normalizedSessionId,
|
|
1085
|
+
resume: !!normalizedSessionId,
|
|
1086
|
+
model: model || OPENCODE_MODELS.DEFAULT,
|
|
1087
|
+
permissionMode: 'bypassPermissions'
|
|
1088
|
+
}, writer);
|
|
1065
1089
|
}
|
|
1066
1090
|
|
|
1067
1091
|
// Handle GitHub branch and PR creation after successful agent completion
|
|
@@ -5,6 +5,172 @@ import path from 'path';
|
|
|
5
5
|
import os from 'os';
|
|
6
6
|
|
|
7
7
|
const router = express.Router();
|
|
8
|
+
const INSTALLATION_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
9
|
+
const SUPPORTED_PROVIDER_COMMANDS = {
|
|
10
|
+
claude: 'claude',
|
|
11
|
+
cursor: 'cursor-agent',
|
|
12
|
+
codex: 'codex',
|
|
13
|
+
gemini: 'gemini',
|
|
14
|
+
opencode: 'opencode'
|
|
15
|
+
};
|
|
16
|
+
const installationStatusCache = new Map(); // provider -> status
|
|
17
|
+
const installationStatusInFlight = new Map(); // provider -> Promise
|
|
18
|
+
|
|
19
|
+
function getLocatorCommand() {
|
|
20
|
+
return process.platform === 'win32' ? 'where' : 'which';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveCommandPath(command) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let childProcess;
|
|
26
|
+
let stdout = '';
|
|
27
|
+
let stderr = '';
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
childProcess = spawn(getLocatorCommand(), [command], {
|
|
31
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
resolve({
|
|
35
|
+
found: false,
|
|
36
|
+
resolvedPath: null,
|
|
37
|
+
reason: `Failed to run command locator: ${error.message}`
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
childProcess.stdout.on('data', (data) => {
|
|
43
|
+
stdout += data.toString();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
childProcess.stderr.on('data', (data) => {
|
|
47
|
+
stderr += data.toString();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
childProcess.on('close', (code) => {
|
|
51
|
+
const lines = stdout
|
|
52
|
+
.split(/\r?\n/)
|
|
53
|
+
.map(line => line.trim())
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
const resolvedPath = lines[0] || null;
|
|
56
|
+
|
|
57
|
+
if (code === 0 && resolvedPath) {
|
|
58
|
+
resolve({
|
|
59
|
+
found: true,
|
|
60
|
+
resolvedPath,
|
|
61
|
+
reason: null
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
resolve({
|
|
67
|
+
found: false,
|
|
68
|
+
resolvedPath: null,
|
|
69
|
+
reason: `${command} not found in PATH${stderr?.trim() ? ` (${stderr.trim()})` : ''}`
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
childProcess.on('error', (error) => {
|
|
74
|
+
resolve({
|
|
75
|
+
found: false,
|
|
76
|
+
resolvedPath: null,
|
|
77
|
+
reason: `Failed to detect ${command}: ${error.message}`
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildInstallationStatus(provider, command, installed, resolvedPath, reason, cacheHit = false) {
|
|
84
|
+
return {
|
|
85
|
+
success: true,
|
|
86
|
+
provider,
|
|
87
|
+
command,
|
|
88
|
+
installed,
|
|
89
|
+
reason,
|
|
90
|
+
resolvedPath,
|
|
91
|
+
checkedAt: new Date().toISOString(),
|
|
92
|
+
cacheHit,
|
|
93
|
+
ttlMs: INSTALLATION_CACHE_TTL_MS
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function detectProviderInstallationStatus(provider) {
|
|
98
|
+
const command = SUPPORTED_PROVIDER_COMMANDS[provider];
|
|
99
|
+
if (!command) {
|
|
100
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const cached = installationStatusCache.get(provider);
|
|
105
|
+
if (cached && cached.expiresAt > now) {
|
|
106
|
+
return {
|
|
107
|
+
...cached.status,
|
|
108
|
+
cacheHit: true
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (installationStatusInFlight.has(provider)) {
|
|
113
|
+
return installationStatusInFlight.get(provider);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const detectionPromise = (async () => {
|
|
117
|
+
const commandCheck = await resolveCommandPath(command);
|
|
118
|
+
const installed = commandCheck.found;
|
|
119
|
+
const status = buildInstallationStatus(
|
|
120
|
+
provider,
|
|
121
|
+
command,
|
|
122
|
+
installed,
|
|
123
|
+
commandCheck.resolvedPath,
|
|
124
|
+
commandCheck.reason,
|
|
125
|
+
false
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (installed) {
|
|
129
|
+
installationStatusCache.set(provider, {
|
|
130
|
+
status,
|
|
131
|
+
expiresAt: now + INSTALLATION_CACHE_TTL_MS
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
installationStatusCache.delete(provider);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return status;
|
|
138
|
+
})();
|
|
139
|
+
|
|
140
|
+
installationStatusInFlight.set(provider, detectionPromise);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
return await detectionPromise;
|
|
144
|
+
} finally {
|
|
145
|
+
installationStatusInFlight.delete(provider);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
router.get('/providers/:provider/installation-status', async (req, res) => {
|
|
150
|
+
try {
|
|
151
|
+
const provider = String(req.params.provider || '').toLowerCase();
|
|
152
|
+
if (!Object.prototype.hasOwnProperty.call(SUPPORTED_PROVIDER_COMMANDS, provider)) {
|
|
153
|
+
return res.status(400).json({
|
|
154
|
+
success: false,
|
|
155
|
+
error: `Unsupported provider: ${provider}`
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const forceRefresh = String(req.query.force || '').toLowerCase() === 'true';
|
|
160
|
+
if (forceRefresh) {
|
|
161
|
+
installationStatusCache.delete(provider);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const status = await detectProviderInstallationStatus(provider);
|
|
165
|
+
return res.json(status);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Error checking provider installation status:', error);
|
|
168
|
+
return res.status(500).json({
|
|
169
|
+
success: false,
|
|
170
|
+
error: error.message
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
8
174
|
|
|
9
175
|
router.get('/claude/status', async (req, res) => {
|
|
10
176
|
try {
|
|
@@ -74,6 +240,26 @@ router.get('/codex/status', async (req, res) => {
|
|
|
74
240
|
}
|
|
75
241
|
});
|
|
76
242
|
|
|
243
|
+
router.get('/gemini/status', async (req, res) => {
|
|
244
|
+
try {
|
|
245
|
+
const result = await checkGeminiCredentials();
|
|
246
|
+
|
|
247
|
+
res.json({
|
|
248
|
+
authenticated: result.authenticated,
|
|
249
|
+
email: result.email,
|
|
250
|
+
error: result.error
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error('Error checking Gemini auth status:', error);
|
|
255
|
+
res.status(500).json({
|
|
256
|
+
authenticated: false,
|
|
257
|
+
email: null,
|
|
258
|
+
error: error.message
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
77
263
|
async function checkClaudeCredentials() {
|
|
78
264
|
try {
|
|
79
265
|
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
@@ -260,4 +446,89 @@ async function checkCodexCredentials() {
|
|
|
260
446
|
}
|
|
261
447
|
}
|
|
262
448
|
|
|
449
|
+
async function checkGeminiCredentials() {
|
|
450
|
+
try {
|
|
451
|
+
const geminiDir = path.join(os.homedir(), '.gemini');
|
|
452
|
+
const oauthCredPath = path.join(geminiDir, 'oauth_creds.json');
|
|
453
|
+
const content = await fs.readFile(oauthCredPath, 'utf8');
|
|
454
|
+
const creds = JSON.parse(content);
|
|
455
|
+
|
|
456
|
+
if (creds?.access_token || creds?.refresh_token || creds?.token) {
|
|
457
|
+
return {
|
|
458
|
+
authenticated: true,
|
|
459
|
+
email: creds?.email || 'Authenticated'
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
authenticated: false,
|
|
465
|
+
email: null,
|
|
466
|
+
error: 'No valid Gemini credentials found'
|
|
467
|
+
};
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (error.code === 'ENOENT') {
|
|
470
|
+
return {
|
|
471
|
+
authenticated: false,
|
|
472
|
+
email: null,
|
|
473
|
+
error: 'Gemini not configured'
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
authenticated: false,
|
|
479
|
+
email: null,
|
|
480
|
+
error: error.message
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// OpenCode status helper
|
|
486
|
+
async function checkOpencodeCredentials() {
|
|
487
|
+
try {
|
|
488
|
+
const candidates = [
|
|
489
|
+
path.join(os.homedir(), '.config', 'opencode', 'auth.json'),
|
|
490
|
+
path.join(os.homedir(), '.opencode', 'auth.json')
|
|
491
|
+
];
|
|
492
|
+
let anyFileAccessible = false;
|
|
493
|
+
for (const p of candidates) {
|
|
494
|
+
try {
|
|
495
|
+
await fs.access(p);
|
|
496
|
+
anyFileAccessible = true;
|
|
497
|
+
const raw = await fs.readFile(p, 'utf8');
|
|
498
|
+
const data = JSON.parse(raw);
|
|
499
|
+
const token = data?.access_token || data?.token || data?.api_key;
|
|
500
|
+
if (token) {
|
|
501
|
+
const email = data.email || 'Authenticated';
|
|
502
|
+
return { authenticated: true, email, error: null };
|
|
503
|
+
}
|
|
504
|
+
// else try next candidate
|
|
505
|
+
} catch (err) {
|
|
506
|
+
// ignore and try next
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (anyFileAccessible) {
|
|
511
|
+
return { authenticated: false, email: null, error: 'Invalid OpenCode credentials format' };
|
|
512
|
+
}
|
|
513
|
+
return { authenticated: false, email: null, error: 'OpenCode not configured' };
|
|
514
|
+
} catch (error) {
|
|
515
|
+
return { authenticated: false, email: null, error: 'OpenCode not configured' };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Route: GET /opencode/status
|
|
520
|
+
router.get('/opencode/status', async (req, res) => {
|
|
521
|
+
try {
|
|
522
|
+
const result = await checkOpencodeCredentials();
|
|
523
|
+
res.json({
|
|
524
|
+
authenticated: result.authenticated,
|
|
525
|
+
email: result.email,
|
|
526
|
+
error: result.error
|
|
527
|
+
});
|
|
528
|
+
} catch (error) {
|
|
529
|
+
console.error('Error checking OpenCode auth status:', error);
|
|
530
|
+
res.status(500).json({ authenticated: false, email: null, error: error.message });
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
263
534
|
export default router;
|
|
@@ -4,7 +4,8 @@ import path from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import matter from 'gray-matter';
|
|
7
|
-
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
|
|
7
|
+
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, GEMINI_MODELS, OPENCODE_MODELS } from '../../shared/modelConstants.js';
|
|
8
|
+
import { listOpencodeModels } from '../opencode-cli.js';
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = path.dirname(__filename);
|
|
@@ -187,11 +188,35 @@ Custom commands can be created in:
|
|
|
187
188
|
const availableModels = {
|
|
188
189
|
claude: CLAUDE_MODELS.OPTIONS.map(o => o.value),
|
|
189
190
|
cursor: CURSOR_MODELS.OPTIONS.map(o => o.value),
|
|
190
|
-
codex: CODEX_MODELS.OPTIONS.map(o => o.value)
|
|
191
|
+
codex: CODEX_MODELS.OPTIONS.map(o => o.value),
|
|
192
|
+
gemini: GEMINI_MODELS.OPTIONS.map(o => o.value),
|
|
193
|
+
opencode: OPENCODE_MODELS.OPTIONS.map(o => o.value)
|
|
191
194
|
};
|
|
192
195
|
|
|
193
196
|
const currentProvider = context?.provider || 'claude';
|
|
194
|
-
const
|
|
197
|
+
const providerDefaults = {
|
|
198
|
+
claude: CLAUDE_MODELS.DEFAULT,
|
|
199
|
+
cursor: CURSOR_MODELS.DEFAULT,
|
|
200
|
+
codex: CODEX_MODELS.DEFAULT,
|
|
201
|
+
gemini: GEMINI_MODELS.DEFAULT,
|
|
202
|
+
opencode: OPENCODE_MODELS.DEFAULT
|
|
203
|
+
};
|
|
204
|
+
const currentModel = context?.model || providerDefaults[currentProvider] || CLAUDE_MODELS.DEFAULT;
|
|
205
|
+
let opencodeDiscovery = null;
|
|
206
|
+
|
|
207
|
+
if (context?.projectPath) {
|
|
208
|
+
try {
|
|
209
|
+
opencodeDiscovery = await listOpencodeModels({
|
|
210
|
+
projectPath: context?.projectPath,
|
|
211
|
+
cwd: context?.projectPath
|
|
212
|
+
});
|
|
213
|
+
if (opencodeDiscovery.models.length > 0) {
|
|
214
|
+
availableModels.opencode = opencodeDiscovery.models;
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.warn('Failed to query OpenCode runtime models:', error.message);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
195
220
|
|
|
196
221
|
return {
|
|
197
222
|
type: 'builtin',
|
|
@@ -202,6 +227,7 @@ Custom commands can be created in:
|
|
|
202
227
|
model: currentModel
|
|
203
228
|
},
|
|
204
229
|
available: availableModels,
|
|
230
|
+
opencodeDiscovery,
|
|
205
231
|
message: args.length > 0
|
|
206
232
|
? `Switching to model: ${args[0]}`
|
|
207
233
|
: `Current model: ${currentModel}`
|
package/server/routes/git.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promises as fs } from 'fs';
|
|
|
6
6
|
import { extractProjectDirectory } from '../projects.js';
|
|
7
7
|
import { queryClaudeSDK } from '../claude-sdk.js';
|
|
8
8
|
import { spawnCursor } from '../cursor-cli.js';
|
|
9
|
+
import { queryGemini } from '../gemini-cli.js';
|
|
9
10
|
|
|
10
11
|
const router = express.Router();
|
|
11
12
|
const execAsync = promisify(exec);
|
|
@@ -519,8 +520,8 @@ router.post('/generate-commit-message', async (req, res) => {
|
|
|
519
520
|
}
|
|
520
521
|
|
|
521
522
|
// Validate provider
|
|
522
|
-
if (!['claude', 'cursor'].includes(provider)) {
|
|
523
|
-
return res.status(400).json({ error: 'provider must be "claude" or "
|
|
523
|
+
if (!['claude', 'cursor', 'gemini'].includes(provider)) {
|
|
524
|
+
return res.status(400).json({ error: 'provider must be "claude", "cursor", or "gemini"' });
|
|
524
525
|
}
|
|
525
526
|
|
|
526
527
|
try {
|
|
@@ -573,10 +574,10 @@ router.post('/generate-commit-message', async (req, res) => {
|
|
|
573
574
|
});
|
|
574
575
|
|
|
575
576
|
/**
|
|
576
|
-
* Generates a commit message using AI (Claude SDK or
|
|
577
|
+
* Generates a commit message using AI (Claude SDK, Cursor CLI, or Gemini CLI)
|
|
577
578
|
* @param {Array<string>} files - List of changed files
|
|
578
579
|
* @param {string} diffContext - Git diff content
|
|
579
|
-
* @param {string} provider - 'claude' or '
|
|
580
|
+
* @param {string} provider - 'claude', 'cursor', or 'gemini'
|
|
580
581
|
* @param {string} projectPath - Project directory path
|
|
581
582
|
* @returns {Promise<string>} Generated commit message
|
|
582
583
|
*/
|
|
@@ -630,6 +631,9 @@ Generate the commit message:`;
|
|
|
630
631
|
console.log('✅ Cursor output:', parsed.output.substring(0, 100));
|
|
631
632
|
responseText += parsed.output;
|
|
632
633
|
}
|
|
634
|
+
else if (parsed.type === 'claude-response' && parsed.data?.type === 'content_block_delta' && parsed.data?.delta?.text) {
|
|
635
|
+
responseText += parsed.data.delta.text;
|
|
636
|
+
}
|
|
633
637
|
// Also handle direct text messages
|
|
634
638
|
else if (parsed.type === 'text' && parsed.text) {
|
|
635
639
|
console.log('✅ Direct text:', parsed.text.substring(0, 100));
|
|
@@ -658,6 +662,12 @@ Generate the commit message:`;
|
|
|
658
662
|
cwd: projectPath,
|
|
659
663
|
skipPermissions: true
|
|
660
664
|
}, writer);
|
|
665
|
+
} else if (provider === 'gemini') {
|
|
666
|
+
await queryGemini(prompt, {
|
|
667
|
+
cwd: projectPath,
|
|
668
|
+
projectPath,
|
|
669
|
+
permissionMode: 'bypassPermissions'
|
|
670
|
+
}, writer);
|
|
661
671
|
}
|
|
662
672
|
|
|
663
673
|
console.log('📊 Total response text collected:', responseText.length, 'characters');
|
|
@@ -1125,4 +1135,4 @@ router.post('/delete-untracked', async (req, res) => {
|
|
|
1125
1135
|
}
|
|
1126
1136
|
});
|
|
1127
1137
|
|
|
1128
|
-
export default router;
|
|
1138
|
+
export default router;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import {
|
|
3
|
+
getOpencodeSessions,
|
|
4
|
+
getOpencodeSessionMessages,
|
|
5
|
+
deleteOpencodeSession
|
|
6
|
+
} from '../projects.js';
|
|
7
|
+
import { listOpencodeModels } from '../opencode-cli.js';
|
|
8
|
+
|
|
9
|
+
const router = express.Router();
|
|
10
|
+
|
|
11
|
+
router.get('/models', async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
const { projectPath } = req.query;
|
|
14
|
+
|
|
15
|
+
if (!projectPath) {
|
|
16
|
+
return res.status(400).json({ success: false, error: 'projectPath query parameter required' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const discovery = await listOpencodeModels({ projectPath, cwd: projectPath });
|
|
20
|
+
res.json({ success: true, ...discovery });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Error fetching OpenCode models:', error);
|
|
23
|
+
res.status(500).json({ success: false, error: error.message });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
router.get('/sessions', async (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
const { projectPath } = req.query;
|
|
30
|
+
|
|
31
|
+
if (!projectPath) {
|
|
32
|
+
return res.status(400).json({ success: false, error: 'projectPath query parameter required' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sessions = await getOpencodeSessions(projectPath);
|
|
36
|
+
res.json({ success: true, sessions });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error fetching OpenCode sessions:', error);
|
|
39
|
+
res.status(500).json({ success: false, error: error.message });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
router.get('/sessions/:sessionId/messages', async (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const { sessionId } = req.params;
|
|
46
|
+
const { limit, offset } = req.query;
|
|
47
|
+
|
|
48
|
+
const result = await getOpencodeSessionMessages(
|
|
49
|
+
sessionId,
|
|
50
|
+
limit ? parseInt(limit, 10) : null,
|
|
51
|
+
offset ? parseInt(offset, 10) : 0
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
res.json({ success: true, ...result });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Error fetching OpenCode session messages:', error);
|
|
57
|
+
res.status(500).json({ success: false, error: error.message });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
router.delete('/sessions/:sessionId', async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const { sessionId } = req.params;
|
|
64
|
+
await deleteOpencodeSession(sessionId);
|
|
65
|
+
res.json({ success: true });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`Error deleting OpenCode session ${req.params.sessionId}:`, error);
|
|
68
|
+
res.status(500).json({ success: false, error: error.message });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export default router;
|
package/shared/modelConstants.js
CHANGED
|
@@ -28,26 +28,35 @@ export const CLAUDE_MODELS = {
|
|
|
28
28
|
*/
|
|
29
29
|
export const CURSOR_MODELS = {
|
|
30
30
|
OPTIONS: [
|
|
31
|
-
{ value: 'gpt-5.2-high', label: 'GPT-5.2 High' },
|
|
32
|
-
{ value: 'gemini-3-pro', label: 'Gemini 3 Pro' },
|
|
33
|
-
{ value: 'opus-4.5-thinking', label: 'Claude 4.5 Opus (Thinking)' },
|
|
34
|
-
{ value: 'gpt-5.2', label: 'GPT-5.2' },
|
|
35
|
-
{ value: 'gpt-5.1', label: 'GPT-5.1' },
|
|
36
|
-
{ value: 'gpt-5.1-high', label: 'GPT-5.1 High' },
|
|
37
|
-
{ value: 'composer-1', label: 'Composer 1' },
|
|
38
31
|
{ value: 'auto', label: 'Auto' },
|
|
39
|
-
{ value: 'sonnet-4
|
|
40
|
-
{ value: 'sonnet-4
|
|
32
|
+
{ value: 'sonnet-4', label: 'Claude 4 Sonnet' },
|
|
33
|
+
{ value: 'sonnet-4-1m', label: 'Claude 4 Sonnet 1M' },
|
|
34
|
+
{ value: 'haiku-4.5', label: 'Claude 4.5 Haiku' },
|
|
41
35
|
{ value: 'opus-4.5', label: 'Claude 4.5 Opus' },
|
|
36
|
+
{ value: 'sonnet-4.5', label: 'Claude 4.5 Sonnet' },
|
|
37
|
+
{ value: 'opus-4.6', label: 'Claude 4.6 Opus' },
|
|
38
|
+
{ value: 'opus-4.6-fast', label: 'Claude 4.6 Opus (Fast mode)' },
|
|
39
|
+
{ value: 'composer-1', label: 'Composer 1' },
|
|
40
|
+
{ value: 'composer-1.5', label: 'Composer 1.5' },
|
|
41
|
+
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
|
42
|
+
{ value: 'gemini-3-flash', label: 'Gemini 3 Flash' },
|
|
43
|
+
{ value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' },
|
|
44
|
+
{ value: 'gemini-3-pro', label: 'Gemini 3 Pro' },
|
|
45
|
+
{ value: 'gemini-3-pro-image-preview', label: 'Gemini 3 Pro Image Preview' },
|
|
46
|
+
{ value: 'gpt-5', label: 'GPT-5' },
|
|
47
|
+
{ value: 'gpt-5-fast', label: 'GPT-5 Fast' },
|
|
48
|
+
{ value: 'gpt-5-mini', label: 'GPT-5 Mini' },
|
|
49
|
+
{ value: 'gpt-5-codex', label: 'GPT-5-Codex' },
|
|
42
50
|
{ value: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' },
|
|
43
|
-
{ value: 'gpt-5.1-codex-high', label: 'GPT-5.1 Codex High' },
|
|
44
51
|
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
|
|
45
|
-
{ value: 'gpt-5.1-codex-
|
|
46
|
-
{ value: '
|
|
47
|
-
{ value: '
|
|
52
|
+
{ value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' },
|
|
53
|
+
{ value: 'gpt-5.2', label: 'GPT-5.2' },
|
|
54
|
+
{ value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' },
|
|
55
|
+
{ value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' },
|
|
56
|
+
{ value: 'grok-code', label: 'Grok Code' }
|
|
48
57
|
],
|
|
49
58
|
|
|
50
|
-
DEFAULT: '
|
|
59
|
+
DEFAULT: 'auto'
|
|
51
60
|
};
|
|
52
61
|
|
|
53
62
|
/**
|
|
@@ -55,11 +64,47 @@ export const CURSOR_MODELS = {
|
|
|
55
64
|
*/
|
|
56
65
|
export const CODEX_MODELS = {
|
|
57
66
|
OPTIONS: [
|
|
58
|
-
{ value: 'gpt-5
|
|
59
|
-
{ value: 'gpt-5
|
|
67
|
+
{ value: 'gpt-5', label: 'GPT-5' },
|
|
68
|
+
{ value: 'gpt-5-codex', label: 'GPT-5 Codex' },
|
|
69
|
+
{ value: 'gpt-5-codex-mini', label: 'GPT-5 Codex Mini' },
|
|
70
|
+
{ value: 'gpt-5.1', label: 'GPT-5.1' },
|
|
71
|
+
{ value: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' },
|
|
60
72
|
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
|
|
61
|
-
{ value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' }
|
|
73
|
+
{ value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' },
|
|
74
|
+
{ value: 'gpt-5.2', label: 'GPT-5.2' },
|
|
75
|
+
{ value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' },
|
|
76
|
+
{ value: 'gpt-5.2-high', label: 'GPT-5.2 High' },
|
|
77
|
+
{ value: 'gpt-5.2-low', label: 'GPT-5.2 Low' },
|
|
78
|
+
{ value: 'gpt-5.2-medium', label: 'GPT-5.2 Medium' },
|
|
79
|
+
{ value: 'gpt-5.2-xhigh', label: 'GPT-5.2 XHigh' },
|
|
80
|
+
{ value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' },
|
|
81
|
+
{ value: 'gpt-5.3-codex-high', label: 'GPT-5.3 Codex High' },
|
|
82
|
+
{ value: 'gpt-5.3-codex-low', label: 'GPT-5.3 Codex Low' },
|
|
83
|
+
{ value: 'gpt-5.3-codex-medium', label: 'GPT-5.3 Codex Medium' },
|
|
84
|
+
{ value: 'gpt-5.3-codex-xhigh', label: 'GPT-5.3 Codex XHigh' }
|
|
62
85
|
],
|
|
63
86
|
|
|
64
87
|
DEFAULT: 'gpt-5.2-codex'
|
|
65
88
|
};
|
|
89
|
+
|
|
90
|
+
export const GEMINI_MODELS = {
|
|
91
|
+
OPTIONS: [
|
|
92
|
+
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
|
93
|
+
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
|
94
|
+
{ value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' },
|
|
95
|
+
{ value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' }
|
|
96
|
+
],
|
|
97
|
+
|
|
98
|
+
DEFAULT: 'gemini-3-pro-preview'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* OpenCode (OPENCODE) Models
|
|
103
|
+
*/
|
|
104
|
+
export const OPENCODE_MODELS = {
|
|
105
|
+
OPTIONS: [
|
|
106
|
+
{ value: 'kimi-k2.5', label: 'Kimi K2.5' }
|
|
107
|
+
],
|
|
108
|
+
|
|
109
|
+
DEFAULT: 'kimi-k2.5'
|
|
110
|
+
};
|