@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.
- package/dist/assets/{index-QNWLsise.js → index-58IIiyST.js} +163 -163
- package/dist/assets/index-BpUexHb8.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +80 -6
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +39 -0
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/hermes/configure-pixcode-mcp.mjs +87 -0
- package/scripts/hermes/pixcode-mcp-server.mjs +216 -0
- package/scripts/smoke/git-install-update.mjs +133 -54
- package/scripts/smoke/pixcode-workbench-1-48.mjs +15 -1
- package/scripts/smoke/vscode-workbench-polish.mjs +21 -2
- package/scripts/update-git-install.mjs +162 -4
- package/server/index.js +88 -6
- package/server/modules/orchestration/hermes/hermes.routes.ts +55 -0
- package/dist/assets/index-B3lN7dBd.css +0 -32
|
@@ -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
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
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(
|
|
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
|
-
|
|
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) {
|