@axhub/genie 0.1.6 → 0.1.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.
- package/dist/api-docs.html +351 -909
- package/dist/assets/index-CVjMty4a.js +902 -0
- package/dist/assets/index-eo5scY_Z.css +32 -0
- package/dist/index.html +5 -5
- package/dist/manifest.json +2 -2
- package/package.json +8 -2
- package/server/channels/core/ChannelManager.js +399 -0
- package/server/channels/core/PluginManager.js +59 -0
- package/server/channels/index.js +3 -0
- package/server/channels/plugins/BasePlugin.js +46 -0
- package/server/channels/plugins/dingtalk/DingTalkAdapter.js +156 -0
- package/server/channels/plugins/dingtalk/DingTalkPlugin.js +592 -0
- package/server/channels/plugins/dingtalk/index.js +2 -0
- package/server/channels/plugins/lark/LarkAdapter.js +100 -0
- package/server/channels/plugins/lark/LarkCards.js +43 -0
- package/server/channels/plugins/lark/LarkPlugin.js +260 -0
- package/server/channels/runtime/AgentRuntimeAdapter.js +179 -0
- package/server/channels/runtime/DingTalkStreamWriter.js +105 -0
- package/server/channels/runtime/LarkStreamWriter.js +99 -0
- package/server/channels/store/ChannelStore.js +236 -0
- package/server/database/db.js +109 -1
- package/server/database/init.sql +47 -1
- package/server/gemini-cli.js +280 -0
- package/server/index.js +230 -11
- package/server/openai-codex.js +104 -8
- package/server/opencode-cli.js +673 -0
- package/server/projects.js +645 -5
- package/server/routes/agent.js +40 -12
- package/server/routes/channels.js +221 -0
- package/server/routes/cli-auth.js +317 -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-CtRxrKDm.css +0 -32
- package/dist/assets/index-OENtErNy.js +0 -1249
- package/server/database/auth.db +0 -0
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';
|
|
@@ -85,20 +87,27 @@ import taskmasterRoutes from './routes/taskmaster.js';
|
|
|
85
87
|
import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
86
88
|
import commandsRoutes from './routes/commands.js';
|
|
87
89
|
import settingsRoutes from './routes/settings.js';
|
|
90
|
+
import channelsRoutes from './routes/channels.js';
|
|
88
91
|
import agentRoutes from './routes/agent.js';
|
|
89
92
|
import projectsRoutes, { WORKSPACES_ROOT, validateWorkspacePath } from './routes/projects.js';
|
|
90
93
|
import cliAuthRoutes from './routes/cli-auth.js';
|
|
91
94
|
import userRoutes from './routes/user.js';
|
|
92
95
|
import codexRoutes from './routes/codex.js';
|
|
96
|
+
import opencodeRoutes from './routes/opencode.js';
|
|
93
97
|
import { parseCodexTokenCountInfo } from './utils/codexTokenUsage.js';
|
|
94
98
|
import { initializeDatabase } from './database/db.js';
|
|
95
99
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
96
100
|
import { IS_PLATFORM } from './constants/config.js';
|
|
101
|
+
import { getChannelManager } from './channels/index.js';
|
|
97
102
|
|
|
98
103
|
// File system watcher for projects folder
|
|
99
104
|
let projectsWatcher = null;
|
|
100
105
|
const connectedClients = new Set();
|
|
101
106
|
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
|
107
|
+
const UPDATE_PACKAGE_NAME = process.env.UPDATE_PACKAGE_NAME || packageInfo.id || '@axhub/genie';
|
|
108
|
+
const VERSION_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
109
|
+
let cachedSystemVersion = null;
|
|
110
|
+
let cachedSystemVersionExpiresAt = 0;
|
|
102
111
|
const RUNTIME_STATUS_PATH = (() => {
|
|
103
112
|
if (process.env.AXHUB_GENIE_STATUS_PATH) {
|
|
104
113
|
return process.env.AXHUB_GENIE_STATUS_PATH;
|
|
@@ -107,6 +116,120 @@ const RUNTIME_STATUS_PATH = (() => {
|
|
|
107
116
|
return path.join(os.homedir(), '.axhub-genie', 'runtime-status.json');
|
|
108
117
|
})();
|
|
109
118
|
|
|
119
|
+
function getNpmCommand() {
|
|
120
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function compareSemver(v1, v2) {
|
|
124
|
+
const normalizedV1 = String(v1 || '').replace(/^v/i, '').split('-')[0];
|
|
125
|
+
const normalizedV2 = String(v2 || '').replace(/^v/i, '').split('-')[0];
|
|
126
|
+
const parts1 = normalizedV1.split('.').map((part) => Number(part));
|
|
127
|
+
const parts2 = normalizedV2.split('.').map((part) => Number(part));
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < 3; i += 1) {
|
|
130
|
+
const a = Number.isFinite(parts1[i]) ? parts1[i] : 0;
|
|
131
|
+
const b = Number.isFinite(parts2[i]) ? parts2[i] : 0;
|
|
132
|
+
if (a > b) return 1;
|
|
133
|
+
if (a < b) return -1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readLatestVersionFromNpmRegistry() {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const npmCommand = getNpmCommand();
|
|
142
|
+
const child = spawn(npmCommand, ['view', UPDATE_PACKAGE_NAME, 'version', 'time', '--json'], {
|
|
143
|
+
cwd: path.join(__dirname, '..'),
|
|
144
|
+
env: process.env
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let output = '';
|
|
148
|
+
let errorOutput = '';
|
|
149
|
+
|
|
150
|
+
child.stdout.on('data', (data) => {
|
|
151
|
+
output += data.toString();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
child.stderr.on('data', (data) => {
|
|
155
|
+
errorOutput += data.toString();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
child.on('close', (code) => {
|
|
159
|
+
if (code !== 0) {
|
|
160
|
+
reject(new Error(errorOutput || `npm view exited with code ${code}`));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(output.trim());
|
|
166
|
+
const latestVersion = parsed?.version;
|
|
167
|
+
if (!latestVersion) {
|
|
168
|
+
reject(new Error('npm view returned no version'));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const publishedAt = parsed?.time?.[latestVersion] || parsed?.time?.modified || null;
|
|
173
|
+
resolve({
|
|
174
|
+
latestVersion,
|
|
175
|
+
publishedAt
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
reject(new Error(`failed to parse npm view output: ${error.message}`));
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
child.on('error', (error) => {
|
|
183
|
+
reject(error);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function getSystemVersionInfo(forceRefresh = false) {
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
if (!forceRefresh && cachedSystemVersion && now < cachedSystemVersionExpiresAt) {
|
|
191
|
+
return { ...cachedSystemVersion, cacheHit: true, stale: false };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const npmInfo = await readLatestVersionFromNpmRegistry();
|
|
196
|
+
const currentVersion = packageInfo.version;
|
|
197
|
+
const latestVersion = npmInfo.latestVersion;
|
|
198
|
+
const updateAvailable = compareSemver(latestVersion, currentVersion) > 0;
|
|
199
|
+
const fetchedAt = new Date().toISOString();
|
|
200
|
+
const expiresAt = now + VERSION_CACHE_TTL_MS;
|
|
201
|
+
|
|
202
|
+
cachedSystemVersion = {
|
|
203
|
+
packageName: UPDATE_PACKAGE_NAME,
|
|
204
|
+
currentVersion,
|
|
205
|
+
latestVersion,
|
|
206
|
+
updateAvailable,
|
|
207
|
+
releaseInfo: {
|
|
208
|
+
title: `v${latestVersion}`,
|
|
209
|
+
body: '',
|
|
210
|
+
htmlUrl: `https://www.npmjs.com/package/${UPDATE_PACKAGE_NAME}`,
|
|
211
|
+
publishedAt: npmInfo.publishedAt
|
|
212
|
+
},
|
|
213
|
+
fetchedAt,
|
|
214
|
+
expiresAt: new Date(expiresAt).toISOString()
|
|
215
|
+
};
|
|
216
|
+
cachedSystemVersionExpiresAt = expiresAt;
|
|
217
|
+
|
|
218
|
+
return { ...cachedSystemVersion, cacheHit: false, stale: false };
|
|
219
|
+
} catch (error) {
|
|
220
|
+
// Return stale cache when npm registry is temporarily unavailable.
|
|
221
|
+
if (cachedSystemVersion) {
|
|
222
|
+
return {
|
|
223
|
+
...cachedSystemVersion,
|
|
224
|
+
cacheHit: true,
|
|
225
|
+
stale: true,
|
|
226
|
+
warning: `Using stale cached version data: ${error.message}`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
110
233
|
// Broadcast progress to all connected WebSocket clients
|
|
111
234
|
function broadcastProgress(progress) {
|
|
112
235
|
const message = JSON.stringify({
|
|
@@ -325,6 +448,9 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
|
325
448
|
// Settings API Routes (protected)
|
|
326
449
|
app.use('/api/settings', authenticateToken, settingsRoutes);
|
|
327
450
|
|
|
451
|
+
// Channels API Routes (protected)
|
|
452
|
+
app.use('/api/channels', authenticateToken, channelsRoutes);
|
|
453
|
+
|
|
328
454
|
// CLI Authentication API Routes (protected)
|
|
329
455
|
app.use('/api/cli', authenticateToken, cliAuthRoutes);
|
|
330
456
|
|
|
@@ -333,6 +459,7 @@ app.use('/api/user', authenticateToken, userRoutes);
|
|
|
333
459
|
|
|
334
460
|
// Codex API Routes (protected)
|
|
335
461
|
app.use('/api/codex', authenticateToken, codexRoutes);
|
|
462
|
+
app.use('/api/opencode', authenticateToken, opencodeRoutes);
|
|
336
463
|
|
|
337
464
|
// Agent API Routes (uses API key authentication)
|
|
338
465
|
app.use('/api/agent', agentRoutes);
|
|
@@ -360,6 +487,27 @@ app.use(express.static(path.join(__dirname, '../dist'), {
|
|
|
360
487
|
// /api/config endpoint removed - no longer needed
|
|
361
488
|
// Frontend now uses window.location for WebSocket URLs
|
|
362
489
|
|
|
490
|
+
// System version check endpoint (npm registry with server-side cache)
|
|
491
|
+
app.get('/api/system/version', authenticateToken, async (req, res) => {
|
|
492
|
+
try {
|
|
493
|
+
const forceRefresh = String(req.query.force || '').toLowerCase() === 'true';
|
|
494
|
+
const versionInfo = await getSystemVersionInfo(forceRefresh);
|
|
495
|
+
|
|
496
|
+
res.setHeader('Cache-Control', 'private, max-age=3600, stale-while-revalidate=300');
|
|
497
|
+
res.json({
|
|
498
|
+
success: true,
|
|
499
|
+
...versionInfo,
|
|
500
|
+
cacheTtlMs: VERSION_CACHE_TTL_MS
|
|
501
|
+
});
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error('System version check error:', error);
|
|
504
|
+
res.status(500).json({
|
|
505
|
+
success: false,
|
|
506
|
+
error: error.message
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
363
511
|
// System update endpoint
|
|
364
512
|
app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|
365
513
|
try {
|
|
@@ -368,10 +516,11 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|
|
368
516
|
|
|
369
517
|
console.log('Starting system update from directory:', projectRoot);
|
|
370
518
|
|
|
371
|
-
|
|
372
|
-
const
|
|
519
|
+
const npmCommand = getNpmCommand();
|
|
520
|
+
const updateArgs = ['update', '-g', UPDATE_PACKAGE_NAME];
|
|
373
521
|
|
|
374
|
-
|
|
522
|
+
// Run npm global update command
|
|
523
|
+
const child = spawn(npmCommand, updateArgs, {
|
|
375
524
|
cwd: projectRoot,
|
|
376
525
|
env: process.env
|
|
377
526
|
});
|
|
@@ -393,17 +542,21 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|
|
393
542
|
|
|
394
543
|
child.on('close', (code) => {
|
|
395
544
|
if (code === 0) {
|
|
545
|
+
cachedSystemVersion = null;
|
|
546
|
+
cachedSystemVersionExpiresAt = 0;
|
|
396
547
|
res.json({
|
|
397
548
|
success: true,
|
|
398
549
|
output: output || 'Update completed successfully',
|
|
399
|
-
message: 'Update completed. Please restart the server to apply changes.'
|
|
550
|
+
message: 'Update completed. Please restart the server to apply changes.',
|
|
551
|
+
command: `${npmCommand} ${updateArgs.join(' ')}`
|
|
400
552
|
});
|
|
401
553
|
} else {
|
|
402
554
|
res.status(500).json({
|
|
403
555
|
success: false,
|
|
404
556
|
error: 'Update command failed',
|
|
405
557
|
output: output,
|
|
406
|
-
errorOutput: errorOutput
|
|
558
|
+
errorOutput: errorOutput,
|
|
559
|
+
command: `${npmCommand} ${updateArgs.join(' ')}`
|
|
407
560
|
});
|
|
408
561
|
}
|
|
409
562
|
});
|
|
@@ -469,6 +622,20 @@ app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateT
|
|
|
469
622
|
}
|
|
470
623
|
});
|
|
471
624
|
|
|
625
|
+
app.get('/api/gemini/sessions/:sessionId/messages', authenticateToken, async (req, res) => {
|
|
626
|
+
try {
|
|
627
|
+
const { sessionId } = req.params;
|
|
628
|
+
const { limit, offset } = req.query;
|
|
629
|
+
|
|
630
|
+
const parsedLimit = limit ? parseInt(limit, 10) : null;
|
|
631
|
+
const parsedOffset = offset ? parseInt(offset, 10) : 0;
|
|
632
|
+
const result = await getGeminiSessionMessages(sessionId, parsedLimit, parsedOffset);
|
|
633
|
+
res.json(result);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
res.status(500).json({ error: error.message });
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
472
639
|
// Rename project endpoint
|
|
473
640
|
app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
|
|
474
641
|
try {
|
|
@@ -910,6 +1077,18 @@ function handleChatConnection(ws) {
|
|
|
910
1077
|
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
911
1078
|
console.log('🤖 Model:', data.options?.model || 'default');
|
|
912
1079
|
await queryCodex(data.command, data.options, writer);
|
|
1080
|
+
} else if (data.type === 'gemini-command') {
|
|
1081
|
+
console.log('[DEBUG] Gemini message:', data.command || '[Continue/Resume]');
|
|
1082
|
+
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
|
1083
|
+
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1084
|
+
console.log('🤖 Model:', data.options?.model || 'default');
|
|
1085
|
+
await queryGemini(data.command, data.options, writer);
|
|
1086
|
+
} else if (data.type === 'opencode-command') {
|
|
1087
|
+
console.log('[DEBUG] OpenCode message:', data.command || '[Continue/Resume]');
|
|
1088
|
+
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
|
1089
|
+
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1090
|
+
console.log('🤖 Model:', data.options?.model || 'default');
|
|
1091
|
+
await queryOpencode(data.command, data.options, writer);
|
|
913
1092
|
} else if (data.type === 'cursor-resume') {
|
|
914
1093
|
// Backward compatibility: treat as cursor-command with resume and no prompt
|
|
915
1094
|
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
|
|
@@ -927,6 +1106,10 @@ function handleChatConnection(ws) {
|
|
|
927
1106
|
success = abortCursorSession(data.sessionId);
|
|
928
1107
|
} else if (provider === 'codex') {
|
|
929
1108
|
success = abortCodexSession(data.sessionId);
|
|
1109
|
+
} else if (provider === 'gemini') {
|
|
1110
|
+
success = abortGeminiSession(data.sessionId);
|
|
1111
|
+
} else if (provider === 'opencode') {
|
|
1112
|
+
success = abortOpencodeSession(data.sessionId);
|
|
930
1113
|
} else {
|
|
931
1114
|
// Use Claude Agents SDK
|
|
932
1115
|
success = await abortClaudeSDKSession(data.sessionId);
|
|
@@ -969,6 +1152,10 @@ function handleChatConnection(ws) {
|
|
|
969
1152
|
isActive = isCursorSessionActive(sessionId);
|
|
970
1153
|
} else if (provider === 'codex') {
|
|
971
1154
|
isActive = isCodexSessionActive(sessionId);
|
|
1155
|
+
} else if (provider === 'gemini') {
|
|
1156
|
+
isActive = isGeminiSessionActive(sessionId);
|
|
1157
|
+
} else if (provider === 'opencode') {
|
|
1158
|
+
isActive = isOpencodeSessionActive(sessionId);
|
|
972
1159
|
} else {
|
|
973
1160
|
// Use Claude Agents SDK
|
|
974
1161
|
isActive = isClaudeSDKSessionActive(sessionId);
|
|
@@ -985,7 +1172,9 @@ function handleChatConnection(ws) {
|
|
|
985
1172
|
const activeSessions = {
|
|
986
1173
|
claude: getActiveClaudeSDKSessions(),
|
|
987
1174
|
cursor: getActiveCursorSessions(),
|
|
988
|
-
codex: getActiveCodexSessions()
|
|
1175
|
+
codex: getActiveCodexSessions(),
|
|
1176
|
+
gemini: getActiveGeminiSessions(),
|
|
1177
|
+
opencode: getActiveOpencodeSessions()
|
|
989
1178
|
};
|
|
990
1179
|
writer.send({
|
|
991
1180
|
type: 'active-sessions',
|
|
@@ -1091,7 +1280,7 @@ function handleShellConnection(ws) {
|
|
|
1091
1280
|
if (isPlainShell) {
|
|
1092
1281
|
welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
|
|
1093
1282
|
} else {
|
|
1094
|
-
const providerName = provider === 'cursor' ? 'Cursor' : 'Claude';
|
|
1283
|
+
const providerName = provider === 'cursor' ? 'Cursor' : provider === 'gemini' ? 'Gemini' : 'Claude';
|
|
1095
1284
|
welcomeMsg = hasSession ?
|
|
1096
1285
|
`\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
|
|
1097
1286
|
`\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
|
|
@@ -1127,6 +1316,20 @@ function handleShellConnection(ws) {
|
|
|
1127
1316
|
shellCommand = `cd "${projectPath}" && cursor-agent`;
|
|
1128
1317
|
}
|
|
1129
1318
|
}
|
|
1319
|
+
} else if (provider === 'gemini') {
|
|
1320
|
+
if (os.platform() === 'win32') {
|
|
1321
|
+
if (hasSession && sessionId) {
|
|
1322
|
+
shellCommand = `Set-Location -Path "${projectPath}"; npx -y @google/gemini-cli --resume ${sessionId}`;
|
|
1323
|
+
} else {
|
|
1324
|
+
shellCommand = `Set-Location -Path "${projectPath}"; npx -y @google/gemini-cli`;
|
|
1325
|
+
}
|
|
1326
|
+
} else {
|
|
1327
|
+
if (hasSession && sessionId) {
|
|
1328
|
+
shellCommand = `cd "${projectPath}" && npx -y @google/gemini-cli --resume ${sessionId}`;
|
|
1329
|
+
} else {
|
|
1330
|
+
shellCommand = `cd "${projectPath}" && npx -y @google/gemini-cli`;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1130
1333
|
} else {
|
|
1131
1334
|
// Use claude command (default) or initialCommand if provided
|
|
1132
1335
|
const command = initialCommand || 'claude';
|
|
@@ -1888,16 +2091,31 @@ function clearRuntimeStatusFile() {
|
|
|
1888
2091
|
}
|
|
1889
2092
|
|
|
1890
2093
|
function registerShutdownHandlers() {
|
|
1891
|
-
process.on('exit', () => {
|
|
2094
|
+
process.on('exit', async () => {
|
|
2095
|
+
try {
|
|
2096
|
+
await getChannelManager().shutdown();
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
console.warn('[WARN] Failed to shutdown channel manager on exit:', error.message);
|
|
2099
|
+
}
|
|
1892
2100
|
clearRuntimeStatusFile();
|
|
1893
2101
|
});
|
|
1894
2102
|
|
|
1895
|
-
process.on('SIGINT', () => {
|
|
2103
|
+
process.on('SIGINT', async () => {
|
|
2104
|
+
try {
|
|
2105
|
+
await getChannelManager().shutdown();
|
|
2106
|
+
} catch (error) {
|
|
2107
|
+
console.warn('[WARN] Failed to shutdown channel manager on SIGINT:', error.message);
|
|
2108
|
+
}
|
|
1896
2109
|
clearRuntimeStatusFile();
|
|
1897
2110
|
process.exit(0);
|
|
1898
2111
|
});
|
|
1899
2112
|
|
|
1900
|
-
process.on('SIGTERM', () => {
|
|
2113
|
+
process.on('SIGTERM', async () => {
|
|
2114
|
+
try {
|
|
2115
|
+
await getChannelManager().shutdown();
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
console.warn('[WARN] Failed to shutdown channel manager on SIGTERM:', error.message);
|
|
2118
|
+
}
|
|
1901
2119
|
clearRuntimeStatusFile();
|
|
1902
2120
|
process.exit(0);
|
|
1903
2121
|
});
|
|
@@ -1908,6 +2126,7 @@ async function startServer() {
|
|
|
1908
2126
|
try {
|
|
1909
2127
|
// Initialize authentication database
|
|
1910
2128
|
await initializeDatabase();
|
|
2129
|
+
await getChannelManager().initialize();
|
|
1911
2130
|
|
|
1912
2131
|
// Check if running in production mode (dist folder exists)
|
|
1913
2132
|
const distIndexPath = path.join(__dirname, '../dist/index.html');
|
package/server/openai-codex.js
CHANGED
|
@@ -140,7 +140,7 @@ function transformCodexEvent(event) {
|
|
|
140
140
|
case 'thread.started':
|
|
141
141
|
return {
|
|
142
142
|
type: 'thread_started',
|
|
143
|
-
threadId: event.id
|
|
143
|
+
threadId: event.thread_id || event.id
|
|
144
144
|
};
|
|
145
145
|
|
|
146
146
|
case 'error':
|
|
@@ -157,6 +157,42 @@ function transformCodexEvent(event) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Extract thread id from thread.started events (SDK uses thread_id)
|
|
162
|
+
* @param {object} event
|
|
163
|
+
* @returns {string|null}
|
|
164
|
+
*/
|
|
165
|
+
function getThreadIdFromEvent(event) {
|
|
166
|
+
const threadId = event?.thread_id || event?.id;
|
|
167
|
+
return typeof threadId === 'string' && threadId.trim() ? threadId.trim() : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Extract text-bearing item info from Codex item events.
|
|
172
|
+
* Only agent_message and reasoning currently stream textual deltas.
|
|
173
|
+
* @param {object} item
|
|
174
|
+
* @returns {{itemId: string, itemType: string, text: string, isReasoning: boolean}|null}
|
|
175
|
+
*/
|
|
176
|
+
function getTextItemInfo(item) {
|
|
177
|
+
if (!item || (item.type !== 'agent_message' && item.type !== 'reasoning')) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const itemId = typeof item.id === 'string' && item.id.trim()
|
|
182
|
+
? item.id.trim()
|
|
183
|
+
: null;
|
|
184
|
+
if (!itemId) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
itemId,
|
|
190
|
+
itemType: item.type,
|
|
191
|
+
text: typeof item.text === 'string' ? item.text : '',
|
|
192
|
+
isReasoning: item.type === 'reasoning'
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
160
196
|
/**
|
|
161
197
|
* Map permission mode to Codex SDK options
|
|
162
198
|
* @param {string} permissionMode - 'default', 'acceptEdits', or 'bypassPermissions'
|
|
@@ -178,7 +214,8 @@ function mapPermissionModeToCodexOptions(permissionMode) {
|
|
|
178
214
|
default:
|
|
179
215
|
return {
|
|
180
216
|
sandboxMode: 'workspace-write',
|
|
181
|
-
|
|
217
|
+
// Keep default mode writable and non-blocking for coding workflows.
|
|
218
|
+
approvalPolicy: 'never'
|
|
182
219
|
};
|
|
183
220
|
}
|
|
184
221
|
}
|
|
@@ -195,7 +232,8 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
195
232
|
cwd,
|
|
196
233
|
projectPath,
|
|
197
234
|
model,
|
|
198
|
-
permissionMode = 'default'
|
|
235
|
+
permissionMode = 'default',
|
|
236
|
+
modelReasoningEffort
|
|
199
237
|
} = options;
|
|
200
238
|
|
|
201
239
|
const workingDirectory = cwd || projectPath || process.cwd();
|
|
@@ -205,6 +243,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
205
243
|
let thread;
|
|
206
244
|
let currentSessionId = sessionId;
|
|
207
245
|
let resolvedSessionIdFromEvent = null;
|
|
246
|
+
const streamedTextByItemId = new Map();
|
|
208
247
|
|
|
209
248
|
const syncResolvedSessionId = (broadcastUpdate = false) => {
|
|
210
249
|
const resolvedThreadId = typeof thread?.id === 'string' && thread.id.trim()
|
|
@@ -252,7 +291,8 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
252
291
|
skipGitRepoCheck: true,
|
|
253
292
|
sandboxMode,
|
|
254
293
|
approvalPolicy,
|
|
255
|
-
model
|
|
294
|
+
model,
|
|
295
|
+
...(modelReasoningEffort ? { modelReasoningEffort } : {})
|
|
256
296
|
};
|
|
257
297
|
|
|
258
298
|
// Start or resume thread
|
|
@@ -289,8 +329,11 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
289
329
|
syncResolvedSessionId(true);
|
|
290
330
|
|
|
291
331
|
for await (const event of streamedTurn.events) {
|
|
292
|
-
if (event?.type === 'thread.started'
|
|
293
|
-
|
|
332
|
+
if (event?.type === 'thread.started') {
|
|
333
|
+
const eventThreadId = getThreadIdFromEvent(event);
|
|
334
|
+
if (eventThreadId) {
|
|
335
|
+
resolvedSessionIdFromEvent = eventThreadId;
|
|
336
|
+
}
|
|
294
337
|
}
|
|
295
338
|
|
|
296
339
|
syncResolvedSessionId(true);
|
|
@@ -301,8 +344,61 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
301
344
|
break;
|
|
302
345
|
}
|
|
303
346
|
|
|
304
|
-
|
|
305
|
-
|
|
347
|
+
const isItemEvent = event.type === 'item.started' || event.type === 'item.updated' || event.type === 'item.completed';
|
|
348
|
+
if (isItemEvent) {
|
|
349
|
+
const textItemInfo = getTextItemInfo(event.item);
|
|
350
|
+
|
|
351
|
+
if (textItemInfo) {
|
|
352
|
+
const previousText = streamedTextByItemId.get(textItemInfo.itemId) || '';
|
|
353
|
+
const nextText = textItemInfo.text || '';
|
|
354
|
+
|
|
355
|
+
let delta = '';
|
|
356
|
+
if (nextText.startsWith(previousText)) {
|
|
357
|
+
delta = nextText.slice(previousText.length);
|
|
358
|
+
} else if (nextText !== previousText) {
|
|
359
|
+
// Fallback when text is rewritten rather than appended
|
|
360
|
+
delta = nextText;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
streamedTextByItemId.set(textItemInfo.itemId, nextText);
|
|
364
|
+
|
|
365
|
+
if (delta) {
|
|
366
|
+
sendMessage(ws, {
|
|
367
|
+
type: 'codex-response',
|
|
368
|
+
data: {
|
|
369
|
+
type: 'item_delta',
|
|
370
|
+
itemType: textItemInfo.itemType,
|
|
371
|
+
itemId: textItemInfo.itemId,
|
|
372
|
+
isReasoning: textItemInfo.isReasoning,
|
|
373
|
+
delta
|
|
374
|
+
},
|
|
375
|
+
sessionId: currentSessionId
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (event.type === 'item.completed') {
|
|
380
|
+
sendMessage(ws, {
|
|
381
|
+
type: 'codex-response',
|
|
382
|
+
data: {
|
|
383
|
+
type: 'item_done',
|
|
384
|
+
itemType: textItemInfo.itemType,
|
|
385
|
+
itemId: textItemInfo.itemId,
|
|
386
|
+
isReasoning: textItemInfo.isReasoning,
|
|
387
|
+
content: nextText
|
|
388
|
+
},
|
|
389
|
+
sessionId: currentSessionId
|
|
390
|
+
});
|
|
391
|
+
streamedTextByItemId.delete(textItemInfo.itemId);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Non-text items can be noisy during started/updated phases.
|
|
398
|
+
// Keep existing behavior: emit them when completed.
|
|
399
|
+
if (event.type !== 'item.completed') {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
306
402
|
}
|
|
307
403
|
|
|
308
404
|
const transformed = transformCodexEvent(event);
|