@pixelbyte-software/pixcode 1.48.6 → 1.49.1
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-Dw9ELh9s.js → index-B3sFoGyf.js} +161 -161
- package/dist/assets/index-BtdtY_p1.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/gemini-cli.js +5 -1
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/index.js +189 -17
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +87 -0
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
- package/dist-server/server/qwen-code-cli.js +5 -1
- package/dist-server/server/qwen-code-cli.js.map +1 -1
- package/package.json +1 -1
- package/scripts/hermes/configure-pixcode-mcp.mjs +87 -0
- package/scripts/hermes/pixcode-mcp-server.mjs +216 -0
- package/scripts/smoke/git-install-update.mjs +133 -54
- package/scripts/smoke/pixcode-workbench-1-48.mjs +36 -1
- package/scripts/smoke/vscode-workbench-polish.mjs +21 -2
- package/scripts/update-git-install.mjs +162 -4
- package/server/gemini-cli.js +7 -1
- package/server/index.js +207 -17
- package/server/modules/orchestration/hermes/hermes.routes.ts +119 -0
- package/server/qwen-code-cli.js +7 -1
- package/dist/assets/index-B3lN7dBd.css +0 -32
package/server/index.js
CHANGED
|
@@ -32,6 +32,10 @@ const SERVER_VERSION = (() => {
|
|
|
32
32
|
return '0.0.0';
|
|
33
33
|
}
|
|
34
34
|
})();
|
|
35
|
+
const HERMES_SHELL_COMMANDS = new Set([
|
|
36
|
+
'pixcode:hermes:start',
|
|
37
|
+
'pixcode:hermes:install',
|
|
38
|
+
]);
|
|
35
39
|
const DAEMON_COMMAND_CONTEXT = {
|
|
36
40
|
appRoot: APP_ROOT,
|
|
37
41
|
cliEntry: path.join(APP_ROOT, 'server', 'cli.js'),
|
|
@@ -105,7 +109,7 @@ import {
|
|
|
105
109
|
} from './services/provider-credentials.js';
|
|
106
110
|
import { primeCliBinPath } from './services/install-jobs.js';
|
|
107
111
|
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
|
108
|
-
import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
|
|
112
|
+
import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, apiKeysDb } from './database/db.js';
|
|
109
113
|
import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
|
|
110
114
|
import { configureWebPush } from './services/vapid-keys.js';
|
|
111
115
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
@@ -323,6 +327,174 @@ function killProviderPtySessions(projectPath, provider) {
|
|
|
323
327
|
return killed;
|
|
324
328
|
}
|
|
325
329
|
|
|
330
|
+
function shellQuotePosix(value) {
|
|
331
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function shellQuotePowerShell(value) {
|
|
335
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function normalizeShellPermissionMode(value) {
|
|
339
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function shouldBypassShellPermissions(permissionMode, skipPermissions) {
|
|
343
|
+
return Boolean(skipPermissions) || permissionMode === 'bypassPermissions' || permissionMode === 'acceptEdits' || permissionMode === 'yolo';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function buildProviderShellPermissionFlags(provider, permissionMode, skipPermissions) {
|
|
347
|
+
const mode = normalizeShellPermissionMode(permissionMode);
|
|
348
|
+
const bypass = shouldBypassShellPermissions(mode, skipPermissions);
|
|
349
|
+
|
|
350
|
+
if (provider === 'codex') {
|
|
351
|
+
if (mode === 'bypassPermissions' || mode === 'yolo') {
|
|
352
|
+
return ['--dangerously-bypass-approvals-and-sandbox'];
|
|
353
|
+
}
|
|
354
|
+
if (mode === 'acceptEdits' || mode === 'auto_edit' || bypass) {
|
|
355
|
+
return ['--sandbox', 'workspace-write', '--ask-for-approval', 'never'];
|
|
356
|
+
}
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (provider === 'gemini' || provider === 'qwen') {
|
|
361
|
+
if (bypass) {
|
|
362
|
+
return ['--yolo'];
|
|
363
|
+
}
|
|
364
|
+
if (mode === 'auto_edit') {
|
|
365
|
+
return ['--approval-mode', 'auto_edit'];
|
|
366
|
+
}
|
|
367
|
+
if (mode === 'plan') {
|
|
368
|
+
return ['--approval-mode', 'plan'];
|
|
369
|
+
}
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (provider === 'cursor') {
|
|
374
|
+
return bypass ? ['-f'] : [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (provider === 'opencode') {
|
|
378
|
+
if (mode === 'plan') {
|
|
379
|
+
return ['--agent', 'plan'];
|
|
380
|
+
}
|
|
381
|
+
return bypass ? ['--dangerously-skip-permissions'] : [];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (provider === 'claude') {
|
|
385
|
+
return bypass ? ['--dangerously-skip-permissions'] : [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function buildProviderShellCommand(command, permissionFlags = []) {
|
|
392
|
+
const flags = Array.isArray(permissionFlags) ? permissionFlags.filter(Boolean) : [];
|
|
393
|
+
return flags.length > 0 ? `${command} ${flags.join(' ')}` : command;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function resolvePublicBaseUrl(request) {
|
|
397
|
+
const headers = request?.headers || {};
|
|
398
|
+
const forwardedProto = String(headers['x-forwarded-proto'] || '').split(',')[0].trim();
|
|
399
|
+
const proto = forwardedProto || (request?.socket?.encrypted ? 'https' : 'http');
|
|
400
|
+
const host = headers['x-forwarded-host'] || headers.host || `127.0.0.1:${process.env.SERVER_PORT || process.env.PORT || '3001'}`;
|
|
401
|
+
return `${proto}://${String(host).split(',')[0].trim()}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function getOrCreateHermesApiKey(userId) {
|
|
405
|
+
if (!userId) return null;
|
|
406
|
+
|
|
407
|
+
const existing = apiKeysDb
|
|
408
|
+
.getApiKeys(userId)
|
|
409
|
+
.find((key) => key.key_name === 'Hermes Agent MCP' && key.is_active);
|
|
410
|
+
if (existing?.api_key) {
|
|
411
|
+
return existing.api_key;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return apiKeysDb.createApiKey(userId, 'Hermes Agent MCP', [
|
|
415
|
+
'hermes:mcp',
|
|
416
|
+
'projects:read',
|
|
417
|
+
'providers:read',
|
|
418
|
+
'terminal:launch',
|
|
419
|
+
]).apiKey;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function buildHermesShellCommand(kind, env) {
|
|
423
|
+
const configureScript = path.join(APP_ROOT, 'scripts', 'hermes', 'configure-pixcode-mcp.mjs');
|
|
424
|
+
const isWindows = os.platform() === 'win32';
|
|
425
|
+
const quote = isWindows ? shellQuotePowerShell : shellQuotePosix;
|
|
426
|
+
const configure = `node ${quote(configureScript)}`;
|
|
427
|
+
|
|
428
|
+
if (isWindows) {
|
|
429
|
+
const setEnv = [
|
|
430
|
+
`$env:PIXCODE_BASE_URL=${quote(env.PIXCODE_BASE_URL)}`,
|
|
431
|
+
`$env:PIXCODE_API_KEY=${quote(env.PIXCODE_API_KEY)}`,
|
|
432
|
+
`$env:PIXCODE_APP_ROOT=${quote(env.PIXCODE_APP_ROOT)}`,
|
|
433
|
+
].join('; ');
|
|
434
|
+
const resolveHermesCommand = [
|
|
435
|
+
'function Resolve-HermesCommand {',
|
|
436
|
+
'$cmd = Get-Command hermes -ErrorAction SilentlyContinue;',
|
|
437
|
+
'if ($cmd) { return $cmd.Source; }',
|
|
438
|
+
'$candidates = @(',
|
|
439
|
+
'$env:HERMES_CLI_PATH,',
|
|
440
|
+
'(Join-Path $env:LOCALAPPDATA "hermes\\hermes-agent\\venv\\Scripts\\hermes.exe"),',
|
|
441
|
+
'(Join-Path $env:LOCALAPPDATA "hermes\\hermes-agent\\.venv\\Scripts\\hermes.exe"),',
|
|
442
|
+
'(Join-Path $env:LOCALAPPDATA "hermes\\hermes-agent\\hermes.exe")',
|
|
443
|
+
');',
|
|
444
|
+
'foreach ($candidate in $candidates) { if ($candidate -and (Test-Path $candidate)) { return $candidate; } }',
|
|
445
|
+
'return $null;',
|
|
446
|
+
'}',
|
|
447
|
+
].join(' ');
|
|
448
|
+
const installHermesIfMissing = [
|
|
449
|
+
'function Install-HermesIfMissing {',
|
|
450
|
+
'$script:HermesCmd = Resolve-HermesCommand;',
|
|
451
|
+
'if ($script:HermesCmd) { Write-Host "Hermes already installed:"; & $script:HermesCmd --version; return; }',
|
|
452
|
+
'$installer = Join-Path $env:TEMP "pixcode-hermes-install.ps1";',
|
|
453
|
+
'Invoke-WebRequest -Uri "https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1" -UseBasicParsing -OutFile $installer;',
|
|
454
|
+
'& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer -SkipSetup -Branch main;',
|
|
455
|
+
'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE; }',
|
|
456
|
+
'$env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";" + [Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + $env:Path;',
|
|
457
|
+
'$script:HermesCmd = Resolve-HermesCommand;',
|
|
458
|
+
'if (-not $script:HermesCmd) { throw "Hermes installed, but the hermes command could not be found. Restart Pixcode or add Hermes to PATH."; }',
|
|
459
|
+
'}',
|
|
460
|
+
].join(' ');
|
|
461
|
+
if (kind === 'pixcode:hermes:install') {
|
|
462
|
+
return `${setEnv}; ${resolveHermesCommand}; ${installHermesIfMissing}; Install-HermesIfMissing; ${configure}`;
|
|
463
|
+
}
|
|
464
|
+
return `${setEnv}; ${resolveHermesCommand}; ${installHermesIfMissing}; Install-HermesIfMissing; ${configure}; & $script:HermesCmd chat --toolsets "hermes-cli,mcp-pixcode"`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const setEnv = [
|
|
468
|
+
`PIXCODE_BASE_URL=${quote(env.PIXCODE_BASE_URL)}`,
|
|
469
|
+
`PIXCODE_API_KEY=${quote(env.PIXCODE_API_KEY)}`,
|
|
470
|
+
`PIXCODE_APP_ROOT=${quote(env.PIXCODE_APP_ROOT)}`,
|
|
471
|
+
].join(' ');
|
|
472
|
+
const resolveHermesCommand = [
|
|
473
|
+
'resolveHermesCommand() {',
|
|
474
|
+
'if command -v hermes >/dev/null 2>&1; then command -v hermes; return 0; fi;',
|
|
475
|
+
'if [ -n "${HERMES_CLI_PATH:-}" ] && [ -x "$HERMES_CLI_PATH" ]; then printf "%s\\n" "$HERMES_CLI_PATH"; return 0; fi;',
|
|
476
|
+
'if [ -x "$HOME/.local/bin/hermes" ]; then printf "%s\\n" "$HOME/.local/bin/hermes"; return 0; fi;',
|
|
477
|
+
'if [ -x "$HOME/.hermes/hermes-agent/venv/bin/hermes" ]; then printf "%s\\n" "$HOME/.hermes/hermes-agent/venv/bin/hermes"; return 0; fi;',
|
|
478
|
+
'if [ -x "/usr/local/bin/hermes" ]; then printf "%s\\n" "/usr/local/bin/hermes"; return 0; fi;',
|
|
479
|
+
'return 1;',
|
|
480
|
+
'}',
|
|
481
|
+
].join(' ');
|
|
482
|
+
const install = 'curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash';
|
|
483
|
+
const installHermesIfMissing = [
|
|
484
|
+
'installHermesIfMissing() {',
|
|
485
|
+
'HERMES_CMD="$(resolveHermesCommand 2>/dev/null || true)";',
|
|
486
|
+
'if [ -n "$HERMES_CMD" ]; then echo "Hermes already installed:"; "$HERMES_CMD" --version 2>/dev/null || true; return 0; fi;',
|
|
487
|
+
`${install};`,
|
|
488
|
+
'HERMES_CMD="$(resolveHermesCommand 2>/dev/null || true)";',
|
|
489
|
+
'if [ -z "$HERMES_CMD" ]; then echo "Hermes installed, but the hermes command could not be found. Reload PATH and retry." >&2; exit 127; fi;',
|
|
490
|
+
'}',
|
|
491
|
+
].join(' ');
|
|
492
|
+
if (kind === 'pixcode:hermes:install') {
|
|
493
|
+
return `${setEnv} sh -lc ${quote(`${resolveHermesCommand} ${installHermesIfMissing} installHermesIfMissing && node ${shellQuotePosix(configureScript)}`)}`;
|
|
494
|
+
}
|
|
495
|
+
return `${setEnv} sh -lc ${quote(`${resolveHermesCommand} ${installHermesIfMissing} installHermesIfMissing && node ${shellQuotePosix(configureScript)} && "$HERMES_CMD" chat --toolsets "hermes-cli,mcp-pixcode"`)}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
326
498
|
// Single WebSocket server that handles both paths
|
|
327
499
|
const wss = new WebSocketServer({
|
|
328
500
|
server,
|
|
@@ -1851,7 +2023,7 @@ wss.on('connection', (ws, request) => {
|
|
|
1851
2023
|
const pathname = urlObj.pathname;
|
|
1852
2024
|
|
|
1853
2025
|
if (pathname === '/shell') {
|
|
1854
|
-
handleShellConnection(ws);
|
|
2026
|
+
handleShellConnection(ws, request);
|
|
1855
2027
|
} else if (pathname === '/ws') {
|
|
1856
2028
|
handleChatConnection(ws, request);
|
|
1857
2029
|
} else if (pathname.startsWith('/plugin-ws/')) {
|
|
@@ -2060,7 +2232,7 @@ function handleChatConnection(ws, request) {
|
|
|
2060
2232
|
}
|
|
2061
2233
|
|
|
2062
2234
|
// Handle shell WebSocket connections
|
|
2063
|
-
function handleShellConnection(ws) {
|
|
2235
|
+
function handleShellConnection(ws, request) {
|
|
2064
2236
|
console.log('🐚 Shell client connected');
|
|
2065
2237
|
let shellProcess = null;
|
|
2066
2238
|
let ptySessionKey = null;
|
|
@@ -2090,9 +2262,25 @@ function handleShellConnection(ws) {
|
|
|
2090
2262
|
const sessionId = data.sessionId;
|
|
2091
2263
|
const hasSession = data.hasSession;
|
|
2092
2264
|
const provider = data.provider || 'claude';
|
|
2093
|
-
|
|
2265
|
+
let initialCommand = data.initialCommand;
|
|
2266
|
+
const hermesCommand = HERMES_SHELL_COMMANDS.has(initialCommand) ? initialCommand : null;
|
|
2267
|
+
if (hermesCommand) {
|
|
2268
|
+
const apiKey = getOrCreateHermesApiKey(request?.user?.id);
|
|
2269
|
+
if (!apiKey) {
|
|
2270
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Hermes MCP could not create a Pixcode API key for this user.' }));
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
initialCommand = buildHermesShellCommand(hermesCommand, {
|
|
2274
|
+
PIXCODE_BASE_URL: resolvePublicBaseUrl(request),
|
|
2275
|
+
PIXCODE_API_KEY: apiKey,
|
|
2276
|
+
PIXCODE_APP_ROOT: APP_ROOT,
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2094
2279
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
2095
2280
|
const forceNewSession = Boolean(data.forceNewSession);
|
|
2281
|
+
const shellPermissionMode = normalizeShellPermissionMode(data.permissionMode);
|
|
2282
|
+
const shellSkipPermissions = Boolean(data.skipPermissions);
|
|
2283
|
+
const shellPermissionFlags = buildProviderShellPermissionFlags(provider, shellPermissionMode, shellSkipPermissions);
|
|
2096
2284
|
urlDetectionBuffer = '';
|
|
2097
2285
|
announcedAuthUrls.clear();
|
|
2098
2286
|
|
|
@@ -2173,7 +2361,7 @@ function handleShellConnection(ws) {
|
|
|
2173
2361
|
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
|
|
2174
2362
|
console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
|
|
2175
2363
|
if (initialCommand) {
|
|
2176
|
-
console.log('⚡ Initial command:', initialCommand);
|
|
2364
|
+
console.log('⚡ Initial command:', hermesCommand ? hermesCommand : initialCommand);
|
|
2177
2365
|
}
|
|
2178
2366
|
|
|
2179
2367
|
// First send a welcome message
|
|
@@ -2223,25 +2411,27 @@ function handleShellConnection(ws) {
|
|
|
2223
2411
|
// Plain shell mode without an initial command must stay interactive.
|
|
2224
2412
|
shellCommand = initialCommand || null;
|
|
2225
2413
|
} else if (provider === 'cursor') {
|
|
2414
|
+
const command = buildProviderShellCommand('cursor-agent', shellPermissionFlags);
|
|
2226
2415
|
if (hasSession && sessionId) {
|
|
2227
|
-
shellCommand =
|
|
2416
|
+
shellCommand = `${command} --resume="${sessionId}"`;
|
|
2228
2417
|
} else {
|
|
2229
|
-
shellCommand =
|
|
2418
|
+
shellCommand = command;
|
|
2230
2419
|
}
|
|
2231
2420
|
} else if (provider === 'codex') {
|
|
2232
2421
|
// Use codex command; attempt to resume and fall back to a new session when the resume fails.
|
|
2422
|
+
const command = buildProviderShellCommand('codex', shellPermissionFlags);
|
|
2233
2423
|
if (hasSession && sessionId) {
|
|
2234
2424
|
if (os.platform() === 'win32') {
|
|
2235
2425
|
// PowerShell syntax for fallback
|
|
2236
|
-
shellCommand =
|
|
2426
|
+
shellCommand = `${command} resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { ${command} }`;
|
|
2237
2427
|
} else {
|
|
2238
|
-
shellCommand =
|
|
2428
|
+
shellCommand = `${command} resume "${sessionId}" || ${command}`;
|
|
2239
2429
|
}
|
|
2240
2430
|
} else {
|
|
2241
|
-
shellCommand =
|
|
2431
|
+
shellCommand = command;
|
|
2242
2432
|
}
|
|
2243
2433
|
} else if (provider === 'gemini') {
|
|
2244
|
-
const command = initialCommand || 'gemini';
|
|
2434
|
+
const command = buildProviderShellCommand(initialCommand || 'gemini', shellPermissionFlags);
|
|
2245
2435
|
let resumeId = sessionId;
|
|
2246
2436
|
if (hasSession && sessionId) {
|
|
2247
2437
|
try {
|
|
@@ -2270,7 +2460,7 @@ function handleShellConnection(ws) {
|
|
|
2270
2460
|
// Qwen Code shares Gemini CLI's --resume semantics (it's a fork),
|
|
2271
2461
|
// so the resume path resolves the backend-tracked cliSessionId the
|
|
2272
2462
|
// same way. Falls back to a fresh session when the ID can't be found.
|
|
2273
|
-
const command = initialCommand || 'qwen';
|
|
2463
|
+
const command = buildProviderShellCommand(initialCommand || 'qwen', shellPermissionFlags);
|
|
2274
2464
|
let resumeId = sessionId;
|
|
2275
2465
|
if (hasSession && sessionId) {
|
|
2276
2466
|
try {
|
|
@@ -2298,7 +2488,7 @@ function handleShellConnection(ws) {
|
|
|
2298
2488
|
// we pass them straight through without a cliSessionId
|
|
2299
2489
|
// mapping layer — OpenCode doesn't renumber IDs the way
|
|
2300
2490
|
// Gemini does.
|
|
2301
|
-
const command = initialCommand || 'opencode';
|
|
2491
|
+
const command = buildProviderShellCommand(initialCommand || 'opencode', shellPermissionFlags);
|
|
2302
2492
|
if (hasSession && sessionId && safeSessionIdPattern.test(sessionId)) {
|
|
2303
2493
|
shellCommand = `${command} --session "${sessionId}"`;
|
|
2304
2494
|
} else {
|
|
@@ -2306,19 +2496,19 @@ function handleShellConnection(ws) {
|
|
|
2306
2496
|
}
|
|
2307
2497
|
} else {
|
|
2308
2498
|
// Claude (default provider)
|
|
2309
|
-
const command = initialCommand || 'claude';
|
|
2499
|
+
const command = buildProviderShellCommand(initialCommand || 'claude', shellPermissionFlags);
|
|
2310
2500
|
if (hasSession && sessionId) {
|
|
2311
2501
|
if (os.platform() === 'win32') {
|
|
2312
|
-
shellCommand =
|
|
2502
|
+
shellCommand = `${command} --resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { ${command} }`;
|
|
2313
2503
|
} else {
|
|
2314
|
-
shellCommand =
|
|
2504
|
+
shellCommand = `${command} --resume "${sessionId}" || ${command}`;
|
|
2315
2505
|
}
|
|
2316
2506
|
} else {
|
|
2317
2507
|
shellCommand = command;
|
|
2318
2508
|
}
|
|
2319
2509
|
}
|
|
2320
2510
|
|
|
2321
|
-
console.log('🔧 Executing shell command:', shellCommand || 'interactive shell');
|
|
2511
|
+
console.log('🔧 Executing shell command:', hermesCommand ? hermesCommand : (shellCommand || 'interactive shell'));
|
|
2322
2512
|
|
|
2323
2513
|
// Use appropriate shell based on platform
|
|
2324
2514
|
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
@@ -1,10 +1,85 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
1
3
|
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
2
5
|
|
|
3
6
|
import express, { type Router } from 'express';
|
|
4
7
|
|
|
5
8
|
import { adapterRegistry } from '@/modules/orchestration/a2a/adapter-registry.js';
|
|
6
9
|
import { a2aTaskStore as hermesTaskStore } from '@/modules/orchestration/a2a/task-store.js';
|
|
7
10
|
|
|
11
|
+
const HERMES_TERMINAL_LAUNCH_LIMIT = 100;
|
|
12
|
+
const HERMES_TERMINAL_LAUNCH_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
|
|
13
|
+
|
|
14
|
+
type HermesTerminalLaunchEvent = {
|
|
15
|
+
id: number;
|
|
16
|
+
provider: string;
|
|
17
|
+
projectPath: string | null;
|
|
18
|
+
prompt: string | null;
|
|
19
|
+
source: string;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let nextHermesTerminalLaunchId = 1;
|
|
24
|
+
const hermesTerminalLaunches: HermesTerminalLaunchEvent[] = [];
|
|
25
|
+
|
|
26
|
+
function hermesCommandCandidates(): string[] {
|
|
27
|
+
const candidates = [process.env.HERMES_CLI_PATH, 'hermes'].filter((candidate): candidate is string => (
|
|
28
|
+
typeof candidate === 'string' && candidate.trim().length > 0
|
|
29
|
+
));
|
|
30
|
+
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
33
|
+
if (localAppData) {
|
|
34
|
+
candidates.push(
|
|
35
|
+
path.join(localAppData, 'hermes', 'hermes-agent', 'venv', 'Scripts', 'hermes.exe'),
|
|
36
|
+
path.join(localAppData, 'hermes', 'hermes-agent', '.venv', 'Scripts', 'hermes.exe'),
|
|
37
|
+
path.join(localAppData, 'hermes', 'hermes-agent', 'hermes.exe'),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
candidates.push(
|
|
42
|
+
path.join(os.homedir(), '.local', 'bin', 'hermes'),
|
|
43
|
+
path.join(os.homedir(), '.hermes', 'hermes-agent', 'venv', 'bin', 'hermes'),
|
|
44
|
+
'/usr/local/bin/hermes',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return [...new Set(candidates)];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readHermesInstallStatus() {
|
|
52
|
+
for (const candidate of hermesCommandCandidates()) {
|
|
53
|
+
const isBareCommand = candidate === 'hermes';
|
|
54
|
+
if (!isBareCommand && !existsSync(candidate)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = spawnSync(candidate, ['--version'], {
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
|
+
timeout: 5000,
|
|
62
|
+
shell: false,
|
|
63
|
+
});
|
|
64
|
+
if (!result.error && result.status === 0) {
|
|
65
|
+
const version = `${result.stdout || result.stderr || ''}`.trim() || null;
|
|
66
|
+
return {
|
|
67
|
+
installed: true,
|
|
68
|
+
command: candidate,
|
|
69
|
+
version,
|
|
70
|
+
error: null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
installed: false,
|
|
77
|
+
command: null,
|
|
78
|
+
version: null,
|
|
79
|
+
error: 'Hermes Agent CLI is not installed or is not on PATH.',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
8
83
|
export function createHermesRouter(): Router {
|
|
9
84
|
const router = express.Router();
|
|
10
85
|
|
|
@@ -43,6 +118,10 @@ export function createHermesRouter(): Router {
|
|
|
43
118
|
});
|
|
44
119
|
});
|
|
45
120
|
|
|
121
|
+
router.get('/install-status', (_req, res) => {
|
|
122
|
+
res.json(readHermesInstallStatus());
|
|
123
|
+
});
|
|
124
|
+
|
|
46
125
|
router.get('/agents', (_req, res) => {
|
|
47
126
|
res.json({
|
|
48
127
|
agent: 'hermes',
|
|
@@ -53,6 +132,46 @@ export function createHermesRouter(): Router {
|
|
|
53
132
|
});
|
|
54
133
|
});
|
|
55
134
|
|
|
135
|
+
router.get('/terminal-launches', (req, res) => {
|
|
136
|
+
const after = Number.parseInt(typeof req.query.after === 'string' ? req.query.after : '0', 10);
|
|
137
|
+
const afterId = Number.isFinite(after) ? after : 0;
|
|
138
|
+
res.json({
|
|
139
|
+
events: hermesTerminalLaunches.filter((event) => event.id > afterId),
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
router.post('/terminal-launches', (req, res) => {
|
|
144
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
145
|
+
const provider = typeof body.provider === 'string' ? body.provider.trim() : '';
|
|
146
|
+
if (!HERMES_TERMINAL_LAUNCH_PROVIDERS.has(provider)) {
|
|
147
|
+
res.status(400).json({ error: { code: 'INVALID_PROVIDER', message: provider || 'provider is required' } });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const projectPath = typeof body.projectPath === 'string' && body.projectPath.trim()
|
|
152
|
+
? body.projectPath.trim()
|
|
153
|
+
: null;
|
|
154
|
+
const prompt = typeof body.prompt === 'string' && body.prompt.trim()
|
|
155
|
+
? body.prompt.trim()
|
|
156
|
+
: null;
|
|
157
|
+
|
|
158
|
+
const event: HermesTerminalLaunchEvent = {
|
|
159
|
+
id: nextHermesTerminalLaunchId,
|
|
160
|
+
provider,
|
|
161
|
+
projectPath,
|
|
162
|
+
prompt,
|
|
163
|
+
source: 'hermes-mcp',
|
|
164
|
+
createdAt: new Date().toISOString(),
|
|
165
|
+
};
|
|
166
|
+
nextHermesTerminalLaunchId += 1;
|
|
167
|
+
hermesTerminalLaunches.push(event);
|
|
168
|
+
if (hermesTerminalLaunches.length > HERMES_TERMINAL_LAUNCH_LIMIT) {
|
|
169
|
+
hermesTerminalLaunches.splice(0, hermesTerminalLaunches.length - HERMES_TERMINAL_LAUNCH_LIMIT);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
res.status(201).json({ event });
|
|
173
|
+
});
|
|
174
|
+
|
|
56
175
|
router.get('/tasks/:id', (req, res) => {
|
|
57
176
|
const task = hermesTaskStore.get(req.params.id);
|
|
58
177
|
if (!task) {
|
package/server/qwen-code-cli.js
CHANGED
|
@@ -102,7 +102,13 @@ async function spawnQwen(command, options = {}, ws) {
|
|
|
102
102
|
args.push('--model', modelToUse);
|
|
103
103
|
args.push('--output-format', 'stream-json');
|
|
104
104
|
|
|
105
|
-
if (
|
|
105
|
+
if (
|
|
106
|
+
settings.skipPermissions ||
|
|
107
|
+
options.skipPermissions ||
|
|
108
|
+
permissionMode === 'yolo' ||
|
|
109
|
+
permissionMode === 'bypassPermissions' ||
|
|
110
|
+
permissionMode === 'acceptEdits'
|
|
111
|
+
) {
|
|
106
112
|
args.push('--yolo');
|
|
107
113
|
} else if (permissionMode === 'auto_edit') {
|
|
108
114
|
args.push('--approval-mode', 'auto_edit');
|