@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.
@@ -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 --toolsets mcp-pixcode'/,
70
- 'Hermes terminal should default to Pixcode MCP-only toolsets so provider CLIs stay visible inside Pixcode.',
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
- /startIfNeeded:\s*args\.startIfNeeded !== false/,
90
- 'Pixcode MCP gateway probes should start the managed REST gateway by default unless Hermes explicitly opts out.',
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 --toolsets mcp-pixcode'/, 'Hermes Agent bottom panel should launch the actual `hermes` CLI with Pixcode MCP-only tools.');
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 --toolsets mcp-pixcode'/,
328
- 'Hermes Agent should launch the Hermes CLI directly in bypass mode with only the Pixcode MCP toolset.',
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;