@pixelbyte-software/pixcode 1.49.0 → 1.49.2
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-B8TbMftk.css +32 -0
- package/dist/assets/{index-58IIiyST.js → index-BsQAPOnf.js} +172 -172
- 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 +119 -17
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +128 -1
- 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/dist-server/server/services/hermes-install-jobs.js +417 -0
- package/dist-server/server/services/hermes-install-jobs.js.map +1 -0
- package/package.json +1 -1
- package/scripts/smoke/hermes-api-install.mjs +29 -0
- package/scripts/smoke/pixcode-workbench-1-48.mjs +23 -0
- package/server/gemini-cli.js +7 -1
- package/server/index.js +129 -17
- package/server/modules/orchestration/hermes/hermes.routes.ts +144 -2
- package/server/qwen-code-cli.js +7 -1
- package/server/services/hermes-install-jobs.js +467 -0
- package/dist/assets/index-BpUexHb8.css +0 -32
package/server/index.js
CHANGED
|
@@ -335,6 +335,64 @@ function shellQuotePowerShell(value) {
|
|
|
335
335
|
return `'${String(value).replace(/'/g, "''")}'`;
|
|
336
336
|
}
|
|
337
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
|
+
|
|
338
396
|
function resolvePublicBaseUrl(request) {
|
|
339
397
|
const headers = request?.headers || {};
|
|
340
398
|
const forwardedProto = String(headers['x-forwarded-proto'] || '').split(',')[0].trim();
|
|
@@ -373,11 +431,37 @@ function buildHermesShellCommand(kind, env) {
|
|
|
373
431
|
`$env:PIXCODE_API_KEY=${quote(env.PIXCODE_API_KEY)}`,
|
|
374
432
|
`$env:PIXCODE_APP_ROOT=${quote(env.PIXCODE_APP_ROOT)}`,
|
|
375
433
|
].join('; ');
|
|
376
|
-
const
|
|
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(' ');
|
|
377
461
|
if (kind === 'pixcode:hermes:install') {
|
|
378
|
-
return `${setEnv}; ${
|
|
462
|
+
return `${setEnv}; ${resolveHermesCommand}; ${installHermesIfMissing}; Install-HermesIfMissing; ${configure}`;
|
|
379
463
|
}
|
|
380
|
-
return `${setEnv}; ${
|
|
464
|
+
return `${setEnv}; ${resolveHermesCommand}; ${installHermesIfMissing}; Install-HermesIfMissing; ${configure}; & $script:HermesCmd chat --toolsets "hermes-cli,mcp-pixcode"`;
|
|
381
465
|
}
|
|
382
466
|
|
|
383
467
|
const setEnv = [
|
|
@@ -385,11 +469,30 @@ function buildHermesShellCommand(kind, env) {
|
|
|
385
469
|
`PIXCODE_API_KEY=${quote(env.PIXCODE_API_KEY)}`,
|
|
386
470
|
`PIXCODE_APP_ROOT=${quote(env.PIXCODE_APP_ROOT)}`,
|
|
387
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(' ');
|
|
388
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(' ');
|
|
389
492
|
if (kind === 'pixcode:hermes:install') {
|
|
390
|
-
return `${setEnv} sh -lc ${quote(`${
|
|
493
|
+
return `${setEnv} sh -lc ${quote(`${resolveHermesCommand} ${installHermesIfMissing} installHermesIfMissing && node ${shellQuotePosix(configureScript)}`)}`;
|
|
391
494
|
}
|
|
392
|
-
return `${setEnv} sh -lc ${quote(
|
|
495
|
+
return `${setEnv} sh -lc ${quote(`${resolveHermesCommand} ${installHermesIfMissing} installHermesIfMissing && node ${shellQuotePosix(configureScript)} && "$HERMES_CMD" chat --toolsets "hermes-cli,mcp-pixcode"`)}`;
|
|
393
496
|
}
|
|
394
497
|
|
|
395
498
|
// Single WebSocket server that handles both paths
|
|
@@ -548,7 +651,11 @@ adapterRegistry.register(new OpenCodeA2AAdapter());
|
|
|
548
651
|
app.use('/hermes', createHermesTaskRouter());
|
|
549
652
|
app.use('/preview', authenticateToken, createPreviewProxyRouter());
|
|
550
653
|
app.use('/api/orchestration', authenticateToken, createOrchestrationTaskRouter());
|
|
551
|
-
app.use('/api/orchestration/hermes', authenticateToken, createHermesRouter(
|
|
654
|
+
app.use('/api/orchestration/hermes', authenticateToken, createHermesRouter({
|
|
655
|
+
appRoot: APP_ROOT,
|
|
656
|
+
createHermesApiKey: getOrCreateHermesApiKey,
|
|
657
|
+
resolvePublicBaseUrl,
|
|
658
|
+
}));
|
|
552
659
|
app.use('/api/orchestration', authenticateToken, createWorkflowRouter());
|
|
553
660
|
app.use('/live', createLiveViewPublicRouter());
|
|
554
661
|
|
|
@@ -2175,6 +2282,9 @@ function handleShellConnection(ws, request) {
|
|
|
2175
2282
|
}
|
|
2176
2283
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
2177
2284
|
const forceNewSession = Boolean(data.forceNewSession);
|
|
2285
|
+
const shellPermissionMode = normalizeShellPermissionMode(data.permissionMode);
|
|
2286
|
+
const shellSkipPermissions = Boolean(data.skipPermissions);
|
|
2287
|
+
const shellPermissionFlags = buildProviderShellPermissionFlags(provider, shellPermissionMode, shellSkipPermissions);
|
|
2178
2288
|
urlDetectionBuffer = '';
|
|
2179
2289
|
announcedAuthUrls.clear();
|
|
2180
2290
|
|
|
@@ -2305,25 +2415,27 @@ function handleShellConnection(ws, request) {
|
|
|
2305
2415
|
// Plain shell mode without an initial command must stay interactive.
|
|
2306
2416
|
shellCommand = initialCommand || null;
|
|
2307
2417
|
} else if (provider === 'cursor') {
|
|
2418
|
+
const command = buildProviderShellCommand('cursor-agent', shellPermissionFlags);
|
|
2308
2419
|
if (hasSession && sessionId) {
|
|
2309
|
-
shellCommand =
|
|
2420
|
+
shellCommand = `${command} --resume="${sessionId}"`;
|
|
2310
2421
|
} else {
|
|
2311
|
-
shellCommand =
|
|
2422
|
+
shellCommand = command;
|
|
2312
2423
|
}
|
|
2313
2424
|
} else if (provider === 'codex') {
|
|
2314
2425
|
// Use codex command; attempt to resume and fall back to a new session when the resume fails.
|
|
2426
|
+
const command = buildProviderShellCommand('codex', shellPermissionFlags);
|
|
2315
2427
|
if (hasSession && sessionId) {
|
|
2316
2428
|
if (os.platform() === 'win32') {
|
|
2317
2429
|
// PowerShell syntax for fallback
|
|
2318
|
-
shellCommand =
|
|
2430
|
+
shellCommand = `${command} resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { ${command} }`;
|
|
2319
2431
|
} else {
|
|
2320
|
-
shellCommand =
|
|
2432
|
+
shellCommand = `${command} resume "${sessionId}" || ${command}`;
|
|
2321
2433
|
}
|
|
2322
2434
|
} else {
|
|
2323
|
-
shellCommand =
|
|
2435
|
+
shellCommand = command;
|
|
2324
2436
|
}
|
|
2325
2437
|
} else if (provider === 'gemini') {
|
|
2326
|
-
const command = initialCommand || 'gemini';
|
|
2438
|
+
const command = buildProviderShellCommand(initialCommand || 'gemini', shellPermissionFlags);
|
|
2327
2439
|
let resumeId = sessionId;
|
|
2328
2440
|
if (hasSession && sessionId) {
|
|
2329
2441
|
try {
|
|
@@ -2352,7 +2464,7 @@ function handleShellConnection(ws, request) {
|
|
|
2352
2464
|
// Qwen Code shares Gemini CLI's --resume semantics (it's a fork),
|
|
2353
2465
|
// so the resume path resolves the backend-tracked cliSessionId the
|
|
2354
2466
|
// same way. Falls back to a fresh session when the ID can't be found.
|
|
2355
|
-
const command = initialCommand || 'qwen';
|
|
2467
|
+
const command = buildProviderShellCommand(initialCommand || 'qwen', shellPermissionFlags);
|
|
2356
2468
|
let resumeId = sessionId;
|
|
2357
2469
|
if (hasSession && sessionId) {
|
|
2358
2470
|
try {
|
|
@@ -2380,7 +2492,7 @@ function handleShellConnection(ws, request) {
|
|
|
2380
2492
|
// we pass them straight through without a cliSessionId
|
|
2381
2493
|
// mapping layer — OpenCode doesn't renumber IDs the way
|
|
2382
2494
|
// Gemini does.
|
|
2383
|
-
const command = initialCommand || 'opencode';
|
|
2495
|
+
const command = buildProviderShellCommand(initialCommand || 'opencode', shellPermissionFlags);
|
|
2384
2496
|
if (hasSession && sessionId && safeSessionIdPattern.test(sessionId)) {
|
|
2385
2497
|
shellCommand = `${command} --session "${sessionId}"`;
|
|
2386
2498
|
} else {
|
|
@@ -2388,12 +2500,12 @@ function handleShellConnection(ws, request) {
|
|
|
2388
2500
|
}
|
|
2389
2501
|
} else {
|
|
2390
2502
|
// Claude (default provider)
|
|
2391
|
-
const command = initialCommand || 'claude';
|
|
2503
|
+
const command = buildProviderShellCommand(initialCommand || 'claude', shellPermissionFlags);
|
|
2392
2504
|
if (hasSession && sessionId) {
|
|
2393
2505
|
if (os.platform() === 'win32') {
|
|
2394
|
-
shellCommand =
|
|
2506
|
+
shellCommand = `${command} --resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { ${command} }`;
|
|
2395
2507
|
} else {
|
|
2396
|
-
shellCommand =
|
|
2508
|
+
shellCommand = `${command} --resume "${sessionId}" || ${command}`;
|
|
2397
2509
|
}
|
|
2398
2510
|
} else {
|
|
2399
2511
|
shellCommand = command;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
|
|
3
|
-
import express, { type Router } from 'express';
|
|
3
|
+
import express, { type Request, type Response, type Router } from 'express';
|
|
4
4
|
|
|
5
5
|
import { adapterRegistry } from '@/modules/orchestration/a2a/adapter-registry.js';
|
|
6
6
|
import { a2aTaskStore as hermesTaskStore } from '@/modules/orchestration/a2a/task-store.js';
|
|
7
|
+
import {
|
|
8
|
+
cancelHermesInstallJob,
|
|
9
|
+
createHermesInstallJob,
|
|
10
|
+
getHermesInstallJob,
|
|
11
|
+
readHermesInstallStatus,
|
|
12
|
+
snapshotHermesInstallDonePayload,
|
|
13
|
+
} from '@/services/hermes-install-jobs.js';
|
|
7
14
|
|
|
8
15
|
const HERMES_TERMINAL_LAUNCH_LIMIT = 100;
|
|
9
16
|
const HERMES_TERMINAL_LAUNCH_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
|
|
@@ -17,10 +24,32 @@ type HermesTerminalLaunchEvent = {
|
|
|
17
24
|
createdAt: string;
|
|
18
25
|
};
|
|
19
26
|
|
|
27
|
+
type HermesRouterOptions = {
|
|
28
|
+
appRoot?: string;
|
|
29
|
+
createHermesApiKey?: (userId: number | string | null | undefined) => string | null;
|
|
30
|
+
resolvePublicBaseUrl?: (req: Request) => string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type PixcodeRequest = Request & {
|
|
34
|
+
user?: {
|
|
35
|
+
id?: number | string;
|
|
36
|
+
userId?: number | string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
20
40
|
let nextHermesTerminalLaunchId = 1;
|
|
21
41
|
const hermesTerminalLaunches: HermesTerminalLaunchEvent[] = [];
|
|
22
42
|
|
|
23
|
-
|
|
43
|
+
function writeSse(res: Response, event: string, payload: unknown) {
|
|
44
|
+
res.write(`event: ${event}\n`);
|
|
45
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readUserId(req: PixcodeRequest) {
|
|
49
|
+
return req.user?.id ?? req.user?.userId ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createHermesRouter(options: HermesRouterOptions = {}): Router {
|
|
24
53
|
const router = express.Router();
|
|
25
54
|
|
|
26
55
|
router.get('/status', (_req, res) => {
|
|
@@ -58,6 +87,119 @@ export function createHermesRouter(): Router {
|
|
|
58
87
|
});
|
|
59
88
|
});
|
|
60
89
|
|
|
90
|
+
router.get('/install-status', (_req, res) => {
|
|
91
|
+
res.json(readHermesInstallStatus());
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
router.post('/install', (req: PixcodeRequest, res) => {
|
|
95
|
+
const apiKey = options.createHermesApiKey?.(readUserId(req)) ?? null;
|
|
96
|
+
if (!apiKey) {
|
|
97
|
+
res.status(500).json({
|
|
98
|
+
error: {
|
|
99
|
+
code: 'HERMES_API_KEY_UNAVAILABLE',
|
|
100
|
+
message: 'Pixcode could not create a Hermes MCP API key for this user.',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
107
|
+
const job = createHermesInstallJob({
|
|
108
|
+
appRoot: options.appRoot ?? process.cwd(),
|
|
109
|
+
force: Boolean(body.force),
|
|
110
|
+
pixcodeApiKey: apiKey,
|
|
111
|
+
pixcodeBaseUrl: options.resolvePublicBaseUrl?.(req) ?? `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}`,
|
|
112
|
+
skipBrowser: body.skipBrowser !== false,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
res.status(202).json({
|
|
116
|
+
jobId: job.id,
|
|
117
|
+
provider: 'hermes',
|
|
118
|
+
status: job.status,
|
|
119
|
+
startedAt: job.startedAt,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
router.get('/install/:jobId/stream', (req, res) => {
|
|
124
|
+
const job = getHermesInstallJob(req.params.jobId);
|
|
125
|
+
if (!job) {
|
|
126
|
+
res.status(404).json({
|
|
127
|
+
error: {
|
|
128
|
+
code: 'HERMES_INSTALL_JOB_NOT_FOUND',
|
|
129
|
+
message: 'Hermes install job not found or already expired.',
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
136
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
137
|
+
res.setHeader('Connection', 'keep-alive');
|
|
138
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
139
|
+
if (typeof res.flushHeaders === 'function') res.flushHeaders();
|
|
140
|
+
try {
|
|
141
|
+
(res.socket as NodeJS.Socket & { setNoDelay?: (on: boolean) => void })?.setNoDelay?.(true);
|
|
142
|
+
} catch { /* noop */ }
|
|
143
|
+
|
|
144
|
+
let closed = false;
|
|
145
|
+
const safeWrite = (event: string, payload: unknown) => {
|
|
146
|
+
if (closed) return;
|
|
147
|
+
try { writeSse(res, event, payload); } catch { /* socket gone */ }
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
try { res.write(': start\n\n'); } catch { /* noop */ }
|
|
151
|
+
const heartbeat = setInterval(() => {
|
|
152
|
+
if (closed) return;
|
|
153
|
+
try { res.write(': ping\n\n'); } catch { /* noop */ }
|
|
154
|
+
}, 5000);
|
|
155
|
+
|
|
156
|
+
for (const entry of job.logs) {
|
|
157
|
+
safeWrite('log', { stream: entry.stream, chunk: entry.chunk });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const onLog = (entry: { stream: string; chunk: string }) => {
|
|
161
|
+
safeWrite('log', { stream: entry.stream, chunk: entry.chunk });
|
|
162
|
+
};
|
|
163
|
+
const onDone = (payload: Record<string, unknown>) => {
|
|
164
|
+
safeWrite('done', payload);
|
|
165
|
+
cleanup();
|
|
166
|
+
try { res.end(); } catch { /* noop */ }
|
|
167
|
+
};
|
|
168
|
+
function cleanup() {
|
|
169
|
+
if (closed) return;
|
|
170
|
+
closed = true;
|
|
171
|
+
clearInterval(heartbeat);
|
|
172
|
+
job.emitter.off('log', onLog);
|
|
173
|
+
job.emitter.off('done', onDone);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (job.status !== 'running') {
|
|
177
|
+
safeWrite('done', snapshotHermesInstallDonePayload(job));
|
|
178
|
+
cleanup();
|
|
179
|
+
try { res.end(); } catch { /* noop */ }
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
job.emitter.on('log', onLog);
|
|
184
|
+
job.emitter.once('done', onDone);
|
|
185
|
+
req.on('close', cleanup);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
router.delete('/install/:jobId', (req, res) => {
|
|
189
|
+
const job = getHermesInstallJob(req.params.jobId);
|
|
190
|
+
if (!job) {
|
|
191
|
+
res.status(404).json({
|
|
192
|
+
error: {
|
|
193
|
+
code: 'HERMES_INSTALL_JOB_NOT_FOUND',
|
|
194
|
+
message: 'Hermes install job not found.',
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
res.json({ cancelled: cancelHermesInstallJob(req.params.jobId) });
|
|
201
|
+
});
|
|
202
|
+
|
|
61
203
|
router.get('/agents', (_req, res) => {
|
|
62
204
|
res.json({
|
|
63
205
|
agent: 'hermes',
|
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');
|