@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.
@@ -7,6 +7,28 @@ const repoRoot = process.cwd();
7
7
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
8
8
  const stashMessage = `pixcode-auto-update-${timestamp}`;
9
9
  const backupBranch = `pixcode-backup-before-update-${timestamp}`;
10
+ const dependencyManifestFiles = new Set([
11
+ 'package.json',
12
+ 'package-lock.json',
13
+ 'npm-shrinkwrap.json',
14
+ ]);
15
+ const buildInputFiles = new Set([
16
+ 'index.html',
17
+ 'tsconfig.json',
18
+ 'vite.config.js',
19
+ 'vite.config.ts',
20
+ 'tailwind.config.js',
21
+ 'tailwind.config.ts',
22
+ 'postcss.config.js',
23
+ 'postcss.config.cjs',
24
+ 'server/tsconfig.json',
25
+ ]);
26
+ const buildInputPrefixes = [
27
+ 'src/',
28
+ 'server/',
29
+ 'shared/',
30
+ 'public/',
31
+ ];
10
32
 
11
33
  function log(message) {
12
34
  process.stdout.write(`${message}\n`);
@@ -63,6 +85,113 @@ async function getOutput(command, args, options = {}) {
63
85
  return result.stdout.trim();
64
86
  }
65
87
 
88
+ function normalizeGitPath(filePath) {
89
+ return filePath.replace(/\\/g, '/').replace(/^\.\//, '');
90
+ }
91
+
92
+ function readJson(relativePath) {
93
+ try {
94
+ return JSON.parse(fs.readFileSync(path.join(repoRoot, relativePath), 'utf8'));
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function dependencySignature(packageJson) {
101
+ if (!packageJson || typeof packageJson !== 'object') return null;
102
+
103
+ return JSON.stringify({
104
+ dependencies: packageJson.dependencies || {},
105
+ devDependencies: packageJson.devDependencies || {},
106
+ optionalDependencies: packageJson.optionalDependencies || {},
107
+ peerDependencies: packageJson.peerDependencies || {},
108
+ bundledDependencies: packageJson.bundledDependencies || packageJson.bundleDependencies || [],
109
+ });
110
+ }
111
+
112
+ function lockfileSignature(lockfile) {
113
+ if (!lockfile || typeof lockfile !== 'object') return null;
114
+
115
+ const normalizedPackages = {};
116
+ if (lockfile.packages && typeof lockfile.packages === 'object') {
117
+ for (const [packagePath, packageInfo] of Object.entries(lockfile.packages)) {
118
+ if (!packageInfo || typeof packageInfo !== 'object') {
119
+ normalizedPackages[packagePath] = packageInfo;
120
+ continue;
121
+ }
122
+
123
+ if (packagePath === '') {
124
+ const {
125
+ version: _version,
126
+ ...rootPackageInfo
127
+ } = packageInfo;
128
+ normalizedPackages[packagePath] = rootPackageInfo;
129
+ } else {
130
+ normalizedPackages[packagePath] = packageInfo;
131
+ }
132
+ }
133
+ }
134
+
135
+ return JSON.stringify({
136
+ dependencies: lockfile.dependencies || {},
137
+ packages: normalizedPackages,
138
+ });
139
+ }
140
+
141
+ function shouldRunNpmInstall(changedFiles, previousPackageJson, nextPackageJson, previousLockfile, nextLockfile) {
142
+ const changedManifest = changedFiles.some((filePath) => dependencyManifestFiles.has(filePath));
143
+ if (!changedManifest) return false;
144
+
145
+ if (!previousPackageJson || !nextPackageJson) return true;
146
+
147
+ if (dependencySignature(previousPackageJson) !== dependencySignature(nextPackageJson)) {
148
+ return true;
149
+ }
150
+
151
+ if (changedFiles.some((filePath) => filePath === 'package-lock.json' || filePath === 'npm-shrinkwrap.json')) {
152
+ return lockfileSignature(previousLockfile) !== lockfileSignature(nextLockfile);
153
+ }
154
+
155
+ return false;
156
+ }
157
+
158
+ function shouldRunBuild(changedFiles, previousPackageJson, nextPackageJson, installNeeded) {
159
+ if (!nextPackageJson?.scripts?.build) return false;
160
+ if (installNeeded) return true;
161
+
162
+ if (previousPackageJson?.scripts?.build !== nextPackageJson.scripts.build) {
163
+ return true;
164
+ }
165
+
166
+ return changedFiles.some((filePath) => (
167
+ buildInputFiles.has(filePath)
168
+ || buildInputPrefixes.some((prefix) => filePath.startsWith(prefix))
169
+ ));
170
+ }
171
+
172
+ async function getChangedFiles(fromRef, toRef) {
173
+ const output = await getOutput('git', ['diff', '--name-only', fromRef, toRef]);
174
+ return output
175
+ .split('\n')
176
+ .map((filePath) => normalizeGitPath(filePath.trim()))
177
+ .filter(Boolean);
178
+ }
179
+
180
+ function logChangedFiles(changedFiles) {
181
+ if (changedFiles.length === 0) {
182
+ log('Changed files: none.');
183
+ return;
184
+ }
185
+
186
+ log(`Changed files: ${changedFiles.length}.`);
187
+ for (const filePath of changedFiles.slice(0, 25)) {
188
+ log(` - ${filePath}`);
189
+ }
190
+ if (changedFiles.length > 25) {
191
+ log(` ... and ${changedFiles.length - 25} more`);
192
+ }
193
+ }
194
+
66
195
  async function main() {
67
196
  if (!fs.existsSync(path.join(repoRoot, '.git'))) {
68
197
  throw new Error(`Git metadata not found in ${repoRoot}`);
@@ -95,9 +224,20 @@ async function main() {
95
224
  }
96
225
 
97
226
  const checkoutMain = await run('git', ['checkout', 'main'], { allowFailure: true, collectOutput: true });
227
+ let changedFiles = [];
228
+ let previousPackageJson = null;
229
+ let previousLockfile = null;
230
+
98
231
  if (checkoutMain.code !== 0) {
99
232
  log('Local main branch checkout failed; recreating main from origin/main.');
100
233
  await run('git', ['checkout', '-B', 'main', 'origin/main']);
234
+ changedFiles = ['package.json', 'src/__unknown__'];
235
+ log('Changed files could not be compared because local main was recreated; running safe reconciliation.');
236
+ } else {
237
+ previousPackageJson = readJson('package.json');
238
+ previousLockfile = readJson('package-lock.json') || readJson('npm-shrinkwrap.json');
239
+ changedFiles = await getChangedFiles('HEAD', 'origin/main');
240
+ logChangedFiles(changedFiles);
101
241
  }
102
242
 
103
243
  const isAncestor = await run('git', ['merge-base', '--is-ancestor', 'HEAD', 'origin/main'], {
@@ -118,13 +258,31 @@ async function main() {
118
258
  const packageVersion = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).version;
119
259
  log(`Repository updated to Pixcode ${packageVersion}.`);
120
260
 
121
- await run('npm', ['install', '--no-audit', '--no-fund']);
122
- const updatedPackageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
123
- if (updatedPackageJson.scripts?.build) {
261
+ const updatedPackageJson = readJson('package.json');
262
+ const updatedLockfile = readJson('package-lock.json') || readJson('npm-shrinkwrap.json');
263
+ const installNeeded = shouldRunNpmInstall(
264
+ changedFiles,
265
+ previousPackageJson,
266
+ updatedPackageJson,
267
+ previousLockfile,
268
+ updatedLockfile,
269
+ );
270
+ const buildNeeded = shouldRunBuild(changedFiles, previousPackageJson, updatedPackageJson, installNeeded);
271
+
272
+ if (installNeeded) {
273
+ log('Installing dependencies because package manifests changed.');
274
+ await run('npm', ['install', '--no-audit', '--no-fund']);
275
+ } else {
276
+ log('Dependencies unchanged; skipping npm install.');
277
+ }
278
+
279
+ if (buildNeeded) {
124
280
  log('Building Pixcode source install.');
125
281
  await run('npm', ['run', 'build']);
126
282
  } else {
127
- log('No build script found; skipping build.');
283
+ log(updatedPackageJson?.scripts?.build
284
+ ? 'Build inputs unchanged; skipping build.'
285
+ : 'No build script found; skipping build.');
128
286
  }
129
287
  log('Pixcode git install update completed.');
130
288
  }
package/server/index.js CHANGED
@@ -32,6 +32,10 @@ const SERVER_VERSION = (() => {
32
32
  return '0.0.0';
33
33
  }
34
34
  })();
35
+ const HERMES_SHELL_COMMANDS = new Set([
36
+ 'pixcode:hermes:start',
37
+ 'pixcode:hermes:install',
38
+ ]);
35
39
  const DAEMON_COMMAND_CONTEXT = {
36
40
  appRoot: APP_ROOT,
37
41
  cliEntry: path.join(APP_ROOT, 'server', 'cli.js'),
@@ -105,7 +109,7 @@ import {
105
109
  } from './services/provider-credentials.js';
106
110
  import { primeCliBinPath } from './services/install-jobs.js';
107
111
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
108
- import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
112
+ import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, apiKeysDb } from './database/db.js';
109
113
  import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
110
114
  import { configureWebPush } from './services/vapid-keys.js';
111
115
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
@@ -323,6 +327,71 @@ function killProviderPtySessions(projectPath, provider) {
323
327
  return killed;
324
328
  }
325
329
 
330
+ function shellQuotePosix(value) {
331
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
332
+ }
333
+
334
+ function shellQuotePowerShell(value) {
335
+ return `'${String(value).replace(/'/g, "''")}'`;
336
+ }
337
+
338
+ function resolvePublicBaseUrl(request) {
339
+ const headers = request?.headers || {};
340
+ const forwardedProto = String(headers['x-forwarded-proto'] || '').split(',')[0].trim();
341
+ const proto = forwardedProto || (request?.socket?.encrypted ? 'https' : 'http');
342
+ const host = headers['x-forwarded-host'] || headers.host || `127.0.0.1:${process.env.SERVER_PORT || process.env.PORT || '3001'}`;
343
+ return `${proto}://${String(host).split(',')[0].trim()}`;
344
+ }
345
+
346
+ function getOrCreateHermesApiKey(userId) {
347
+ if (!userId) return null;
348
+
349
+ const existing = apiKeysDb
350
+ .getApiKeys(userId)
351
+ .find((key) => key.key_name === 'Hermes Agent MCP' && key.is_active);
352
+ if (existing?.api_key) {
353
+ return existing.api_key;
354
+ }
355
+
356
+ return apiKeysDb.createApiKey(userId, 'Hermes Agent MCP', [
357
+ 'hermes:mcp',
358
+ 'projects:read',
359
+ 'providers:read',
360
+ 'terminal:launch',
361
+ ]).apiKey;
362
+ }
363
+
364
+ function buildHermesShellCommand(kind, env) {
365
+ const configureScript = path.join(APP_ROOT, 'scripts', 'hermes', 'configure-pixcode-mcp.mjs');
366
+ const isWindows = os.platform() === 'win32';
367
+ const quote = isWindows ? shellQuotePowerShell : shellQuotePosix;
368
+ const configure = `node ${quote(configureScript)}`;
369
+
370
+ if (isWindows) {
371
+ const setEnv = [
372
+ `$env:PIXCODE_BASE_URL=${quote(env.PIXCODE_BASE_URL)}`,
373
+ `$env:PIXCODE_API_KEY=${quote(env.PIXCODE_API_KEY)}`,
374
+ `$env:PIXCODE_APP_ROOT=${quote(env.PIXCODE_APP_ROOT)}`,
375
+ ].join('; ');
376
+ const install = '& ([scriptblock]::Create((irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1))) -SkipSetup -Branch main';
377
+ if (kind === 'pixcode:hermes:install') {
378
+ return `${setEnv}; ${install}; ${configure}`;
379
+ }
380
+ return `${setEnv}; ${configure}; if (-not (Get-Command hermes -ErrorAction SilentlyContinue)) { ${install} }; hermes chat --toolsets "hermes-cli,mcp-pixcode"`;
381
+ }
382
+
383
+ const setEnv = [
384
+ `PIXCODE_BASE_URL=${quote(env.PIXCODE_BASE_URL)}`,
385
+ `PIXCODE_API_KEY=${quote(env.PIXCODE_API_KEY)}`,
386
+ `PIXCODE_APP_ROOT=${quote(env.PIXCODE_APP_ROOT)}`,
387
+ ].join(' ');
388
+ const install = 'curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash';
389
+ if (kind === 'pixcode:hermes:install') {
390
+ return `${setEnv} sh -lc ${quote(`${install} && node ${shellQuotePosix(configureScript)}`)}`;
391
+ }
392
+ 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"`)}`;
393
+ }
394
+
326
395
  // Single WebSocket server that handles both paths
327
396
  const wss = new WebSocketServer({
328
397
  server,
@@ -1851,7 +1920,7 @@ wss.on('connection', (ws, request) => {
1851
1920
  const pathname = urlObj.pathname;
1852
1921
 
1853
1922
  if (pathname === '/shell') {
1854
- handleShellConnection(ws);
1923
+ handleShellConnection(ws, request);
1855
1924
  } else if (pathname === '/ws') {
1856
1925
  handleChatConnection(ws, request);
1857
1926
  } else if (pathname.startsWith('/plugin-ws/')) {
@@ -2060,7 +2129,7 @@ function handleChatConnection(ws, request) {
2060
2129
  }
2061
2130
 
2062
2131
  // Handle shell WebSocket connections
2063
- function handleShellConnection(ws) {
2132
+ function handleShellConnection(ws, request) {
2064
2133
  console.log('🐚 Shell client connected');
2065
2134
  let shellProcess = null;
2066
2135
  let ptySessionKey = null;
@@ -2090,7 +2159,20 @@ function handleShellConnection(ws) {
2090
2159
  const sessionId = data.sessionId;
2091
2160
  const hasSession = data.hasSession;
2092
2161
  const provider = data.provider || 'claude';
2093
- const initialCommand = data.initialCommand;
2162
+ let initialCommand = data.initialCommand;
2163
+ const hermesCommand = HERMES_SHELL_COMMANDS.has(initialCommand) ? initialCommand : null;
2164
+ if (hermesCommand) {
2165
+ const apiKey = getOrCreateHermesApiKey(request?.user?.id);
2166
+ if (!apiKey) {
2167
+ ws.send(JSON.stringify({ type: 'error', message: 'Hermes MCP could not create a Pixcode API key for this user.' }));
2168
+ return;
2169
+ }
2170
+ initialCommand = buildHermesShellCommand(hermesCommand, {
2171
+ PIXCODE_BASE_URL: resolvePublicBaseUrl(request),
2172
+ PIXCODE_API_KEY: apiKey,
2173
+ PIXCODE_APP_ROOT: APP_ROOT,
2174
+ });
2175
+ }
2094
2176
  const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
2095
2177
  const forceNewSession = Boolean(data.forceNewSession);
2096
2178
  urlDetectionBuffer = '';
@@ -2173,7 +2255,7 @@ function handleShellConnection(ws) {
2173
2255
  console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
2174
2256
  console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
2175
2257
  if (initialCommand) {
2176
- console.log('⚡ Initial command:', initialCommand);
2258
+ console.log('⚡ Initial command:', hermesCommand ? hermesCommand : initialCommand);
2177
2259
  }
2178
2260
 
2179
2261
  // First send a welcome message
@@ -2318,7 +2400,7 @@ function handleShellConnection(ws) {
2318
2400
  }
2319
2401
  }
2320
2402
 
2321
- console.log('🔧 Executing shell command:', shellCommand || 'interactive shell');
2403
+ console.log('🔧 Executing shell command:', hermesCommand ? hermesCommand : (shellCommand || 'interactive shell'));
2322
2404
 
2323
2405
  // Use appropriate shell based on platform
2324
2406
  const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
@@ -5,6 +5,21 @@ import express, { type Router } from 'express';
5
5
  import { adapterRegistry } from '@/modules/orchestration/a2a/adapter-registry.js';
6
6
  import { a2aTaskStore as hermesTaskStore } from '@/modules/orchestration/a2a/task-store.js';
7
7
 
8
+ const HERMES_TERMINAL_LAUNCH_LIMIT = 100;
9
+ const HERMES_TERMINAL_LAUNCH_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
10
+
11
+ type HermesTerminalLaunchEvent = {
12
+ id: number;
13
+ provider: string;
14
+ projectPath: string | null;
15
+ prompt: string | null;
16
+ source: string;
17
+ createdAt: string;
18
+ };
19
+
20
+ let nextHermesTerminalLaunchId = 1;
21
+ const hermesTerminalLaunches: HermesTerminalLaunchEvent[] = [];
22
+
8
23
  export function createHermesRouter(): Router {
9
24
  const router = express.Router();
10
25
 
@@ -53,6 +68,46 @@ export function createHermesRouter(): Router {
53
68
  });
54
69
  });
55
70
 
71
+ router.get('/terminal-launches', (req, res) => {
72
+ const after = Number.parseInt(typeof req.query.after === 'string' ? req.query.after : '0', 10);
73
+ const afterId = Number.isFinite(after) ? after : 0;
74
+ res.json({
75
+ events: hermesTerminalLaunches.filter((event) => event.id > afterId),
76
+ });
77
+ });
78
+
79
+ router.post('/terminal-launches', (req, res) => {
80
+ const body = (req.body ?? {}) as Record<string, unknown>;
81
+ const provider = typeof body.provider === 'string' ? body.provider.trim() : '';
82
+ if (!HERMES_TERMINAL_LAUNCH_PROVIDERS.has(provider)) {
83
+ res.status(400).json({ error: { code: 'INVALID_PROVIDER', message: provider || 'provider is required' } });
84
+ return;
85
+ }
86
+
87
+ const projectPath = typeof body.projectPath === 'string' && body.projectPath.trim()
88
+ ? body.projectPath.trim()
89
+ : null;
90
+ const prompt = typeof body.prompt === 'string' && body.prompt.trim()
91
+ ? body.prompt.trim()
92
+ : null;
93
+
94
+ const event: HermesTerminalLaunchEvent = {
95
+ id: nextHermesTerminalLaunchId,
96
+ provider,
97
+ projectPath,
98
+ prompt,
99
+ source: 'hermes-mcp',
100
+ createdAt: new Date().toISOString(),
101
+ };
102
+ nextHermesTerminalLaunchId += 1;
103
+ hermesTerminalLaunches.push(event);
104
+ if (hermesTerminalLaunches.length > HERMES_TERMINAL_LAUNCH_LIMIT) {
105
+ hermesTerminalLaunches.splice(0, hermesTerminalLaunches.length - HERMES_TERMINAL_LAUNCH_LIMIT);
106
+ }
107
+
108
+ res.status(201).json({ event });
109
+ });
110
+
56
111
  router.get('/tasks/:id', (req, res) => {
57
112
  const task = hermesTaskStore.get(req.params.id);
58
113
  if (!task) {