@pixelbyte-software/pixcode 1.48.5 → 1.49.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.
@@ -27,6 +27,10 @@ const SERVER_VERSION = (() => {
27
27
  return '0.0.0';
28
28
  }
29
29
  })();
30
+ const HERMES_SHELL_COMMANDS = new Set([
31
+ 'pixcode:hermes:start',
32
+ 'pixcode:hermes:install',
33
+ ]);
30
34
  const DAEMON_COMMAND_CONTEXT = {
31
35
  appRoot: APP_ROOT,
32
36
  cliEntry: path.join(APP_ROOT, 'server', 'cli.js'),
@@ -74,7 +78,7 @@ import { ensurePortOpen } from './utils/port-access.js';
74
78
  import { applyAllStoredCredentialsToEnv, } from './services/provider-credentials.js';
75
79
  import { primeCliBinPath } from './services/install-jobs.js';
76
80
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
77
- import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
81
+ import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, apiKeysDb } from './database/db.js';
78
82
  import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
79
83
  import { configureWebPush } from './services/vapid-keys.js';
80
84
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
@@ -263,6 +267,63 @@ function killProviderPtySessions(projectPath, provider) {
263
267
  }
264
268
  return killed;
265
269
  }
270
+ function shellQuotePosix(value) {
271
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
272
+ }
273
+ function shellQuotePowerShell(value) {
274
+ return `'${String(value).replace(/'/g, "''")}'`;
275
+ }
276
+ function resolvePublicBaseUrl(request) {
277
+ const headers = request?.headers || {};
278
+ const forwardedProto = String(headers['x-forwarded-proto'] || '').split(',')[0].trim();
279
+ const proto = forwardedProto || (request?.socket?.encrypted ? 'https' : 'http');
280
+ const host = headers['x-forwarded-host'] || headers.host || `127.0.0.1:${process.env.SERVER_PORT || process.env.PORT || '3001'}`;
281
+ return `${proto}://${String(host).split(',')[0].trim()}`;
282
+ }
283
+ function getOrCreateHermesApiKey(userId) {
284
+ if (!userId)
285
+ return null;
286
+ const existing = apiKeysDb
287
+ .getApiKeys(userId)
288
+ .find((key) => key.key_name === 'Hermes Agent MCP' && key.is_active);
289
+ if (existing?.api_key) {
290
+ return existing.api_key;
291
+ }
292
+ return apiKeysDb.createApiKey(userId, 'Hermes Agent MCP', [
293
+ 'hermes:mcp',
294
+ 'projects:read',
295
+ 'providers:read',
296
+ 'terminal:launch',
297
+ ]).apiKey;
298
+ }
299
+ function buildHermesShellCommand(kind, env) {
300
+ const configureScript = path.join(APP_ROOT, 'scripts', 'hermes', 'configure-pixcode-mcp.mjs');
301
+ const isWindows = os.platform() === 'win32';
302
+ const quote = isWindows ? shellQuotePowerShell : shellQuotePosix;
303
+ const configure = `node ${quote(configureScript)}`;
304
+ if (isWindows) {
305
+ const setEnv = [
306
+ `$env:PIXCODE_BASE_URL=${quote(env.PIXCODE_BASE_URL)}`,
307
+ `$env:PIXCODE_API_KEY=${quote(env.PIXCODE_API_KEY)}`,
308
+ `$env:PIXCODE_APP_ROOT=${quote(env.PIXCODE_APP_ROOT)}`,
309
+ ].join('; ');
310
+ const install = '& ([scriptblock]::Create((irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1))) -SkipSetup -Branch main';
311
+ if (kind === 'pixcode:hermes:install') {
312
+ return `${setEnv}; ${install}; ${configure}`;
313
+ }
314
+ return `${setEnv}; ${configure}; if (-not (Get-Command hermes -ErrorAction SilentlyContinue)) { ${install} }; hermes chat --toolsets "hermes-cli,mcp-pixcode"`;
315
+ }
316
+ const setEnv = [
317
+ `PIXCODE_BASE_URL=${quote(env.PIXCODE_BASE_URL)}`,
318
+ `PIXCODE_API_KEY=${quote(env.PIXCODE_API_KEY)}`,
319
+ `PIXCODE_APP_ROOT=${quote(env.PIXCODE_APP_ROOT)}`,
320
+ ].join(' ');
321
+ const install = 'curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash';
322
+ if (kind === 'pixcode:hermes:install') {
323
+ return `${setEnv} sh -lc ${quote(`${install} && node ${shellQuotePosix(configureScript)}`)}`;
324
+ }
325
+ return `${setEnv} sh -lc ${quote(`node ${shellQuotePosix(configureScript)} && if ! command -v hermes >/dev/null 2>&1; then ${install}; fi; hermes chat --toolsets "hermes-cli,mcp-pixcode"`)}`;
326
+ }
266
327
  // Single WebSocket server that handles both paths
