@yancyyu/openhermit 1.5.10 → 1.5.11
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/bin/hermit.mjs +179 -24
- package/package.json +1 -1
- package/src/main/server.ts +9 -3
package/bin/hermit.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { spawn, execSync } from 'node:child_process';
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
19
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, closeSync, unlinkSync } from 'node:fs';
|
|
20
20
|
import { createRequire } from 'node:module';
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import path from 'node:path';
|
|
@@ -56,12 +56,18 @@ Usage:
|
|
|
56
56
|
Options:
|
|
57
57
|
--port <number> HTTP server port (default: 5680)
|
|
58
58
|
--no-cc-connect Do not auto-start bundled cc-connect
|
|
59
|
+
--daemon Run in the background
|
|
59
60
|
--version Show current version
|
|
60
61
|
--help Show this help message
|
|
61
62
|
update Check and install updates
|
|
63
|
+
status Show background service status
|
|
64
|
+
stop Stop the background service
|
|
62
65
|
|
|
63
66
|
Examples:
|
|
64
67
|
openhermit # Start on port 5680
|
|
68
|
+
openhermit --daemon # Start in background
|
|
69
|
+
openhermit status # Show background status
|
|
70
|
+
openhermit stop # Stop background service
|
|
65
71
|
openhermit --port 8080 # Start on port 8080
|
|
66
72
|
openhermit --no-cc-connect # Start only openHermit
|
|
67
73
|
openhermit --version # Show version
|
|
@@ -81,11 +87,131 @@ const portIndex = args.indexOf('--port');
|
|
|
81
87
|
const port = portIndex !== -1 && args[portIndex + 1] ? args[portIndex + 1] : '5680';
|
|
82
88
|
const skipCcConnect = args.includes('--no-cc-connect') || process.env.HERMIT_NO_CC_CONNECT === '1';
|
|
83
89
|
const hermitHome = process.env.HERMIT_HOME || path.join(os.homedir(), '.hermit');
|
|
90
|
+
const daemonRequested = args.includes('--daemon');
|
|
91
|
+
const daemonChild = process.env.HERMIT_DAEMON_CHILD === '1';
|
|
92
|
+
const daemonPidPath = path.join(hermitHome, 'openhermit.pid');
|
|
93
|
+
const daemonLogPath = path.join(hermitHome, 'logs', 'openhermit.log');
|
|
84
94
|
const ccConnectConfigPath =
|
|
85
95
|
process.env.HERMIT_CC_CONNECT_CONFIG ||
|
|
86
96
|
process.env.CC_CONNECT_CONFIG ||
|
|
87
97
|
path.join(hermitHome, 'cc-connect', 'config.toml');
|
|
88
98
|
|
|
99
|
+
function readDaemonPid() {
|
|
100
|
+
try {
|
|
101
|
+
const raw = readFileSync(daemonPidPath, 'utf-8').trim();
|
|
102
|
+
const pid = Number.parseInt(raw, 10);
|
|
103
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPidRunning(pid) {
|
|
110
|
+
try {
|
|
111
|
+
process.kill(pid, 0);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function removeDaemonPidFile() {
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(daemonPidPath);
|
|
121
|
+
} catch {
|
|
122
|
+
// Already gone.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function signalDaemon(pid, signal) {
|
|
127
|
+
try {
|
|
128
|
+
process.kill(-pid, signal);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
// Fall back to direct process signal.
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
process.kill(pid, signal);
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function printDaemonStatus() {
|
|
142
|
+
const pid = readDaemonPid();
|
|
143
|
+
if (pid && isPidRunning(pid)) {
|
|
144
|
+
console.log(`[openHermit] Running in background (pid ${pid})`);
|
|
145
|
+
console.log(`[openHermit] Log: ${daemonLogPath}`);
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
if (pid) removeDaemonPidFile();
|
|
149
|
+
console.log('[openHermit] Not running');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function stopDaemon() {
|
|
154
|
+
const pid = readDaemonPid();
|
|
155
|
+
if (!pid || !isPidRunning(pid)) {
|
|
156
|
+
if (pid) removeDaemonPidFile();
|
|
157
|
+
console.log('[openHermit] Not running');
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
console.log(`[openHermit] Stopping background service (pid ${pid})...`);
|
|
161
|
+
signalDaemon(pid, 'SIGTERM');
|
|
162
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
163
|
+
if (isPidRunning(pid)) {
|
|
164
|
+
signalDaemon(pid, 'SIGKILL');
|
|
165
|
+
}
|
|
166
|
+
removeDaemonPidFile();
|
|
167
|
+
console.log('[openHermit] Stopped');
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function startDaemon() {
|
|
172
|
+
const existingPid = readDaemonPid();
|
|
173
|
+
if (existingPid && isPidRunning(existingPid)) {
|
|
174
|
+
console.log(`[openHermit] Already running in background (pid ${existingPid})`);
|
|
175
|
+
console.log(`[openHermit] Log: ${daemonLogPath}`);
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
mkdirSync(path.dirname(daemonPidPath), { recursive: true });
|
|
180
|
+
mkdirSync(path.dirname(daemonLogPath), { recursive: true });
|
|
181
|
+
const out = openSync(daemonLogPath, 'a');
|
|
182
|
+
const err = openSync(daemonLogPath, 'a');
|
|
183
|
+
const childArgs = process.argv.slice(2).filter((arg) => arg !== '--daemon');
|
|
184
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...childArgs], {
|
|
185
|
+
cwd: repoRoot,
|
|
186
|
+
detached: true,
|
|
187
|
+
env: {
|
|
188
|
+
...process.env,
|
|
189
|
+
HERMIT_DAEMON_CHILD: '1',
|
|
190
|
+
},
|
|
191
|
+
stdio: ['ignore', out, err],
|
|
192
|
+
});
|
|
193
|
+
child.unref();
|
|
194
|
+
closeSync(out);
|
|
195
|
+
closeSync(err);
|
|
196
|
+
writeFileSync(daemonPidPath, String(child.pid), 'utf-8');
|
|
197
|
+
console.log(`[openHermit] Started in background (pid ${child.pid})`);
|
|
198
|
+
console.log(`[openHermit] URL: http://127.0.0.1:${port}`);
|
|
199
|
+
console.log(`[openHermit] Log: ${daemonLogPath}`);
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (args.includes('status')) {
|
|
204
|
+
printDaemonStatus();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (args.includes('stop')) {
|
|
208
|
+
await stopDaemon();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (daemonRequested && !daemonChild) {
|
|
212
|
+
startDaemon();
|
|
213
|
+
}
|
|
214
|
+
|
|
89
215
|
// ---------------------------------------------------------------------------
|
|
90
216
|
// Update command
|
|
91
217
|
// ---------------------------------------------------------------------------
|
|
@@ -261,6 +387,35 @@ function resolveAliasLoaderRegister() {
|
|
|
261
387
|
return `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(${JSON.stringify(aliasLoaderUrl)}, pathToFileURL("./"));`;
|
|
262
388
|
}
|
|
263
389
|
|
|
390
|
+
function terminateProcessGroup(child, signal = 'SIGTERM') {
|
|
391
|
+
if (!child?.pid) return;
|
|
392
|
+
try {
|
|
393
|
+
process.kill(-child.pid, signal);
|
|
394
|
+
return;
|
|
395
|
+
} catch {
|
|
396
|
+
// Fall back to the direct child if it was not started as a process group.
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
child.kill(signal);
|
|
400
|
+
} catch {
|
|
401
|
+
// Already gone.
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let shuttingDown = false;
|
|
406
|
+
function shutdown(exitCode = 0) {
|
|
407
|
+
if (shuttingDown) return;
|
|
408
|
+
shuttingDown = true;
|
|
409
|
+
console.log('\n[openHermit] Shutting down...');
|
|
410
|
+
terminateProcessGroup(serverProcess, 'SIGTERM');
|
|
411
|
+
terminateProcessGroup(ccConnectProcess, 'SIGTERM');
|
|
412
|
+
setTimeout(() => {
|
|
413
|
+
terminateProcessGroup(serverProcess, 'SIGKILL');
|
|
414
|
+
terminateProcessGroup(ccConnectProcess, 'SIGKILL');
|
|
415
|
+
process.exit(exitCode);
|
|
416
|
+
}, 2_000).unref();
|
|
417
|
+
}
|
|
418
|
+
|
|
264
419
|
let ccConnectProcess = null;
|
|
265
420
|
let ccTokens = {
|
|
266
421
|
managementToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || '',
|
|
@@ -278,6 +433,7 @@ if (!skipCcConnect) {
|
|
|
278
433
|
console.log(`[openHermit] cc-connect config: ${ccConnectConfigPath}`);
|
|
279
434
|
ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
|
|
280
435
|
cwd: repoRoot,
|
|
436
|
+
detached: true,
|
|
281
437
|
env: {
|
|
282
438
|
...process.env,
|
|
283
439
|
CC_CONNECT_TOKEN: ccTokens.managementToken,
|
|
@@ -334,39 +490,38 @@ const serverProcess = spawn(
|
|
|
334
490
|
process.execPath,
|
|
335
491
|
['--import', resolveAliasLoaderRegister(), '--import', resolveTsxLoader(), 'src/main/server.ts'],
|
|
336
492
|
{
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
493
|
+
cwd: repoRoot,
|
|
494
|
+
detached: true,
|
|
495
|
+
env: {
|
|
496
|
+
...process.env,
|
|
497
|
+
PORT: port,
|
|
498
|
+
HOST: process.env.HOST || '127.0.0.1',
|
|
499
|
+
NODE_ENV: 'production',
|
|
500
|
+
HERMIT_HOME: hermitHome,
|
|
501
|
+
CC_CONNECT_TOKEN: ccTokens.managementToken,
|
|
502
|
+
CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
|
|
503
|
+
CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
|
|
504
|
+
CC_CONNECT_CONFIG: ccConnectConfigPath,
|
|
505
|
+
},
|
|
506
|
+
stdio: 'inherit',
|
|
350
507
|
}
|
|
351
508
|
);
|
|
352
509
|
|
|
353
510
|
serverProcess.on('exit', (code) => {
|
|
511
|
+
if (shuttingDown) return;
|
|
512
|
+
terminateProcessGroup(ccConnectProcess, 'SIGTERM');
|
|
354
513
|
if (code !== 0) {
|
|
355
514
|
console.error(`[openHermit] Server exited with code ${code}`);
|
|
356
515
|
process.exit(code ?? 1);
|
|
357
516
|
}
|
|
517
|
+
process.exit(0);
|
|
358
518
|
});
|
|
359
519
|
|
|
360
|
-
process.on('SIGINT', () =>
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
process.on('SIGTERM', () => {
|
|
367
|
-
console.log('\n[openHermit] Shutting down...');
|
|
368
|
-
serverProcess.kill('SIGTERM');
|
|
369
|
-
ccConnectProcess?.kill('SIGTERM');
|
|
520
|
+
process.on('SIGINT', () => shutdown(0));
|
|
521
|
+
process.on('SIGTERM', () => shutdown(0));
|
|
522
|
+
process.on('exit', () => {
|
|
523
|
+
terminateProcessGroup(serverProcess, 'SIGTERM');
|
|
524
|
+
terminateProcessGroup(ccConnectProcess, 'SIGTERM');
|
|
370
525
|
});
|
|
371
526
|
|
|
372
527
|
console.log(`[openHermit] Server starting on http://127.0.0.1:${port}`);
|
package/package.json
CHANGED
package/src/main/server.ts
CHANGED
|
@@ -299,7 +299,10 @@ function resolveTeamFromSessionKey(sessionKey: string): string | null {
|
|
|
299
299
|
return sessionKey;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
const app = Fastify({
|
|
302
|
+
const app = Fastify({
|
|
303
|
+
logger: { level: process.env.HERMIT_LOG_LEVEL ?? 'warn' },
|
|
304
|
+
disableRequestLogging: true,
|
|
305
|
+
});
|
|
303
306
|
|
|
304
307
|
// ===========================================================================
|
|
305
308
|
// Plugins
|
|
@@ -3839,14 +3842,17 @@ app.get('/api/events', (request, reply) => {
|
|
|
3839
3842
|
|
|
3840
3843
|
const SSE_FALLBACK_RE = /^\/api\/(.*\/(events|stream|notifications\/stream))$/;
|
|
3841
3844
|
|
|
3845
|
+
app.get('/api/extensions/mcp/browse', async () => ({
|
|
3846
|
+
servers: [],
|
|
3847
|
+
items: [],
|
|
3848
|
+
}));
|
|
3849
|
+
|
|
3842
3850
|
app.setNotFoundHandler((request, reply) => {
|
|
3843
3851
|
const u = request.url;
|
|
3844
3852
|
if (!u.startsWith('/api/')) {
|
|
3845
3853
|
return reply.code(404).type('text/plain').send('not found');
|
|
3846
3854
|
}
|
|
3847
3855
|
|
|
3848
|
-
request.log.info({ method: request.method, url: u }, '[stub]');
|
|
3849
|
-
|
|
3850
3856
|
if (request.method === 'GET' && SSE_FALLBACK_RE.test(u)) {
|
|
3851
3857
|
reply.raw.writeHead(200, {
|
|
3852
3858
|
'Content-Type': 'text/event-stream; charset=utf-8',
|