@pixelbyte-software/pixcode 1.49.8 → 1.49.10
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-BT6txdBK.css +32 -0
- package/dist/assets/{index-Cwsu2tLq.js → index-DVpGrdpT.js} +94 -94
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +2 -2
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/services/hermes-gateway.js +190 -4
- package/dist-server/server/services/hermes-gateway.js.map +1 -1
- package/dist-server/server/services/hermes-install-jobs.js +51 -5
- package/dist-server/server/services/hermes-install-jobs.js.map +1 -1
- package/package.json +1 -1
- package/scripts/smoke/hermes-api-install.mjs +2 -0
- package/scripts/smoke/hermes-rest-chat-api.mjs +131 -0
- package/scripts/smoke/hermes-rest-chat-live.mjs +41 -0
- package/scripts/smoke/hermes-rest-gateway.mjs +6 -1
- package/scripts/smoke/hermes-smoke-launcher-guard.mjs +34 -0
- package/scripts/smoke/pixcode-workbench-1-48.mjs +4 -0
- package/server/index.js +2 -2
- package/server/services/hermes-gateway.js +199 -4
- package/server/services/hermes-install-jobs.js +48 -5
- package/dist/assets/index-Bw6PxVkB.css +0 -32
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
runHermesGatewayPrompt,
|
|
9
|
+
stopHermesGateway,
|
|
10
|
+
} from '../../server/services/hermes-gateway.js';
|
|
11
|
+
|
|
12
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
13
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'pixcode-hermes-chat-api-'));
|
|
14
|
+
const fakeHermes = path.join(tempRoot, 'hermes');
|
|
15
|
+
const projectPath = path.join(tempRoot, 'project');
|
|
16
|
+
const hermesHome = path.join(tempRoot, 'home');
|
|
17
|
+
|
|
18
|
+
await fs.mkdir(projectPath, { recursive: true });
|
|
19
|
+
await fs.writeFile(fakeHermes, `#!/usr/bin/env node
|
|
20
|
+
import http from 'node:http';
|
|
21
|
+
|
|
22
|
+
if (process.argv.includes('--version')) {
|
|
23
|
+
console.log('Hermes Agent v0.0.0 smoke');
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!process.argv.includes('gateway')) {
|
|
28
|
+
console.error('expected gateway');
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const host = process.env.API_SERVER_HOST || '127.0.0.1';
|
|
33
|
+
const port = Number(process.env.API_SERVER_PORT || 8642);
|
|
34
|
+
const key = process.env.API_SERVER_KEY || '';
|
|
35
|
+
const server = http.createServer(async (req, res) => {
|
|
36
|
+
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
37
|
+
res.setHeader('content-type', 'application/json');
|
|
38
|
+
if (url.pathname !== '/health' && req.headers.authorization !== \`Bearer \${key}\`) {
|
|
39
|
+
res.statusCode = 401;
|
|
40
|
+
res.end(JSON.stringify({ error: 'bad auth' }));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (req.method === 'GET' && url.pathname === '/health') {
|
|
44
|
+
res.end(JSON.stringify({ ok: true }));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (req.method === 'GET' && url.pathname === '/v1/capabilities') {
|
|
48
|
+
res.end(JSON.stringify({ capabilities: ['chat'] }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (req.method === 'GET' && url.pathname === '/v1/models') {
|
|
52
|
+
res.end(JSON.stringify({ data: [{ id: 'hermes-agent' }] }));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (req.method === 'POST' && url.pathname === '/v1/chat/completions') {
|
|
56
|
+
let body = '';
|
|
57
|
+
for await (const chunk of req) body += chunk.toString();
|
|
58
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
59
|
+
res.end(JSON.stringify({
|
|
60
|
+
id: 'chatcmpl-smoke',
|
|
61
|
+
choices: [{
|
|
62
|
+
index: 0,
|
|
63
|
+
message: {
|
|
64
|
+
role: 'assistant',
|
|
65
|
+
content: \`pixcode-hermes-chat-ok via \${parsed.model}\`,
|
|
66
|
+
},
|
|
67
|
+
finish_reason: 'stop',
|
|
68
|
+
}],
|
|
69
|
+
}));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (req.method === 'POST' && url.pathname === '/v1/responses') {
|
|
73
|
+
let body = '';
|
|
74
|
+
for await (const chunk of req) body += chunk.toString();
|
|
75
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
76
|
+
res.end(JSON.stringify({
|
|
77
|
+
id: 'resp-smoke',
|
|
78
|
+
object: 'response',
|
|
79
|
+
status: 'completed',
|
|
80
|
+
model: parsed.model || 'hermes-agent',
|
|
81
|
+
output: [{
|
|
82
|
+
type: 'message',
|
|
83
|
+
role: 'assistant',
|
|
84
|
+
content: [{
|
|
85
|
+
type: 'output_text',
|
|
86
|
+
text: \`pixcode-hermes-rest-ok via \${parsed.model}\`,
|
|
87
|
+
}],
|
|
88
|
+
}],
|
|
89
|
+
}));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
res.statusCode = 404;
|
|
93
|
+
res.end(JSON.stringify({ error: url.pathname }));
|
|
94
|
+
});
|
|
95
|
+
server.listen(port, host);
|
|
96
|
+
`, { mode: 0o755 });
|
|
97
|
+
|
|
98
|
+
process.env.HERMES_CLI_PATH = fakeHermes;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const gateway = await ensureHermesGateway({
|
|
102
|
+
appRoot: repoRoot,
|
|
103
|
+
projectPath,
|
|
104
|
+
hermesHome,
|
|
105
|
+
pixcodeBaseUrl: 'http://127.0.0.1:9',
|
|
106
|
+
pixcodeApiKey: 'px_chat_api_smoke_key',
|
|
107
|
+
port: 18752,
|
|
108
|
+
allowSmokeHermes: true,
|
|
109
|
+
repairLaunchers: false,
|
|
110
|
+
});
|
|
111
|
+
if (!gateway.running || !gateway.probe?.ok) {
|
|
112
|
+
throw new Error(`Fake Hermes gateway did not start cleanly: ${JSON.stringify(gateway)}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const run = await runHermesGatewayPrompt(projectPath, {
|
|
116
|
+
input: 'selam',
|
|
117
|
+
timeoutMs: 10000,
|
|
118
|
+
});
|
|
119
|
+
if (!run.ok || run.transport !== 'responses' || !String(run.message || '').includes('pixcode-hermes-rest-ok')) {
|
|
120
|
+
throw new Error(`Hermes REST chat did not use responses: ${JSON.stringify(run)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(JSON.stringify({
|
|
124
|
+
ok: true,
|
|
125
|
+
transport: run.transport,
|
|
126
|
+
message: run.message,
|
|
127
|
+
}, null, 2));
|
|
128
|
+
} finally {
|
|
129
|
+
stopHermesGateway(projectPath);
|
|
130
|
+
delete process.env.HERMES_CLI_PATH;
|
|
131
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ensureHermesGateway,
|
|
6
|
+
runHermesGatewayPrompt,
|
|
7
|
+
stopHermesGateway,
|
|
8
|
+
} from '../../server/services/hermes-gateway.js';
|
|
9
|
+
|
|
10
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
11
|
+
const projectPath = path.resolve(process.argv[2] || repoRoot);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const gateway = await ensureHermesGateway({
|
|
15
|
+
appRoot: repoRoot,
|
|
16
|
+
projectPath,
|
|
17
|
+
pixcodeBaseUrl: 'http://127.0.0.1:9',
|
|
18
|
+
pixcodeApiKey: 'px_live_chat_smoke_key',
|
|
19
|
+
port: Number(process.env.PIXCODE_HERMES_LIVE_CHAT_PORT || 18652),
|
|
20
|
+
});
|
|
21
|
+
if (!gateway.running || !gateway.probe?.ok) {
|
|
22
|
+
throw new Error(`Hermes gateway did not start cleanly: ${JSON.stringify(gateway)}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const run = await runHermesGatewayPrompt(projectPath, {
|
|
26
|
+
input: 'Reply with exactly: pixcode-hermes-chat-ok',
|
|
27
|
+
timeoutMs: 90000,
|
|
28
|
+
});
|
|
29
|
+
if (!run.ok || !String(run.message || '').includes('pixcode-hermes-chat-ok')) {
|
|
30
|
+
throw new Error(`Hermes chat did not return the expected response: ${JSON.stringify(run)}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(JSON.stringify({
|
|
34
|
+
ok: true,
|
|
35
|
+
transport: run.transport,
|
|
36
|
+
status: run.status,
|
|
37
|
+
message: run.message,
|
|
38
|
+
}, null, 2));
|
|
39
|
+
} finally {
|
|
40
|
+
stopHermesGateway(projectPath);
|
|
41
|
+
}
|
|
@@ -13,10 +13,15 @@ assert.match(service, /export async function ensureHermesGateway/, 'Pixcode shou
|
|
|
13
13
|
assert.match(service, /export async function probeHermesGateway/, 'Pixcode should probe Hermes through its REST API.');
|
|
14
14
|
assert.match(service, /export async function runHermesGatewayPrompt/, 'Pixcode should submit Hermes prompts through the managed REST gateway.');
|
|
15
15
|
assert.match(service, /export function stopHermesGateway/, 'Pixcode should be able to stop a managed Hermes gateway process.');
|
|
16
|
+
assert.match(service, /\/v1\/chat\/completions/, 'Hermes UI chat should use the documented OpenAI-compatible chat completions endpoint first.');
|
|
17
|
+
assert.match(service, /\/v1\/responses/, 'Hermes UI chat should use the stateful OpenAI-compatible responses endpoint before legacy chat fallback.');
|
|
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.');
|
|
20
|
+
assert.match(service, /gatewayExitMessage/, 'Hermes gateway failures should include recent stderr/stdout instead of only exit code 1.');
|
|
16
21
|
assert.match(service, /API_SERVER_ENABLED:\s*'true'/, 'Hermes gateway env should enable the API server.');
|
|
17
22
|
assert.match(service, /API_SERVER_KEY/, 'Hermes gateway env should set a bearer key.');
|
|
18
23
|
assert.match(service, /API_SERVER_PORT/, 'Hermes gateway env should choose a REST port.');
|
|
19
|
-
assert.match(service, /spawn\(installStatus\.command,\s
|
|
24
|
+
assert.match(service, /spawn\(installStatus\.command,\s*gatewayArgs/, 'Pixcode should start Hermes with explicit gateway args for REST control.');
|
|
20
25
|
assert.match(service, /\/health/, 'Gateway probe should call Hermes health.');
|
|
21
26
|
assert.match(service, /\/v1\/capabilities/, 'Gateway probe should verify Hermes capabilities.');
|
|
22
27
|
assert.match(service, /\/v1\/models/, 'Gateway probe should verify OpenAI-compatible model discovery.');
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
readHermesInstallStatus,
|
|
8
|
+
} from '../../server/services/hermes-install-jobs.js';
|
|
9
|
+
|
|
10
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'pixcode-hermes-smoke-guard-'));
|
|
11
|
+
const fakeHermes = path.join(tempRoot, 'hermes');
|
|
12
|
+
|
|
13
|
+
await fs.writeFile(fakeHermes, `#!/usr/bin/env bash
|
|
14
|
+
if [ "$1" = "--version" ]; then
|
|
15
|
+
echo "Hermes Agent v0.0.0 smoke"
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
echo "fake smoke hermes should not run"
|
|
19
|
+
exit 2
|
|
20
|
+
`, { mode: 0o755 });
|
|
21
|
+
|
|
22
|
+
const status = readHermesInstallStatus({
|
|
23
|
+
...process.env,
|
|
24
|
+
HERMES_CLI_PATH: fakeHermes,
|
|
25
|
+
PATH: tempRoot,
|
|
26
|
+
}, {
|
|
27
|
+
repairLaunchers: false,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.notEqual(status.command, fakeHermes, 'Smoke-test HERMES_CLI_PATH must not be selected as the Hermes command.');
|
|
31
|
+
assert.doesNotMatch(String(status.version || ''), /smoke/i, 'Smoke-test Hermes version output must not be reported as installed.');
|
|
32
|
+
assert.doesNotMatch(String(status.error || ''), /fake smoke hermes should not run/i, 'Smoke launcher should be rejected before any non-version use.');
|
|
33
|
+
|
|
34
|
+
console.log('hermes smoke launcher guard passed');
|
|
@@ -67,6 +67,9 @@ assert.match(workbench, /isPlainShell/, 'Bottom terminal should open the selecte
|
|
|
67
67
|
assert.doesNotMatch(workbench, /HERMES_AGENT_START_COMMAND/, 'Hermes Agent should not launch from the bottom terminal through a server-side sentinel.');
|
|
68
68
|
assert.match(workbench, /HermesApiChatPanel/, 'Hermes Agent should render a REST-backed chat panel in the bottom area.');
|
|
69
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.');
|
|
72
|
+
assert.doesNotMatch(workbench, /ml-auto border-blue-500\/40 bg-blue-500\/10/, 'Hermes REST panel must not use right-aligned chat bubbles.');
|
|
70
73
|
assert.match(workbench, /terminal-launches\/stream/, 'Hermes CLI launch requests should arrive through an EventSource stream.');
|
|
71
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.');
|
|
72
75
|
assert.doesNotMatch(workbench, /Project-scoped agent terminal\. Installs Hermes when missing/, 'Right CLI panel should not show the old Hermes card.');
|
|
@@ -85,6 +88,7 @@ assert.doesNotMatch(workbench, /suspendAutoConnect/, 'Right CLI provider starts
|
|
|
85
88
|
assert.match(serverIndex, /\/api\/shell\/sessions\/terminate/, 'Backend should expose an authenticated endpoint to terminate cached provider PTYs immediately.');
|
|
86
89
|
assert.match(serverIndex, /isPlainShell && !initialCommand/, 'Backend should spawn an interactive plain shell when no terminal command is provided.');
|
|
87
90
|
assert.doesNotMatch(serverIndex, /pixcode:hermes:start/, 'Backend should not need a Hermes terminal sentinel for the workbench Hermes panel.');
|
|
91
|
+
assert.doesNotMatch(serverIndex, /hermesCommand/, 'Provider shell starts should not reference the removed Hermes sentinel variable.');
|
|
88
92
|
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.');
|
|
89
93
|
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.');
|
|
90
94
|
assert.match(hermesInstallJobs, /downloadHermesInstaller/, 'Windows Hermes install should download the installer through backend API code before running it.');
|
package/server/index.js
CHANGED
|
@@ -2266,7 +2266,7 @@ function handleShellConnection(ws, request) {
|
|
|
2266
2266
|
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
|
|
2267
2267
|
console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
|
|
2268
2268
|
if (initialCommand) {
|
|
2269
|
-
console.log('⚡ Initial command:',
|
|
2269
|
+
console.log('⚡ Initial command:', initialCommand || 'interactive shell');
|
|
2270
2270
|
}
|
|
2271
2271
|
|
|
2272
2272
|
// First send a welcome message
|
|
@@ -2413,7 +2413,7 @@ function handleShellConnection(ws, request) {
|
|
|
2413
2413
|
}
|
|
2414
2414
|
}
|
|
2415
2415
|
|
|
2416
|
-
console.log('🔧 Executing shell command:',
|
|
2416
|
+
console.log('🔧 Executing shell command:', shellCommand || 'interactive shell');
|
|
2417
2417
|
|
|
2418
2418
|
// Use appropriate shell based on platform
|
|
2419
2419
|
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
@@ -173,6 +173,64 @@ function extractRunOutput(body) {
|
|
|
173
173
|
return null;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
function extractResponsesOutput(body) {
|
|
177
|
+
if (!body || typeof body !== 'object') return null;
|
|
178
|
+
|
|
179
|
+
const output = Array.isArray(body.output) ? body.output : [];
|
|
180
|
+
for (const item of output) {
|
|
181
|
+
if (!item || typeof item !== 'object') continue;
|
|
182
|
+
if (item.type === 'message' || item.role === 'assistant') {
|
|
183
|
+
const text = extractTextFromValue(item.content);
|
|
184
|
+
if (text) return text;
|
|
185
|
+
}
|
|
186
|
+
const text = extractTextFromValue(item.output_text)
|
|
187
|
+
|| extractTextFromValue(item.text)
|
|
188
|
+
|| extractTextFromValue(item.message)
|
|
189
|
+
|| extractTextFromValue(item.output);
|
|
190
|
+
if (text) return text;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return extractTextFromValue(body.output_text)
|
|
194
|
+
|| extractTextFromValue(body.message)
|
|
195
|
+
|| extractTextFromValue(body.response)
|
|
196
|
+
|| null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function extractChatCompletionOutput(body) {
|
|
200
|
+
if (!body || typeof body !== 'object') return null;
|
|
201
|
+
const choices = Array.isArray(body.choices) ? body.choices : [];
|
|
202
|
+
for (const choice of choices) {
|
|
203
|
+
const text = extractTextFromValue(choice?.message?.content)
|
|
204
|
+
|| extractTextFromValue(choice?.delta?.content)
|
|
205
|
+
|| extractTextFromValue(choice?.text);
|
|
206
|
+
if (text) return text;
|
|
207
|
+
}
|
|
208
|
+
return extractTextFromValue(body.output_text)
|
|
209
|
+
|| extractTextFromValue(body.output)
|
|
210
|
+
|| extractTextFromValue(body.message)
|
|
211
|
+
|| extractTextFromValue(body.response)
|
|
212
|
+
|| null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function recentGatewayLogText(gateway) {
|
|
216
|
+
if (!gateway?.logs?.length) return '';
|
|
217
|
+
return gateway.logs
|
|
218
|
+
.slice(-16)
|
|
219
|
+
.map((entry) => String(entry.chunk || '').trim())
|
|
220
|
+
.filter(Boolean)
|
|
221
|
+
.join('\n')
|
|
222
|
+
.trim();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function gatewayExitMessage(gateway, fallback = 'Hermes gateway is not running.') {
|
|
226
|
+
if (!gateway) return fallback;
|
|
227
|
+
const exit = gateway.exitSignal
|
|
228
|
+
? `Hermes gateway exited with signal ${gateway.exitSignal}.`
|
|
229
|
+
: `Hermes gateway exited with code ${gateway.exitCode ?? 'unknown'}.`;
|
|
230
|
+
const logs = recentGatewayLogText(gateway);
|
|
231
|
+
return logs ? `${exit}\n${logs}` : (gateway.error || exit);
|
|
232
|
+
}
|
|
233
|
+
|
|
176
234
|
function makeRunRequest(options) {
|
|
177
235
|
const input = String(options.input || '').trim();
|
|
178
236
|
return {
|
|
@@ -186,13 +244,51 @@ function makeRunRequest(options) {
|
|
|
186
244
|
};
|
|
187
245
|
}
|
|
188
246
|
|
|
247
|
+
function makeChatCompletionRequest(options) {
|
|
248
|
+
const input = String(options.input || '').trim();
|
|
249
|
+
const messages = Array.isArray(options.messages) ? options.messages : [
|
|
250
|
+
{
|
|
251
|
+
role: 'system',
|
|
252
|
+
content: options.instructions || [
|
|
253
|
+
'You are Hermes Agent running inside Pixcode.',
|
|
254
|
+
'Use Pixcode MCP tools when they help inspect projects, launch CLIs, or perform workspace actions.',
|
|
255
|
+
'Keep answers concise and include concrete next steps when work is blocked.',
|
|
256
|
+
].join(' '),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
role: 'user',
|
|
260
|
+
content: input,
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
return {
|
|
264
|
+
model: options.model || 'hermes-agent',
|
|
265
|
+
messages,
|
|
266
|
+
stream: false,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function makeResponsesRequest(options) {
|
|
271
|
+
const input = String(options.input || '').trim();
|
|
272
|
+
return {
|
|
273
|
+
model: options.model || 'hermes-agent',
|
|
274
|
+
input,
|
|
275
|
+
instructions: options.instructions || [
|
|
276
|
+
'You are Hermes Agent running inside Pixcode.',
|
|
277
|
+
'Use Pixcode MCP tools when they help inspect projects, launch CLIs, or perform workspace actions.',
|
|
278
|
+
'Keep answers concise and include concrete next steps when work is blocked.',
|
|
279
|
+
].join(' '),
|
|
280
|
+
conversation: options.sessionId || undefined,
|
|
281
|
+
store: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
189
285
|
async function waitForGatewayReady(gateway) {
|
|
190
286
|
const started = Date.now();
|
|
191
287
|
let lastError = null;
|
|
192
288
|
|
|
193
289
|
while (Date.now() - started < STARTUP_TIMEOUT_MS) {
|
|
194
290
|
if (!isGatewayRunning(gateway)) {
|
|
195
|
-
throw new Error(gateway
|
|
291
|
+
throw new Error(gatewayExitMessage(gateway));
|
|
196
292
|
}
|
|
197
293
|
|
|
198
294
|
try {
|
|
@@ -319,7 +415,10 @@ export async function ensureHermesGateway(options = {}) {
|
|
|
319
415
|
apiServerKey,
|
|
320
416
|
appRoot,
|
|
321
417
|
});
|
|
322
|
-
const installStatus = readHermesInstallStatus(env
|
|
418
|
+
const installStatus = readHermesInstallStatus(env, {
|
|
419
|
+
allowSmokeHermes: options.allowSmokeHermes === true,
|
|
420
|
+
repairLaunchers: options.repairLaunchers !== false,
|
|
421
|
+
});
|
|
323
422
|
if (!installStatus.installed || !installStatus.command) {
|
|
324
423
|
throw new Error(installStatus.error || 'Hermes Agent CLI is not installed.');
|
|
325
424
|
}
|
|
@@ -345,14 +444,15 @@ export async function ensureHermesGateway(options = {}) {
|
|
|
345
444
|
|
|
346
445
|
await configurePixcodeMcp({ appRoot, env, gateway });
|
|
347
446
|
|
|
348
|
-
const
|
|
447
|
+
const gatewayArgs = options.gatewayArgs || ['gateway', 'run', '--replace'];
|
|
448
|
+
const child = spawn(installStatus.command, gatewayArgs, {
|
|
349
449
|
cwd: projectPath,
|
|
350
450
|
env,
|
|
351
451
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
352
452
|
windowsHide: true,
|
|
353
453
|
});
|
|
354
454
|
gateway.child = child;
|
|
355
|
-
appendGatewayLog(gateway, 'meta', `$ ${installStatus.command}
|
|
455
|
+
appendGatewayLog(gateway, 'meta', `$ ${installStatus.command} ${gatewayArgs.join(' ')}\n`);
|
|
356
456
|
|
|
357
457
|
child.stdout?.on('data', (buf) => appendGatewayLog(gateway, 'stdout', buf.toString()));
|
|
358
458
|
child.stderr?.on('data', (buf) => appendGatewayLog(gateway, 'stderr', buf.toString()));
|
|
@@ -458,13 +558,99 @@ export async function runHermesGatewayPrompt(projectPath, options = {}) {
|
|
|
458
558
|
throw new Error('Hermes prompt is required.');
|
|
459
559
|
}
|
|
460
560
|
|
|
561
|
+
const responsesRequest = makeResponsesRequest({ ...options, input });
|
|
562
|
+
const responseRun = await callGateway(gateway, '/v1/responses', {
|
|
563
|
+
method: 'POST',
|
|
564
|
+
body: JSON.stringify(responsesRequest),
|
|
565
|
+
timeoutMs: options.responsesTimeoutMs || options.timeoutMs || RUN_TIMEOUT_MS,
|
|
566
|
+
}).catch((error) => {
|
|
567
|
+
if (!isGatewayRunning(gateway)) {
|
|
568
|
+
throw new Error(gatewayExitMessage(gateway));
|
|
569
|
+
}
|
|
570
|
+
throw error;
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (!isGatewayRunning(gateway)) {
|
|
574
|
+
throw new Error(gatewayExitMessage(gateway));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (responseRun.ok) {
|
|
578
|
+
const status = extractRunStatus(responseRun.body) || 'completed';
|
|
579
|
+
const message = extractResponsesOutput(responseRun.body);
|
|
580
|
+
return {
|
|
581
|
+
ok: status === 'completed' || status === 'succeeded',
|
|
582
|
+
projectPath: gateway.projectPath,
|
|
583
|
+
baseUrl: gateway.baseUrl,
|
|
584
|
+
sessionId: options.sessionId || responsesRequest.conversation || null,
|
|
585
|
+
runId: null,
|
|
586
|
+
responseId: responseRun.body?.id || null,
|
|
587
|
+
status,
|
|
588
|
+
message,
|
|
589
|
+
error: (status === 'completed' || status === 'succeeded') ? null : extractTextFromValue(responseRun.body?.error) || message || 'Hermes response failed.',
|
|
590
|
+
raw: responseRun.body,
|
|
591
|
+
transport: 'responses',
|
|
592
|
+
endpoint: '/v1/responses',
|
|
593
|
+
httpStatus: responseRun.status,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (responseRun.status && responseRun.status !== 404 && responseRun.status !== 405) {
|
|
598
|
+
throw new Error(`Hermes /v1/responses failed with HTTP ${responseRun.status}: ${JSON.stringify(responseRun.body)}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const chatRequest = makeChatCompletionRequest({ ...options, input });
|
|
602
|
+
const chat = await callGateway(gateway, '/v1/chat/completions', {
|
|
603
|
+
method: 'POST',
|
|
604
|
+
body: JSON.stringify(chatRequest),
|
|
605
|
+
timeoutMs: options.chatTimeoutMs || options.timeoutMs || RUN_TIMEOUT_MS,
|
|
606
|
+
}).catch((error) => {
|
|
607
|
+
if (!isGatewayRunning(gateway)) {
|
|
608
|
+
throw new Error(gatewayExitMessage(gateway));
|
|
609
|
+
}
|
|
610
|
+
throw error;
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
if (!isGatewayRunning(gateway)) {
|
|
614
|
+
throw new Error(gatewayExitMessage(gateway));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (chat.ok) {
|
|
618
|
+
const message = extractChatCompletionOutput(chat.body);
|
|
619
|
+
return {
|
|
620
|
+
ok: true,
|
|
621
|
+
projectPath: gateway.projectPath,
|
|
622
|
+
baseUrl: gateway.baseUrl,
|
|
623
|
+
sessionId: options.sessionId || null,
|
|
624
|
+
runId: null,
|
|
625
|
+
status: 'completed',
|
|
626
|
+
message,
|
|
627
|
+
raw: chat.body,
|
|
628
|
+
transport: 'chat.completions',
|
|
629
|
+
endpoint: '/v1/chat/completions',
|
|
630
|
+
httpStatus: chat.status,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (chat.status && chat.status !== 404 && chat.status !== 405) {
|
|
635
|
+
throw new Error(`Hermes /v1/chat/completions failed with HTTP ${chat.status}: ${JSON.stringify(chat.body)}`);
|
|
636
|
+
}
|
|
637
|
+
|
|
461
638
|
const request = makeRunRequest({ ...options, input });
|
|
462
639
|
const create = await callGateway(gateway, '/v1/runs', {
|
|
463
640
|
method: 'POST',
|
|
464
641
|
body: JSON.stringify(request),
|
|
465
642
|
timeoutMs: options.createTimeoutMs || 15000,
|
|
643
|
+
}).catch((error) => {
|
|
644
|
+
if (!isGatewayRunning(gateway)) {
|
|
645
|
+
throw new Error(gatewayExitMessage(gateway));
|
|
646
|
+
}
|
|
647
|
+
throw error;
|
|
466
648
|
});
|
|
467
649
|
|
|
650
|
+
if (!isGatewayRunning(gateway)) {
|
|
651
|
+
throw new Error(gatewayExitMessage(gateway));
|
|
652
|
+
}
|
|
653
|
+
|
|
468
654
|
if (!create.ok) {
|
|
469
655
|
throw new Error(`Hermes /v1/runs failed with HTTP ${create.status}: ${JSON.stringify(create.body)}`);
|
|
470
656
|
}
|
|
@@ -481,6 +667,9 @@ export async function runHermesGatewayPrompt(projectPath, options = {}) {
|
|
|
481
667
|
status: initialStatus || 'completed',
|
|
482
668
|
message: extractRunOutput(create.body),
|
|
483
669
|
raw: create.body,
|
|
670
|
+
transport: 'runs',
|
|
671
|
+
endpoint: '/v1/runs',
|
|
672
|
+
httpStatus: create.status,
|
|
484
673
|
};
|
|
485
674
|
}
|
|
486
675
|
|
|
@@ -497,6 +686,9 @@ export async function runHermesGatewayPrompt(projectPath, options = {}) {
|
|
|
497
686
|
if (!poll.ok) {
|
|
498
687
|
throw new Error(`Hermes /v1/runs/${runId} failed with HTTP ${poll.status}: ${JSON.stringify(poll.body)}`);
|
|
499
688
|
}
|
|
689
|
+
if (!isGatewayRunning(gateway)) {
|
|
690
|
+
throw new Error(gatewayExitMessage(gateway));
|
|
691
|
+
}
|
|
500
692
|
latest = poll.body;
|
|
501
693
|
status = extractRunStatus(latest) || status;
|
|
502
694
|
}
|
|
@@ -516,6 +708,9 @@ export async function runHermesGatewayPrompt(projectPath, options = {}) {
|
|
|
516
708
|
message,
|
|
517
709
|
error: status === 'completed' ? null : extractTextFromValue(latest?.error) || message || 'Hermes run failed.',
|
|
518
710
|
raw: latest,
|
|
711
|
+
transport: 'runs',
|
|
712
|
+
endpoint: '/v1/runs',
|
|
713
|
+
httpStatus: create.status,
|
|
519
714
|
};
|
|
520
715
|
}
|
|
521
716
|
|
|
@@ -144,8 +144,40 @@ function runHermesVersion(candidate, env) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
return
|
|
147
|
+
function isHermesSmokeCommandOutput(output) {
|
|
148
|
+
return /Hermes Agent v0\.0\.0\s+smoke/i.test(String(output || ''))
|
|
149
|
+
|| /pixcode-hermes-(?:chat-api|smoke)/i.test(String(output || ''));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isExplicitHermesCliPath(candidate, env = process.env) {
|
|
153
|
+
if (!candidate || !env.HERMES_CLI_PATH || !path.isAbsolute(candidate)) return false;
|
|
154
|
+
try {
|
|
155
|
+
return path.resolve(candidate) === path.resolve(env.HERMES_CLI_PATH);
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isTemporaryHermesLauncher(candidate) {
|
|
162
|
+
if (!candidate || !path.isAbsolute(candidate)) return false;
|
|
163
|
+
const normalized = path.resolve(candidate);
|
|
164
|
+
const tempRoot = path.resolve(os.tmpdir());
|
|
165
|
+
return normalized === tempRoot || normalized.startsWith(`${tempRoot}${path.sep}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shouldRepairHermesLauncher(command, env = process.env, options = {}) {
|
|
169
|
+
if (options.repairLaunchers === false) return false;
|
|
170
|
+
if (!command || command === 'hermes') return false;
|
|
171
|
+
if (isExplicitHermesCliPath(command, env)) return false;
|
|
172
|
+
if (isTemporaryHermesLauncher(command)) return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function isUsableHermesCommand(candidate, env = process.env, options = {}) {
|
|
177
|
+
const result = runHermesVersion(candidate, buildHermesEnv(env));
|
|
178
|
+
if (!result.ok) return false;
|
|
179
|
+
if (!options.allowSmokeHermes && isHermesSmokeCommandOutput(result.output)) return false;
|
|
180
|
+
return true;
|
|
149
181
|
}
|
|
150
182
|
|
|
151
183
|
function isHermesPythonLauncher(candidate) {
|
|
@@ -313,8 +345,9 @@ export function hermesCommandCandidates(env = process.env) {
|
|
|
313
345
|
return [...new Set(candidates.filter(Boolean))];
|
|
314
346
|
}
|
|
315
347
|
|
|
316
|
-
export function readHermesInstallStatus(env = process.env) {
|
|
348
|
+
export function readHermesInstallStatus(env = process.env, options = {}) {
|
|
317
349
|
const hermesEnv = buildHermesEnv(env);
|
|
350
|
+
const rejected = [];
|
|
318
351
|
|
|
319
352
|
for (const candidate of hermesCommandCandidates(hermesEnv)) {
|
|
320
353
|
const isBareCommand = candidate === 'hermes';
|
|
@@ -324,8 +357,16 @@ export function readHermesInstallStatus(env = process.env) {
|
|
|
324
357
|
|
|
325
358
|
const result = runHermesVersion(candidate, hermesEnv);
|
|
326
359
|
if (result.ok) {
|
|
327
|
-
repairHermesCommandLaunchers(candidate, hermesEnv);
|
|
328
360
|
const version = formatHermesVersionOutput(result.output);
|
|
361
|
+
if (!options.allowSmokeHermes && isHermesSmokeCommandOutput(result.output)) {
|
|
362
|
+
rejected.push(`${candidate} (${version || 'smoke-test Hermes launcher'})`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (shouldRepairHermesLauncher(candidate, hermesEnv, options)) {
|
|
367
|
+
repairHermesCommandLaunchers(candidate, hermesEnv);
|
|
368
|
+
}
|
|
369
|
+
|
|
329
370
|
return {
|
|
330
371
|
installed: true,
|
|
331
372
|
command: candidate,
|
|
@@ -339,7 +380,9 @@ export function readHermesInstallStatus(env = process.env) {
|
|
|
339
380
|
installed: false,
|
|
340
381
|
command: null,
|
|
341
382
|
version: null,
|
|
342
|
-
error:
|
|
383
|
+
error: rejected.length > 0
|
|
384
|
+
? `Only smoke-test Hermes launchers were found and rejected: ${rejected.join(', ')}. Install or repair Hermes Agent.`
|
|
385
|
+
: 'Hermes Agent CLI is not installed or is not on PATH.',
|
|
343
386
|
};
|
|
344
387
|
}
|
|
345
388
|
|