@pixelbyte-software/pixcode 1.49.10 → 1.50.0

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.
Files changed (28) hide show
  1. package/dist/assets/{index-DVpGrdpT.js → index-81sOpj25.js} +182 -182
  2. package/dist/assets/index-DMz0zv6T.css +32 -0
  3. package/dist/hermes-agent.png +0 -0
  4. package/dist/index.html +2 -2
  5. package/dist-server/server/index.js +77 -1
  6. package/dist-server/server/index.js.map +1 -1
  7. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +40 -7
  8. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
  9. package/dist-server/server/services/hermes-gateway.js +115 -1
  10. package/dist-server/server/services/hermes-gateway.js.map +1 -1
  11. package/dist-server/server/services/hermes-install-jobs.js +9 -1
  12. package/dist-server/server/services/hermes-install-jobs.js.map +1 -1
  13. package/package.json +1 -1
  14. package/scripts/hermes/configure-pixcode-mcp.mjs +37 -1
  15. package/scripts/hermes/pixcode-mcp-server.mjs +166 -8
  16. package/scripts/smoke/hermes-api-install.mjs +4 -4
  17. package/scripts/smoke/hermes-gateway-persistence.mjs +104 -0
  18. package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +27 -0
  19. package/scripts/smoke/hermes-rest-chat-live.mjs +4 -0
  20. package/scripts/smoke/hermes-rest-codex-launch.mjs +11 -4
  21. package/scripts/smoke/hermes-rest-gateway.mjs +9 -1
  22. package/scripts/smoke/hermes-settings-commands.mjs +166 -0
  23. package/scripts/smoke/pixcode-workbench-1-48.mjs +13 -5
  24. package/server/index.js +94 -1
  25. package/server/modules/orchestration/hermes/hermes.routes.ts +45 -7
  26. package/server/services/hermes-gateway.js +128 -1
  27. package/server/services/hermes-install-jobs.js +11 -1
  28. package/dist/assets/index-BT6txdBK.css +0 -32
@@ -71,6 +71,10 @@ const server = createServer(async (req, res) => {
71
71
  provider: body.provider,
72
72
  projectPath: body.projectPath,
73
73
  prompt: body.prompt,
74
+ startupInput: body.startupInput,
75
+ permissionMode: body.permissionMode,
76
+ skipPermissions: Boolean(body.skipPermissions),
77
+ bypassPermissions: Boolean(body.bypassPermissions),
74
78
  source: 'hermes-mcp',
75
79
  createdAt: new Date().toISOString(),
76
80
  };
@@ -149,7 +153,7 @@ try {
149
153
  });
150
154
  assert.equal(gateway.running, true, 'Hermes REST gateway should be running before /v1/runs');
151
155
 
