@yancyyu/openhermit 1.6.24 → 1.6.25

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
@@ -15,9 +15,9 @@
15
15
  */
16
16
 
17
17
  import { spawn, execSync } from 'node:child_process';
18
+ import crypto from 'node:crypto';
18
19
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
19
20
  import { createRequire } from 'node:module';
20
- import net from 'node:net';
21
21
  import os from 'node:os';
22
22
  import path from 'node:path';
23
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -96,7 +96,8 @@ const ccConnectConfigPath =
96
96
  process.env.HERMIT_CC_CONNECT_CONFIG ||
97
97
  process.env.CC_CONNECT_CONFIG ||
98
98
  path.join(hermitHome, 'cc-connect', 'config.toml');
99
- const starterProjectName = 'my-project';
99
+ const bootstrapProjectName = 'default';
100
+ const legacyBootstrapProjectName = '__openhermit_bootstrap__';
100
101
 
101
102
  // ---------------------------------------------------------------------------
102
103
  // Update command
@@ -350,6 +351,10 @@ Please install dependencies first:
350
351
  // cc-connect sidecar
351
352
  // ---------------------------------------------------------------------------
352
353
 
354
+ function randomToken() {
355
+ return crypto.randomBytes(16).toString('hex');
356
+ }
357
+
353
358
  function escapeTomlPath(value) {
354
359
  return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
355
360
  }
@@ -359,6 +364,36 @@ function parseTomlToken(raw, section) {
359
364
  return match?.[1] || '';
360
365
  }
361
366
 
367
+ function hasProjectEntries(raw) {
368
+ return /^\s*\[\[projects\]\]/m.test(raw);
369
+ }
370
+
371
+ function buildBootstrapProjectToml() {
372
+ return `
373
+ # Internal bootstrap project used only so cc-connect can start with an otherwise empty config.
374
+ # It is safe to keep this project; users can replace or delete it after creating real teams.
375
+ [[projects]]
376
+ name = "${bootstrapProjectName}"
377
+ disabled_commands = ["*"]
378
+
379
+ [projects.agent]
380
+ type = "claudecode"
381
+
382
+ [projects.agent.options]
383
+ work_dir = "${escapeTomlPath(hermitHome)}"
384
+ mode = "default"
385
+
386
+ [[projects.platforms]]
387
+ type = "line"
388
+
389
+ [projects.platforms.options]
390
+ channel_secret = "openhermit-bootstrap"
391
+ channel_token = "openhermit-bootstrap"
392
+ port = "0"
393
+ callback_path = "/openhermit-bootstrap"
394
+ `;
395
+ }
396
+
362
397
  function escapeRegExp(value) {
363
398
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
364
399
  }
@@ -380,28 +415,22 @@ function isManagedBootstrapBlock(block) {
380
415
  );
381
416
  }
382
417
 
