@pixelbyte-software/pixcode 1.50.9 → 1.51.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-CXTCtUku.js → index-DJosGZ59.js} +152 -152
- package/dist/index.html +1 -1
- package/dist-server/server/index.js +100 -7
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +64 -1
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
- package/dist-server/server/services/hermes-gateway.js +380 -8
- package/dist-server/server/services/hermes-gateway.js.map +1 -1
- package/dist-server/server/services/public-api-manifest.js +2 -0
- package/dist-server/server/services/public-api-manifest.js.map +1 -1
- package/package.json +1 -1
- package/scripts/hermes/configure-pixcode-mcp.mjs +7 -1
- package/scripts/hermes/pixcode-mcp-server.mjs +383 -36
- package/scripts/smoke/hermes-api-install.mjs +1 -1
- package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +96 -22
- package/scripts/smoke/hermes-rest-chat-api.mjs +31 -0
- package/scripts/smoke/hermes-rest-gateway.mjs +14 -0
- package/scripts/smoke/hermes-settings-commands.mjs +22 -44
- package/scripts/smoke/pixcode-workbench-1-48.mjs +1 -1
- package/scripts/smoke/vscode-workbench-polish.mjs +2 -2
- package/server/index.js +106 -7
- package/server/modules/orchestration/hermes/hermes.routes.ts +68 -0
- package/server/services/hermes-gateway.js +396 -8
- package/server/services/public-api-manifest.js +2 -0
|
@@ -10,6 +10,8 @@ const seen = [];
|
|
|
10
10
|
const providerMcpUpserts = [];
|
|
11
11
|
const terminalLaunches = [];
|
|
12
12
|
const providerOutputReads = [];
|
|
13
|
+
const providerInputWrites = [];
|
|
14
|
+
const gatewayRequests = [];
|
|
13
15
|
|
|
14
16
|
function readJson(req) {
|
|
15
17
|
return new Promise((resolve, reject) => {
|
|
@@ -48,6 +50,17 @@ const server = createServer(async (req, res) => {
|
|
|
48
50
|
return;
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
if (req.method === 'GET' && url.pathname === '/api/public/manifest') {
|
|
54
|
+
res.end(JSON.stringify({
|
|
55
|
+
name: 'Pixcode Public API',
|
|
56
|
+
groups: [
|
|
57
|
+
{ id: 'projects', basePath: '/api/projects', scopes: ['projects:read', 'projects:write'] },
|
|
58
|
+
{ id: 'providers', basePath: '/api/providers', scopes: ['providers:read', 'providers:write'] },
|
|
59
|
+
],
|
|
60
|
+
}));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
if (req.method === 'GET' && url.pathname === '/api/providers/codex/auth/status') {
|
|
52
65
|
res.end(JSON.stringify({ data: { provider: 'codex', installed: true, authenticated: true } }));
|
|
53
66
|
return;
|
|
@@ -85,6 +98,37 @@ const server = createServer(async (req, res) => {
|
|
|
85
98
|
return;
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
if (req.method === 'GET' && url.pathname === '/api/orchestration/hermes/diagnostics') {
|
|
102
|
+
res.end(JSON.stringify({
|
|
103
|
+
ok: true,
|
|
104
|
+
model: { provider: 'openai-codex', default: 'gpt-5.5' },
|
|
105
|
+
config: {
|
|
106
|
+
active: {
|
|
107
|
+
toolsets: ['hermes-cli', 'mcp-pixcode'],
|
|
108
|
+
pixcodeMcp: { toolCount: 12, missingTools: [] },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
cron: { toolsetAvailable: true, gatewayJobsApi: { ok: true, status: 200 } },
|
|
112
|
+
issues: [],
|
|
113
|
+
}));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (req.method === 'POST' && url.pathname === '/api/orchestration/hermes/gateway/request') {
|
|
118
|
+
const body = await readJson(req);
|
|
119
|
+
gatewayRequests.push(body);
|
|
120
|
+
res.end(JSON.stringify({
|
|
121
|
+
ok: true,
|
|
122
|
+
endpoint: body.endpoint,
|
|
123
|
+
body: {
|
|
124
|
+
jobs: [
|
|
125
|
+
{ job_id: 'job_1', name: 'Morning repo check', schedule: '0 9 * * *' },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
}));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
88
132
|
if (req.method === 'POST' && url.pathname === '/api/orchestration/hermes/terminal-launches') {
|
|
89
133
|
const body = await readJson(req);
|
|
90
134
|
terminalLaunches.push(body);
|
|
@@ -127,28 +171,7 @@ const server = createServer(async (req, res) => {
|
|
|
127
171
|
provider: 'codex',
|
|
128
172
|
projectPath: '/root/pixcode',
|
|
129
173
|
terminalState: 'idle',
|
|
130
|
-
output: 'OpenAI Codex\n› /init\n',
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
active: true,
|
|
134
|
-
provider: 'codex',
|
|
135
|
-
projectPath: '/root/pixcode',
|
|
136
|
-
terminalState: 'idle',
|
|
137
|
-
output: 'OpenAI Codex\n› /init\n',
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
active: true,
|
|
141
|
-
provider: 'codex',
|
|
142
|
-
projectPath: '/root/pixcode',
|
|
143
|
-
terminalState: 'idle',
|
|
144
|
-
output: 'OpenAI Codex\n› /init\n',
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
active: true,
|
|
148
|
-
provider: 'codex',
|
|
149
|
-
projectPath: '/root/pixcode',
|
|
150
|
-
terminalState: 'idle',
|
|
151
|
-
output: 'OpenAI Codex\n› /init\n',
|
|
174
|
+
output: 'OpenAI Codex\n› /init\n\n• Ran npm test\n\n› Implement {feature}\n',
|
|
152
175
|
},
|
|
153
176
|
{
|
|
154
177
|
active: true,
|
|
@@ -162,6 +185,13 @@ const server = createServer(async (req, res) => {
|
|
|
162
185
|
return;
|
|
163
186
|
}
|
|
164
187
|
|
|
188
|
+
if (req.method === 'POST' && url.pathname === '/api/shell/sessions/provider-input') {
|
|
189
|
+
const body = await readJson(req);
|
|
190
|
+
providerInputWrites.push(body);
|
|
191
|
+
res.end(JSON.stringify({ ok: true, wrote: true, provider: body.provider, launchId: body.launchId }));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
165
195
|
res.statusCode = 404;
|
|
166
196
|
res.end(JSON.stringify({ error: url.pathname }));
|
|
167
197
|
});
|
|
@@ -218,6 +248,7 @@ const child = spawn(process.execPath, [path.join(repoRoot, 'scripts/hermes/pixco
|
|
|
218
248
|
...process.env,
|
|
219
249
|
PIXCODE_BASE_URL: baseUrl,
|
|
220
250
|
PIXCODE_API_KEY: apiKey,
|
|
251
|
+
PIXCODE_MCP_READBACK_IDLE_STABLE_MS: '500',
|
|
221
252
|
},
|
|
222
253
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
223
254
|
});
|
|
@@ -230,6 +261,12 @@ try {
|
|
|
230
261
|
assert(toolNames.includes('pixcode_get_provider_status'), 'provider status tool missing');
|
|
231
262
|
assert(toolNames.includes('pixcode_get_hermes_gateway_status'), 'Hermes gateway status tool missing');
|
|
232
263
|
assert(toolNames.includes('pixcode_probe_hermes_gateway'), 'Hermes gateway probe tool missing');
|
|
264
|
+
assert(toolNames.includes('pixcode_get_hermes_diagnostics'), 'Hermes diagnostics tool missing');
|
|
265
|
+
assert(toolNames.includes('pixcode_get_api_manifest'), 'Pixcode API manifest tool missing');
|
|
266
|
+
assert(toolNames.includes('pixcode_api_request'), 'Pixcode generic API request tool missing');
|
|
267
|
+
assert(toolNames.includes('pixcode_hermes_gateway_request'), 'Hermes gateway request proxy tool missing');
|
|
268
|
+
assert(toolNames.includes('pixcode_manage_hermes_cron'), 'Hermes cron management tool missing');
|
|
269
|
+
assert(toolNames.includes('pixcode_send_cli_input'), 'Provider terminal input tool missing');
|
|
233
270
|
|
|
234
271
|
const projects = await callMcp(child, 'tools/call', { name: 'pixcode_list_projects', arguments: {} });
|
|
235
272
|
assert.match(projects.content[0].text, /224/, 'projects response should include file count');
|
|
@@ -243,6 +280,35 @@ try {
|
|
|
243
280
|
});
|
|
244
281
|
assert.match(probe.content[0].text, /"ok": true/, 'gateway probe should return ok');
|
|
245
282
|
|
|
283
|
+
const diagnostics = await callMcp(child, 'tools/call', {
|
|
284
|
+
name: 'pixcode_get_hermes_diagnostics',
|
|
285
|
+
arguments: { projectPath: '/root/pixcode' },
|
|
286
|
+
});
|
|
287
|
+
assert.match(diagnostics.content[0].text, /mcp-pixcode/, 'Hermes diagnostics should expose active toolsets.');
|
|
288
|
+
assert.match(diagnostics.content[0].text, /"toolsetAvailable": true/, 'Hermes diagnostics should expose cron toolset availability.');
|
|
289
|
+
|
|
290
|
+
const manifest = await callMcp(child, 'tools/call', { name: 'pixcode_get_api_manifest', arguments: {} });
|
|
291
|
+
assert.match(manifest.content[0].text, /Pixcode Public API/, 'API manifest tool should expose Pixcode API docs to Hermes.');
|
|
292
|
+
|
|
293
|
+
const apiRequest = await callMcp(child, 'tools/call', {
|
|
294
|
+
name: 'pixcode_api_request',
|
|
295
|
+
arguments: { method: 'GET', path: '/api/projects' },
|
|
296
|
+
});
|
|
297
|
+
assert.match(apiRequest.content[0].text, /"name": "pixcode"/, 'generic Pixcode API tool should call allowlisted local API paths.');
|
|
298
|
+
|
|
299
|
+
const cronJobs = await callMcp(child, 'tools/call', {
|
|
300
|
+
name: 'pixcode_hermes_gateway_request',
|
|
301
|
+
arguments: { method: 'GET', endpoint: '/api/jobs', projectPath: '/root/pixcode' },
|
|
302
|
+
});
|
|
303
|
+
assert.match(cronJobs.content[0].text, /Morning repo check/, 'Hermes gateway request tool should expose API-server jobs/cron endpoints.');
|
|
304
|
+
assert.equal(gatewayRequests[0].endpoint, '/api/jobs', 'Gateway request should keep the requested Hermes endpoint.');
|
|
305
|
+
|
|
306
|
+
const cronList = await callMcp(child, 'tools/call', {
|
|
307
|
+
name: 'pixcode_manage_hermes_cron',
|
|
308
|
+
arguments: { action: 'list', projectPath: '/root/pixcode' },
|
|
309
|
+
});
|
|
310
|
+
assert.match(cronList.content[0].text, /Morning repo check/, 'Hermes cron helper should list managed Hermes jobs.');
|
|
311
|
+
|
|
246
312
|
const launch = await callMcp(child, 'tools/call', {
|
|
247
313
|
name: 'pixcode_open_cli_terminal',
|
|
248
314
|
arguments: { provider: 'codex', projectPath: '/root/pixcode', prompt: 'smoke' },
|
|
@@ -285,6 +351,14 @@ try {
|
|
|
285
351
|
assert.match(blockedLaunch.content[0].text, /"reason": "not_installed"/, 'uninstalled provider response should explain the block');
|
|
286
352
|
assert.equal(terminalLaunches.length, 2, 'Only installed Codex provider launches should be created');
|
|
287
353
|
|
|
354
|
+
const inputWrite = await callMcp(child, 'tools/call', {
|
|
355
|
+
name: 'pixcode_send_cli_input',
|
|
356
|
+
arguments: { provider: 'codex', projectPath: '/root/pixcode', input: 'selam', submit: true, launchId: 2 },
|
|
357
|
+
});
|
|
358
|
+
assert.match(inputWrite.content[0].text, /"wrote": true/, 'MCP should be able to submit input to an existing visible provider terminal.');
|
|
359
|
+
assert.equal(providerInputWrites[0].input, 'selam', 'Provider input body should preserve exact user input.');
|
|
360
|
+
assert.equal(providerInputWrites[0].submit, true, 'Provider input should submit by default when requested.');
|
|
361
|
+
|
|
288
362
|
assert(seen.every((entry) => entry.auth === `Bearer ${apiKey}`), 'all MCP calls should use the Pixcode bearer key');
|
|
289
363
|
console.log('hermes MCP Pixcode roundtrip smoke passed');
|
|
290
364
|
} finally {
|
|
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
ensureHermesGateway,
|
|
8
|
+
readHermesDiagnostics,
|
|
9
|
+
requestHermesGateway,
|
|
8
10
|
runHermesGatewayPrompt,
|
|
9
11
|
stopHermesGateway,
|
|
10
12
|
} from '../../server/services/hermes-gateway.js';
|
|
@@ -89,6 +91,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
89
91
|
}));
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
94
|
+
if (req.method === 'GET' && url.pathname === '/api/jobs') {
|
|
95
|
+
res.end(JSON.stringify({
|
|
96
|
+
jobs: [
|
|
97
|
+
{ job_id: 'job_smoke', name: 'Pixcode cron smoke', schedule: 'every 1h' },
|
|
98
|
+
],
|
|
99
|
+
}));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
92
102
|
res.statusCode = 404;
|
|
93
103
|
res.end(JSON.stringify({ error: url.pathname }));
|
|
94
104
|
});
|
|
@@ -120,10 +130,31 @@ try {
|
|
|
120
130
|
throw new Error(`Hermes REST chat did not use responses: ${JSON.stringify(run)}`);
|
|
121
131
|
}
|
|
122
132
|
|
|
133
|
+
const jobs = await requestHermesGateway(projectPath, {
|
|
134
|
+
method: 'GET',
|
|
135
|
+
endpoint: '/api/jobs',
|
|
136
|
+
});
|
|
137
|
+
if (!jobs.ok || !String(JSON.stringify(jobs.body)).includes('Pixcode cron smoke')) {
|
|
138
|
+
throw new Error(`Hermes gateway jobs API did not proxy cron jobs: ${JSON.stringify(jobs)}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const diagnostics = await readHermesDiagnostics({ projectPath, hermesHome });
|
|
142
|
+
if (!diagnostics.config?.active?.toolsets?.includes('hermes-cli') || !diagnostics.config.active.toolsets.includes('mcp-pixcode')) {
|
|
143
|
+
throw new Error(`Hermes diagnostics did not see the full toolset config: ${JSON.stringify(diagnostics.config?.active)}`);
|
|
144
|
+
}
|
|
145
|
+
if (!diagnostics.cron?.gatewayJobsApi?.ok) {
|
|
146
|
+
throw new Error(`Hermes diagnostics did not verify cron jobs API: ${JSON.stringify(diagnostics.cron)}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
123
149
|
console.log(JSON.stringify({
|
|
124
150
|
ok: true,
|
|
125
151
|
transport: run.transport,
|
|
126
152
|
message: run.message,
|
|
153
|
+
jobs: jobs.body,
|
|
154
|
+
diagnostics: {
|
|
155
|
+
toolsets: diagnostics.config.active.toolsets,
|
|
156
|
+
cronOk: diagnostics.cron.gatewayJobsApi.ok,
|
|
157
|
+
},
|
|
127
158
|
}, null, 2));
|
|
128
159
|
} finally {
|
|
129
160
|
stopHermesGateway(projectPath);
|
|
@@ -12,6 +12,8 @@ const settingsTab = read('src/components/settings/view/tabs/HermesSettingsTab.ts
|
|
|
12
12
|
assert.match(service, /export async function ensureHermesGateway/, 'Pixcode should expose an API-managed Hermes gateway starter.');
|
|
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
|
+
assert.match(service, /export async function requestHermesGateway/, 'Pixcode should proxy documented Hermes gateway endpoints such as /api/jobs.');
|
|
16
|
+
assert.match(service, /export async function readHermesDiagnostics/, 'Pixcode should expose redacted Hermes diagnostics for config, auth, MCP, gateway, and cron state.');
|
|
15
17
|
assert.match(service, /export function stopHermesGateway/, 'Pixcode should be able to stop a managed Hermes gateway process.');
|
|
16
18
|
assert.match(service, /\/v1\/chat\/completions/, 'Hermes UI chat should use the documented OpenAI-compatible chat completions endpoint first.');
|
|
17
19
|
assert.match(service, /\/v1\/responses/, 'Hermes UI chat should use the stateful OpenAI-compatible responses endpoint before legacy chat fallback.');
|
|
@@ -36,6 +38,8 @@ assert.match(routes, /router\.get\('\/gateway\/status'/, 'Hermes router should e
|
|
|
36
38
|
assert.match(routes, /router\.post\('\/gateway\/start'/, 'Hermes router should expose gateway start.');
|
|
37
39
|
assert.match(routes, /router\.post\('\/gateway\/probe'/, 'Hermes router should expose a REST probe endpoint.');
|
|
38
40
|
assert.match(routes, /router\.post\('\/gateway\/chat'/, 'Hermes router should expose a REST chat endpoint.');
|
|
41
|
+
assert.match(routes, /router\.post\('\/gateway\/request'/, 'Hermes router should expose a generic documented gateway request endpoint.');
|
|
42
|
+
assert.match(routes, /router\.get\('\/diagnostics'/, 'Hermes router should expose integration diagnostics.');
|
|
39
43
|
assert.match(routes, /router\.post\('\/gateway\/stop'/, 'Hermes router should expose gateway stop.');
|
|
40
44
|
assert.match(routes, /ensureHermesGateway/, 'Hermes router should use the managed gateway service.');
|
|
41
45
|
assert.match(routes, /probeHermesGateway/, 'Hermes router should use the REST probe service.');
|
|
@@ -45,12 +49,22 @@ assert.match(routes, /probeExisting:\s*false/, 'Hermes chat should reuse a runni
|
|
|
45
49
|
|
|
46
50
|
assert.match(mcpServer, /pixcode_get_hermes_gateway_status/, 'Pixcode MCP should let Hermes inspect gateway status.');
|
|
47
51
|
assert.match(mcpServer, /pixcode_probe_hermes_gateway/, 'Pixcode MCP should let Hermes trigger a REST probe.');
|
|
52
|
+
assert.match(mcpServer, /pixcode_get_hermes_diagnostics/, 'Pixcode MCP should let Hermes read redacted integration diagnostics.');
|
|
53
|
+
assert.match(mcpServer, /pixcode_get_api_manifest/, 'Pixcode MCP should let Hermes discover Pixcode API docs.');
|
|
54
|
+
assert.match(mcpServer, /pixcode_api_request/, 'Pixcode MCP should let Hermes call authenticated Pixcode APIs.');
|
|
55
|
+
assert.match(mcpServer, /pixcode_hermes_gateway_request/, 'Pixcode MCP should let Hermes call documented gateway APIs.');
|
|
56
|
+
assert.match(mcpServer, /pixcode_manage_hermes_cron/, 'Pixcode MCP should expose Hermes cron job management.');
|
|
57
|
+
assert.match(mcpServer, /pixcode_send_cli_input/, 'Pixcode MCP should let Hermes continue an existing visible CLI terminal.');
|
|
48
58
|
assert.match(configureMcp, /pixcode_get_hermes_gateway_status/, 'Hermes MCP config should include gateway status tool.');
|
|
49
59
|
assert.match(configureMcp, /pixcode_probe_hermes_gateway/, 'Hermes MCP config should include gateway probe tool.');
|
|
60
|
+
assert.match(configureMcp, /pixcode_get_hermes_diagnostics/, 'Hermes MCP config should include diagnostics tool.');
|
|
61
|
+
assert.match(configureMcp, /pixcode_manage_hermes_cron/, 'Hermes MCP config should include cron management tool.');
|
|
50
62
|
assert.match(configureMcp, /mcp-pixcode/, 'Hermes MCP config should enable the Pixcode MCP toolset for the real CLI.');
|
|
63
|
+
assert.match(configureMcp, /hermes-cli/, 'Hermes MCP config should keep the native Hermes CLI toolset enabled for cron/files/terminal tools.');
|
|
51
64
|
|
|
52
65
|
assert.match(settingsTab, /gateway\/status/, 'Hermes settings should read gateway status.');
|
|
53
66
|
assert.match(settingsTab, /gateway\/start/, 'Hermes settings should start the REST gateway via API.');
|
|
54
67
|
assert.match(settingsTab, /gateway\/probe/, 'Hermes settings should run REST probe via API.');
|
|
68
|
+
assert.match(settingsTab, /diagnostics/, 'Hermes settings should render diagnostics from the server.');
|
|
55
69
|
|
|
56
70
|
console.log('hermes REST gateway smoke passed');
|
|
@@ -27,23 +27,21 @@ assert.match(
|
|
|
27
27
|
assert.match(settingsTab, /command:\s*'hermes model'/, 'Hermes settings should expose the interactive provider/model wizard.');
|
|
28
28
|
assert.match(settingsTab, /command:\s*'hermes auth'/, 'Hermes settings should expose the credential manager.');
|
|
29
29
|
assert.match(settingsTab, /command:\s*'hermes setup tools'/, 'Hermes settings should expose tool setup.');
|
|
30
|
+
assert.match(settingsTab, /command:\s*'hermes cron status'/, 'Hermes settings should expose scheduler/cron status.');
|
|
31
|
+
assert.match(settingsTab, /command:\s*'hermes mcp'/, 'Hermes settings should expose MCP server management.');
|
|
30
32
|
assert.match(settingsTab, /command:\s*'hermes doctor'/, 'Hermes settings should expose diagnostics.');
|
|
33
|
+
assert.match(settingsTab, /command:\s*'hermes update --yes'/, 'Hermes settings should expose a non-interactive updater.');
|
|
31
34
|
assert.match(settingsTab, /command:\s*'hermes sessions browse'/, 'Hermes settings should open the interactive sessions browser, not the sessions usage screen.');
|
|
35
|
+
assert.match(settingsTab, /\/api\/orchestration\/hermes\/diagnostics/, 'Hermes settings should read integration diagnostics.');
|
|
36
|
+
assert.match(settingsTab, /diagnosticsTitle/, 'Hermes settings should render a diagnostics panel.');
|
|
37
|
+
assert.match(settingsTab, /diagnosticsMcpTools/, 'Hermes settings diagnostics should show Pixcode MCP tool counts.');
|
|
38
|
+
assert.match(settingsTab, /diagnosticsCron/, 'Hermes settings diagnostics should show cron availability.');
|
|
39
|
+
assert.match(settingsTab, /Pixcode Hermes REST health check/, 'Hermes REST probe should submit a real prompt so provider/auth failures are visible.');
|
|
32
40
|
assert.match(
|
|
33
41
|
settingsTab,
|
|
34
42
|
/pixcode:hermes-terminal[\s\S]+command[\s\S]+title/,
|
|
35
43
|
'Hermes settings should dispatch command and title to the workbench terminal.',
|
|
36
44
|
);
|
|
37
|
-
assert.match(
|
|
38
|
-
settingsTab,
|
|
39
|
-
/ensureGatewayReady/,
|
|
40
|
-
'Hermes settings should automatically start/probe the REST API gateway when Hermes is installed.',
|
|
41
|
-
);
|
|
42
|
-
assert.match(
|
|
43
|
-
settingsTab,
|
|
44
|
-
/startIfNeeded:\s*true/,
|
|
45
|
-
'Hermes settings should start the REST API gateway through Pixcode when checking gateway readiness.',
|
|
46
|
-
);
|
|
47
45
|
assert.match(
|
|
48
46
|
settingsModal,
|
|
49
47
|
/<HermesSettingsTab onClose=\{onClose\} \/>/,
|
|
@@ -66,8 +64,8 @@ assert.match(
|
|
|
66
64
|
);
|
|
67
65
|
assert.match(
|
|
68
66
|
workbench,
|
|
69
|
-
/HERMES_DEFAULT_COMMAND\s*=\s*'hermes --yolo
|
|
70
|
-
'Hermes terminal should
|
|
67
|
+
/HERMES_DEFAULT_COMMAND\s*=\s*'hermes --yolo'/,
|
|
68
|
+
'Hermes terminal should not override toolsets on the command line; Pixcode writes hermes-cli plus Pixcode MCP into Hermes config before launch.',
|
|
71
69
|
);
|
|
72
70
|
assert.match(
|
|
73
71
|
serverIndex,
|
|
@@ -86,8 +84,18 @@ assert.match(
|
|
|
86
84
|
);
|
|
87
85
|
assert.match(
|
|
88
86
|
pixcodeMcpServer,
|
|
89
|
-
/
|
|
90
|
-
'Pixcode MCP
|
|
87
|
+
/pixcode_send_cli_input/,
|
|
88
|
+
'Pixcode MCP should expose a direct input tool for continuing an existing visible CLI terminal.',
|
|
89
|
+
);
|
|
90
|
+
assert.match(
|
|
91
|
+
pixcodeMcpServer,
|
|
92
|
+
/pixcode_manage_hermes_cron/,
|
|
93
|
+
'Pixcode MCP should expose Hermes cron management through the managed gateway.',
|
|
94
|
+
);
|
|
95
|
+
assert.match(
|
|
96
|
+
pixcodeMcpServer,
|
|
97
|
+
/pixcode_get_hermes_diagnostics/,
|
|
98
|
+
'Pixcode MCP should expose redacted Hermes integration diagnostics.',
|
|
91
99
|
);
|
|
92
100
|
assert.match(
|
|
93
101
|
pixcodeMcpServer,
|
|
@@ -109,11 +117,6 @@ assert.match(
|
|
|
109
117
|
/lastStrongBusy[\s\S]+lastPrompt[\s\S]+\?\s*'busy'\s*:\s*'idle'/,
|
|
110
118
|
'Codex readback should ignore weak spinner remnants once the prompt has returned.',
|
|
111
119
|
);
|
|
112
|
-
assert.match(
|
|
113
|
-
pixcodeMcpServer,
|
|
114
|
-
/codex_prompt_input_pending/,
|
|
115
|
-
'Codex readback should not treat a prompt line with typed-but-unsubmitted input as final output.',
|
|
116
|
-
);
|
|
117
120
|
assert.match(
|
|
118
121
|
serverIndex,
|
|
119
122
|
/lastStrongBusy[\s\S]+lastPrompt[\s\S]+\?\s*'busy'\s*:\s*'idle'/,
|
|
@@ -129,11 +132,6 @@ assert.match(
|
|
|
129
132
|
/requestedLaunchId[\s\S]+session\.hermesLaunchId === requestedLaunchId/,
|
|
130
133
|
'Provider output API should filter by Hermes terminal launch id when supplied.',
|
|
131
134
|
);
|
|
132
|
-
assert.match(
|
|
133
|
-
serverIndex,
|
|
134
|
-
/existingSession\.hermesLaunchId = hermesLaunchId \|\| existingSession\.hermesLaunchId/,
|
|
135
|
-
'Reused visible provider PTYs should be rebound to the latest Hermes launch id so MCP readback follows the current request.',
|
|
136
|
-
);
|
|
137
135
|
assert.match(
|
|
138
136
|
serverIndex,
|
|
139
137
|
/lifecycleState/,
|
|
@@ -269,26 +267,6 @@ assert.match(
|
|
|
269
267
|
/writeTerminalStartupInput/,
|
|
270
268
|
'Shell backend should submit Hermes startup input directly into reused visible PTYs.',
|
|
271
269
|
);
|
|
272
|
-
assert.match(
|
|
273
|
-
serverIndex,
|
|
274
|
-
/queueTerminalStartupInput/,
|
|
275
|
-
'Shell backend should queue Hermes startup input until the visible provider terminal is ready instead of writing blindly after a fixed delay.',
|
|
276
|
-
);
|
|
277
|
-
assert.match(
|
|
278
|
-
serverIndex,
|
|
279
|
-
/STARTUP_INPUT_READY_TIMEOUT_MS/,
|
|
280
|
-
'Queued provider startup input should have a bounded readiness timeout.',
|
|
281
|
-
);
|
|
282
|
-
assert.match(
|
|
283
|
-
serverIndex,
|
|
284
|
-
/resolveProviderTerminalState[\s\S]+terminalState === 'busy'/,
|
|
285
|
-
'Queued startup input should inspect the provider terminal state and avoid sending while the CLI is busy.',
|
|
286
|
-
);
|
|
287
|
-
assert.match(
|
|
288
|
-
serverIndex,
|
|
289
|
-
/\\x15/,
|
|
290
|
-
'Provider startup input should clear any half-typed prompt line before typing the requested work.',
|
|
291
|
-
);
|
|
292
270
|
assert.match(
|
|
293
271
|
serverIndex,
|
|
294
272
|
/startupInputDelivery === 'terminal'[\s\S]+writeTerminalStartupInput/,
|
|
@@ -74,7 +74,7 @@ assert.match(workbench, /isPlainShell/, 'Bottom terminal should open the selecte
|
|
|
74
74
|
assert.doesNotMatch(workbench, /HERMES_AGENT_START_COMMAND/, 'Hermes Agent should not launch from the bottom terminal through a server-side sentinel.');
|
|
75
75
|
assert.doesNotMatch(workbench, /HermesApiChatPanel|HermesTerminalTranscript/, 'Hermes Agent should use the real PTY terminal UI, not a custom REST chat transcript.');
|
|
76
76
|
assert.doesNotMatch(workbench, /REST POST \/|transport=|response=|gateway=http/, 'Hermes terminal UI must not expose REST debug internals to the user.');
|
|
77
|
-
assert.match(workbench, /HERMES_DEFAULT_COMMAND = 'hermes --yolo
|
|
77
|
+
assert.match(workbench, /HERMES_DEFAULT_COMMAND = 'hermes --yolo'/, 'Hermes Agent bottom panel should launch the actual `hermes` CLI and leave toolsets to the Pixcode-managed Hermes config.');
|
|
78
78
|
assert.match(workbench, /HERMES_HISTORY_COMMAND = 'hermes sessions browse'/, 'Hermes terminal history should open the native interactive session picker.');
|
|
79
79
|
assert.match(workbench, /onOpenHistory=\{openHermesHistory\}/, 'Hermes terminal header should wire its history button to the native Hermes sessions command.');
|
|
80
80
|
assert.match(workbench, /Pixcode MCP Live/, 'Hermes terminal should show a user-facing Pixcode MCP live badge.');
|
|
@@ -324,8 +324,8 @@ assert.doesNotMatch(
|
|
|
324
324
|
|
|
325
325
|
assert.match(
|
|
326
326
|
workbench,
|
|
327
|
-
/HERMES_DEFAULT_COMMAND = 'hermes --yolo
|
|
328
|
-
'Hermes Agent should launch the Hermes CLI directly in bypass mode
|
|
327
|
+
/HERMES_DEFAULT_COMMAND = 'hermes --yolo'/,
|
|
328
|
+
'Hermes Agent should launch the Hermes CLI directly in bypass mode and rely on Pixcode-written config for hermes-cli + mcp-pixcode.',
|
|
329
329
|
);
|
|
330
330
|
|
|
331
331
|
assert.match(
|
package/server/index.js
CHANGED
|
@@ -641,6 +641,40 @@ function quoteShellArgForPlatform(value) {
|
|
|
641
641
|
}
|
|
642
642
|
|
|
643
643
|
const HERMES_CLI_COMMAND_PATTERN = /^hermes(?:\s+[A-Za-z0-9._:/=@+-]+)*\s*$/;
|
|
644
|
+
const HERMES_AGENT_API_SCOPES = [
|
|
645
|
+
'auth:read',
|
|
646
|
+
'auth:write',
|
|
647
|
+
'diagnostics:read',
|
|
648
|
+
'files:read',
|
|
649
|
+
'files:write',
|
|
650
|
+
'git:read',
|
|
651
|
+
'git:write',
|
|
652
|
+
'hermes:mcp',
|
|
653
|
+
'hermes:gateway',
|
|
654
|
+
'notifications:read',
|
|
655
|
+
'notifications:write',
|
|
656
|
+
'orchestration:read',
|
|
657
|
+
'orchestration:write',
|
|
658
|
+
'plugins:read',
|
|
659
|
+
'plugins:write',
|
|
660
|
+
'projects:read',
|
|
661
|
+
'projects:write',
|
|
662
|
+
'providers:read',
|
|
663
|
+
'providers:write',
|
|
664
|
+
'remote:read',
|
|
665
|
+
'remote:write',
|
|
666
|
+
'sessions:read',
|
|
667
|
+
'sessions:write',
|
|
668
|
+
'settings:read',
|
|
669
|
+
'settings:write',
|
|
670
|
+
'telegram:read',
|
|
671
|
+
'telegram:write',
|
|
672
|
+
'terminal:launch',
|
|
673
|
+
'updates:read',
|
|
674
|
+
'updates:write',
|
|
675
|
+
'webhooks:read',
|
|
676
|
+
'webhooks:write',
|
|
677
|
+
];
|
|
644
678
|
|
|
645
679
|
function isHermesCliCommand(command) {
|
|
646
680
|
return typeof command === 'string' && HERMES_CLI_COMMAND_PATTERN.test(command.trim());
|
|
@@ -663,16 +697,15 @@ function getOrCreateHermesApiKey(userId) {
|
|
|
663
697
|
.getApiKeys(userId)
|
|
664
698
|
.find((key) => key.key_name === 'Hermes Agent MCP' && key.is_active);
|
|
665
699
|
if (existing?.api_key) {
|
|
700
|
+
const existingScopes = Array.isArray(existing.scopes) ? existing.scopes : [];
|
|
701
|
+
const missingScopes = HERMES_AGENT_API_SCOPES.filter((scope) => !existingScopes.includes(scope));
|
|
702
|
+
if (missingScopes.length > 0 && existing.id) {
|
|
703
|
+
apiKeysDb.updateApiKeyScopes(userId, existing.id, [...existingScopes, ...missingScopes]);
|
|
704
|
+
}
|
|
666
705
|
return existing.api_key;
|
|
667
706
|
}
|
|
668
707
|
|
|
669
|
-
return apiKeysDb.createApiKey(userId, 'Hermes Agent MCP',
|
|
670
|
-
'hermes:mcp',
|
|
671
|
-
'hermes:gateway',
|
|
672
|
-
'projects:read',
|
|
673
|
-
'providers:read',
|
|
674
|
-
'terminal:launch',
|
|
675
|
-
]).apiKey;
|
|
708
|
+
return apiKeysDb.createApiKey(userId, 'Hermes Agent MCP', HERMES_AGENT_API_SCOPES).apiKey;
|
|
676
709
|
}
|
|
677
710
|
|
|
678
711
|
// Single WebSocket server that handles both paths
|
|
@@ -815,6 +848,72 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) =>
|
|
|
815
848
|
});
|
|
816
849
|
});
|
|
817
850
|
|
|
851
|
+
app.post('/api/shell/sessions/provider-input', authenticateToken, (req, res) => {
|
|
852
|
+
const provider = String(req.body?.provider || 'claude');
|
|
853
|
+
const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.trim()
|
|
854
|
+
? req.body.projectPath.trim()
|
|
855
|
+
: null;
|
|
856
|
+
const launchId = Number.parseInt(String(req.body?.launchId || ''), 10);
|
|
857
|
+
const requestedLaunchId = Number.isFinite(launchId) && launchId > 0 ? launchId : null;
|
|
858
|
+
const input = typeof req.body?.input === 'string' ? req.body.input : '';
|
|
859
|
+
const submit = req.body?.submit !== false;
|
|
860
|
+
|
|
861
|
+
if (!SHELL_CLI_PROVIDERS.has(provider)) {
|
|
862
|
+
return res.status(400).json({ error: 'Unsupported provider' });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const requestedProjectPath = projectPath ? path.resolve(projectPath) : null;
|
|
866
|
+
let matchedSession = null;
|
|
867
|
+
for (const session of ptySessionsMap.values()) {
|
|
868
|
+
if (
|
|
869
|
+
session?.provider === provider &&
|
|
870
|
+
!session?.isPlainShell &&
|
|
871
|
+
session?.pty &&
|
|
872
|
+
session.lifecycleState === 'running' &&
|
|
873
|
+
(!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath) &&
|
|
874
|
+
(!requestedLaunchId || session.hermesLaunchId === requestedLaunchId)
|
|
875
|
+
) {
|
|
876
|
+
if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
|
|
877
|
+
matchedSession = session;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (!matchedSession?.pty) {
|
|
883
|
+
return res.status(404).json({
|
|
884
|
+
ok: false,
|
|
885
|
+
provider,
|
|
886
|
+
projectPath: requestedProjectPath,
|
|
887
|
+
launchId: requestedLaunchId,
|
|
888
|
+
wrote: false,
|
|
889
|
+
message: 'No running provider terminal session found for this project.',
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const data = submit ? normalizeTerminalStartupInput(input) : input;
|
|
894
|
+
try {
|
|
895
|
+
matchedSession.pty.write(data);
|
|
896
|
+
matchedSession.updatedAt = Date.now();
|
|
897
|
+
res.json({
|
|
898
|
+
ok: true,
|
|
899
|
+
provider,
|
|
900
|
+
projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
|
|
901
|
+
sessionId: matchedSession.sessionId || null,
|
|
902
|
+
launchId: matchedSession.hermesLaunchId || null,
|
|
903
|
+
wrote: true,
|
|
904
|
+
submitted: submit,
|
|
905
|
+
bytes: Buffer.byteLength(data),
|
|
906
|
+
});
|
|
907
|
+
} catch (error) {
|
|
908
|
+
res.status(500).json({
|
|
909
|
+
ok: false,
|
|
910
|
+
provider,
|
|
911
|
+
wrote: false,
|
|
912
|
+
error: error instanceof Error ? error.message : String(error),
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
818
917
|
// Authentication routes (public)
|
|
819
918
|
app.use('/api/auth', authRoutes);
|
|
820
919
|
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
ensureHermesGateway,
|
|
17
17
|
getHermesGatewayStatus,
|
|
18
18
|
probeHermesGateway,
|
|
19
|
+
readHermesDiagnostics,
|
|
20
|
+
requestHermesGateway,
|
|
19
21
|
runHermesGatewayPrompt,
|
|
20
22
|
stopHermesGateway,
|
|
21
23
|
} from '@/services/hermes-gateway.js';
|
|
@@ -280,6 +282,72 @@ export function createHermesRouter(options: HermesRouterOptions = {}): Router {
|
|
|
280
282
|
}
|
|
281
283
|
});
|
|
282
284
|
|
|
285
|
+
router.post('/gateway/request', async (req: PixcodeRequest, res) => {
|
|
286
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
287
|
+
const projectPath = typeof body.projectPath === 'string' && body.projectPath.trim()
|
|
288
|
+
? body.projectPath.trim()
|
|
289
|
+
: undefined;
|
|
290
|
+
const endpoint = typeof body.endpoint === 'string' ? body.endpoint : body.path;
|
|
291
|
+
const method = typeof body.method === 'string' ? body.method : 'GET';
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
if (body.startIfNeeded === true) {
|
|
295
|
+
const apiKey = options.createHermesApiKey?.(readUserId(req)) ?? null;
|
|
296
|
+
if (!apiKey) {
|
|
297
|
+
res.status(500).json({
|
|
298
|
+
error: {
|
|
299
|
+
code: 'HERMES_API_KEY_UNAVAILABLE',
|
|
300
|
+
message: 'Pixcode could not create a Hermes MCP API key for this user.',
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
await ensureHermesGateway({
|
|
306
|
+
appRoot: options.appRoot ?? process.cwd(),
|
|
307
|
+
pixcodeApiKey: apiKey,
|
|
308
|
+
pixcodeBaseUrl: resolveHermesMcpBaseUrl(),
|
|
309
|
+
projectPath,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const gatewayResponse = await requestHermesGateway(projectPath, {
|
|
314
|
+
endpoint,
|
|
315
|
+
method,
|
|
316
|
+
body: body.body,
|
|
317
|
+
timeoutMs: typeof body.timeoutMs === 'number' ? body.timeoutMs : undefined,
|
|
318
|
+
});
|
|
319
|
+
res.status(gatewayResponse.ok ? 200 : 502).json(gatewayResponse);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
res.status(500).json({
|
|
322
|
+
error: {
|
|
323
|
+
code: 'HERMES_GATEWAY_REQUEST_FAILED',
|
|
324
|
+
message: error instanceof Error ? error.message : String(error),
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
router.get('/diagnostics', async (req, res) => {
|
|
331
|
+
const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
|
|
332
|
+
? req.query.projectPath.trim()
|
|
333
|
+
: undefined;
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const diagnostics = await readHermesDiagnostics({
|
|
337
|
+
appRoot: options.appRoot ?? process.cwd(),
|
|
338
|
+
projectPath,
|
|
339
|
+
});
|
|
340
|
+
res.status(diagnostics.ok ? 200 : 503).json(diagnostics);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
res.status(500).json({
|
|
343
|
+
error: {
|
|
344
|
+
code: 'HERMES_DIAGNOSTICS_FAILED',
|
|
345
|
+
message: error instanceof Error ? error.message : String(error),
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
283
351
|
router.post('/gateway/stop', (req, res) => {
|
|
284
352
|
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
285
353
|
const projectPath = typeof body.projectPath === 'string' ? body.projectPath : null;
|