@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 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
- cwd: repoRoot,
338
- env: {
339
- ...process.env,
340
- PORT: port,
341
- HOST: process.env.HOST || '127.0.0.1',
342
- NODE_ENV: 'production',
343
- HERMIT_HOME: hermitHome,
344
- CC_CONNECT_TOKEN: ccTokens.managementToken,
345
- CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
346
- CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
347
- CC_CONNECT_CONFIG: ccConnectConfigPath,
348
- },
349
- stdio: 'inherit',
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
- console.log('\n[openHermit] Shutting down...');
362
- serverProcess.kill('SIGINT');
363
- ccConnectProcess?.kill('SIGINT');
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yancyyu/openhermit",
3
3
  "type": "module",
4
- "version": "1.5.10",
4
+ "version": "1.5.11",
5
5
  "description": "openHermit: team-oriented agent management workbench atop cc-connect.",
6
6
  "license": "AGPL-3.0",
7
7
  "author": {
@@ -299,7 +299,10 @@ function resolveTeamFromSessionKey(sessionKey: string): string | null {
299
299
  return sessionKey;
300
300
  }
301
301
 
302
- const app = Fastify({ logger: { level: 'info' } });
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',