@pixelbyte-software/pixcode 1.49.4 → 1.49.6

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.
@@ -0,0 +1,40 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+
4
+ const read = (path) => fs.readFileSync(path, 'utf8');
5
+
6
+ const service = read('server/services/hermes-gateway.js');
7
+ const routes = read('server/modules/orchestration/hermes/hermes.routes.ts');
8
+ const mcpServer = read('scripts/hermes/pixcode-mcp-server.mjs');
9
+ const configureMcp = read('scripts/hermes/configure-pixcode-mcp.mjs');
10
+ const settingsTab = read('src/components/settings/view/tabs/HermesSettingsTab.tsx');
11
+
12
+ assert.match(service, /export async function ensureHermesGateway/, 'Pixcode should expose an API-managed Hermes gateway starter.');
13
+ assert.match(service, /export async function probeHermesGateway/, 'Pixcode should probe Hermes through its REST API.');
14
+ assert.match(service, /export function stopHermesGateway/, 'Pixcode should be able to stop a managed Hermes gateway process.');
15
+ assert.match(service, /API_SERVER_ENABLED:\s*'true'/, 'Hermes gateway env should enable the API server.');
16
+ assert.match(service, /API_SERVER_KEY/, 'Hermes gateway env should set a bearer key.');
17
+ assert.match(service, /API_SERVER_PORT/, 'Hermes gateway env should choose a REST port.');
18
+ assert.match(service, /spawn\(installStatus\.command,\s*\['gateway'\]/, 'Pixcode should start Hermes with `hermes gateway` for REST control.');
19
+ assert.match(service, /\/health/, 'Gateway probe should call Hermes health.');
20
+ assert.match(service, /\/v1\/capabilities/, 'Gateway probe should verify Hermes capabilities.');
21
+ assert.match(service, /\/v1\/models/, 'Gateway probe should verify OpenAI-compatible model discovery.');
22
+ assert.match(service, /\/v1\/runs/, 'Gateway probe should support a real run submission when requested.');
23
+
24
+ assert.match(routes, /router\.get\('\/gateway\/status'/, 'Hermes router should expose gateway status.');
25
+ assert.match(routes, /router\.post\('\/gateway\/start'/, 'Hermes router should expose gateway start.');
26
+ assert.match(routes, /router\.post\('\/gateway\/probe'/, 'Hermes router should expose a REST probe endpoint.');
27
+ assert.match(routes, /router\.post\('\/gateway\/stop'/, 'Hermes router should expose gateway stop.');
28
+ assert.match(routes, /ensureHermesGateway/, 'Hermes router should use the managed gateway service.');
29
+ assert.match(routes, /probeHermesGateway/, 'Hermes router should use the REST probe service.');
30
+
31
+ assert.match(mcpServer, /pixcode_get_hermes_gateway_status/, 'Pixcode MCP should let Hermes inspect gateway status.');
32
+ assert.match(mcpServer, /pixcode_probe_hermes_gateway/, 'Pixcode MCP should let Hermes trigger a REST probe.');
33
+ assert.match(configureMcp, /pixcode_get_hermes_gateway_status/, 'Hermes MCP config should include gateway status tool.');
34
+ assert.match(configureMcp, /pixcode_probe_hermes_gateway/, 'Hermes MCP config should include gateway probe tool.');
35
+
36
+ assert.match(settingsTab, /gateway\/status/, 'Hermes settings should read gateway status.');
37
+ assert.match(settingsTab, /gateway\/start/, 'Hermes settings should start the REST gateway via API.');
38
+ assert.match(settingsTab, /gateway\/probe/, 'Hermes settings should run REST probe via API.');
39
+
40
+ console.log('hermes REST gateway smoke passed');
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import {
7
+ ensureHermesGateway,
8
+ probeHermesGateway,
9
+ stopHermesGateway,
10
+ } from '../../server/services/hermes-gateway.js';
11
+
12
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
13
+ const projectPath = path.resolve(process.argv[2] || repoRoot);
14
+ const hermesHome = await fs.mkdtemp(path.join(os.tmpdir(), 'pixcode-hermes-rest-live-'));
15
+
16
+ try {
17
+ const gateway = await ensureHermesGateway({
18
+ appRoot: repoRoot,
19
+ projectPath,
20
+ hermesHome,
21
+ pixcodeBaseUrl: 'http://127.0.0.1:9',
22
+ pixcodeApiKey: 'px_live_smoke_key',
23
+ port: Number(process.env.PIXCODE_HERMES_LIVE_PORT || 18642),
24
+ });
25
+ if (!gateway.running || !gateway.probe?.ok) {
26
+ throw new Error(`Hermes gateway did not start cleanly: ${JSON.stringify(gateway)}`);
27
+ }
28
+
29
+ const probe = await probeHermesGateway(projectPath);
30
+ if (!probe.ok) {
31
+ throw new Error(`Hermes REST probe failed: ${JSON.stringify(probe)}`);
32
+ }
33
+
34
+ console.log(JSON.stringify({
35
+ ok: true,
36
+ baseUrl: probe.baseUrl,
37
+ projectPath: probe.projectPath,
38
+ checks: Object.fromEntries(Object.entries(probe.checks).map(([name, check]) => [name, check.status])),
39
+ }, null, 2));
40
+ } finally {
41
+ stopHermesGateway(projectPath);
42
+ }
@@ -107,6 +107,8 @@ assert.match(hermesRoutes, /terminal-launches/, 'Hermes MCP should be able to re
107
107
  assert.match(hermesRoutes, /install-status/, 'Hermes settings and terminal UI should have an install-status endpoint.');
108
108
  assert.match(hermesRoutes, /router\.post\('\/install'/, 'Hermes should install through the backend API instead of terminal command paste.');
109
109
  assert.match(read('scripts/smoke/hermes-api-install.mjs'), /hermes API install smoke passed/, 'Hermes API install behavior should have a focused smoke test.');
110
+ assert.match(workbench, /terminalStartupInput/, 'Hermes terminal launch prompts should be passed into the selected CLI.');
111
+ assert.match(read('src/components/shell/hooks/useShellConnection.ts'), /startupInputRef/, 'Shell connections should support one-shot startup input for Hermes-triggered CLI work.');
110
112
  assert.match(serverIndex, /forceNewSession/, 'Shell backend should support explicit fresh-session launches from the workbench.');
111
113
  assert.match(serverIndex, /killProviderPtySessions/, 'Shell backend should terminate old provider PTYs when a fresh CLI session is requested.');
112
114
 
@@ -56,10 +56,35 @@ for (const token of [
56
56
  'vscodeWorkbench.welcome.cloneProject',
57
57
  'vscodeWorkbench.welcome.startHermes',
58
58
  'DarkModeToggle',
59
+ 'welcomeActionCards',
60
+ 'welcomeAppearancePanel',
59
61
  ]) {
60
62
  assert.match(workbench, new RegExp(token.replaceAll('.', '\\.')), `Workbench welcome should include ${token}.`);
61
63
  }
62
64
 
65
+ const welcomeSource = workbench.slice(
66
+ workbench.indexOf('function WorkbenchProjectLanding'),
67
+ workbench.indexOf('const cliProviders'),
68
+ );
69
+
70
+ assert.doesNotMatch(
71
+ welcomeSource,
72
+ /lg:grid-cols-\[minmax\(0,1fr\)_18rem\]/,
73
+ 'Workbench welcome should not reserve a right column that squeezes the three start actions.',
74
+ );
75
+
76
+ assert.match(
77
+ welcomeSource,
78
+ /welcomeAppearancePanel[\s\S]*?recentProjects/,
79
+ 'Appearance controls should sit below the three primary welcome actions and above recent projects.',
80
+ );
81
+
82
+ assert.match(
83
+ welcomeSource,
84
+ /gridTemplateColumns:\s*'repeat\(auto-fit, minmax\(min\(100%, 13rem\), 1fr\)\)'/,
85
+ 'Workbench welcome action cards should auto-wrap responsively instead of staying cramped.',
86
+ );
87
+
63
88
  assert.match(
64
89
  workbench,
65
90
  /function WorkbenchCliPanel/,
package/server/index.js CHANGED
@@ -414,6 +414,7 @@ function getOrCreateHermesApiKey(userId) {
414
414
 
415
415
  return apiKeysDb.createApiKey(userId, 'Hermes Agent MCP', [
416
416
  'hermes:mcp',
417
+ 'hermes:gateway',
417
418
  'projects:read',
418
419
  'providers:read',
419
420
  'terminal:launch',
@@ -424,7 +425,6 @@ function buildHermesShellCommand(kind, env) {
424
425
  const configureScript = path.join(APP_ROOT, 'scripts', 'hermes', 'configure-pixcode-mcp.mjs');
425
426
  const isWindows = os.platform() === 'win32';
426
427
  const quote = isWindows ? shellQuotePowerShell : shellQuotePosix;
427
- const configure = `node ${quote(configureScript)}`;
428
428
 
429
429
  if (isWindows) {
430
430
  const setEnv = [
@@ -462,10 +462,16 @@ function buildHermesShellCommand(kind, env) {
462
462
  'return $null;',
463
463
  '}',
464
464
  ].join(' ');
465
+ const configure = [
466
+ 'function Invoke-PixcodeHermesConfigure {',
467
+ `& node ${quote(configureScript)};`,
468
+ 'if ($LASTEXITCODE -ne 0) { Write-Warning "Pixcode MCP configure failed; starting Hermes anyway."; $global:LASTEXITCODE = 0; }',
469
+ '}',
470
+ ].join(' ');
465
471
  const installHermesIfMissing = [
466
472
  'function Install-HermesIfMissing {',
467
473
  '$script:HermesCmd = Resolve-HermesCommand;',
468
- 'if ($script:HermesCmd) { Write-Host "Hermes already installed:"; & $script:HermesCmd --version; return; }',
474
+ 'if ($script:HermesCmd) { & $script:HermesCmd --version *> $null; return; }',
469
475
  '$installer = Join-Path $env:TEMP "pixcode-hermes-install.ps1";',
470
476
  'Invoke-WebRequest -Uri "https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1" -UseBasicParsing -OutFile $installer;',
471
477
  '& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer -SkipSetup -Branch main;',
@@ -476,9 +482,9 @@ function buildHermesShellCommand(kind, env) {
476
482
  '}',
477
483
  ].join(' ');
478
484
  if (kind === 'pixcode:hermes:install') {
479
- return `${setEnv}; ${resolveHermesCommand}; ${installHermesIfMissing}; Install-HermesIfMissing; ${configure}`;
485
+ return `${setEnv}; ${resolveHermesCommand}; ${configure}; ${installHermesIfMissing}; Install-HermesIfMissing; Invoke-PixcodeHermesConfigure`;
480
486
  }
481
- return `${setEnv}; ${resolveHermesCommand}; ${installHermesIfMissing}; Install-HermesIfMissing; ${configure}; & $script:HermesCmd chat --toolsets "hermes-cli,mcp-pixcode"`;
487
+ return `${setEnv}; ${resolveHermesCommand}; ${configure}; ${installHermesIfMissing}; Install-HermesIfMissing; Invoke-PixcodeHermesConfigure; & $script:HermesCmd`;
482
488
  }
483
489
 
484
490
  const setEnv = [
@@ -502,15 +508,15 @@ function buildHermesShellCommand(kind, env) {
502
508
  const installHermesIfMissing = [
503
509
  'installHermesIfMissing() {',
504
510
  'HERMES_CMD="$(resolveHermesCommand 2>/dev/null || true)";',
505
- 'if [ -n "$HERMES_CMD" ]; then echo "Hermes already installed:"; "$HERMES_CMD" --version 2>/dev/null || true; return 0; fi;',
511
+ 'if [ -n "$HERMES_CMD" ]; then "$HERMES_CMD" --version >/dev/null 2>&1 || true; return 0; fi;',
506
512
  'echo "Hermes is not installed. Use Pixcode Settings > Hermes Agent > Install or repair, then start again." >&2;',
507
513
  'exit 127;',
508
514
  '}',
509
515
  ].join(' ');
510
516
  if (kind === 'pixcode:hermes:install') {
511
- return `${setEnv} sh -lc ${quote(`${resolveHermesCommand} ${installHermesIfMissing} installHermesIfMissing && node ${shellQuotePosix(configureScript)}`)}`;
517
+ return `${setEnv} sh -lc ${quote(`${resolveHermesCommand} ${installHermesIfMissing} installHermesIfMissing && { node ${shellQuotePosix(configureScript)} || echo "Pixcode MCP configure failed; continuing."; }`)}`;
512
518
  }
513
- return `${setEnv} sh -lc ${quote(`${resolveHermesCommand} ${installHermesIfMissing} installHermesIfMissing && node ${shellQuotePosix(configureScript)} && "$HERMES_CMD" chat --toolsets "hermes-cli,mcp-pixcode"`)}`;
519
+ return `${setEnv} sh -lc ${quote(`${resolveHermesCommand} ${installHermesIfMissing} installHermesIfMissing && { node ${shellQuotePosix(configureScript)} || echo "Pixcode MCP configure failed; starting Hermes anyway."; } && "$HERMES_CMD"`)}`;
514
520
  }
515
521
 
516
522
  // Single WebSocket server that handles both paths
@@ -2286,6 +2292,7 @@ function handleShellConnection(ws, request) {
2286
2292
  const provider = data.provider || 'claude';
2287
2293
  let initialCommand = data.initialCommand;
2288
2294
  const hermesCommand = HERMES_SHELL_COMMANDS.has(initialCommand) ? initialCommand : null;
2295
+ const isHermesShellSession = Boolean(hermesCommand);
2289
2296
  if (hermesCommand) {
2290
2297
  const apiKey = getOrCreateHermesApiKey(request?.user?.id);
2291
2298
  if (!apiKey) {
@@ -2322,7 +2329,7 @@ function handleShellConnection(ws, request) {
2322
2329
 
2323
2330
  // Include command hash in session key so different commands get separate sessions
2324
2331
  const commandSuffix = isPlainShell && initialCommand
2325
- ? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
2332
+ ? (isHermesShellSession ? '_hermes' : `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`)
2326
2333
  : '';
2327
2334
  // Include provider in the key so a fresh "new session" in OpenCode
2328
2335
  // doesn't reattach to a cached Claude PTY for the same project (or
@@ -2566,7 +2573,8 @@ function handleShellConnection(ws, request) {
2566
2573
  projectPath,
2567
2574
  sessionId,
2568
2575
  provider,
2569
- isPlainShell
2576
+ isPlainShell,
2577
+ keepAliveUntilExit: isHermesShellSession,
2570
2578
  });
2571
2579
 
2572
2580
  // Handle data output
@@ -2694,6 +2702,12 @@ function handleShellConnection(ws, request) {
2694
2702
  if (ptySessionKey) {
2695
2703
  const session = ptySessionsMap.get(ptySessionKey);
2696
2704
  if (session) {
2705
+ if (session.keepAliveUntilExit) {
2706
+ console.log('⏳ PTY session kept alive until process exit:', ptySessionKey);
2707
+ session.ws = null;
2708
+ return;
2709
+ }
2710
+
2697
2711
  console.log('⏳ PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
2698
2712
  session.ws = null;
2699
2713
 
@@ -11,6 +11,12 @@ import {
11
11
  readHermesInstallStatus,
12
12
  snapshotHermesInstallDonePayload,
13
13
  } from '@/services/hermes-install-jobs.js';
14
+ import {
15
+ ensureHermesGateway,
16
+ getHermesGatewayStatus,
17
+ probeHermesGateway,
18
+ stopHermesGateway,
19
+ } from '@/services/hermes-gateway.js';
14
20
 
15
21
  const HERMES_TERMINAL_LAUNCH_LIMIT = 100;
16
22
  const HERMES_TERMINAL_LAUNCH_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
@@ -91,6 +97,86 @@ export function createHermesRouter(options: HermesRouterOptions = {}): Router {
91
97
  res.json(readHermesInstallStatus());
92
98
  });
93
99
 
100
+ router.get('/gateway/status', (req, res) => {
101
+ const projectPath = typeof req.query.projectPath === 'string' ? req.query.projectPath : null;
102
+ res.json(getHermesGatewayStatus(projectPath));
103
+ });
104
+
105
+ router.post('/gateway/start', async (req: PixcodeRequest, res) => {
106
+ const apiKey = options.createHermesApiKey?.(readUserId(req)) ?? null;
107
+ if (!apiKey) {
108
+ res.status(500).json({
109
+ error: {
110
+ code: 'HERMES_API_KEY_UNAVAILABLE',
111
+ message: 'Pixcode could not create a Hermes MCP API key for this user.',
112
+ },
113
+ });
114
+ return;
115
+ }
116
+
117
+ const body = (req.body ?? {}) as Record<string, unknown>;
118
+ try {
119
+ const gateway = await ensureHermesGateway({
120
+ appRoot: options.appRoot ?? process.cwd(),
121
+ pixcodeApiKey: apiKey,
122
+ pixcodeBaseUrl: options.resolvePublicBaseUrl?.(req) ?? `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}`,
123
+ projectPath: typeof body.projectPath === 'string' ? body.projectPath : undefined,
124
+ });
125
+ res.status(202).json(gateway);
126
+ } catch (error) {
127
+ res.status(500).json({
128
+ error: {
129
+ code: 'HERMES_GATEWAY_START_FAILED',
130
+ message: error instanceof Error ? error.message : String(error),
131
+ },
132
+ });
133
+ }
134
+ });
135
+
136
+ router.post('/gateway/probe', async (req: PixcodeRequest, res) => {
137
+ const body = (req.body ?? {}) as Record<string, unknown>;
138
+ const projectPath = typeof body.projectPath === 'string' ? body.projectPath : null;
139
+ const input = typeof body.input === 'string' ? body.input : undefined;
140
+ const shouldStart = body.startIfNeeded === true;
141
+
142
+ try {
143
+ if (shouldStart) {
144
+ const apiKey = options.createHermesApiKey?.(readUserId(req)) ?? null;
145
+ if (!apiKey) {
146
+ res.status(500).json({
147
+ error: {
148
+ code: 'HERMES_API_KEY_UNAVAILABLE',
149
+ message: 'Pixcode could not create a Hermes MCP API key for this user.',
150
+ },
151
+ });
152
+ return;
153
+ }
154
+ await ensureHermesGateway({
155
+ appRoot: options.appRoot ?? process.cwd(),
156
+ pixcodeApiKey: apiKey,
157
+ pixcodeBaseUrl: options.resolvePublicBaseUrl?.(req) ?? `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}`,
158
+ projectPath: projectPath ?? undefined,
159
+ });
160
+ }
161
+
162
+ const probe = await probeHermesGateway(projectPath, { input });
163
+ res.status(probe.ok ? 200 : 503).json(probe);
164
+ } catch (error) {
165
+ res.status(500).json({
166
+ error: {
167
+ code: 'HERMES_GATEWAY_PROBE_FAILED',
168
+ message: error instanceof Error ? error.message : String(error),
169
+ },
170
+ });
171
+ }
172
+ });
173
+
174
+ router.post('/gateway/stop', (req, res) => {
175
+ const body = (req.body ?? {}) as Record<string, unknown>;
176
+ const projectPath = typeof body.projectPath === 'string' ? body.projectPath : null;
177
+ res.json(stopHermesGateway(projectPath));
178
+ });
179
+
94
180
  router.post('/install', (req: PixcodeRequest, res) => {
95
181
  const apiKey = options.createHermesApiKey?.(readUserId(req)) ?? null;
96
182
  if (!apiKey) {