@ian2018cs/agenthub 0.1.16 → 0.1.17
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/index.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Prevent zoom on iOS -->
|
|
27
27
|
<meta name="format-detection" content="telephone=no" />
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-DzJ3SGxJ.js"></script>
|
|
29
29
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BeVl62c0.js">
|
|
30
30
|
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-C_VWDoZS.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vendor-utils-00TdZexr.js">
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -54,9 +54,9 @@ import adminRoutes from './routes/admin.js';
|
|
|
54
54
|
import usageRoutes from './routes/usage.js';
|
|
55
55
|
import skillsRoutes from './routes/skills.js';
|
|
56
56
|
import settingsRoutes from './routes/settings.js';
|
|
57
|
-
import { initializeDatabase } from './database/db.js';
|
|
57
|
+
import { initializeDatabase, userDb } from './database/db.js';
|
|
58
58
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
59
|
-
import { getUserPaths } from './services/user-directories.js';
|
|
59
|
+
import { getUserPaths, initCodexDirectories } from './services/user-directories.js';
|
|
60
60
|
import { startUsageScanner } from './services/usage-scanner.js';
|
|
61
61
|
|
|
62
62
|
// File system watcher for projects folder - per user
|
|
@@ -183,6 +183,7 @@ const app = express();
|
|
|
183
183
|
const server = http.createServer(app);
|
|
184
184
|
|
|
185
185
|
const ptySessionsMap = new Map();
|
|
186
|
+
const codexPtySessionsMap = new Map();
|
|
186
187
|
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
187
188
|
|
|
188
189
|
// Single WebSocket server that handles both paths
|
|
@@ -599,6 +600,8 @@ wss.on('connection', (ws, request) => {
|
|
|
599
600
|
|
|
600
601
|
if (pathname === '/shell') {
|
|
601
602
|
handleShellConnection(ws, userData);
|
|
603
|
+
} else if (pathname === '/codex') {
|
|
604
|
+
handleCodexConnection(ws, userData);
|
|
602
605
|
} else if (pathname === '/ws') {
|
|
603
606
|
handleChatConnection(ws, userData);
|
|
604
607
|
} else {
|
|
@@ -1047,7 +1050,225 @@ function handleShellConnection(ws, userData) {
|
|
|
1047
1050
|
console.error('[ERROR] Shell WebSocket error:', error);
|
|
1048
1051
|
});
|
|
1049
1052
|
}
|
|
1050
|
-
|
|
1053
|
+
|
|
1054
|
+
function handleCodexConnection(ws, userData) {
|
|
1055
|
+
console.log('🤖 Codex client connected');
|
|
1056
|
+
const userUuid = userData?.uuid || null;
|
|
1057
|
+
let codexProcess = null;
|
|
1058
|
+
let ptySessionKey = null;
|
|
1059
|
+
let outputBuffer = [];
|
|
1060
|
+
|
|
1061
|
+
ws.on('message', async (message) => {
|
|
1062
|
+
try {
|
|
1063
|
+
const data = JSON.parse(message);
|
|
1064
|
+
console.log('📨 Codex message received:', data.type);
|
|
1065
|
+
|
|
1066
|
+
if (data.type === 'init') {
|
|
1067
|
+
const projectPath = data.projectPath || process.cwd();
|
|
1068
|
+
|
|
1069
|
+
ptySessionKey = `codex_${projectPath}_${userUuid || 'default'}`;
|
|
1070
|
+
|
|
1071
|
+
const existingSession = codexPtySessionsMap.get(ptySessionKey);
|
|
1072
|
+
if (existingSession) {
|
|
1073
|
+
console.log('♻️ Reconnecting to existing Codex PTY session:', ptySessionKey);
|
|
1074
|
+
codexProcess = existingSession.pty;
|
|
1075
|
+
clearTimeout(existingSession.timeoutId);
|
|
1076
|
+
|
|
1077
|
+
ws.send(JSON.stringify({
|
|
1078
|
+
type: 'output',
|
|
1079
|
+
data: `\x1b[36m[Reconnected to existing Codex session]\x1b[0m\r\n`
|
|
1080
|
+
}));
|
|
1081
|
+
|
|
1082
|
+
if (existingSession.buffer && existingSession.buffer.length > 0) {
|
|
1083
|
+
console.log(`📜 Sending ${existingSession.buffer.length} buffered Codex messages`);
|
|
1084
|
+
existingSession.buffer.forEach(bufferedData => {
|
|
1085
|
+
ws.send(JSON.stringify({ type: 'output', data: bufferedData }));
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
existingSession.ws = ws;
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
console.log('[INFO] Starting Codex in:', projectPath);
|
|
1094
|
+
|
|
1095
|
+
ws.send(JSON.stringify({
|
|
1096
|
+
type: 'output',
|
|
1097
|
+
data: `\x1b[36mStarting Codex in: ${projectPath}\x1b[0m\r\n`
|
|
1098
|
+
}));
|
|
1099
|
+
|
|
1100
|
+
try {
|
|
1101
|
+
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
1102
|
+
const shellCommand = os.platform() === 'win32'
|
|
1103
|
+
? `Set-Location -Path "${projectPath}"; codex`
|
|
1104
|
+
: `cd "${projectPath}" && codex`;
|
|
1105
|
+
const shellArgs = os.platform() === 'win32'
|
|
1106
|
+
? ['-Command', shellCommand]
|
|
1107
|
+
: ['-c', shellCommand];
|
|
1108
|
+
|
|
1109
|
+
const termCols = data.cols || 80;
|
|
1110
|
+
const termRows = data.rows || 24;
|
|
1111
|
+
console.log('📐 Codex terminal dimensions:', termCols, 'x', termRows);
|
|
1112
|
+
|
|
1113
|
+
// Build environment with per-user isolated HOME for codex config isolation
|
|
1114
|
+
const codexEnv = {
|
|
1115
|
+
...process.env,
|
|
1116
|
+
TERM: 'xterm-256color',
|
|
1117
|
+
COLORTERM: 'truecolor',
|
|
1118
|
+
FORCE_COLOR: '3',
|
|
1119
|
+
BROWSER: 'echo "OPEN_URL:"'
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
if (userUuid) {
|
|
1123
|
+
const userPaths = getUserPaths(userUuid);
|
|
1124
|
+
// Set HOME to per-user fake home so codex reads <codexHomeDir>/.codex/
|
|
1125
|
+
// This completely isolates from the host user's ~/.codex
|
|
1126
|
+
codexEnv.HOME = userPaths.codexHomeDir;
|
|
1127
|
+
console.log('🔐 Set HOME for Codex user isolation:', userPaths.codexHomeDir);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Inject API credentials from server environment variables
|
|
1131
|
+
if (process.env.OPENAI_API_KEY) {
|
|
1132
|
+
codexEnv.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
1133
|
+
}
|
|
1134
|
+
if (process.env.OPENAI_BASE_URL) {
|
|
1135
|
+
codexEnv.OPENAI_BASE_URL = process.env.OPENAI_BASE_URL;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
codexProcess = pty.spawn(shell, shellArgs, {
|
|
1139
|
+
name: 'xterm-256color',
|
|
1140
|
+
cols: termCols,
|
|
1141
|
+
rows: termRows,
|
|
1142
|
+
cwd: os.homedir(),
|
|
1143
|
+
env: codexEnv
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
console.log('🟢 Codex process started with PTY, PID:', codexProcess.pid);
|
|
1147
|
+
|
|
1148
|
+
codexPtySessionsMap.set(ptySessionKey, {
|
|
1149
|
+
pty: codexProcess,
|
|
1150
|
+
ws: ws,
|
|
1151
|
+
buffer: [],
|
|
1152
|
+
timeoutId: null,
|
|
1153
|
+
projectPath
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
codexProcess.onData((rawData) => {
|
|
1157
|
+
const session = codexPtySessionsMap.get(ptySessionKey);
|
|
1158
|
+
if (!session) return;
|
|
1159
|
+
|
|
1160
|
+
if (session.buffer.length < 5000) {
|
|
1161
|
+
session.buffer.push(rawData);
|
|
1162
|
+
} else {
|
|
1163
|
+
session.buffer.shift();
|
|
1164
|
+
session.buffer.push(rawData);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1168
|
+
let outputData = rawData;
|
|
1169
|
+
|
|
1170
|
+
// URL detection
|
|
1171
|
+
const patterns = [
|
|
1172
|
+
/(?:xdg-open|open|start)\s+(https?:\/\/[^\s\x1b\x07]+)/g,
|
|
1173
|
+
/OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
|
|
1174
|
+
/Opening\s+(https?:\/\/[^\s\x1b\x07]+)/gi,
|
|
1175
|
+
/Visit:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
|
|
1176
|
+
];
|
|
1177
|
+
|
|
1178
|
+
patterns.forEach(pattern => {
|
|
1179
|
+
let match;
|
|
1180
|
+
while ((match = pattern.exec(rawData)) !== null) {
|
|
1181
|
+
const url = match[1];
|
|
1182
|
+
session.ws.send(JSON.stringify({ type: 'url_open', url }));
|
|
1183
|
+
if (pattern.source.includes('OPEN_URL')) {
|
|
1184
|
+
outputData = outputData.replace(match[0], `[INFO] Opening in browser: ${url}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
session.ws.send(JSON.stringify({ type: 'output', data: outputData }));
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
codexProcess.onExit((exitCode) => {
|
|
1194
|
+
console.log('🔚 Codex process exited with code:', exitCode.exitCode);
|
|
1195
|
+
const session = codexPtySessionsMap.get(ptySessionKey);
|
|
1196
|
+
if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1197
|
+
session.ws.send(JSON.stringify({
|
|
1198
|
+
type: 'output',
|
|
1199
|
+
data: `\r\n\x1b[33mCodex exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
|
|
1200
|
+
}));
|
|
1201
|
+
}
|
|
1202
|
+
if (session && session.timeoutId) {
|
|
1203
|
+
clearTimeout(session.timeoutId);
|
|
1204
|
+
}
|
|
1205
|
+
codexPtySessionsMap.delete(ptySessionKey);
|
|
1206
|
+
codexProcess = null;
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
} catch (spawnError) {
|
|
1210
|
+
console.error('[ERROR] Error spawning Codex process:', spawnError);
|
|
1211
|
+
ws.send(JSON.stringify({
|
|
1212
|
+
type: 'output',
|
|
1213
|
+
data: `\r\n\x1b[31mError starting Codex: ${spawnError.message}\x1b[0m\r\n`
|
|
1214
|
+
}));
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
} else if (data.type === 'input') {
|
|
1218
|
+
if (codexProcess && codexProcess.write) {
|
|
1219
|
+
try {
|
|
1220
|
+
codexProcess.write(data.data);
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
console.error('Error writing to Codex process:', error);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
} else if (data.type === 'resize') {
|
|
1226
|
+
if (codexProcess && codexProcess.resize) {
|
|
1227
|
+
codexProcess.resize(data.cols, data.rows);
|
|
1228
|
+
}
|
|
1229
|
+
} else if (data.type === 'terminate') {
|
|
1230
|
+
console.log('🛑 Codex terminate request for session:', ptySessionKey);
|
|
1231
|
+
const session = codexPtySessionsMap.get(ptySessionKey);
|
|
1232
|
+
if (session) {
|
|
1233
|
+
if (session.timeoutId) clearTimeout(session.timeoutId);
|
|
1234
|
+
if (session.pty && session.pty.kill) session.pty.kill();
|
|
1235
|
+
codexPtySessionsMap.delete(ptySessionKey);
|
|
1236
|
+
}
|
|
1237
|
+
codexProcess = null;
|
|
1238
|
+
}
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
console.error('[ERROR] Codex WebSocket error:', error.message);
|
|
1241
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1242
|
+
ws.send(JSON.stringify({
|
|
1243
|
+
type: 'output',
|
|
1244
|
+
data: `\r\n\x1b[31mError: ${error.message}\x1b[0m\r\n`
|
|
1245
|
+
}));
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
ws.on('close', () => {
|
|
1251
|
+
console.log('🔌 Codex client disconnected');
|
|
1252
|
+
if (ptySessionKey) {
|
|
1253
|
+
const session = codexPtySessionsMap.get(ptySessionKey);
|
|
1254
|
+
if (session) {
|
|
1255
|
+
console.log('⏳ Codex PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
|
|
1256
|
+
session.ws = null;
|
|
1257
|
+
session.timeoutId = setTimeout(() => {
|
|
1258
|
+
console.log('⏰ Codex PTY session timeout, killing process:', ptySessionKey);
|
|
1259
|
+
if (session.pty && session.pty.kill) {
|
|
1260
|
+
session.pty.kill();
|
|
1261
|
+
}
|
|
1262
|
+
codexPtySessionsMap.delete(ptySessionKey);
|
|
1263
|
+
}, PTY_SESSION_TIMEOUT);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
ws.on('error', (error) => {
|
|
1269
|
+
console.error('[ERROR] Codex WebSocket error:', error);
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1051
1272
|
app.post('/api/transcribe', authenticateToken, async (req, res) => {
|
|
1052
1273
|
try {
|
|
1053
1274
|
const multer = (await import('multer')).default;
|
|
@@ -1698,6 +1919,22 @@ async function startServer() {
|
|
|
1698
1919
|
// Start usage scanner service
|
|
1699
1920
|
startUsageScanner();
|
|
1700
1921
|
|
|
1922
|
+
// Migrate existing users: ensure codex directories and config files exist
|
|
1923
|
+
try {
|
|
1924
|
+
const allUsers = userDb.getAllUsers();
|
|
1925
|
+
for (const user of allUsers) {
|
|
1926
|
+
if (!user.uuid) continue;
|
|
1927
|
+
try {
|
|
1928
|
+
await initCodexDirectories(user.uuid);
|
|
1929
|
+
} catch (err) {
|
|
1930
|
+
console.error(`[WARN] Failed to init codex dirs for user ${user.uuid}:`, err.message);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
console.log(`[INFO] Codex directory migration complete for ${allUsers.length} user(s)`);
|
|
1934
|
+
} catch (err) {
|
|
1935
|
+
console.error('[WARN] Codex migration error:', err.message);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1701
1938
|
// Projects watcher is now per-user, initialized when user connects via WebSocket
|
|
1702
1939
|
});
|
|
1703
1940
|
} catch (error) {
|
|
@@ -26,6 +26,7 @@ export function getUserPaths(userUuid) {
|
|
|
26
26
|
skillsDir: path.join(DATA_DIR, 'user-data', userUuid, '.claude', 'skills'),
|
|
27
27
|
skillsImportDir: path.join(DATA_DIR, 'user-data', userUuid, 'skills-import'),
|
|
28
28
|
skillsRepoDir: path.join(DATA_DIR, 'user-data', userUuid, 'skills-repo'),
|
|
29
|
+
codexHomeDir: path.join(DATA_DIR, 'user-data', userUuid, 'codex-home'),
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -38,6 +39,44 @@ export function getPublicPaths() {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Initialize codex home directory and config files for a user.
|
|
44
|
+
* Safe to call multiple times - only creates files that don't already exist.
|
|
45
|
+
*/
|
|
46
|
+
export async function initCodexDirectories(userUuid) {
|
|
47
|
+
validateUuid(userUuid);
|
|
48
|
+
const paths = getUserPaths(userUuid);
|
|
49
|
+
const codexDir = path.join(paths.codexHomeDir, '.codex');
|
|
50
|
+
|
|
51
|
+
await fs.mkdir(codexDir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
// Write config.toml (overwrite to keep in sync with env)
|
|
54
|
+
const baseUrl = process.env.OPENAI_BASE_URL || '';
|
|
55
|
+
const configToml = `model_provider = "custom"
|
|
56
|
+
|
|
57
|
+
[model_providers.custom]
|
|
58
|
+
name = "custom"
|
|
59
|
+
wire_api = "responses"
|
|
60
|
+
requires_openai_auth = true
|
|
61
|
+
base_url = "${baseUrl}"
|
|
62
|
+
`;
|
|
63
|
+
await fs.writeFile(path.join(codexDir, 'config.toml'), configToml, 'utf8');
|
|
64
|
+
|
|
65
|
+
// Write auth.json only if it doesn't exist, to avoid overwriting user tokens
|
|
66
|
+
const authJsonPath = path.join(codexDir, 'auth.json');
|
|
67
|
+
try {
|
|
68
|
+
await fs.access(authJsonPath);
|
|
69
|
+
// File exists - skip to preserve any user-level overrides
|
|
70
|
+
} catch {
|
|
71
|
+
const apiKey = process.env.OPENAI_API_KEY || '';
|
|
72
|
+
const authJson = JSON.stringify({ OPENAI_API_KEY: apiKey }, null, 2);
|
|
73
|
+
await fs.writeFile(authJsonPath, authJson, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`[Codex] Initialized codex directories for user ${userUuid}`);
|
|
77
|
+
return codexDir;
|
|
78
|
+
}
|
|
79
|
+
|
|
41
80
|
/**
|
|
42
81
|
* Initialize directories for a new user
|
|
43
82
|
*/
|
|
@@ -49,6 +88,9 @@ export async function initUserDirectories(userUuid) {
|
|
|
49
88
|
await fs.mkdir(paths.claudeDir, { recursive: true });
|
|
50
89
|
await fs.mkdir(paths.projectsDir, { recursive: true });
|
|
51
90
|
|
|
91
|
+
// Initialize codex home directory with config files
|
|
92
|
+
await initCodexDirectories(userUuid);
|
|
93
|
+
|
|
52
94
|
// Create projects directory for Claude session files
|
|
53
95
|
const projectsDir = path.join(paths.claudeDir, 'projects');
|
|
54
96
|
await fs.mkdir(projectsDir, { recursive: true });
|