@pixelbyte-software/pixcode 1.50.8 → 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
@@ -7,6 +7,7 @@ import path from 'path';
7
7
  import os from 'os';
8
8
  import http from 'http';
9
9
  import net from 'node:net';
10
+ import { createRequire } from 'node:module';
10
11
  import { spawn } from 'child_process';
11
12
 
12
13
  import express from 'express';
@@ -23,6 +24,8 @@ const __dirname = getModuleDir(import.meta.url);
23
24
  // The server source runs from /server, while the compiled output runs from /dist-server/server.
24
25
  // Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
25
26
  const APP_ROOT = findAppRoot(__dirname);
27
+ const require = createRequire(import.meta.url);
28
+ const MONACO_ASSETS_ROUTE = '/vendor/monaco-editor/min/vs';
26
29
  const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
27
30
  const SERVER_VERSION = (() => {
28
31
  try {
@@ -38,6 +41,23 @@ const DAEMON_COMMAND_CONTEXT = {
38
41
  nodeExecPath: process.execPath,
39
42
  };
40
43
 
44
+ function resolveMonacoAssetsPath() {
45
+ const candidates = [
46
+ path.join(APP_ROOT, 'node_modules', 'monaco-editor', 'min', 'vs'),
47
+ ];
48
+
49
+ try {
50
+ const monacoPackagePath = require.resolve('monaco-editor/package.json', {
51
+ paths: [APP_ROOT, __dirname],
52
+ });
53
+ candidates.push(path.join(path.dirname(monacoPackagePath), 'min', 'vs'));
54
+ } catch {
55
+ // The editor will show its normal load failure if the dependency is unavailable.
56
+ }
57
+
58
+ return candidates.find((candidate) => fs.existsSync(path.join(candidate, 'loader.js'))) || null;
59
+ }
60
+
41
61
  import { c } from './utils/colors.js';
42
62
 
43
63
  console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
@@ -621,6 +641,40 @@ function quoteShellArgForPlatform(value) {
621
641
  }
622
642
 
623
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
+ ];
624
678
 
625
679
  function isHermesCliCommand(command) {
626
680
  return typeof command === 'string' && HERMES_CLI_COMMAND_PATTERN.test(command.trim());
@@ -643,16 +697,15 @@ function getOrCreateHermesApiKey(userId) {
643
697
  .getApiKeys(userId)
644
698
  .find((key) => key.key_name === 'Hermes Agent MCP' && key.is_active);
645
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
+ }
646
705
  return existing.api_key;
647
706
  }
648
707
 
649
- return apiKeysDb.createApiKey(userId, 'Hermes Agent MCP', [
650
- 'hermes:mcp',
651
- 'hermes:gateway',
652
- 'projects:read',
653
- 'providers:read',
654
- 'terminal:launch',
655
- ]).apiKey;
708
+ return apiKeysDb.createApiKey(userId, 'Hermes Agent MCP', HERMES_AGENT_API_SCOPES).apiKey;
656
709
  }
657
710
 
658
711
  // Single WebSocket server that handles both paths
@@ -795,6 +848,72 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) =>
795
848
  });
796
849
  });
797
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
+
798
917
  // Authentication routes (public)
799
918
  app.use('/api/auth', authRoutes);
800
919
 
@@ -888,6 +1007,18 @@ app.use('/api/agent', agentRoutes);
888
1007
  // Static app files served after API routes. Keep dist before public so
889
1008
  // / and /index.html always resolve to the Pixcode app, not the GitHub Pages
890
1009
  // landing page that also lives in public/index.html.
1010
+ const monacoAssetsPath = resolveMonacoAssetsPath();
1011
+ if (monacoAssetsPath) {
1012
+ app.use(MONACO_ASSETS_ROUTE, express.static(monacoAssetsPath, {
1013
+ index: false,
1014
+ setHeaders: (res) => {
1015
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
1016
+ },
1017
+ }));
1018
+ } else {
1019
+ console.warn('[monaco] Local Monaco assets not found; code editor loader may fail.');
1020
+ }
1021
+
891
1022
  app.use(express.static(path.join(APP_ROOT, 'dist'), {
892
1023
  setHeaders: (res, filePath) => {
893
1024
  if (filePath.endsWith('.html')) {