@pixelbyte-software/pixcode 1.49.10 → 1.50.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.
Files changed (28) hide show
  1. package/dist/assets/{index-DVpGrdpT.js → index-81sOpj25.js} +182 -182
  2. package/dist/assets/index-DMz0zv6T.css +32 -0
  3. package/dist/hermes-agent.png +0 -0
  4. package/dist/index.html +2 -2
  5. package/dist-server/server/index.js +77 -1
  6. package/dist-server/server/index.js.map +1 -1
  7. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +40 -7
  8. package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
  9. package/dist-server/server/services/hermes-gateway.js +115 -1
  10. package/dist-server/server/services/hermes-gateway.js.map +1 -1
  11. package/dist-server/server/services/hermes-install-jobs.js +9 -1
  12. package/dist-server/server/services/hermes-install-jobs.js.map +1 -1
  13. package/package.json +1 -1
  14. package/scripts/hermes/configure-pixcode-mcp.mjs +37 -1
  15. package/scripts/hermes/pixcode-mcp-server.mjs +166 -8
  16. package/scripts/smoke/hermes-api-install.mjs +4 -4
  17. package/scripts/smoke/hermes-gateway-persistence.mjs +104 -0
  18. package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +27 -0
  19. package/scripts/smoke/hermes-rest-chat-live.mjs +4 -0
  20. package/scripts/smoke/hermes-rest-codex-launch.mjs +11 -4
  21. package/scripts/smoke/hermes-rest-gateway.mjs +9 -1
  22. package/scripts/smoke/hermes-settings-commands.mjs +166 -0
  23. package/scripts/smoke/pixcode-workbench-1-48.mjs +13 -5
  24. package/server/index.js +94 -1
  25. package/server/modules/orchestration/hermes/hermes.routes.ts +45 -7
  26. package/server/services/hermes-gateway.js +128 -1
  27. package/server/services/hermes-install-jobs.js +11 -1
  28. package/dist/assets/index-BT6txdBK.css +0 -32
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import fs from 'node:fs';
2
3
  import net from 'node:net';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
@@ -18,6 +19,27 @@ const FETCH_TIMEOUT_MS = 5000;
18
19
  const RUN_TIMEOUT_MS = 120000;
19
20
  const RUN_POLL_INTERVAL_MS = 1000;
20
21
  const LOG_LIMIT = 800;
22
+ const PIXCODE_MANAGED_HERMES_ENV_PREFIXES = [
23
+ 'API_SERVER_',
24
+ 'BLUEBUBBLES_',
25
+ 'DINGTALK_',
26
+ 'DISCORD_',
27
+ 'EMAIL_',
28
+ 'FEISHU_',
29
+ 'MATTERMOST_',
30
+ 'MATRIX_',
31
+ 'MSGRAPH_',
32
+ 'QQ_',
33
+ 'SIGNAL_',
34
+ 'SLACK_',
35
+ 'SMS_',
36
+ 'TELEGRAM_',
37
+ 'TWILIO_',
38
+ 'WECOM_',
39
+ 'WEIXIN_',
40
+ 'WHATSAPP_',
41
+ 'YUANBAO_',
42
+ ];
21
43
 
22
44
  const gateways = new Map();
23
45
 
@@ -53,6 +75,96 @@ function sleep(ms) {
53
75
  return new Promise((resolve) => setTimeout(resolve, ms));
54
76
  }
55
77
 
