@axhub/genie 0.1.2 → 0.1.3
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/assets/index-BYKlB9hp.css +32 -0
- package/dist/assets/index-YzZ559FA.js +1249 -0
- package/dist/icons/opencode-white.svg +4 -0
- package/dist/icons/opencode.svg +10 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/cli.js +93 -13
- package/server/index.js +18 -2
- package/server/opencode-manager.js +605 -0
- package/server/opencode-sdk.js +474 -0
- package/server/projects.js +72 -3
- package/server/routes/agent.js +54 -9
- package/server/routes/opencode.js +99 -0
- package/shared/modelConstants.js +14 -0
- package/dist/assets/index-CtRxrKDm.css +0 -32
- package/dist/assets/index-DfLTVRPO.js +0 -1249
package/server/routes/agent.js
CHANGED
|
@@ -9,17 +9,52 @@ 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 { queryOpencode } from '../opencode-sdk.js';
|
|
12
13
|
import { Octokit } from '@octokit/rest';
|
|
13
|
-
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
|
|
14
|
+
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, OPENCODE_MODELS } from '../../shared/modelConstants.js';
|
|
14
15
|
import { IS_PLATFORM } from '../constants/config.js';
|
|
15
16
|
|
|
16
17
|
const router = express.Router();
|
|
17
18
|
|
|
18
|
-
function
|
|
19
|
+
function getForwardedHeaderValue(value) {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value[0]?.split(',')[0]?.trim() || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
return value.split(',')[0].trim() || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getRequestOrigin(req) {
|
|
32
|
+
if (!req) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const forwardedProto = getForwardedHeaderValue(req.headers['x-forwarded-proto']);
|
|
37
|
+
const forwardedHost = getForwardedHeaderValue(req.headers['x-forwarded-host']);
|
|
38
|
+
if (forwardedProto && forwardedHost) {
|
|
39
|
+
return `${forwardedProto}://${forwardedHost}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const host = req.get('host');
|
|
43
|
+
if (host) {
|
|
44
|
+
return `${req.protocol || 'http'}://${host}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildSessionNavigation(sessionId, req) {
|
|
19
51
|
const normalizedSessionId = typeof sessionId === 'string' && sessionId.trim() ? sessionId.trim() : null;
|
|
20
52
|
const sessionPath = normalizedSessionId ? `/session/${normalizedSessionId}` : null;
|
|
53
|
+
const configuredFrontendUrl = (process.env.FRONTEND_URL || '').trim().replace(/\/+$/, '');
|
|
54
|
+
const requestOrigin = getRequestOrigin(req);
|
|
21
55
|
const frontendPort = process.env.VITE_PORT || '5173';
|
|
22
|
-
const
|
|
56
|
+
const frontendBaseUrl = configuredFrontendUrl || requestOrigin || `http://localhost:${frontendPort}`;
|
|
57
|
+
const sessionUrl = normalizedSessionId ? `${frontendBaseUrl}${sessionPath}` : null;
|
|
23
58
|
|
|
24
59
|
return {
|
|
25
60
|
sessionPath,
|
|
@@ -661,7 +696,7 @@ class ResponseCollector {
|
|
|
661
696
|
* @param {string} sessionId - (Optional) Existing session ID to resume.
|
|
662
697
|
* If provided, the request continues that session instead of creating a new one.
|
|
663
698
|
*
|
|
664
|
-
* @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor'
|
|
699
|
+
* @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'opencode'
|
|
665
700
|
* Default: 'claude'
|
|
666
701
|
*
|
|
667
702
|
* @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates.
|
|
@@ -779,7 +814,7 @@ class ResponseCollector {
|
|
|
779
814
|
* Input Validations (400 Bad Request):
|
|
780
815
|
* - Either githubUrl OR projectPath must be provided (not neither)
|
|
781
816
|
* - message must be non-empty string
|
|
782
|
-
* - provider must be 'claude' or '
|
|
817
|
+
* - provider must be 'claude', 'cursor', 'codex', or 'opencode'
|
|
783
818
|
* - createBranch/createPR requires githubUrl OR projectPath (not neither)
|
|
784
819
|
* - branchName must pass Git naming rules (if provided)
|
|
785
820
|
*
|
|
@@ -883,8 +918,8 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
883
918
|
const isOpenOnly = requestedOpenOnly || (!normalizedMessage && !!normalizedSessionId);
|
|
884
919
|
|
|
885
920
|
// Validate inputs
|
|
886
|
-
if (!['claude', 'cursor', 'codex'].includes(provider)) {
|
|
887
|
-
return res.status(400).json({ error: 'provider must be "claude", "cursor", or "
|
|
921
|
+
if (!['claude', 'cursor', 'codex', 'opencode'].includes(provider)) {
|
|
922
|
+
return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", or "opencode"' });
|
|
888
923
|
}
|
|
889
924
|
|
|
890
925
|
if (sessionId !== undefined && (typeof sessionId !== 'string' || !sessionId.trim())) {
|
|
@@ -922,7 +957,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
922
957
|
}
|
|
923
958
|
|
|
924
959
|
if (isOpenOnly) {
|
|
925
|
-
const { sessionPath, sessionUrl } = buildSessionNavigation(normalizedSessionId);
|
|
960
|
+
const { sessionPath, sessionUrl } = buildSessionNavigation(normalizedSessionId, req);
|
|
926
961
|
const response = {
|
|
927
962
|
success: true,
|
|
928
963
|
openOnly: true,
|
|
@@ -1062,6 +1097,16 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
1062
1097
|
model: model || CODEX_MODELS.DEFAULT,
|
|
1063
1098
|
permissionMode: 'bypassPermissions'
|
|
1064
1099
|
}, writer);
|
|
1100
|
+
} else if (provider === 'opencode') {
|
|
1101
|
+
console.log('🧠 Starting OpenCode session');
|
|
1102
|
+
|
|
1103
|
+
await queryOpencode(normalizedMessage, {
|
|
1104
|
+
projectPath: finalProjectPath,
|
|
1105
|
+
cwd: finalProjectPath,
|
|
1106
|
+
sessionId: normalizedSessionId,
|
|
1107
|
+
model: model || OPENCODE_MODELS.DEFAULT,
|
|
1108
|
+
permissionMode: 'bypassPermissions'
|
|
1109
|
+
}, writer);
|
|
1065
1110
|
}
|
|
1066
1111
|
|
|
1067
1112
|
// Handle GitHub branch and PR creation after successful agent completion
|
|
@@ -1254,7 +1299,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
1254
1299
|
const assistantMessages = writer.getAssistantMessages();
|
|
1255
1300
|
const tokenSummary = writer.getTotalTokens();
|
|
1256
1301
|
const resolvedSessionId = writer.getSessionId() || normalizedSessionId;
|
|
1257
|
-
const { sessionPath, sessionUrl } = buildSessionNavigation(resolvedSessionId);
|
|
1302
|
+
const { sessionPath, sessionUrl } = buildSessionNavigation(resolvedSessionId, req);
|
|
1258
1303
|
|
|
1259
1304
|
const response = {
|
|
1260
1305
|
success: true,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import {
|
|
3
|
+
listOpencodeSessions,
|
|
4
|
+
getOpencodeSessionMessages,
|
|
5
|
+
deleteOpencodeSession,
|
|
6
|
+
listOpencodeModels,
|
|
7
|
+
getOpencodeStatus
|
|
8
|
+
} from '../opencode-manager.js';
|
|
9
|
+
|
|
10
|
+
const router = express.Router();
|
|
11
|
+
|
|
12
|
+
router.get('/sessions', async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const { projectPath } = req.query;
|
|
15
|
+
|
|
16
|
+
if (!projectPath || typeof projectPath !== 'string') {
|
|
17
|
+
return res.status(400).json({
|
|
18
|
+
success: false,
|
|
19
|
+
error: 'projectPath query parameter required'
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sessions = await listOpencodeSessions(projectPath);
|
|
24
|
+
res.json({ success: true, sessions });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Error fetching OpenCode sessions:', error);
|
|
27
|
+
res.status(500).json({ success: false, error: error.message });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
router.get('/sessions/:sessionId/messages', async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const { sessionId } = req.params;
|
|
34
|
+
const { limit, offset, projectPath } = req.query;
|
|
35
|
+
|
|
36
|
+
const result = await getOpencodeSessionMessages(sessionId, {
|
|
37
|
+
directory: typeof projectPath === 'string' ? projectPath : null,
|
|
38
|
+
limit: limit ? parseInt(limit, 10) : null,
|
|
39
|
+
offset: offset ? parseInt(offset, 10) : 0
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
res.json({ success: true, ...result });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error fetching OpenCode session messages:', error);
|
|
45
|
+
res.status(500).json({ success: false, error: error.message });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
router.delete('/sessions/:sessionId', async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const { sessionId } = req.params;
|
|
52
|
+
const { projectPath } = req.query;
|
|
53
|
+
|
|
54
|
+
const deleted = await deleteOpencodeSession(sessionId, {
|
|
55
|
+
directory: typeof projectPath === 'string' ? projectPath : null
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
res.json({ success: deleted });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(`Error deleting OpenCode session ${req.params.sessionId}:`, error);
|
|
61
|
+
res.status(500).json({ success: false, error: error.message });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
router.get('/models', async (req, res) => {
|
|
66
|
+
try {
|
|
67
|
+
const { projectPath } = req.query;
|
|
68
|
+
|
|
69
|
+
const models = await listOpencodeModels({
|
|
70
|
+
directory: typeof projectPath === 'string' ? projectPath : null
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
res.json({
|
|
74
|
+
success: true,
|
|
75
|
+
models: models.options,
|
|
76
|
+
defaultModel: models.defaultModel
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Error loading OpenCode models:', error);
|
|
80
|
+
res.status(500).json({ success: false, error: error.message });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
router.get('/status', async (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const status = await getOpencodeStatus();
|
|
87
|
+
res.json(status);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Error checking OpenCode status:', error);
|
|
90
|
+
res.status(500).json({
|
|
91
|
+
authenticated: false,
|
|
92
|
+
email: null,
|
|
93
|
+
error: error.message
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export default router;
|
|
99
|
+
|
package/shared/modelConstants.js
CHANGED
|
@@ -63,3 +63,17 @@ export const CODEX_MODELS = {
|
|
|
63
63
|
|
|
64
64
|
DEFAULT: 'gpt-5.2-codex'
|
|
65
65
|
};
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* OpenCode Models
|
|
70
|
+
*/
|
|
71
|
+
export const OPENCODE_MODELS = {
|
|
72
|
+
OPTIONS: [
|
|
73
|
+
{ value: 'opencode/gpt-5-nano', label: 'GPT-5 Nano (OpenCode)' },
|
|
74
|
+
{ value: 'openai/gpt-5.2', label: 'GPT-5.2 (OpenAI)' },
|
|
75
|
+
{ value: 'anthropic/claude-sonnet-4.5', label: 'Claude Sonnet 4.5 (Anthropic)' }
|
|
76
|
+
],
|
|
77
|
+
|
|
78
|
+
DEFAULT: 'opencode/gpt-5-nano'
|
|
79
|
+
};
|