@yancyyu/openhermit 1.5.9 → 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.
@@ -0,0 +1,51 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
6
+
7
+ const ALIASES = [
8
+ ['@features/', 'src/features/'],
9
+ ['@main/', 'src/main/'],
10
+ ['@renderer/', 'src/renderer/'],
11
+ ['@shared/', 'src/shared/'],
12
+ ];
13
+
14
+ const EXACT_ALIASES = new Map([
15
+ ['@shared/types', 'src/shared/types/index.ts'],
16
+ ['@main/types', 'src/main/types/index.ts'],
17
+ ]);
18
+
19
+ function resolveAlias(specifier) {
20
+ const exactTarget = EXACT_ALIASES.get(specifier);
21
+ if (exactTarget) {
22
+ const absolutePath = path.join(repoRoot, exactTarget);
23
+ if (existsSync(absolutePath)) return pathToFileURL(absolutePath).href;
24
+ }
25
+
26
+ for (const [prefix, target] of ALIASES) {
27
+ if (!specifier.startsWith(prefix)) continue;
28
+ const relativePath = specifier.slice(prefix.length);
29
+ const basePath = path.join(repoRoot, target, relativePath);
30
+ const candidates = [
31
+ basePath,
32
+ `${basePath}.ts`,
33
+ `${basePath}.tsx`,
34
+ `${basePath}.js`,
35
+ path.join(basePath, 'index.ts'),
36
+ path.join(basePath, 'index.tsx'),
37
+ path.join(basePath, 'index.js'),
38
+ ];
39
+ const match = candidates.find((candidate) => existsSync(candidate));
40
+ if (match) return pathToFileURL(match).href;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ export async function resolve(specifier, context, nextResolve) {
46
+ const aliasUrl = resolveAlias(specifier);
47
+ if (aliasUrl) {
48
+ return { url: aliasUrl, shortCircuit: true };
49
+ }
50
+ return nextResolve(specifier, context);
51
+ }
package/bin/hermit.mjs CHANGED
@@ -16,11 +16,11 @@
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';
23
- import { fileURLToPath } from 'node:url';
23
+ import { fileURLToPath, pathToFileURL } from 'node:url';
24
24
 
25
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
26
  const repoRoot = path.resolve(__dirname, '..');
@@ -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
  // ---------------------------------------------------------------------------
@@ -252,8 +378,42 @@ function resolveCcConnectRunner() {
252
378
  return path.join(path.dirname(pkgPath), 'run.js');
253
379
  }
254
380
 
255
- function resolveTsxCli() {
256
- return require.resolve('tsx/cli');
381
+ function resolveTsxLoader() {
382
+ return require.resolve('tsx');
383
+ }
384
+
385
+ function resolveAliasLoaderRegister() {
386
+ const aliasLoaderUrl = pathToFileURL(path.join(__dirname, 'alias-loader.mjs')).href;
387
+ return `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(${JSON.stringify(aliasLoaderUrl)}, pathToFileURL("./"));`;
388
+ }
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();
257
417
  }
258
418
 
259
419
  let ccConnectProcess = null;
@@ -273,6 +433,7 @@ if (!skipCcConnect) {
273
433
  console.log(`[openHermit] cc-connect config: ${ccConnectConfigPath}`);
274
434
  ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
275
435
  cwd: repoRoot,
436
+ detached: true,
276
437
  env: {
277
438
  ...process.env,
278
439
  CC_CONNECT_TOKEN: ccTokens.managementToken,
@@ -325,39 +486,42 @@ if (!existsSync(distRenderererDir) || !existsSync(path.join(distRenderererDir, '
325
486
  // Start the server
326
487
  console.log('[openHermit] Launching server...\n');
327
488
 
328
- const serverProcess = spawn(process.execPath, [resolveTsxCli(), 'src/main/server.ts'], {
329
- cwd: repoRoot,
330
- env: {
331
- ...process.env,
332
- PORT: port,
333
- HOST: process.env.HOST || '127.0.0.1',
334
- NODE_ENV: 'production',
335
- HERMIT_HOME: hermitHome,
336
- CC_CONNECT_TOKEN: ccTokens.managementToken,
337
- CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
338
- CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
339
- CC_CONNECT_CONFIG: ccConnectConfigPath,
340
- },
341
- stdio: 'inherit',
342
- });
489
+ const serverProcess = spawn(
490
+ process.execPath,
491
+ ['--import', resolveAliasLoaderRegister(), '--import', resolveTsxLoader(), 'src/main/server.ts'],
492
+ {
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',
507
+ }
508
+ );
343
509
 
344
510
  serverProcess.on('exit', (code) => {
511
+ if (shuttingDown) return;
512
+ terminateProcessGroup(ccConnectProcess, 'SIGTERM');
345
513
  if (code !== 0) {
346
514
  console.error(`[openHermit] Server exited with code ${code}`);
347
515
  process.exit(code ?? 1);
348
516
  }
517
+ process.exit(0);
349
518
  });
350
519
 
351
- process.on('SIGINT', () => {
352
- console.log('\n[openHermit] Shutting down...');
353
- serverProcess.kill('SIGINT');
354
- ccConnectProcess?.kill('SIGINT');
355
- });
356
-
357
- process.on('SIGTERM', () => {
358
- console.log('\n[openHermit] Shutting down...');
359
- serverProcess.kill('SIGTERM');
360
- 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');
361
525
  });
362
526
 
363
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.9",
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',