267
328
  const wss = new WebSocketServer({
268
329
  server,
@@ -1690,7 +1751,7 @@ wss.on('connection', (ws, request) => {
1690
1751
  const urlObj = new URL(url, 'http://localhost');
1691
1752
  const pathname = urlObj.pathname;
1692
1753
  if (pathname === '/shell') {
1693
- handleShellConnection(ws);
1754
+ handleShellConnection(ws, request);
1694
1755
  }
1695
1756
  else if (pathname === '/ws') {
1696
1757
  handleChatConnection(ws, request);
@@ -1905,7 +1966,7 @@ function handleChatConnection(ws, request) {
1905
1966
  });
1906
1967
  }
1907
1968
  // Handle shell WebSocket connections
1908
- function handleShellConnection(ws) {
1969
+ function handleShellConnection(ws, request) {
1909
1970
  console.log('🐚 Shell client connected');
1910
1971
  let shellProcess = null;
1911
1972
  let ptySessionKey = null;
@@ -1933,7 +1994,20 @@ function handleShellConnection(ws) {
1933
1994
  const sessionId = data.sessionId;
1934
1995
  const hasSession = data.hasSession;
1935
1996
  const provider = data.provider || 'claude';
1936
- const initialCommand = data.initialCommand;
1997
+ let initialCommand = data.initialCommand;
1998
+ const hermesCommand = HERMES_SHELL_COMMANDS.has(initialCommand) ? initialCommand : null;
1999
+ if (hermesCommand) {
2000
+ const apiKey = getOrCreateHermesApiKey(request?.user?.id);
2001
+ if (!apiKey) {
2002
+ ws.send(JSON.stringify({ type: 'error', message: 'Hermes MCP could not create a Pixcode API key for this user.' }));
2003
+ return;
2004
+ }
2005
+ initialCommand = buildHermesShellCommand(hermesCommand, {
2006
+ PIXCODE_BASE_URL: resolvePublicBaseUrl(request),
2007
+ PIXCODE_API_KEY: apiKey,
2008
+ PIXCODE_APP_ROOT: APP_ROOT,
2009
+ });
2010
+ }
1937
2011
  const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
1938
2012
  const forceNewSession = Boolean(data.forceNewSession);
1939
2013
  urlDetectionBuffer = '';
@@ -2006,7 +2080,7 @@ function handleShellConnection(ws) {
2006
2080
  console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
2007
2081
  console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
2008
2082
  if (initialCommand) {
2009
- console.log('⚡ Initial command:', initialCommand);
2083
+ console.log('⚡ Initial command:', hermesCommand ? hermesCommand : initialCommand);
2010
2084
  }
2011
2085
  // First send a welcome message
2012
2086
  let welcomeMsg;
@@ -2161,7 +2235,7 @@ function handleShellConnection(ws) {
2161
2235
  shellCommand = command;
2162
2236
  }
2163
2237
  }
2164
- console.log('🔧 Executing shell command:', shellCommand || 'interactive shell');
2238
+ console.log('🔧 Executing shell command:', hermesCommand ? hermesCommand : (shellCommand || 'interactive shell'));
2165
2239
  // Use appropriate shell based on platform
2166
2240
  const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
2167
2241
  const shellArgs = isPlainShell && !initialCommand