@pixelbyte-software/pixcode 1.50.3 → 1.50.5
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-DpdiWohD.js → index-BSxc8Vid.js} +91 -91
- package/dist/index.html +1 -1
- package/dist-server/server/index.js +160 -28
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/routes/network.js +2 -2
- package/dist-server/server/routes/network.js.map +1 -1
- package/dist-server/server/services/external-access.js +193 -11
- package/dist-server/server/services/external-access.js.map +1 -1
- package/package.json +1 -1
- package/scripts/hermes/pixcode-mcp-server.mjs +148 -14
- package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +69 -3
- package/scripts/smoke/hermes-rest-codex-launch.mjs +14 -0
- package/scripts/smoke/hermes-settings-commands.mjs +60 -0
- package/scripts/smoke/tunnel-persistence.mjs +56 -0
- package/server/index.js +179 -30
- package/server/routes/network.js +2 -2
- package/server/services/external-access.js +199 -11
package/dist/index.html
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
<!-- Prevent zoom on iOS -->
|
|
37
37
|
<meta name="format-detection" content="telephone=no" />
|
|
38
|
-
<script type="module" crossorigin src="/assets/index-
|
|
38
|
+
<script type="module" crossorigin src="/assets/index-BSxc8Vid.js"></script>
|
|
39
39
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DB6V5Fl1.js">
|
|
40
40
|
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-CIYNS698.js">
|
|
41
41
|
<link rel="modulepreload" crossorigin href="/assets/vendor-xterm-C7tpxJl7.js">
|
|
@@ -69,6 +69,7 @@ import providerRoutes from './modules/providers/provider.routes.js';
|
|
|
69
69
|
import { createHermesTaskRouter, adapterRegistry, ClaudeCodeA2AAdapter, CodexA2AAdapter, CursorA2AAdapter, GeminiA2AAdapter, OpenCodeA2AAdapter, QwenA2AAdapter, createPreviewProxyRouter, createOrchestrationTaskRouter, createHermesRouter, createWorkflowRouter, } from './modules/orchestration/index.js';
|
|
70
70
|
import networkRoutes from './routes/network.js';
|
|
71
71
|
import telegramRoutes from './routes/telegram.js';
|
|
72
|
+
import { restoreRequestedTunnel } from './services/external-access.js';
|
|
72
73
|
import { restoreBotFromConfig } from './services/telegram/bot.js';
|
|
73
74
|
import { ensurePortOpen } from './utils/port-access.js';
|
|
74
75
|
import { applyAllStoredCredentialsToEnv, } from './services/provider-credentials.js';
|
|
@@ -232,6 +233,7 @@ const app = express();
|
|
|
232
233
|
const server = http.createServer(app);
|
|
233
234
|
const ptySessionsMap = new Map();
|
|
234
235
|
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
236
|
+
const COMPLETED_PTY_SESSION_TTL = 5 * 60 * 1000;
|
|
235
237
|
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
|
|
236
238
|
const SHELL_CLI_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
|
|
237
239
|
import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
|
|
@@ -264,6 +266,97 @@ function killProviderPtySessions(projectPath, provider) {
|
|
|
264
266
|
}
|
|
265
267
|
return killed;
|
|
266
268
|
}
|
|
269
|
+
function getLastRegexMatchIndex(text, pattern) {
|
|
270
|
+
let lastIndex = -1;
|
|
271
|
+
for (const match of text.matchAll(pattern)) {
|
|
272
|
+
lastIndex = match.index ?? lastIndex;
|
|
273
|
+
}
|
|
274
|
+
return lastIndex;
|
|
275
|
+
}
|
|
276
|
+
function detectProviderTerminalState(provider, output) {
|
|
277
|
+
const cleanOutput = String(output || '');
|
|
278
|
+
if (!cleanOutput.trim()) {
|
|
279
|
+
return {
|
|
280
|
+
terminalState: 'unknown',
|
|
281
|
+
isBusy: false,
|
|
282
|
+
terminalStateReason: 'empty_output',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (/Process exited with code/iu.test(cleanOutput)) {
|
|
286
|
+
return {
|
|
287
|
+
terminalState: 'exited',
|
|
288
|
+
isBusy: false,
|
|
289
|
+
terminalStateReason: 'process_exit',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const lastBusy = Math.max(getLastRegexMatchIndex(cleanOutput, /(?:^|\n)\s*[•*]\s*(?:Working|Running|Thinking)\b/giu), getLastRegexMatchIndex(cleanOutput, /\bWorking\s*\([^)]*esc to interrupt[^)]*\)/giu), getLastRegexMatchIndex(cleanOutput, /\bmsg=interrupt\b/giu));
|
|
293
|
+
if (provider === 'codex') {
|
|
294
|
+
const lastPrompt = Math.max(getLastRegexMatchIndex(cleanOutput, /(?:^|\n)\s*›(?:\s|$)/gu), getLastRegexMatchIndex(cleanOutput, /(?:^|\n)\s*❯(?:\s|$)/gu));
|
|
295
|
+
if (lastBusy >= 0) {
|
|
296
|
+
const isBusy = lastPrompt <= lastBusy;
|
|
297
|
+
return {
|
|
298
|
+
terminalState: isBusy ? 'busy' : 'idle',
|
|
299
|
+
isBusy,
|
|
300
|
+
terminalStateReason: isBusy ? 'codex_busy_marker_after_prompt' : 'codex_prompt_after_busy_marker',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (lastPrompt >= 0 && /(?:Initialized|Baseline check passed|I did not modify files|Use \/skills)/iu.test(cleanOutput)) {
|
|
304
|
+
return {
|
|
305
|
+
terminalState: 'idle',
|
|
306
|
+
isBusy: false,
|
|
307
|
+
terminalStateReason: 'codex_idle_prompt',
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (lastBusy >= 0) {
|
|
312
|
+
return {
|
|
313
|
+
terminalState: 'busy',
|
|
314
|
+
isBusy: true,
|
|
315
|
+
terminalStateReason: 'generic_busy_marker',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
terminalState: 'unknown',
|
|
320
|
+
isBusy: false,
|
|
321
|
+
terminalStateReason: 'no_known_marker',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function resolveProviderTerminalState(session, provider, output) {
|
|
325
|
+
if (session?.lifecycleState === 'completed' || session?.lifecycleState === 'failed' || session?.lifecycleState === 'exited') {
|
|
326
|
+
const exitCode = typeof session.exitCode === 'number' ? session.exitCode : null;
|
|
327
|
+
const terminalFailed = exitCode !== null ? exitCode !== 0 : Boolean(session.exitSignal);
|
|
328
|
+
return {
|
|
329
|
+
terminalState: terminalFailed ? 'failed' : 'completed',
|
|
330
|
+
lifecycleState: session.lifecycleState,
|
|
331
|
+
isBusy: false,
|
|
332
|
+
terminalFailed,
|
|
333
|
+
exitCode,
|
|
334
|
+
exitSignal: session.exitSignal || null,
|
|
335
|
+
completedAt: session.completedAt || null,
|
|
336
|
+
terminalStateReason: terminalFailed ? 'pty_failed' : 'pty_completed',
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const detected = detectProviderTerminalState(provider, output);
|
|
340
|
+
return {
|
|
341
|
+
...detected,
|
|
342
|
+
lifecycleState: session?.lifecycleState || 'running',
|
|
343
|
+
terminalFailed: false,
|
|
344
|
+
exitCode: null,
|
|
345
|
+
exitSignal: null,
|
|
346
|
+
completedAt: null,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function appendPtySessionBuffer(session, data) {
|
|
350
|
+
if (!session)
|
|
351
|
+
return;
|
|
352
|
+
if (session.buffer.length < 5000) {
|
|
353
|
+
session.buffer.push(data);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
session.buffer.shift();
|
|
357
|
+
session.buffer.push(data);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
267
360
|
function normalizeShellPermissionMode(value) {
|
|
268
361
|
return typeof value === 'string' ? value.trim() : '';
|
|
269
362
|
}
|
|
@@ -439,6 +532,8 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) =>
|
|
|
439
532
|
const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
|
|
440
533
|
? req.query.projectPath.trim()
|
|
441
534
|
: null;
|
|
535
|
+
const launchId = Number.parseInt(String(req.query.launchId || ''), 10);
|
|
536
|
+
const requestedLaunchId = Number.isFinite(launchId) && launchId > 0 ? launchId : null;
|
|
442
537
|
const maxChars = Math.min(20000, Math.max(1000, Number.parseInt(String(req.query.maxChars || '12000'), 10) || 12000));
|
|
443
538
|
if (!SHELL_CLI_PROVIDERS.has(provider)) {
|
|
444
539
|
return res.status(400).json({ error: 'Unsupported provider' });
|
|
@@ -448,7 +543,8 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) =>
|
|
|
448
543
|
for (const session of ptySessionsMap.values()) {
|
|
449
544
|
if (session?.provider === provider &&
|
|
450
545
|
!session?.isPlainShell &&
|
|
451
|
-
(!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath)
|
|
546
|
+
(!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath) &&
|
|
547
|
+
(!requestedLaunchId || session.hermesLaunchId === requestedLaunchId)) {
|
|
452
548
|
if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
|
|
453
549
|
matchedSession = session;
|
|
454
550
|
}
|
|
@@ -459,18 +555,23 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) =>
|
|
|
459
555
|
active: false,
|
|
460
556
|
provider,
|
|
461
557
|
projectPath: requestedProjectPath,
|
|
558
|
+
launchId: requestedLaunchId,
|
|
462
559
|
output: '',
|
|
463
560
|
message: 'No active provider terminal session found for this project.',
|
|
464
561
|
});
|
|
465
562
|
}
|
|
466
563
|
const rawOutput = matchedSession.buffer.join('').slice(-maxChars);
|
|
564
|
+
const output = stripAnsiSequences(rawOutput);
|
|
565
|
+
const terminalState = resolveProviderTerminalState(matchedSession, provider, output);
|
|
467
566
|
res.json({
|
|
468
567
|
active: true,
|
|
469
568
|
provider,
|
|
470
569
|
projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
|
|
471
570
|
sessionId: matchedSession.sessionId || null,
|
|
571
|
+
launchId: matchedSession.hermesLaunchId || null,
|
|
472
572
|
updatedAt: matchedSession.updatedAt || null,
|
|
473
|
-
|
|
573
|
+
...terminalState,
|
|
574
|
+
output,
|
|
474
575
|
});
|
|
475
576
|
});
|
|
476
577
|
// Authentication routes (public)
|
|
@@ -2080,6 +2181,9 @@ function handleShellConnection(ws, request) {
|
|
|
2080
2181
|
const startupInput = typeof data.startupInput === 'string' && data.startupInput.trim()
|
|
2081
2182
|
? data.startupInput.trim()
|
|
2082
2183
|
: null;
|
|
2184
|
+
const hermesLaunchId = Number.isFinite(Number(data.hermesLaunchId)) && Number(data.hermesLaunchId) > 0
|
|
2185
|
+
? Number(data.hermesLaunchId)
|
|
2186
|
+
: null;
|
|
2083
2187
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
2084
2188
|
const isHermesCliLaunch = isPlainShell && isHermesCliCommand(initialCommand);
|
|
2085
2189
|
const forceNewSession = Boolean(data.forceNewSession);
|
|
@@ -2136,24 +2240,29 @@ function handleShellConnection(ws, request) {
|
|
|
2136
2240
|
}
|
|
2137
2241
|
const existingSession = (isLoginCommand || forceNewSession) ? null : ptySessionsMap.get(ptySessionKey);
|
|
2138
2242
|
if (existingSession) {
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2243
|
+
if (!existingSession.pty || existingSession.lifecycleState === 'completed' || existingSession.lifecycleState === 'failed') {
|
|
2244
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
2245
|
+
}
|
|
2246
|
+
else {
|
|
2247
|
+
console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
|
|
2248
|
+
shellProcess = existingSession.pty;
|
|
2249
|
+
clearTimeout(existingSession.timeoutId);
|
|
2250
|
+
ws.send(JSON.stringify({
|
|
2251
|
+
type: 'output',
|
|
2252
|
+
data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
|
|
2253
|
+
}));
|
|
2254
|
+
if (existingSession.buffer && existingSession.buffer.length > 0) {
|
|
2255
|
+
console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
|
|
2256
|
+
existingSession.buffer.forEach(bufferedData => {
|
|
2257
|
+
ws.send(JSON.stringify({
|
|
2258
|
+
type: 'output',
|
|
2259
|
+
data: bufferedData
|
|
2260
|
+
}));
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
existingSession.ws = ws;
|
|
2264
|
+
return;
|
|
2154
2265
|
}
|
|
2155
|
-
existingSession.ws = ws;
|
|
2156
|
-
return;
|
|
2157
2266
|
}
|
|
2158
2267
|
console.log('[INFO] Starting shell in:', projectPath);
|
|
2159
2268
|
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
|
|
@@ -2356,8 +2465,13 @@ function handleShellConnection(ws, request) {
|
|
|
2356
2465
|
timeoutId: null,
|
|
2357
2466
|
projectPath,
|
|
2358
2467
|
sessionId,
|
|
2468
|
+
hermesLaunchId,
|
|
2359
2469
|
provider,
|
|
2360
2470
|
isPlainShell,
|
|
2471
|
+
lifecycleState: 'running',
|
|
2472
|
+
exitCode: null,
|
|
2473
|
+
exitSignal: null,
|
|
2474
|
+
completedAt: null,
|
|
2361
2475
|
keepAliveUntilExit: false,
|
|
2362
2476
|
updatedAt: Date.now(),
|
|
2363
2477
|
});
|
|
@@ -2367,13 +2481,7 @@ function handleShellConnection(ws, request) {
|
|
|
2367
2481
|
if (!session)
|
|
2368
2482
|
return;
|
|
2369
2483
|
session.updatedAt = Date.now();
|
|
2370
|
-
|
|
2371
|
-
session.buffer.push(data);
|
|
2372
|
-
}
|
|
2373
|
-
else {
|
|
2374
|
-
session.buffer.shift();
|
|
2375
|
-
session.buffer.push(data);
|
|
2376
|
-
}
|
|
2484
|
+
appendPtySessionBuffer(session, data);
|
|
2377
2485
|
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
2378
2486
|
let outputData = data;
|
|
2379
2487
|
const cleanChunk = stripAnsiSequences(data);
|
|
@@ -2414,16 +2522,37 @@ function handleShellConnection(ws, request) {
|
|
|
2414
2522
|
shellProcess.onExit((exitCode) => {
|
|
2415
2523
|
console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
|
|
2416
2524
|
const session = ptySessionsMap.get(ptySessionKey);
|
|
2525
|
+
const exitMessage = `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`;
|
|
2526
|
+
if (session) {
|
|
2527
|
+
session.lifecycleState = exitCode.exitCode === 0 && !exitCode.signal ? 'completed' : 'failed';
|
|
2528
|
+
session.exitCode = typeof exitCode.exitCode === 'number' ? exitCode.exitCode : null;
|
|
2529
|
+
session.exitSignal = exitCode.signal || null;
|
|
2530
|
+
session.completedAt = new Date().toISOString();
|
|
2531
|
+
session.updatedAt = Date.now();
|
|
2532
|
+
session.pty = null;
|
|
2533
|
+
appendPtySessionBuffer(session, exitMessage);
|
|
2534
|
+
}
|
|
2417
2535
|
if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
2418
2536
|
session.ws.send(JSON.stringify({
|
|
2419
2537
|
type: 'output',
|
|
2420
|
-
data:
|
|
2538
|
+
data: exitMessage
|
|
2421
2539
|
}));
|
|
2422
2540
|
}
|
|
2423
2541
|
if (session && session.timeoutId) {
|
|
2424
2542
|
clearTimeout(session.timeoutId);
|
|
2425
2543
|
}
|
|
2426
|
-
|
|
2544
|
+
if (session) {
|
|
2545
|
+
session.ws = null;
|
|
2546
|
+
session.timeoutId = setTimeout(() => {
|
|
2547
|
+
const current = ptySessionsMap.get(ptySessionKey);
|
|
2548
|
+
if (current && current.lifecycleState !== 'running') {
|
|
2549
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
2550
|
+
}
|
|
2551
|
+
}, COMPLETED_PTY_SESSION_TTL);
|
|
2552
|
+
}
|
|
2553
|
+
else {
|
|
2554
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
2555
|
+
}
|
|
2427
2556
|
shellProcess = null;
|
|
2428
2557
|
});
|
|
2429
2558
|
}
|
|
@@ -3128,6 +3257,9 @@ async function startServer() {
|
|
|
3128
3257
|
catch (err) {
|
|
3129
3258
|
console.log(`${c.dim('[INFO]')} Port-access helper failed: ${err?.message || err}`);
|
|
3130
3259
|
}
|
|
3260
|
+
restoreRequestedTunnel({ port: Number(SERVER_PORT) }).catch((err) => {
|
|
3261
|
+
console.warn('[external-access] tunnel restore failed:', err?.message || err);
|
|
3262
|
+
});
|
|
3131
3263
|
console.log(`${c.tip('[TIP]')} Run "pixcode status" for full configuration details`);
|
|
3132
3264
|
console.log('');
|
|
3133
3265
|
// Start watching the projects folder for changes
|