152
- const codexPrompt = 'Pixcode Hermes REST smoke: create HERMES_CODEX_REST_SMOKE.txt with the text "Hermes launched Codex through Pixcode MCP".';
156
+ const codexTask = 'Pixcode Hermes REST smoke: create HERMES_CODEX_REST_SMOKE.txt with the text "Hermes launched Codex through Pixcode MCP", then report the changed file path.';
153
157
  const { response, body } = await gatewayFetch(gateway.baseUrl, '/v1/runs', {
154
158
  method: 'POST',
155
159
  body: JSON.stringify({
@@ -157,10 +161,10 @@ try {
157
161
  instructions: [
158
162
  'You are testing Pixcode integration.',
159
163
  'Use the MCP tool named mcp_pixcode_pixcode_open_cli_terminal exactly once.',
160
- 'Call it with provider="codex", the supplied projectPath, and the supplied prompt.',
164
+ 'Call it with provider="codex", the supplied projectPath, startupInput equal to the supplied task, prompt="Pixcode Hermes REST smoke visible Codex task", and bypassPermissions=true.',
161
165
  'After the tool call, answer with "codex launch requested".',
162
166
  ].join(' '),
163
- input: `Call mcp_pixcode_pixcode_open_cli_terminal with provider codex, projectPath ${JSON.stringify(projectPath)}, and prompt ${JSON.stringify(codexPrompt)}.`,
167
+ input: `Call mcp_pixcode_pixcode_open_cli_terminal with provider codex, projectPath ${JSON.stringify(projectPath)}, startupInput ${JSON.stringify(codexTask)}, prompt "Pixcode Hermes REST smoke visible Codex task", and bypassPermissions true.`,
164
168
  }),
165
169
  });
166
170
  if (!response.ok || !body?.run_id) {
@@ -172,8 +176,11 @@ try {
172
176
  throw new Error(`Hermes /v1/runs failed: ${status.error || JSON.stringify(status)}`);
173
177
  }
174
178
 
175
- const launch = terminalLaunches.find((event) => event.provider === 'codex' && event.prompt === codexPrompt);
179
+ const launch = terminalLaunches.find((event) => event.provider === 'codex' && event.startupInput === codexTask);
176
180
  assert(launch, `Hermes run completed but did not request Codex launch. Launches: ${JSON.stringify(terminalLaunches)}`);
181
+ assert.equal(launch.bypassPermissions, true, 'Hermes Codex launch should request provider permission bypass.');
182
+ assert.equal(launch.skipPermissions, true, 'Hermes Codex launch should carry skipPermissions for providers that use skip flags.');
183
+ assert.equal(launch.permissionMode, 'bypassPermissions', 'Hermes Codex launch should use bypassPermissions mode.');
177
184
  console.log(JSON.stringify({
178
185
  ok: true,
179
186
  runId: body.run_id,
@@ -16,7 +16,12 @@ assert.match(service, /export function stopHermesGateway/, 'Pixcode should be ab
16
16
  assert.match(service, /\/v1\/chat\/completions/, 'Hermes UI chat should use the documented OpenAI-compatible chat completions endpoint first.');
17
17
  assert.match(service, /\/v1\/responses/, 'Hermes UI chat should use the stateful OpenAI-compatible responses endpoint before legacy chat fallback.');
18
18
  assert.match(service, /transport:\s*'responses'/, 'Hermes REST responses should report their transport for terminal proof output.');
19
- assert.match(service, /gatewayArgs[\s\S]+\['gateway', 'run', '--replace'\]/, 'Pixcode should start Hermes gateway in replace mode so an existing gateway does not crash REST chat.');
19
+ assert.match(service, /resolveHermesGatewayHome/, 'Hermes REST gateway should run from a Pixcode-managed Hermes profile.');
20
+ assert.match(service, /seedHermesGatewayHome/, 'Hermes REST gateway should seed the managed profile from the user Hermes profile.');
21
+ assert.match(service, /PIXCODE_HERMES_GATEWAY_HOME/, 'Hermes REST gateway profile path should be overrideable for tests and advanced installs.');
22
+ assert.match(service, /PIXCODE_MANAGED_HERMES_ENV_PREFIXES/, 'Managed Hermes gateway profile should strip messaging platform env vars.');
23
+ assert.match(service, /copyHermesProfileEnv/, 'Managed Hermes gateway profile should copy a sanitized .env instead of raw platform credentials.');
24
+ assert.match(service, /gatewayArgs[\s\S]+\['gateway', 'run', '--replace'\]/, 'Pixcode can use replace mode safely inside its managed Hermes gateway profile.');
20
25
  assert.match(service, /gatewayExitMessage/, 'Hermes gateway failures should include recent stderr/stdout instead of only exit code 1.');
21
26
  assert.match(service, /API_SERVER_ENABLED:\s*'true'/, 'Hermes gateway env should enable the API server.');
22
27
  assert.match(service, /API_SERVER_KEY/, 'Hermes gateway env should set a bearer key.');
@@ -35,11 +40,14 @@ assert.match(routes, /router\.post\('\/gateway\/stop'/, 'Hermes router should ex
35
40
  assert.match(routes, /ensureHermesGateway/, 'Hermes router should use the managed gateway service.');
36
41
  assert.match(routes, /probeHermesGateway/, 'Hermes router should use the REST probe service.');
37
42
  assert.match(routes, /runHermesGatewayPrompt/, 'Hermes router should send chat prompts through the REST gateway service.');
43
+ assert.match(routes, /resolveHermesMcpBaseUrl/, 'Hermes MCP should be configured against the local Pixcode API URL instead of the browser request host.');
44
+ assert.match(routes, /probeExisting:\s*false/, 'Hermes chat should reuse a running gateway instead of killing it on a transient probe failure.');
38
45
 
39
46
  assert.match(mcpServer, /pixcode_get_hermes_gateway_status/, 'Pixcode MCP should let Hermes inspect gateway status.');
40
47
  assert.match(mcpServer, /pixcode_probe_hermes_gateway/, 'Pixcode MCP should let Hermes trigger a REST probe.');
41
48
  assert.match(configureMcp, /pixcode_get_hermes_gateway_status/, 'Hermes MCP config should include gateway status tool.');
42
49
  assert.match(configureMcp, /pixcode_probe_hermes_gateway/, 'Hermes MCP config should include gateway probe tool.');
50
+ assert.match(configureMcp, /mcp-pixcode/, 'Hermes MCP config should enable the Pixcode MCP toolset for the real CLI.');
43
51
 
44
52
  assert.match(settingsTab, /gateway\/status/, 'Hermes settings should read gateway status.');
45
53
  assert.match(settingsTab, /gateway\/start/, 'Hermes settings should start the REST gateway via API.');
@@ -0,0 +1,166 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
7
+ const read = (relativePath) => fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
8
+
9
+ const settingsTab = read('src/components/settings/view/tabs/HermesSettingsTab.tsx');
10
+ const settingsModal = read('src/components/settings/view/Settings.tsx');
11
+ const workbench = read('src/components/vscode-workbench/view/VSCodeWorkbench.tsx');
12
+ const serverIndex = read('server/index.js');
13
+ const shellTypes = read('src/components/shell/types/types.ts');
14
+ const shellRuntime = read('src/components/shell/hooks/useShellRuntime.ts');
15
+ const shellConnection = read('src/components/shell/hooks/useShellConnection.ts');
16
+ const shellView = read('src/components/shell/view/Shell.tsx');
17
+ const standaloneShell = read('src/components/standalone-shell/view/StandaloneShell.tsx');
18
+ const hermesRoutes = read('server/modules/orchestration/hermes/hermes.routes.ts');
19
+ const pixcodeMcpServer = read('scripts/hermes/pixcode-mcp-server.mjs');
20
+
21
+ assert.match(
22
+ settingsTab,
23
+ /HERMES_SETTINGS_COMMANDS/,
24
+ 'Hermes settings should define a visible command launcher list.',
25
+ );
26
+ assert.match(settingsTab, /command:\s*'hermes model'/, 'Hermes settings should expose the interactive provider/model wizard.');
27
+ assert.match(settingsTab, /command:\s*'hermes auth'/, 'Hermes settings should expose the credential manager.');
28
+ assert.match(settingsTab, /command:\s*'hermes setup tools'/, 'Hermes settings should expose tool setup.');
29
+ assert.match(settingsTab, /command:\s*'hermes doctor'/, 'Hermes settings should expose diagnostics.');
30
+ assert.match(
31
+ settingsTab,
32
+ /pixcode:hermes-terminal[\s\S]+command[\s\S]+title/,
33
+ 'Hermes settings should dispatch command and title to the workbench terminal.',
34
+ );
35
+ assert.match(
36
+ settingsModal,
37
+ /<HermesSettingsTab onClose=\{onClose\} \/>/,
38
+ 'Opening a Hermes command from settings should be able to close the settings modal so the terminal is visible.',
39
+ );
40
+ assert.match(
41
+ workbench,
42
+ /bottomTerminalCommand/,
43
+ 'Workbench bottom terminal should track the selected Hermes command separately from the mode.',
44
+ );
45
+ assert.match(
46
+ workbench,
47
+ /detail:\s*CustomEvent<\{ mode\?: string; command\?: string; title\?: string \}>/,
48
+ 'Workbench should accept Hermes terminal command events from settings.',
49
+ );
50
+ assert.match(
51
+ workbench,
52
+ /command=\{hermesCommand\}/,
53
+ 'Hermes bottom terminal should launch the requested Hermes command, not only bare hermes.',
54
+ );
55
+ assert.match(
56
+ workbench,
57
+ /HERMES_DEFAULT_COMMAND\s*=\s*'hermes --yolo'/,
58
+ 'Hermes terminal should default to --yolo so Hermes approval prompts do not stop visible work.',
59
+ );
60
+ assert.match(
61
+ serverIndex,
62
+ /HERMES_CLI_COMMAND_PATTERN/,
63
+ 'Backend should recognize safe Hermes subcommands for Pixcode MCP setup.',
64
+ );
65
+ assert.doesNotMatch(
66
+ serverIndex,
67
+ /command\.trim\(\) === 'hermes'/,
68
+ 'Backend should not limit Pixcode MCP setup to only bare hermes.',
69
+ );
70
+ assert.match(
71
+ pixcodeMcpServer,
72
+ /pixcode_read_cli_terminal/,
73
+ 'Pixcode MCP should expose a terminal transcript reader so Hermes can report provider CLI output.',
74
+ );
75
+ assert.match(
76
+ pixcodeMcpServer,
77
+ /Use this instead of Hermes shell\/proc\/skill execution/,
78
+ 'Pixcode MCP tool descriptions should explicitly steer Hermes away from non-visible provider proc launches.',
79
+ );
80
+ assert.match(
81
+ pixcodeMcpServer,
82
+ /multi-step|piece-by-piece|long-running/i,
83
+ 'Pixcode MCP should tell Hermes to send arbitrary multi-step work as visible provider terminal input.',
84
+ );
85
+ assert.match(
86
+ pixcodeMcpServer,
87
+ /startup input typed into the provider CLI/,
88
+ 'Pixcode MCP should describe prompt as real terminal input, not audit text.',
89
+ );
90
+ assert.match(
91
+ pixcodeMcpServer,
92
+ /startupInput/,
93
+ 'Pixcode MCP should use startupInput for text typed into provider CLIs.',
94
+ );
95
+ assert.match(
96
+ pixcodeMcpServer,
97
+ /audit\/reason text/,
98
+ 'Pixcode MCP should keep prompt as audit text so Hermes does not type explanations into Codex.',
99
+ );
100
+ assert.match(
101
+ hermesRoutes,
102
+ /startupInput/,
103
+ 'Hermes terminal launch events should carry startupInput separately from prompt.',
104
+ );
105
+ assert.match(
106
+ pixcodeMcpServer,
107
+ /bypassPermissions/,
108
+ 'Pixcode MCP should let Hermes request provider permission bypass for visible work.',
109
+ );
110
+ assert.match(
111
+ hermesRoutes,
112
+ /bypassPermissions|skipPermissions/,
113
+ 'Hermes terminal launch events should carry permission bypass state.',
114
+ );
115
+ assert.match(
116
+ hermesRoutes,
117
+ /permissionMode/,
118
+ 'Hermes terminal launch events should carry provider permission mode.',
119
+ );
120
+ assert.match(
121
+ shellTypes,
122
+ /ShellPermissionOverride/,
123
+ 'Shell types should expose a launch-scoped permission override contract.',
124
+ );
125
+ assert.match(
126
+ shellRuntime,
127
+ /permissionOverride/,
128
+ 'Shell runtime should keep launch-scoped permission overrides in refs.',
129
+ );
130
+ assert.match(
131
+ shellConnection,
132
+ /permissionOverrideRef/,
133
+ 'Shell websocket init should read launch-scoped permission overrides.',
134
+ );
135
+ assert.match(
136
+ shellView,
137
+ /permissionOverride/,
138
+ 'Shell view should accept launch-scoped permission overrides.',
139
+ );
140
+ assert.match(
141
+ standaloneShell,
142
+ /permissionOverride/,
143
+ 'Standalone shell should pass launch-scoped permission overrides through to Shell.',
144
+ );
145
+ assert.match(
146
+ workbench,
147
+ /terminalPermissionOverride/,
148
+ 'Workbench should apply Hermes launch permission bypass to the provider shell.',
149
+ );
150
+ assert.match(
151
+ settingsTab,
152
+ /hermes-agent\.png/,
153
+ 'Hermes settings should use the Hermes logo asset instead of an H glyph.',
154
+ );
155
+ assert.match(
156
+ workbench,
157
+ /hermes-agent\.png/,
158
+ 'Workbench Hermes launch surfaces should use the Hermes logo asset instead of an H glyph.',
159
+ );
160
+ assert.match(
161
+ serverIndex,
162
+ /\/api\/shell\/sessions\/provider-output/,
163
+ 'Backend should expose recent provider terminal output for Pixcode MCP readback.',
164
+ );
165
+
166
+ console.log('hermes settings command smoke passed');
@@ -65,10 +65,10 @@ assert.match(workbench, /BOTTOM_TERMINAL_MIN_HEIGHT/, 'Bottom terminal should su
65
65
  assert.match(workbench, /isBottomTerminalMinimized/, 'Bottom terminal should support minimizing without closing.');
66
66
  assert.match(workbench, /isPlainShell/, 'Bottom terminal should open the selected project folder without starting the selected AI CLI.');
67
67
  assert.doesNotMatch(workbench, /HERMES_AGENT_START_COMMAND/, 'Hermes Agent should not launch from the bottom terminal through a server-side sentinel.');
68
- assert.match(workbench, /HermesApiChatPanel/, 'Hermes Agent should render a REST-backed chat panel in the bottom area.');
69
- assert.match(workbench, /\/api\/orchestration\/hermes\/gateway\/chat/, 'Hermes chat panel should send prompts through the Pixcode gateway chat API.');
70
- assert.match(workbench, /HermesTerminalTranscript/, 'Hermes REST panel should render as a terminal transcript, not chat bubbles.');
71
- assert.match(workbench, /REST POST \//, 'Hermes terminal transcript should show the REST endpoint used for each reply.');
68
+ assert.doesNotMatch(workbench, /HermesApiChatPanel|HermesTerminalTranscript/, 'Hermes Agent should use the real PTY terminal UI, not a custom REST chat transcript.');
69
+ assert.doesNotMatch(workbench, /REST POST \/|transport=|response=|gateway=http/, 'Hermes terminal UI must not expose REST debug internals to the user.');
70
+ assert.match(workbench, /command="hermes"/, 'Hermes Agent bottom panel should launch the actual `hermes` CLI in a PTY.');
71
+ assert.match(workbench, /Pixcode MCP Live/, 'Hermes terminal should show a user-facing Pixcode MCP live badge.');
72
72
  assert.doesNotMatch(workbench, /ml-auto border-blue-500\/40 bg-blue-500\/10/, 'Hermes REST panel must not use right-aligned chat bubbles.');
73
73
  assert.match(workbench, /terminal-launches\/stream/, 'Hermes CLI launch requests should arrive through an EventSource stream.');
74
74
  assert.doesNotMatch(workbench, /HERMES_TERMINAL_LAUNCH_POLL_MS|setInterval\([\s\S]*terminal-launches/, 'Hermes CLI launch requests should not be polled every few seconds.');
@@ -89,6 +89,8 @@ assert.match(serverIndex, /\/api\/shell\/sessions\/terminate/, 'Backend should e
89
89
  assert.match(serverIndex, /isPlainShell && !initialCommand/, 'Backend should spawn an interactive plain shell when no terminal command is provided.');
90
90
  assert.doesNotMatch(serverIndex, /pixcode:hermes:start/, 'Backend should not need a Hermes terminal sentinel for the workbench Hermes panel.');
91
91
  assert.doesNotMatch(serverIndex, /hermesCommand/, 'Provider shell starts should not reference the removed Hermes sentinel variable.');
92
+ assert.match(serverIndex, /configure-pixcode-mcp\.mjs/, 'Hermes PTY launches should configure Pixcode MCP before starting the CLI.');
93
+ assert.match(serverIndex, /resolveHermesMcpBaseUrl/, 'Hermes MCP should use the local Pixcode API base URL from the host process.');
92
94
  assert.doesNotMatch(hermesInstallJobs, /iex \(irm https:\/\/raw\.githubusercontent\.com\/NousResearch\/hermes-agent\/main\/scripts\/install\.ps1\)/, 'Windows Hermes install should avoid the old inline iex pattern.');
93
95
  assert.doesNotMatch(hermesInstallJobs, /scriptblock\]::Create\(\(irm https:\/\/raw\.githubusercontent\.com\/NousResearch\/hermes-agent\/main\/scripts\/install\.ps1\)\)/, 'Windows Hermes install should avoid scriptblock Invoke-RestMethod eval patterns.');
94
96
  assert.match(hermesInstallJobs, /downloadHermesInstaller/, 'Windows Hermes install should download the installer through backend API code before running it.');
@@ -118,12 +120,18 @@ assert.match(hermesRoutes, /createHermesRouter/, 'Hermes should have a dedicated
118
120
  assert.match(hermesRoutes, /terminal-launches/, 'Hermes MCP should be able to request visible Pixcode CLI terminal launches.');
119
121
  assert.match(hermesRoutes, /terminal-launches\/stream/, 'Hermes MCP terminal launch requests should stream to the workbench over SSE.');
120
122
  assert.match(hermesRoutes, /hermesTerminalLaunchEmitter/, 'Hermes terminal launch stream should broadcast new events instead of relying on polling.');
121
- assert.match(hermesRoutes, /router\.post\('\/gateway\/chat'/, 'Hermes should expose a REST chat endpoint for the bottom panel.');
123
+ assert.match(hermesRoutes, /router\.post\('\/gateway\/chat'/, 'Hermes should still expose a REST chat endpoint for health checks and integrations.');
122
124
  assert.match(hermesRoutes, /install-status/, 'Hermes settings and terminal UI should have an install-status endpoint.');
123
125
  assert.match(hermesRoutes, /router\.post\('\/install'/, 'Hermes should install through the backend API instead of terminal command paste.');
124
126
  assert.match(read('scripts/smoke/hermes-api-install.mjs'), /hermes API install smoke passed/, 'Hermes API install behavior should have a focused smoke test.');
125
127
  assert.match(workbench, /terminalStartupInput/, 'Hermes terminal launch prompts should be passed into the selected CLI.');
126
128
  assert.match(read('src/components/shell/hooks/useShellConnection.ts'), /startupInputRef/, 'Shell connections should support one-shot startup input for Hermes-triggered CLI work.');
129
+ assert.match(read('src/components/shell/hooks/useShellConnection.ts'), /isCliReadyForStartupInput/, 'Hermes-triggered CLI input should wait until the provider TUI is ready.');
130
+ assert.doesNotMatch(read('src/components/shell/hooks/useShellConnection.ts'), /TERMINAL_INIT_DELAY_MS \* 3/, 'Hermes-triggered CLI input should not be sent on a blind fixed delay.');
131
+ assert.match(shellTerminal, /handleTerminalPaste/, 'Terminal should support browser paste events.');
132
+ assert.match(shellTerminal, /handleCopyPasteShortcut/, 'Terminal should normalize Ctrl/Cmd copy and paste shortcuts.');
133
+ assert.match(shellTerminal, /event\.shiftKey/, 'Terminal should support Ctrl+Shift+C/V style shortcuts.');
134
+ assert.match(shellTerminal, /copyTerminalSelection/, 'Terminal should copy selected terminal text through shortcuts.');
127
135
  assert.match(serverIndex, /forceNewSession/, 'Shell backend should support explicit fresh-session launches from the workbench.');
128
136
  assert.match(serverIndex, /killProviderPtySessions/, 'Shell backend should terminate old provider PTYs when a fresh CLI session is requested.');
129
137
 
package/server/index.js CHANGED
@@ -390,6 +390,37 @@ function resolvePublicBaseUrl(request) {
390
390
  return `${proto}://${String(host).split(',')[0].trim()}`;
391
391
  }
392
392
 
393
+ function resolveHermesMcpBaseUrl() {
394
+ const configured = process.env.PIXCODE_INTERNAL_BASE_URL || process.env.PIXCODE_HERMES_BASE_URL;
395
+ if (configured) return configured.replace(/\/$/, '');
396
+
397
+ return `http://127.0.0.1:${process.env.SERVER_PORT || process.env.PORT || '3001'}`;
398
+ }
399
+
400
+ function quoteBashArg(value) {
401
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
402
+ }
403
+
404
+ function quotePowerShellArg(value) {
405
+ return `"${String(value).replace(/`/g, '``').replace(/\$/g, '`$').replace(/"/g, '`"')}"`;
406
+ }
407
+
408
+ const HERMES_CLI_COMMAND_PATTERN = /^hermes(?:\s+[A-Za-z0-9._:/=@+-]+)*\s*$/;
409
+
410
+ function isHermesCliCommand(command) {
411
+ return typeof command === 'string' && HERMES_CLI_COMMAND_PATTERN.test(command.trim());
412
+ }
413
+
414
+ function buildHermesCliCommand(command) {
415
+ const hermesCommand = typeof command === 'string' && command.trim() ? command.trim() : 'hermes';
416
+ const configureScript = path.join(APP_ROOT, 'scripts', 'hermes', 'configure-pixcode-mcp.mjs');
417
+ if (os.platform() === 'win32') {
418
+ return `& ${quotePowerShellArg(process.execPath)} ${quotePowerShellArg(configureScript)} *> $null; ${hermesCommand}`;
419
+ }
420
+
421
+ return `${quoteBashArg(process.execPath)} ${quoteBashArg(configureScript)} >/dev/null 2>&1; exec ${hermesCommand}`;
422
+ }
423
+
393
424
  function getOrCreateHermesApiKey(userId) {
394
425
  if (!userId) return null;
395
426
 
@@ -492,6 +523,55 @@ app.post('/api/shell/sessions/terminate', authenticateToken, (req, res) => {
492
523
  res.json({ success: true, killedSessions });
493
524
  });
494
525
 
526
+ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) => {
527
+ const provider = String(req.query.provider || 'claude');
528
+ const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
529
+ ? req.query.projectPath.trim()
530
+ : null;
531
+ const maxChars = Math.min(
532
+ 20000,
533
+ Math.max(1000, Number.parseInt(String(req.query.maxChars || '12000'), 10) || 12000)
534
+ );
535
+
536
+ if (!SHELL_CLI_PROVIDERS.has(provider)) {
537
+ return res.status(400).json({ error: 'Unsupported provider' });
538
+ }
539
+
540
+ const requestedProjectPath = projectPath ? path.resolve(projectPath) : null;
541
+ let matchedSession = null;
542
+ for (const session of ptySessionsMap.values()) {
543
+ if (
544
+ session?.provider === provider &&
545
+ !session?.isPlainShell &&
546
+ (!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath)
547
+ ) {
548
+ if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
549
+ matchedSession = session;
550
+ }
551
+ }
552
+ }
553
+
554
+ if (!matchedSession) {
555
+ return res.json({
556
+ active: false,
557
+ provider,
558
+ projectPath: requestedProjectPath,
559
+ output: '',
560
+ message: 'No active provider terminal session found for this project.',
561
+ });
562
+ }
563
+
564
+ const rawOutput = matchedSession.buffer.join('').slice(-maxChars);
565
+ res.json({
566
+ active: true,
567
+ provider,
568
+ projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
569
+ sessionId: matchedSession.sessionId || null,
570
+ updatedAt: matchedSession.updatedAt || null,
571
+ output: stripAnsiSequences(rawOutput),
572
+ });
573
+ });
574
+
495
575
  // Authentication routes (public)
496
576
  app.use('/api/auth', authRoutes);
497
577
 
@@ -2182,10 +2262,14 @@ function handleShellConnection(ws, request) {
2182
2262
  const provider = data.provider || 'claude';
2183
2263
  const initialCommand = data.initialCommand;
2184
2264
  const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
2265
+ const isHermesCliLaunch = isPlainShell && isHermesCliCommand(initialCommand);
2185
2266
  const forceNewSession = Boolean(data.forceNewSession);
2186
2267
  const shellPermissionMode = normalizeShellPermissionMode(data.permissionMode);
2187
2268
  const shellSkipPermissions = Boolean(data.skipPermissions);
2188
2269
  const shellPermissionFlags = buildProviderShellPermissionFlags(provider, shellPermissionMode, shellSkipPermissions);
2270
+ const hermesApiKey = isHermesCliLaunch
2271
+ ? getOrCreateHermesApiKey(request.user?.id ?? request.user?.userId ?? null)
2272
+ : null;
2189
2273
  urlDetectionBuffer = '';
2190
2274
  announcedAuthUrls.clear();
2191
2275
 
@@ -2314,7 +2398,9 @@ function handleShellConnection(ws, request) {
2314
2398
  let shellCommand;
2315
2399
  if (isPlainShell) {
2316
2400
  // Plain shell mode without an initial command must stay interactive.
2317
- shellCommand = initialCommand || null;
2401
+ shellCommand = isHermesCliLaunch
2402
+ ? buildHermesCliCommand(initialCommand)
2403
+ : initialCommand || null;
2318
2404
  } else if (provider === 'cursor') {
2319
2405
  const command = buildProviderShellCommand('cursor-agent', shellPermissionFlags);
2320
2406
  if (hasSession && sessionId) {
@@ -2429,6 +2515,11 @@ function handleShellConnection(ws, request) {
2429
2515
  TERM: 'xterm-256color',
2430
2516
  COLORTERM: 'truecolor',
2431
2517
  FORCE_COLOR: '3',
2518
+ ...(isHermesCliLaunch ? {
2519
+ PIXCODE_BASE_URL: resolveHermesMcpBaseUrl(),
2520
+ PIXCODE_API_KEY: hermesApiKey || '',
2521
+ PIXCODE_APP_ROOT: APP_ROOT,
2522
+ } : {}),
2432
2523
  });
2433
2524
 
2434
2525
  shellProcess = pty.spawn(shell, shellArgs, {
@@ -2451,12 +2542,14 @@ function handleShellConnection(ws, request) {
2451
2542
  provider,
2452
2543
  isPlainShell,
2453
2544
  keepAliveUntilExit: false,
2545
+ updatedAt: Date.now(),
2454
2546
  });
2455
2547
 
2456
2548
  // Handle data output
2457
2549
  shellProcess.onData((data) => {
2458
2550
  const session = ptySessionsMap.get(ptySessionKey);
2459
2551
  if (!session) return;
2552
+ session.updatedAt = Date.now();
2460
2553
 
2461
2554
  if (session.buffer.length < 5000) {
2462
2555
  session.buffer.push(data);
@@ -29,6 +29,10 @@ type HermesTerminalLaunchEvent = {
29
29
  provider: string;
30
30
  projectPath: string | null;
31
31
  prompt: string | null;
32
+ startupInput: string | null;
33
+ permissionMode: string | null;
34
+ skipPermissions: boolean;
35
+ bypassPermissions: boolean;
32
36
  source: string;
33
37
  createdAt: string;
34
38
  };
@@ -60,6 +64,13 @@ function readUserId(req: PixcodeRequest) {
60
64
  return req.user?.id ?? req.user?.userId ?? null;
61
65
  }
62
66
 
67
+ function resolveHermesMcpBaseUrl() {
68
+ const configured = process.env.PIXCODE_INTERNAL_BASE_URL || process.env.PIXCODE_HERMES_BASE_URL;
69
+ if (configured) return configured.replace(/\/$/, '');
70
+
71
+ return `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}`;
72
+ }
73
+
63
74
  function readAfterId(req: Request) {
64
75
  const after = Number.parseInt(typeof req.query.after === 'string' ? req.query.after : '0', 10);
65
76
  return Number.isFinite(after) ? after : 0;
@@ -73,6 +84,24 @@ function rememberHermesTerminalLaunch(event: HermesTerminalLaunchEvent) {
73
84
  hermesTerminalLaunchEmitter.emit('terminal-launch', event);
74
85
  }
75
86
 
87
+ function readTrimmedString(value: unknown) {
88
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
89
+ }
90
+
91
+ function readBoolean(value: unknown) {
92
+ return value === true || value === 'true' || value === '1';
93
+ }
94
+
95
+ function isLegacyPromptLikelyStartupInput(prompt: string | null) {
96
+ if (!prompt || prompt.length > 160 || prompt.includes('\n')) return false;
97
+ if (/^[/:!@]/u.test(prompt)) return true;
98
+ if (prompt.includes(':')) return false;
99
+ if (/\b(user|request|reason|audit|task|kullanıcı|kullanicinin|istek|isteği|gorev|görev|terminal|codex|claude|qwen|gemini|cursor|opencode|open|aç|ac|başlat|baslat|send|gönder|gonder)\b/iu.test(prompt)) {
100
+ return false;
101
+ }
102
+ return prompt.length <= 80;
103
+ }
104
+
76
105
  export function createHermesRouter(options: HermesRouterOptions = {}): Router {
77
106
  const router = express.Router();
78
107
 
@@ -137,7 +166,7 @@ export function createHermesRouter(options: HermesRouterOptions = {}): Router {
137
166
  const gateway = await ensureHermesGateway({
138
167
  appRoot: options.appRoot ?? process.cwd(),
139
168
  pixcodeApiKey: apiKey,
140
- pixcodeBaseUrl: options.resolvePublicBaseUrl?.(req) ?? `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}`,
169
+ pixcodeBaseUrl: resolveHermesMcpBaseUrl(),
141
170
  projectPath: typeof body.projectPath === 'string' ? body.projectPath : undefined,
142
171
  });
143
172
  res.status(202).json(gateway);
@@ -172,7 +201,7 @@ export function createHermesRouter(options: HermesRouterOptions = {}): Router {
172
201
  await ensureHermesGateway({
173
202
  appRoot: options.appRoot ?? process.cwd(),
174
203
  pixcodeApiKey: apiKey,
175
- pixcodeBaseUrl: options.resolvePublicBaseUrl?.(req) ?? `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}`,
204
+ pixcodeBaseUrl: resolveHermesMcpBaseUrl(),
176
205
  projectPath: projectPath ?? undefined,
177
206
  });
178
207
  }
@@ -224,8 +253,9 @@ export function createHermesRouter(options: HermesRouterOptions = {}): Router {
224
253
  const gateway = await ensureHermesGateway({
225
254
  appRoot: options.appRoot ?? process.cwd(),
226
255
  pixcodeApiKey: apiKey,
227
- pixcodeBaseUrl: options.resolvePublicBaseUrl?.(req) ?? `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}`,
256
+ pixcodeBaseUrl: resolveHermesMcpBaseUrl(),
228
257
  projectPath,
258
+ probeExisting: false,
229
259
  });
230
260
  const run = await runHermesGatewayPrompt(projectPath, {
231
261
  input,
@@ -272,7 +302,7 @@ export function createHermesRouter(options: HermesRouterOptions = {}): Router {
272
302
  appRoot: options.appRoot ?? process.cwd(),
273
303
  force: Boolean(body.force),
274
304
  pixcodeApiKey: apiKey,
275
- pixcodeBaseUrl: options.resolvePublicBaseUrl?.(req) ?? `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}`,
305
+ pixcodeBaseUrl: resolveHermesMcpBaseUrl(),
276
306
  skipBrowser: body.skipBrowser !== false,
277
307
  });
278
308
 
@@ -440,15 +470,23 @@ export function createHermesRouter(options: HermesRouterOptions = {}): Router {
440
470
  const projectPath = typeof body.projectPath === 'string' && body.projectPath.trim()
441
471
  ? body.projectPath.trim()
442
472
  : null;
443
- const prompt = typeof body.prompt === 'string' && body.prompt.trim()
444
- ? body.prompt.trim()
445
- : null;
473
+ const prompt = readTrimmedString(body.prompt ?? body.reason);
474
+ const requestedStartupInput = readTrimmedString(body.startupInput ?? body.input);
475
+ const startupInput = requestedStartupInput ?? (isLegacyPromptLikelyStartupInput(prompt) ? prompt : null);
476
+ const bypassPermissions = readBoolean(body.bypassPermissions);
477
+ const skipPermissions = readBoolean(body.skipPermissions) || bypassPermissions;
478
+ const requestedPermissionMode = readTrimmedString(body.permissionMode);
479
+ const permissionMode = requestedPermissionMode ?? (skipPermissions ? 'bypassPermissions' : null);
446
480
 
447
481
  const event: HermesTerminalLaunchEvent = {
448
482
  id: nextHermesTerminalLaunchId,
449
483
  provider,
450
484
  projectPath,
451
485
  prompt,
486
+ startupInput,
487
+ permissionMode,
488
+ skipPermissions,
489
+ bypassPermissions,
452
490
  source: 'hermes-mcp',
453
491
  createdAt: new Date().toISOString(),
454
492
  };