383
- function isStarterProjectConfig(raw) {
384
- const block = findProjectBlock(raw, starterProjectName);
385
- if (!block) return false;
386
- const text = block.match[0];
387
- return (
388
- text.includes('name = "my-project"') &&
389
- text.includes('type = "claudecode"') &&
390
- text.includes('work_dir = "/path/to/your/project"') &&
391
- text.includes('app_id = "your-feishu-app-id"') &&
392
- text.includes('app_secret = "your-feishu-app-secret"')
393
- );
418
+ function migrateManagedBootstrapProject(raw) {
419
+ const legacyBlock = findProjectBlock(raw, legacyBootstrapProjectName);
420
+ if (!legacyBlock || !isManagedBootstrapBlock(legacyBlock.match[0])) return raw;
421
+
422
+ const withoutLegacy = raw.replace(legacyBlock.pattern, '').replace(/\n{3,}/g, '\n\n').trimEnd();
423
+ if (findProjectBlock(withoutLegacy, bootstrapProjectName)) {
424
+ return `${withoutLegacy}\n`;
425
+ }
426
+
427
+ return `${withoutLegacy}\n${buildBootstrapProjectToml()}`;
394
428
  }
395
429
 
396
430
  function configRequiresClaudeCode(raw) {
397
431
  return /type\s*=\s*"claudecode"/.test(raw);
398
432
  }
399
433
 
400
- function hasProjectEntries(raw) {
401
- const projectPattern = /\[\[projects\]\]\nname\s*=\s*"([^"]+)"[\s\S]*?(?=\n\[\[projects\]\]|\s*$)/g;
402
- return [...raw.matchAll(projectPattern)].some((match) => !isManagedBootstrapBlock(match[0]));
403
- }
404
-
405
434
  function commandExists(command) {
406
435
  try {
407
436
  execSync(`${command} --version`, { stdio: 'ignore', shell: true });
@@ -435,22 +464,46 @@ function ensureClaudeCodeCliIfNeeded(raw) {
435
464
  console.log('[openHermit] Claude Code CLI installed and available in PATH.');
436
465
  }
437
466
 
438
- function readCcConnectConfigState() {
467
+ function ensureCcConnectConfig() {
439
468
  mkdirSync(path.dirname(ccConnectConfigPath), { recursive: true });
440
469
  if (!existsSync(ccConnectConfigPath)) {
441
- return {
442
- configExists: false,
443
- managementToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || '',
444
- bridgeToken: process.env.CC_CONNECT_BRIDGE_TOKEN || process.env.CC_CONNECT_TOKEN || '',
445
- hasRunnableProjects: false,
446
- isStarterConfig: false,
447
- };
470
+ const managementToken = randomToken();
471
+ const bridgeToken = randomToken();
472
+ const config = `data_dir = "${escapeTomlPath(path.join(hermitHome, 'cc-connect', 'data'))}"
473
+ language = "zh"
474
+
475
+ [management]
476
+ enabled = true
477
+ host = "127.0.0.1"
478
+ port = 9820
479
+ token = "${managementToken}"
480
+
481
+ [bridge]
482
+ enabled = true
483
+ host = "127.0.0.1"
484
+ port = 9810
485
+ token = "${bridgeToken}"
486
+ path = "/bridge/ws"
487
+
488
+ [log]
489
+ level = "info"
490
+ ${buildBootstrapProjectToml()}`;
491
+ writeFileSync(ccConnectConfigPath, config, 'utf-8');
448
492
  }
449
493
 
450
- const raw = readFileSync(ccConnectConfigPath, 'utf-8');
494
+ let raw = readFileSync(ccConnectConfigPath, 'utf-8');
495
+ if (!hasProjectEntries(raw)) {
496
+ raw = `${raw.trimEnd()}\n${buildBootstrapProjectToml()}`;
497
+ writeFileSync(ccConnectConfigPath, raw, 'utf-8');
498
+ } else {
499
+ const migrated = migrateManagedBootstrapProject(raw);
500
+ if (migrated !== raw) {
501
+ raw = migrated;
502
+ writeFileSync(ccConnectConfigPath, raw, 'utf-8');
503
+ }
504
+ }
451
505
 
452
506
  return {
453
- configExists: true,
454
507
  managementToken:
455
508
  process.env.CC_CONNECT_TOKEN ||
456
509
  process.env.CC_CONNECT_MANAGEMENT_TOKEN ||
@@ -459,8 +512,6 @@ function readCcConnectConfigState() {
459
512
  process.env.CC_CONNECT_BRIDGE_TOKEN ||
460
513
  process.env.CC_CONNECT_TOKEN ||
461
514
  parseTomlToken(raw, 'bridge'),
462
- hasProjects: hasProjectEntries(raw),
463
- isStarterConfig: isStarterProjectConfig(raw),
464
515
  raw,
465
516
  };
466
517
  }
@@ -543,124 +594,25 @@ function resolveAliasLoaderRegister() {
543
594
  return `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(${JSON.stringify(aliasLoaderUrl)}, pathToFileURL("./"));`;
544
595
  }
545
596
 
546
- async function checkExistingOpenHermitServer() {
547
- const url = `http://127.0.0.1:${port}`;
548
- try {
549
- const res = await fetch(`${url}/api/version`, { signal: AbortSignal.timeout(1000) });
550
- if (res.ok) {
551
- const version = (await res.text()).trim() || 'unknown';
552
- return { running: true, version, url };
553
- }
554
- } catch {
555
- // Port may be unused or owned by another process.
556
- }
557
- return { running: false, version: '', url };
558
- }
559
-
560
- async function isTcpPortAvailable(portNumber) {
561
- return new Promise((resolve) => {
562
- const server = net.createServer();
563
- server.once('error', () => resolve(false));
564
- server.once('listening', () => {
565
- server.close(() => resolve(true));
566
- });
567
- server.listen(portNumber, '127.0.0.1');
568
- });
569
- }
570
-
571
- async function assertWebPortAvailable() {
572
- const existingServer = await checkExistingOpenHermitServer();
573
- if (existingServer.running) {
574
- console.log(`[openHermit] Already running: ${existingServer.url}`);
575
- console.log(`[openHermit] Version: ${existingServer.version}`);
576
- console.log('[openHermit] Run `openhermit stop` first, or use `openhermit --port <port>` for another instance.');
577
- process.exit(0);
578
- }
579
-
580
- const available = await isTcpPortAvailable(Number.parseInt(port, 10));
581
- if (!available) {
582
- console.error(`[openHermit] Port ${port} is already in use.`);
583
- console.error('[openHermit] Stop the existing process first, or start with another port:');
584
- console.error(` openhermit --port ${Number.parseInt(port, 10) + 1}`);
585
- console.error('[openHermit] macOS/Linux: lsof -nP -iTCP:' + port + ' -sTCP:LISTEN');
586
- console.error('[openHermit] Windows: netstat -ano | findstr :' + port);
587
- process.exit(1);
588
- }
589
- }
590
-
591
597
  let ccConnectProcess = null;
592
598
  let ccTokens = {
593
599
  managementToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || '',
594
600
  bridgeToken: process.env.CC_CONNECT_BRIDGE_TOKEN || process.env.CC_CONNECT_TOKEN || '',
595
601
  };
596
- let runtimeSetupMode = false;
597
-
598
- await assertWebPortAvailable();
599
602
 
600
603
  if (!skipCcConnect) {
601
- let shouldStartRuntime = false;
602
- ccTokens = readCcConnectConfigState();
604
+ ccTokens = ensureCcConnectConfig();
603
605
  const ccBaseUrl = process.env.CC_CONNECT_BASE_URL || 'http://127.0.0.1:9820';
604
606
  const alreadyRunning = await waitForCcConnect(ccBaseUrl, ccTokens.managementToken, 1_000);
605
607
  if (alreadyRunning) {
606
608
  console.log(`[openHermit] Runtime service already running: ${ccBaseUrl}`);
607
- } else if (!ccTokens.configExists) {
608
- console.log('[openHermit] Initializing runtime config with bundled runtime service...');
609
- console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
610
- const initProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
611
- cwd: repoRoot,
612
- env: {
613
- ...process.env,
614
- CC_CONNECT_TOKEN: ccTokens.managementToken,
615
- CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
616
- CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
617
- },
618
- stdio: ['ignore', 'pipe', 'pipe'],
619
- });
620
- initProcess.stdout?.on('data', (chunk) => {
621
- process.stdout.write(chunk);
622
- appendLog(runtimeLogPath, chunk);
623
- });
624
- initProcess.stderr?.on('data', (chunk) => {
625
- process.stderr.write(chunk);
626
- appendLog(runtimeLogPath, chunk);
627
- });
628
-
629
- const initCode = await new Promise((resolve) => {
630
- initProcess.on('exit', (code) => resolve(code ?? 1));
631
- initProcess.on('error', () => resolve(1));
632
- });
633
-
634
- ccTokens = readCcConnectConfigState();
635
- if (initCode === 0 && ccTokens.configExists) {
636
- console.log('[openHermit] Runtime starter config created.');
637
- try {
638
- ensureClaudeCodeCliIfNeeded(ccTokens.raw);
639
- } catch {
640
- printLogTail('Runtime', runtimeLogPath);
641
- process.exit(1);
642
- }
643
- shouldStartRuntime = true;
644
- } else {
645
- console.error(`[openHermit] Runtime config initialization failed (code ${initCode}).`);
646
- printLogTail('Runtime', runtimeLogPath);
647
- process.exit(1);
648
- }
649
- } else if (ccTokens.hasProjects) {
609
+ } else {
650
610
  try {
651
611
  ensureClaudeCodeCliIfNeeded(ccTokens.raw);
652
612
  } catch {
653
613
  printLogTail('Runtime', runtimeLogPath);
654
614
  process.exit(1);
655
615
  }
656
- shouldStartRuntime = true;
657
- } else {
658
- console.error('[openHermit] Runtime config has no projects. Please edit the config and try again.');
659
- console.error(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
660
- process.exit(1);
661
- }
662
-
663
- if (shouldStartRuntime) {
664
616
  console.log('[openHermit] Starting bundled runtime service...');
665
617
  console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
666
618
  ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
@@ -748,7 +700,6 @@ const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderReg
748
700
  HOST: process.env.HOST || '127.0.0.1',
749
701
  NODE_ENV: 'production',
750
702
  HERMIT_HOME: hermitHome,
751
- HERMIT_RUNTIME_SETUP_MODE: runtimeSetupMode ? '1' : '0',
752
703
  CC_CONNECT_TOKEN: ccTokens.managementToken,
753
704
  CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
754
705
  CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yancyyu/openhermit",
3
3
  "type": "module",
4
- "version": "1.6.24",
4
+ "version": "1.6.25",
5
5
  "description": "openHermit: team-oriented agent management workbench atop cc-connect.",
6
6
  "license": "AGPL-3.0",
7
7
  "author": {
@@ -68,7 +68,6 @@ const HOST = process.env.HOST ?? '127.0.0.1';
68
68
  const PORT = Number.parseInt(process.env.PORT ?? '5680', 10);
69
69
  const STATIC_DIR = process.env.STATIC_DIR ?? path.resolve(REPO_ROOT, 'dist-renderer');
70
70
  const HARNESS_BRIDGE_CONNECT_TIMEOUT_MS = 10_000;
71
- const RUNTIME_SETUP_MODE = process.env.HERMIT_RUNTIME_SETUP_MODE === '1';
72
71
 
73
72
  // ===========================================================================
74
73
  // Hermit runtime config — ~/.hermit/config.json
@@ -2184,11 +2183,6 @@ function isCronNotFoundError(error: unknown): boolean {
2184
2183
  return /(\b404\b|not found|no matching|does not exist|不存在)/i.test(message);
2185
2184
  }
2186
2185
 
2187
- function isRuntimeUnavailableError(error: unknown): boolean {
2188
- const message = error instanceof Error ? error.message : String(error);
2189
- return /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|fetch failed|cc-connect 不可达/i.test(message);
2190
- }
2191
-
2192
2186
  app.get('/api/schedules', async () => {
2193
2187
  try {
2194
2188
  const jobs = await cc.listCronJobs();
@@ -2196,9 +2190,6 @@ app.get('/api/schedules', async () => {
2196
2190
  const workDirMap = await resolveTeamWorkDirs(jobs.map((job) => job.project));
2197
2191
  return jobs.map((job) => mapCronJobToSchedule(job, workDirMap.get(job.project) ?? ''));
2198
2192
  } catch (err) {
2199
- if (RUNTIME_SETUP_MODE && isRuntimeUnavailableError(err)) {
2200
- return [];
2201
- }
2202
2193
  app.log.warn({ err }, 'list schedules from cc-connect failed');
2203
2194
  return [];
2204
2195
  }