@pixelbyte-software/pixcode 1.50.9 → 1.51.1

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.
@@ -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
- checks.run = await callGateway(gateway, '/v1/runs', {
643
- method: 'POST',
644
- body: JSON.stringify({
645
- input: options.input.trim(),
646
- session_id: options.sessionId || `pixcode-${Date.now()}`,
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'] },