78
+ function resolveSourceHermesHome(env = process.env) {
79
+ if (env.HERMES_HOME?.trim()) {
80
+ return path.resolve(env.HERMES_HOME);
81
+ }
82
+
83
+ const defaultHome = path.join(os.homedir(), '.hermes');
84
+ try {
85
+ const activeProfile = fs.readFileSync(path.join(defaultHome, 'active_profile'), 'utf8').trim();
86
+ if (activeProfile && activeProfile !== 'default' && /^[a-z0-9][a-z0-9_-]{0,63}$/.test(activeProfile)) {
87
+ return path.join(defaultHome, 'profiles', activeProfile);
88
+ }
89
+ } catch {
90
+ // Default Hermes profile is fine when no sticky active profile exists.
91
+ }
92
+
93
+ return defaultHome;
94
+ }
95
+
96
+ function resolveHermesGatewayHome(env = process.env, options = {}) {
97
+ const configured = options.hermesHome || env.PIXCODE_HERMES_GATEWAY_HOME;
98
+ if (configured) {
99
+ return path.resolve(configured);
100
+ }
101
+
102
+ return path.join(os.homedir(), '.hermes', 'profiles', 'pixcode');
103
+ }
104
+
105
+ function copyHermesProfileFile(sourceHome, targetHome, fileName, options = {}) {
106
+ const source = path.join(sourceHome, fileName);
107
+ const target = path.join(targetHome, fileName);
108
+ if (!fs.existsSync(source)) return false;
109
+ if (!options.overwrite && fs.existsSync(target)) return false;
110
+ fs.mkdirSync(path.dirname(target), { recursive: true });
111
+ fs.copyFileSync(source, target);
112
+ return true;
113
+ }
114
+
115
+ function shouldStripManagedGatewayEnvLine(line) {
116
+ const match = String(line || '').match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=/);
117
+ if (!match) return false;
118
+ return PIXCODE_MANAGED_HERMES_ENV_PREFIXES.some((prefix) => match[1].startsWith(prefix));
119
+ }
120
+
121
+ function copyHermesProfileEnv(sourceHome, targetHome) {
122
+ const source = path.join(sourceHome, '.env');
123
+ const target = path.join(targetHome, '.env');
124
+ if (!fs.existsSync(source)) return false;
125
+
126
+ const sourceText = fs.readFileSync(source, 'utf8');
127
+ const sanitized = sourceText
128
+ .split(/\r?\n/)
129
+ .filter((line) => !shouldStripManagedGatewayEnvLine(line))
130
+ .join('\n')
131
+ .replace(/\s*$/, '\n');
132
+ fs.mkdirSync(path.dirname(target), { recursive: true });
133
+ fs.writeFileSync(target, sanitized);
134
+ return true;
135
+ }
136
+
137
+ function seedHermesGatewayHome({ sourceHome, targetHome, gateway }) {
138
+ fs.mkdirSync(targetHome, { recursive: true });
139
+ if (path.resolve(sourceHome) === path.resolve(targetHome)) {
140
+ appendGatewayLog(gateway, 'meta', `Using Hermes gateway profile at ${targetHome}\n`);
141
+ return;
142
+ }
143
+
144
+ const copied = [];
145
+ for (const file of ['config.yaml', 'SOUL.md']) {
146
+ if (copyHermesProfileFile(sourceHome, targetHome, file, { overwrite: false })) {
147
+ copied.push(file);
148
+ }
149
+ }
150
+ if (copyHermesProfileEnv(sourceHome, targetHome)) {
151
+ copied.push('.env (without messaging platform credentials)');
152
+ }
153
+ for (const file of ['auth.json']) {
154
+ if (copyHermesProfileFile(sourceHome, targetHome, file, { overwrite: true })) {
155
+ copied.push(file);
156
+ }
157
+ }
158
+
159
+ appendGatewayLog(
160
+ gateway,
161
+ 'meta',
162
+ copied.length > 0
163
+ ? `Seeded Pixcode Hermes gateway profile from ${sourceHome}: ${copied.join(', ')}\n`
164
+ : `Using Pixcode Hermes gateway profile at ${targetHome}\n`,
165
+ );
166
+ }
167
+
56
168
  export function buildHermesGatewayEnv(baseEnv = process.env, options = {}) {
57
169
  const host = options.host || DEFAULT_HOST;
58
170
  const port = String(options.port || DEFAULT_PORT);
@@ -345,6 +457,7 @@ function snapshotGateway(gateway) {
345
457
  running: false,
346
458
  projectPath: null,
347
459
  baseUrl: null,
460
+ hermesHome: null,
348
461
  host: null,
349
462
  port: null,
350
463
  pid: null,
@@ -362,6 +475,7 @@ function snapshotGateway(gateway) {
362
475
  running: isGatewayRunning(gateway),
363
476
  projectPath: gateway.projectPath,
364
477
  baseUrl: gateway.baseUrl,
478
+ hermesHome: gateway.hermesHome,
365
479
  host: gateway.host,
366
480
  port: gateway.port,
367
481
  pid: gateway.child?.pid ?? null,
@@ -391,16 +505,24 @@ export async function ensureHermesGateway(options = {}) {
391
505
  const projectPath = normalizeProjectPath(options.projectPath);
392
506
  const existing = gateways.get(projectPath);
393
507
  if (isGatewayRunning(existing)) {
508
+ if (options.probeExisting !== true) {
509
+ return {
510
+ ...snapshotGateway(existing),
511
+ probe: existing.lastProbe,
512
+ };
513
+ }
514
+
394
515
  const probe = await probeHermesGateway(projectPath, { requireRunning: true }).catch((error) => ({
395
516
  ok: false,
396
517
  error: error instanceof Error ? error.message : String(error),
397
518
  }));
398
- if (probe.ok) {
519
+ if (probe.ok || options.replaceUnhealthy !== true) {
399
520
  return {
400
521
  ...snapshotGateway(existing),
401
522
  probe,
402
523
  };
403
524
  }
525
+
404
526
  stopHermesGateway(projectPath);
405
527
  }
406
528
 
@@ -408,12 +530,15 @@ export async function ensureHermesGateway(options = {}) {
408
530
  const port = await findAvailablePort(Number(options.port || process.env.HERMES_API_SERVER_PORT || DEFAULT_PORT), host);
409
531
  const apiServerKey = options.apiServerKey || makeApiServerKey();
410
532
  const appRoot = options.appRoot || process.cwd();
533
+ const sourceHermesHome = options.sourceHermesHome || resolveSourceHermesHome(process.env);
534
+ const hermesHome = resolveHermesGatewayHome(process.env, options);
411
535
  const env = buildHermesGatewayEnv(process.env, {
412
536
  ...options,
413
537
  host,
414
538
  port,
415
539
  apiServerKey,
416
540
  appRoot,
541
+ hermesHome,
417
542
  });
418
543
  const installStatus = readHermesInstallStatus(env, {
419
544
  allowSmokeHermes: options.allowSmokeHermes === true,
@@ -429,6 +554,7 @@ export async function ensureHermesGateway(options = {}) {
429
554
  host,
430
555
  port,
431
556
  baseUrl: gatewayBaseUrl(host, port),
557
+ hermesHome,
432
558
  apiServerKey,
433
559
  command: installStatus.command,
434
560
  child: null,
@@ -442,6 +568,7 @@ export async function ensureHermesGateway(options = {}) {
442
568
  };
443
569
  gateways.set(projectPath, gateway);
444
570
 
571
+ seedHermesGatewayHome({ sourceHome: sourceHermesHome, targetHome: hermesHome, gateway });
445
572
  await configurePixcodeMcp({ appRoot, env, gateway });
446
573
 
447
574
  const gatewayArgs = options.gatewayArgs || ['gateway', 'run', '--replace'];
@@ -12,6 +12,7 @@ const POSIX_INSTALLER_URL = 'https://raw.githubusercontent.com/NousResearch/herm
12
12
  const WINDOWS_INSTALLER_URL = 'https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1';
13
13
  const FINISHED_TTL_MS = 10 * 60 * 1000;
14
14
  const HARD_TIMEOUT_MS = 20 * 60 * 1000;
15
+ const HERMES_VERSION_TIMEOUT_MS = 20 * 1000;
15
16
  const jobs = new Map();
16
17
 
17
18
  function pathSeparator() {
@@ -125,13 +126,22 @@ function pushHermesCommandFiles(candidates, dir) {
125
126
  }
126
127
  }
127
128
 
129
+ function hermesVersionTimeoutMs(env = process.env) {
130
+ const configured = Number(env.HERMES_VERSION_TIMEOUT_MS);
131
+ if (Number.isFinite(configured) && configured >= 1000) {
132
+ return configured;
133
+ }
134
+
135
+ return HERMES_VERSION_TIMEOUT_MS;
136
+ }
137
+
128
138
  function runHermesVersion(candidate, env) {
129
139
  try {
130
140
  const result = spawn.sync(candidate, ['--version'], {
131
141
  encoding: 'utf8',
132
142
  env,
133
143
  stdio: ['ignore', 'pipe', 'pipe'],
134
- timeout: 5000,
144
+ timeout: hermesVersionTimeoutMs(env),
135
145
  windowsHide: true,
136
146
  });
137
147
  if (result.error || result.status !== 0) {