@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.
- package/dist/assets/{index-gecaamTl.js → index-DJosGZ59.js} +159 -159
- package/dist/assets/localMonaco-DFIbtDNp.js +1 -0
- package/dist/index.html +1 -1
- package/dist-server/server/index.js +130 -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 +2 -2
- package/scripts/hermes/configure-pixcode-mcp.mjs +7 -1
- package/scripts/hermes/pixcode-mcp-server.mjs +383 -36
- package/scripts/smoke/code-editor-vscode-engine.mjs +31 -0
- 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 +138 -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
|
@@ -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;
|
|
@@ -19,6 +19,22 @@ const FETCH_TIMEOUT_MS = 5000;
|
|
|
19
19
|
const RUN_TIMEOUT_MS = 120000;
|
|
20
20
|
const RUN_POLL_INTERVAL_MS = 1000;
|
|
21
21
|
const LOG_LIMIT = 800;
|
|
22
|
+
const HERMES_DIAGNOSTIC_LOG_BYTES = 120000;
|
|
23
|
+
const ALLOWED_GATEWAY_REQUEST_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
24
|
+
const EXPECTED_PIXCODE_MCP_TOOLS = [
|
|
25
|
+
'pixcode_list_projects',
|
|
26
|
+
'pixcode_get_provider_status',
|
|
27
|
+
'pixcode_open_cli_terminal',
|
|
28
|
+
'pixcode_read_cli_terminal',
|
|
29
|
+
'pixcode_get_hermes_gateway_status',
|
|
30
|
+
'pixcode_probe_hermes_gateway',
|
|
31
|
+
'pixcode_get_hermes_diagnostics',
|
|
32
|
+
'pixcode_get_api_manifest',
|
|
33
|
+
'pixcode_api_request',
|
|
34
|
+
'pixcode_hermes_gateway_request',
|
|
35
|
+
'pixcode_manage_hermes_cron',
|
|
36
|
+
'pixcode_send_cli_input',
|
|
37
|
+
];
|
|
22
38
|
const PIXCODE_MANAGED_HERMES_ENV_PREFIXES = [
|
|
23
39
|
'API_SERVER_',
|
|
24
40
|
'BLUEBUBBLES_',
|
|
@@ -334,6 +350,170 @@ function recentGatewayLogText(gateway) {
|
|
|
334
350
|
.trim();
|
|
335
351
|
}
|
|
336
352
|
|
|
353
|
+
function readFileTail(filePath, maxBytes = HERMES_DIAGNOSTIC_LOG_BYTES) {
|
|
354
|
+
try {
|
|
355
|
+
const stat = fs.statSync(filePath);
|
|
356
|
+
const length = Math.min(maxBytes, stat.size);
|
|
357
|
+
const buffer = Buffer.alloc(length);
|
|
358
|
+
const fd = fs.openSync(filePath, 'r');
|
|
359
|
+
try {
|
|
360
|
+
fs.readSync(fd, buffer, 0, length, stat.size - length);
|
|
361
|
+
} finally {
|
|
362
|
+
fs.closeSync(fd);
|
|
363
|
+
}
|
|
364
|
+
return buffer.toString('utf8');
|
|
365
|
+
} catch {
|
|
366
|
+
return '';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function readJsonFileSafe(filePath) {
|
|
371
|
+
try {
|
|
372
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function redactDiagnosticText(text) {
|
|
379
|
+
return String(text || '')
|
|
380
|
+
.replace(/\b(px_|ck_|sk-|ghp_|npm_)[A-Za-z0-9._-]+/gu, '$1[redacted]')
|
|
381
|
+
.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/giu, '$1[redacted]')
|
|
382
|
+
.replace(/((?:api[_-]?key|authorization|access[_-]?token|refresh[_-]?token|id[_-]?token|token)\s*[:=]\s*["']?)[^"',\s}]+/giu, '$1[redacted]');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function findRootBlockEnd(lines, startIndex) {
|
|
386
|
+
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
387
|
+
if (/^\S[^:]*:\s*(?:#.*)?$/u.test(lines[index])) {
|
|
388
|
+
return index;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return lines.length;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function readRootList(text, key) {
|
|
395
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
396
|
+
const start = lines.findIndex((line) => new RegExp(`^${key}:\\s*(?:#.*)?$`, 'u').test(line));
|
|
397
|
+
if (start === -1) return [];
|
|
398
|
+
const end = findRootBlockEnd(lines, start);
|
|
399
|
+
const values = [];
|
|
400
|
+
for (let index = start + 1; index < end; index += 1) {
|
|
401
|
+
const match = lines[index].match(/^\s*-\s*([^#\s][^#]*?)(?:\s+#.*)?$/u);
|
|
402
|
+
if (match) values.push(match[1].trim().replace(/^['"]|['"]$/gu, ''));
|
|
403
|
+
}
|
|
404
|
+
return values;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function readRootMap(text, key) {
|
|
408
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
409
|
+
const start = lines.findIndex((line) => new RegExp(`^${key}:\\s*(?:#.*)?$`, 'u').test(line));
|
|
410
|
+
if (start === -1) return {};
|
|
411
|
+
const end = findRootBlockEnd(lines, start);
|
|
412
|
+
const values = {};
|
|
413
|
+
for (let index = start + 1; index < end; index += 1) {
|
|
414
|
+
const match = lines[index].match(/^\s+([A-Za-z0-9_.-]+):\s*(.*?)(?:\s+#.*)?$/u);
|
|
415
|
+
if (!match) continue;
|
|
416
|
+
values[match[1]] = match[2].trim().replace(/^['"]|['"]$/gu, '');
|
|
417
|
+
}
|
|
418
|
+
return values;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function readPixcodeMcpTools(text) {
|
|
422
|
+
return Array.from(new Set(
|
|
423
|
+
Array.from(String(text || '').matchAll(/^\s*-\s*(pixcode_[A-Za-z0-9_]+)\s*$/gmu))
|
|
424
|
+
.map((match) => match[1]),
|
|
425
|
+
));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function readApiServerToolset(text) {
|
|
429
|
+
const platformText = String(text || '');
|
|
430
|
+
return {
|
|
431
|
+
hasHermesApiServer: /^\s*-\s*hermes-api-server\s*$/gmu.test(platformText),
|
|
432
|
+
hasPixcodePlatform: /^\s*-\s*pixcode\s*$/gmu.test(platformText),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function summarizeHermesConfig(hermesHome) {
|
|
437
|
+
const configPath = path.join(hermesHome, 'config.yaml');
|
|
438
|
+
const text = readFileTail(configPath, HERMES_DIAGNOSTIC_LOG_BYTES);
|
|
439
|
+
const toolsets = readRootList(text, 'toolsets');
|
|
440
|
+
const pixcodeTools = readPixcodeMcpTools(text);
|
|
441
|
+
const missingPixcodeTools = EXPECTED_PIXCODE_MCP_TOOLS.filter((tool) => !pixcodeTools.includes(tool));
|
|
442
|
+
return {
|
|
443
|
+
path: configPath,
|
|
444
|
+
exists: Boolean(text),
|
|
445
|
+
model: readRootMap(text, 'model'),
|
|
446
|
+
toolsets,
|
|
447
|
+
platformToolsets: readApiServerToolset(text),
|
|
448
|
+
pixcodeMcp: {
|
|
449
|
+
configured: /mcp_servers:[\s\S]*^\s+pixcode:\s*$/mu.test(text),
|
|
450
|
+
enabled: /mcp_servers:[\s\S]*^\s+pixcode:[\s\S]*^\s+enabled:\s*true\s*$/mu.test(text),
|
|
451
|
+
toolCount: pixcodeTools.length,
|
|
452
|
+
tools: pixcodeTools,
|
|
453
|
+
missingTools: missingPixcodeTools,
|
|
454
|
+
},
|
|
455
|
+
staleToolsetConfig: toolsets.includes('mcp-pixcode') && !toolsets.includes('hermes-cli'),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function summarizeHermesAuth(hermesHome, provider) {
|
|
460
|
+
const authPath = path.join(hermesHome, 'auth.json');
|
|
461
|
+
const auth = readJsonFileSafe(authPath);
|
|
462
|
+
const providers = auth && typeof auth === 'object' && auth.providers && typeof auth.providers === 'object'
|
|
463
|
+
? Object.keys(auth.providers)
|
|
464
|
+
: [];
|
|
465
|
+
const pools = auth && typeof auth === 'object' && auth.credential_pool && typeof auth.credential_pool === 'object'
|
|
466
|
+
? auth.credential_pool
|
|
467
|
+
: {};
|
|
468
|
+
const selectedProvider = provider || auth?.active_provider || null;
|
|
469
|
+
const providerEntry = selectedProvider && auth?.providers && typeof auth.providers === 'object'
|
|
470
|
+
? auth.providers[selectedProvider]
|
|
471
|
+
: null;
|
|
472
|
+
return {
|
|
473
|
+
path: authPath,
|
|
474
|
+
exists: Boolean(auth),
|
|
475
|
+
activeProvider: auth?.active_provider || null,
|
|
476
|
+
providers,
|
|
477
|
+
selectedProvider,
|
|
478
|
+
selectedProviderConfigured: Boolean(providerEntry),
|
|
479
|
+
selectedProviderLastRefresh: providerEntry?.last_refresh || null,
|
|
480
|
+
selectedProviderAuthMode: providerEntry?.auth_mode || null,
|
|
481
|
+
selectedProviderPoolSize: selectedProvider && Array.isArray(pools?.[selectedProvider])
|
|
482
|
+
? pools[selectedProvider].length
|
|
483
|
+
: 0,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function summarizeHermesLogs(hermesHomes) {
|
|
488
|
+
const files = [];
|
|
489
|
+
const seen = new Set();
|
|
490
|
+
for (const home of hermesHomes.filter(Boolean)) {
|
|
491
|
+
for (const name of ['errors.log', 'agent.log']) {
|
|
492
|
+
const filePath = path.join(home, 'logs', name);
|
|
493
|
+
if (seen.has(filePath)) continue;
|
|
494
|
+
seen.add(filePath);
|
|
495
|
+
const text = redactDiagnosticText(readFileTail(filePath));
|
|
496
|
+
if (!text) continue;
|
|
497
|
+
files.push({
|
|
498
|
+
path: filePath,
|
|
499
|
+
name,
|
|
500
|
+
recent: text.split(/\r?\n/).filter(Boolean).slice(-80),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const combined = files.flatMap((file) => file.recent).join('\n');
|
|
505
|
+
return {
|
|
506
|
+
files,
|
|
507
|
+
signals: {
|
|
508
|
+
codexNoneType: /NoneType' object is not iterable|NoneType object is not iterable/iu.test(combined),
|
|
509
|
+
codexOauthMissing: /openai-codex requested but no Codex OAuth .*found/iu.test(combined),
|
|
510
|
+
mcpTimeout: /MCP call timed out|pixcode_open_cli_terminal call failed/iu.test(combined),
|
|
511
|
+
stalePixcodeMcpToolCount: /MCP server 'pixcode'.*registered\s+[0-9]\s+tool\(s\)/iu.test(combined)
|
|
512
|
+
&& !/registered\s+1[0-9]\s+tool\(s\)/iu.test(combined),
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
337
517
|
function gatewayExitMessage(gateway, fallback = 'Hermes gateway is not running.') {
|
|
338
518
|
if (!gateway) return fallback;
|
|
339
519
|
const exit = gateway.exitSignal
|
|
@@ -343,6 +523,36 @@ function gatewayExitMessage(gateway, fallback = 'Hermes gateway is not running.'
|
|
|
343
523
|
return logs ? `${exit}\n${logs}` : (gateway.error || exit);
|
|
344
524
|
}
|
|
345
525
|
|
|
526
|
+
function normalizeGatewayEndpoint(endpoint) {
|
|
527
|
+
const value = typeof endpoint === 'string' ? endpoint.trim() : '';
|
|
528
|
+
if (!value) {
|
|
529
|
+
throw new Error('Hermes gateway endpoint is required.');
|
|
530
|
+
}
|
|
531
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(value) || value.startsWith('//')) {
|
|
532
|
+
throw new Error('Hermes gateway endpoint must be local; external URLs are not allowed.');
|
|
533
|
+
}
|
|
534
|
+
if (!value.startsWith('/')) {
|
|
535
|
+
throw new Error('Hermes gateway endpoint must start with /.');
|
|
536
|
+
}
|
|
537
|
+
if (
|
|
538
|
+
value !== '/health' &&
|
|
539
|
+
value !== '/health/detailed' &&
|
|
540
|
+
!value.startsWith('/v1/') &&
|
|
541
|
+
!value.startsWith('/api/')
|
|
542
|
+
) {
|
|
543
|
+
throw new Error('Hermes gateway endpoint must be /health, /v1/..., or /api/....');
|
|
544
|
+
}
|
|
545
|
+
return value;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function normalizeGatewayRequestMethod(method) {
|
|
549
|
+
const value = String(method || 'GET').trim().toUpperCase();
|
|
550
|
+
if (!ALLOWED_GATEWAY_REQUEST_METHODS.has(value)) {
|
|
551
|
+
throw new Error(`Unsupported Hermes gateway HTTP method: ${value || '(empty)'}`);
|
|
552
|
+
}
|
|
553
|
+
return value;
|
|
554
|
+
}
|
|
555
|
+
|
|
346
556
|
function makeRunRequest(options) {
|
|
347
557
|
const input = String(options.input || '').trim();
|
|
348
558
|
return {
|
|
@@ -639,15 +849,18 @@ export async function probeHermesGateway(projectPath, options = {}) {
|
|
|
639
849
|
|
|
640
850
|
if (typeof options.input === 'string' && options.input.trim()) {
|
|
641
851
|
try {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
instructions: options.instructions || 'Respond briefly for a Pixcode REST integration check.',
|
|
648
|
-
}),
|
|
649
|
-
timeoutMs: options.runTimeoutMs || 15000,
|
|
852
|
+
const run = await runHermesGatewayPrompt(gateway.projectPath, {
|
|
853
|
+
input: options.input.trim(),
|
|
854
|
+
sessionId: options.sessionId || `pixcode-probe-${Date.now()}`,
|
|
855
|
+
instructions: options.instructions || 'Respond briefly for a Pixcode REST integration check.',
|
|
856
|
+
timeoutMs: options.runTimeoutMs || 30000,
|
|
650
857
|
});
|
|
858
|
+
checks.run = {
|
|
859
|
+
ok: run.ok,
|
|
860
|
+
status: run.httpStatus || 200,
|
|
861
|
+
body: run,
|
|
862
|
+
error: run.error || null,
|
|
863
|
+
};
|
|
651
864
|
} catch (error) {
|
|
652
865
|
checks.run = { ok: false, status: 0, error: error instanceof Error ? error.message : String(error) };
|
|
653
866
|
}
|
|
@@ -841,6 +1054,181 @@ export async function runHermesGatewayPrompt(projectPath, options = {}) {
|
|
|
841
1054
|
};
|
|
842
1055
|
}
|
|
843
1056
|
|
|
1057
|
+
export async function requestHermesGateway(projectPath, options = {}) {
|
|
1058
|
+
const gateway = projectPath
|
|
1059
|
+
? gateways.get(normalizeProjectPath(projectPath))
|
|
1060
|
+
: Array.from(gateways.values()).find(isGatewayRunning);
|
|
1061
|
+
|
|
1062
|
+
if (!isGatewayRunning(gateway)) {
|
|
1063
|
+
throw new Error('Hermes gateway is not running.');
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const endpoint = normalizeGatewayEndpoint(options.endpoint || options.path);
|
|
1067
|
+
const method = normalizeGatewayRequestMethod(options.method);
|
|
1068
|
+
const requestOptions = {
|
|
1069
|
+
method,
|
|
1070
|
+
timeoutMs: options.timeoutMs || FETCH_TIMEOUT_MS,
|
|
1071
|
+
};
|
|
1072
|
+
if (typeof options.body !== 'undefined' && options.body !== null && method !== 'GET') {
|
|
1073
|
+
requestOptions.body = JSON.stringify(options.body);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const response = await callGateway(gateway, endpoint, requestOptions);
|
|
1077
|
+
return {
|
|
1078
|
+
ok: response.ok,
|
|
1079
|
+
status: response.status,
|
|
1080
|
+
projectPath: gateway.projectPath,
|
|
1081
|
+
baseUrl: gateway.baseUrl,
|
|
1082
|
+
endpoint,
|
|
1083
|
+
method,
|
|
1084
|
+
body: response.body,
|
|
1085
|
+
error: response.ok ? null : `Hermes gateway ${method} ${endpoint} failed with HTTP ${response.status}.`,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
export async function readHermesDiagnostics(options = {}) {
|
|
1090
|
+
const projectPath = options.projectPath ? normalizeProjectPath(options.projectPath) : null;
|
|
1091
|
+
const gateway = projectPath
|
|
1092
|
+
? gateways.get(projectPath)
|
|
1093
|
+
: Array.from(gateways.values()).find(isGatewayRunning) || null;
|
|
1094
|
+
const sourceHermesHome = resolveSourceHermesHome(process.env);
|
|
1095
|
+
const gatewayHermesHome = resolveHermesGatewayHome(process.env, options);
|
|
1096
|
+
const installStatus = readHermesInstallStatus(process.env, {
|
|
1097
|
+
allowSmokeHermes: options.allowSmokeHermes === true,
|
|
1098
|
+
repairLaunchers: options.repairLaunchers !== false,
|
|
1099
|
+
});
|
|
1100
|
+
const sourceConfig = summarizeHermesConfig(sourceHermesHome);
|
|
1101
|
+
const gatewayConfig = summarizeHermesConfig(gatewayHermesHome);
|
|
1102
|
+
const activeConfig = gatewayConfig.exists ? gatewayConfig : sourceConfig;
|
|
1103
|
+
const provider = activeConfig.model.provider || sourceConfig.model.provider || null;
|
|
1104
|
+
const sourceAuth = summarizeHermesAuth(sourceHermesHome, provider);
|
|
1105
|
+
const gatewayAuth = summarizeHermesAuth(gatewayHermesHome, provider);
|
|
1106
|
+
const activeAuth = gatewayAuth.exists ? gatewayAuth : sourceAuth;
|
|
1107
|
+
const logs = summarizeHermesLogs([sourceHermesHome, gatewayHermesHome]);
|
|
1108
|
+
const issues = [];
|
|
1109
|
+
|
|
1110
|
+
if (!installStatus.installed) {
|
|
1111
|
+
issues.push({
|
|
1112
|
+
severity: 'error',
|
|
1113
|
+
code: 'HERMES_NOT_INSTALLED',
|
|
1114
|
+
message: installStatus.error || 'Hermes Agent CLI is not installed.',
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
if (!activeConfig.toolsets.includes('hermes-cli')) {
|
|
1118
|
+
issues.push({
|
|
1119
|
+
severity: 'error',
|
|
1120
|
+
code: 'HERMES_CLI_TOOLSET_MISSING',
|
|
1121
|
+
message: 'Hermes CLI toolset is not enabled; cron, file, terminal, skills, and native tools are unavailable.',
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
if (!activeConfig.toolsets.includes('mcp-pixcode')) {
|
|
1125
|
+
issues.push({
|
|
1126
|
+
severity: 'error',
|
|
1127
|
+
code: 'PIXCODE_MCP_TOOLSET_MISSING',
|
|
1128
|
+
message: 'Pixcode MCP toolset is not enabled in Hermes config.',
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
if (activeConfig.pixcodeMcp.missingTools.length > 0) {
|
|
1132
|
+
issues.push({
|
|
1133
|
+
severity: 'warning',
|
|
1134
|
+
code: 'PIXCODE_MCP_TOOLS_STALE',
|
|
1135
|
+
message: `Pixcode MCP config is missing ${activeConfig.pixcodeMcp.missingTools.length} current tool(s). Restart Hermes from Pixcode to rewrite the config.`,
|
|
1136
|
+
tools: activeConfig.pixcodeMcp.missingTools,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
if (provider === 'openai-codex' && !activeAuth.selectedProviderConfigured) {
|
|
1140
|
+
issues.push({
|
|
1141
|
+
severity: 'error',
|
|
1142
|
+
code: 'OPENAI_CODEX_AUTH_MISSING',
|
|
1143
|
+
message: 'Hermes is configured for OpenAI Codex, but Hermes auth.json does not contain an OpenAI Codex OAuth session.',
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
if (logs.signals.codexNoneType) {
|
|
1147
|
+
issues.push({
|
|
1148
|
+
severity: 'error',
|
|
1149
|
+
code: 'OPENAI_CODEX_PROVIDER_FAILURE',
|
|
1150
|
+
message: 'Recent Hermes logs show OpenAI Codex provider failing with "NoneType object is not iterable" before Pixcode MCP tools run.',
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
if (logs.signals.codexOauthMissing) {
|
|
1154
|
+
issues.push({
|
|
1155
|
+
severity: 'warning',
|
|
1156
|
+
code: 'OPENAI_CODEX_OAUTH_WARNING',
|
|
1157
|
+
message: 'Recent Hermes logs reported a missing OpenAI Codex OAuth token. Run Hermes model/auth from Settings if prompts fail.',
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
if (logs.signals.mcpTimeout) {
|
|
1161
|
+
issues.push({
|
|
1162
|
+
severity: 'warning',
|
|
1163
|
+
code: 'PIXCODE_MCP_TIMEOUT',
|
|
1164
|
+
message: 'Recent Hermes logs include Pixcode MCP terminal timeouts; visible CLI readback may still be waiting for provider completion.',
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const cron = {
|
|
1169
|
+
toolsetAvailable: activeConfig.toolsets.includes('hermes-cli'),
|
|
1170
|
+
gatewayJobsApi: null,
|
|
1171
|
+
};
|
|
1172
|
+
if (isGatewayRunning(gateway)) {
|
|
1173
|
+
try {
|
|
1174
|
+
const jobs = await callGateway(gateway, '/api/jobs', { timeoutMs: 3000 });
|
|
1175
|
+
cron.gatewayJobsApi = {
|
|
1176
|
+
ok: jobs.ok,
|
|
1177
|
+
status: jobs.status,
|
|
1178
|
+
body: jobs.body,
|
|
1179
|
+
};
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
cron.gatewayJobsApi = {
|
|
1182
|
+
ok: false,
|
|
1183
|
+
status: 0,
|
|
1184
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const recommendedActions = [];
|
|
1190
|
+
if (issues.some((issue) => issue.code === 'HERMES_CLI_TOOLSET_MISSING' || issue.code === 'PIXCODE_MCP_TOOLS_STALE')) {
|
|
1191
|
+
recommendedActions.push('Restart Hermes from Pixcode so configure-pixcode-mcp.mjs rewrites toolsets to hermes-cli + mcp-pixcode and registers all tools.');
|
|
1192
|
+
}
|
|
1193
|
+
if (issues.some((issue) => issue.code === 'OPENAI_CODEX_AUTH_MISSING' || issue.code === 'OPENAI_CODEX_PROVIDER_FAILURE')) {
|
|
1194
|
+
recommendedActions.push('Open Settings > Hermes Agent > Model and provider, reselect OpenAI Codex or another provider, then run Test REST with a short prompt.');
|
|
1195
|
+
}
|
|
1196
|
+
if (!isGatewayRunning(gateway)) {
|
|
1197
|
+
recommendedActions.push('Start REST in Settings > Hermes Agent to enable /v1 and /api/jobs gateway checks for this workspace.');
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return {
|
|
1201
|
+
ok: installStatus.installed && !issues.some((issue) => issue.severity === 'error'),
|
|
1202
|
+
generatedAt: nowIso(),
|
|
1203
|
+
install: installStatus,
|
|
1204
|
+
hermesHome: {
|
|
1205
|
+
source: sourceHermesHome,
|
|
1206
|
+
gateway: gatewayHermesHome,
|
|
1207
|
+
},
|
|
1208
|
+
model: {
|
|
1209
|
+
provider,
|
|
1210
|
+
default: activeConfig.model.default || null,
|
|
1211
|
+
baseUrl: activeConfig.model.base_url || null,
|
|
1212
|
+
},
|
|
1213
|
+
config: {
|
|
1214
|
+
source: sourceConfig,
|
|
1215
|
+
gateway: gatewayConfig,
|
|
1216
|
+
active: activeConfig,
|
|
1217
|
+
activePath: activeConfig.path,
|
|
1218
|
+
},
|
|
1219
|
+
auth: {
|
|
1220
|
+
source: sourceAuth,
|
|
1221
|
+
gateway: gatewayAuth,
|
|
1222
|
+
active: activeAuth,
|
|
1223
|
+
},
|
|
1224
|
+
gateway: snapshotGateway(gateway),
|
|
1225
|
+
cron,
|
|
1226
|
+
logs,
|
|
1227
|
+
issues,
|
|
1228
|
+
recommendedActions,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
844
1232
|
export function stopHermesGateway(projectPath) {
|
|
845
1233
|
const targets = projectPath
|
|
846
1234
|
? [gateways.get(normalizeProjectPath(projectPath))].filter(Boolean)
|
|
@@ -3,6 +3,8 @@ const API_GROUPS = [
|
|
|
3
3
|
{ id: 'projects', title: 'Projects', basePath: '/api/projects', scopes: ['projects:read', 'projects:write'] },
|
|
4
4
|
{ id: 'sessions', title: 'Sessions and messages', basePath: '/api/sessions', scopes: ['sessions:read', 'sessions:write'] },
|
|
5
5
|
{ id: 'providers', title: 'CLI providers', basePath: '/api/providers', scopes: ['providers:read', 'providers:write'] },
|
|
6
|
+
{ id: 'terminal', title: 'Visible terminal sessions', basePath: '/api/shell/sessions', scopes: ['terminal:launch'] },
|
|
7
|
+
{ id: 'hermes', title: 'Hermes Agent control', basePath: '/api/orchestration/hermes', scopes: ['hermes:mcp', 'hermes:gateway', 'terminal:launch'] },
|
|
6
8
|
{ id: 'orchestration', title: 'Orchestration runs', basePath: '/api/orchestration', scopes: ['orchestration:read', 'orchestration:write'] },
|
|
7
9
|
{ id: 'notifications', title: 'Notifications', basePath: '/api/settings/notifications', scopes: ['notifications:read', 'notifications:write'] },
|
|
8
10
|
{ id: 'files', title: 'Files', basePath: '/api/projects/:projectName/files', scopes: ['files:read', 'files:write'] },
|