@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.
- package/dist/assets/{index-DVpGrdpT.js → index-81sOpj25.js} +182 -182
- package/dist/assets/index-DMz0zv6T.css +32 -0
- package/dist/hermes-agent.png +0 -0
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +77 -1
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +40 -7
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
- package/dist-server/server/services/hermes-gateway.js +115 -1
- package/dist-server/server/services/hermes-gateway.js.map +1 -1
- package/dist-server/server/services/hermes-install-jobs.js +9 -1
- package/dist-server/server/services/hermes-install-jobs.js.map +1 -1
- package/package.json +1 -1
- package/scripts/hermes/configure-pixcode-mcp.mjs +37 -1
- package/scripts/hermes/pixcode-mcp-server.mjs +166 -8
- package/scripts/smoke/hermes-api-install.mjs +4 -4
- package/scripts/smoke/hermes-gateway-persistence.mjs +104 -0
- package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +27 -0
- package/scripts/smoke/hermes-rest-chat-live.mjs +4 -0
- package/scripts/smoke/hermes-rest-codex-launch.mjs +11 -4
- package/scripts/smoke/hermes-rest-gateway.mjs +9 -1
- package/scripts/smoke/hermes-settings-commands.mjs +166 -0
- package/scripts/smoke/pixcode-workbench-1-48.mjs +13 -5
- package/server/index.js +94 -1
- package/server/modules/orchestration/hermes/hermes.routes.ts +45 -7
- package/server/services/hermes-gateway.js +128 -1
- package/server/services/hermes-install-jobs.js +11 -1
- 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
|
|
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,
|
|
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)},
|
|
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.
|
|
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, /
|
|
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.
|
|
69
|
-
assert.
|
|
70
|
-
assert.match(workbench, /
|
|
71
|
-
assert.match(workbench, /
|
|
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
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
444
|
-
|
|
445
|
-
|
|
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
|
};
|