@yancyyu/openhermit 1.6.23 → 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 });
@@ -414,13 +443,16 @@ function commandExists(command) {
414
443
  function ensureClaudeCodeCliIfNeeded(raw) {
415
444
  if (!configRequiresClaudeCode(raw) || commandExists('claude')) return;
416
445
 
417
- console.log('[openHermit] Claude Code CLI not found; installing @anthropic-ai/claude-code...');
446
+ console.log('[openHermit] Claude Code CLI not found.');
447
+ console.log('[openHermit] Installing @anthropic-ai/claude-code globally. This may take a few minutes...');
448
+ console.log('[openHermit] Running: npm install -g @anthropic-ai/claude-code@latest --prefer-online');
418
449
  try {
419
450
  execSync('npm install -g @anthropic-ai/claude-code@latest --prefer-online', {
420
451
  stdio: 'inherit',
421
452
  shell: true,
422
453
  });
423
454
  } catch (err) {
455
+ console.error('[openHermit] Claude Code CLI install command failed.');
424
456
  console.error('[openHermit] Failed to install Claude Code CLI automatically.');
425
457
  console.error('[openHermit] Please install it manually: npm install -g @anthropic-ai/claude-code@latest');
426
458
  throw err;
@@ -429,25 +461,49 @@ function ensureClaudeCodeCliIfNeeded(raw) {
429
461
  if (!commandExists('claude')) {
430
462
  throw new Error('Claude Code CLI was installed but `claude` is still not available in PATH');
431
463
  }
432
- console.log('[openHermit] Claude Code CLI installed.');
464
+ console.log('[openHermit] Claude Code CLI installed and available in PATH.');
433
465
  }
434
466
 
435
- function readCcConnectConfigState() {
467
+ function ensureCcConnectConfig() {
436
468
  mkdirSync(path.dirname(ccConnectConfigPath), { recursive: true });
437
469
  if (!existsSync(ccConnectConfigPath)) {
438
- return {
439
- configExists: false,
440
- managementToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || '',
441
- bridgeToken: process.env.CC_CONNECT_BRIDGE_TOKEN || process.env.CC_CONNECT_TOKEN || '',
442
- hasRunnableProjects: false,
443
- isStarterConfig: false,
444
- };
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');
445
492
  }
446
493
 
447
- 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
+ }
448
505
 
449
506
  return {
450
- configExists: true,
451
507
  managementToken:
452
508
  process.env.CC_CONNECT_TOKEN ||
453
509
  process.env.CC_CONNECT_MANAGEMENT_TOKEN ||
@@ -456,8 +512,6 @@ function readCcConnectConfigState() {
456
512
  process.env.CC_CONNECT_BRIDGE_TOKEN ||
457
513
  process.env.CC_CONNECT_TOKEN ||
458
514
  parseTomlToken(raw, 'bridge'),
459
- hasProjects: hasProjectEntries(raw),
460
- isStarterConfig: isStarterProjectConfig(raw),
461
515
  raw,
462
516
  };
463
517
  }
@@ -540,124 +594,25 @@ function resolveAliasLoaderRegister() {
540
594
  return `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(${JSON.stringify(aliasLoaderUrl)}, pathToFileURL("./"));`;
541
595
  }
542
596
 
543
- async function checkExistingOpenHermitServer() {
544
- const url = `http://127.0.0.1:${port}`;
545
- try {
546
- const res = await fetch(`${url}/api/version`, { signal: AbortSignal.timeout(1000) });
547
- if (res.ok) {
548
- const version = (await res.text()).trim() || 'unknown';
549
- return { running: true, version, url };
550
- }
551
- } catch {
552
- // Port may be unused or owned by another process.
553
- }
554
- return { running: false, version: '', url };
555
- }
556
-
557
- async function isTcpPortAvailable(portNumber) {
558
- return new Promise((resolve) => {
559
- const server = net.createServer();
560
- server.once('error', () => resolve(false));
561
- server.once('listening', () => {
562
- server.close(() => resolve(true));
563
- });
564
- server.listen(portNumber, '127.0.0.1');
565
- });
566
- }
567
-
568
- async function assertWebPortAvailable() {
569
- const existingServer = await checkExistingOpenHermitServer();
570
- if (existingServer.running) {
571
- console.log(`[openHermit] Already running: ${existingServer.url}`);
572
- console.log(`[openHermit] Version: ${existingServer.version}`);
573
- console.log('[openHermit] Run `openhermit stop` first, or use `openhermit --port <port>` for another instance.');
574
- process.exit(0);
575
- }
576
-
577
- const available = await isTcpPortAvailable(Number.parseInt(port, 10));
578
- if (!available) {
579
- console.error(`[openHermit] Port ${port} is already in use.`);
580
- console.error('[openHermit] Stop the existing process first, or start with another port:');
581
- console.error(` openhermit --port ${Number.parseInt(port, 10) + 1}`);
582
- console.error('[openHermit] macOS/Linux: lsof -nP -iTCP:' + port + ' -sTCP:LISTEN');
583
- console.error('[openHermit] Windows: netstat -ano | findstr :' + port);
584
- process.exit(1);
585
- }
586
- }
587
-
588
597
  let ccConnectProcess = null;
589
598
  let ccTokens = {
590
599
  managementToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || '',
591
600
  bridgeToken: process.env.CC_CONNECT_BRIDGE_TOKEN || process.env.CC_CONNECT_TOKEN || '',
592
601
  };
593
- let runtimeSetupMode = false;
594
-
595
- await assertWebPortAvailable();
596
602
 
597
603
  if (!skipCcConnect) {
598
- let shouldStartRuntime = false;
599
- ccTokens = readCcConnectConfigState();
604
+ ccTokens = ensureCcConnectConfig();
600
605
  const ccBaseUrl = process.env.CC_CONNECT_BASE_URL || 'http://127.0.0.1:9820';
601
606
  const alreadyRunning = await waitForCcConnect(ccBaseUrl, ccTokens.managementToken, 1_000);
602
607
  if (alreadyRunning) {
603
608
  console.log(`[openHermit] Runtime service already running: ${ccBaseUrl}`);
604
- } else if (!ccTokens.configExists) {
605
- console.log('[openHermit] Initializing runtime config with bundled runtime service...');
606
- console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
607
- const initProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
608
- cwd: repoRoot,
609
- env: {
610
- ...process.env,
611
- CC_CONNECT_TOKEN: ccTokens.managementToken,
612
- CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
613
- CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
614
- },
615
- stdio: ['ignore', 'pipe', 'pipe'],
616
- });
617
- initProcess.stdout?.on('data', (chunk) => {
618
- process.stdout.write(chunk);
619
- appendLog(runtimeLogPath, chunk);
620
- });
621
- initProcess.stderr?.on('data', (chunk) => {
622
- process.stderr.write(chunk);
623
- appendLog(runtimeLogPath, chunk);
624
- });
625
-
626
- const initCode = await new Promise((resolve) => {
627
- initProcess.on('exit', (code) => resolve(code ?? 1));
628
- initProcess.on('error', () => resolve(1));
629
- });
630
-
631
- ccTokens = readCcConnectConfigState();
632
- if (initCode === 0 && ccTokens.configExists) {
633
- console.log('[openHermit] Runtime starter config created.');
634
- try {
635
- ensureClaudeCodeCliIfNeeded(ccTokens.raw);
636
- } catch {
637
- printLogTail('Runtime', runtimeLogPath);
638
- process.exit(1);
639
- }
640
- shouldStartRuntime = true;
641
- } else {
642
- console.error(`[openHermit] Runtime config initialization failed (code ${initCode}).`);
643
- printLogTail('Runtime', runtimeLogPath);
644
- process.exit(1);
645
- }
646
- } else if (ccTokens.hasProjects) {
609
+ } else {
647
610
  try {
648
611
  ensureClaudeCodeCliIfNeeded(ccTokens.raw);
649
612
  } catch {
650
613
  printLogTail('Runtime', runtimeLogPath);
651
614
  process.exit(1);
652
615
  }
653
- shouldStartRuntime = true;
654
- } else {
655
- console.error('[openHermit] Runtime config has no projects. Please edit the config and try again.');
656
- console.error(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
657
- process.exit(1);
658
- }
659
-
660
- if (shouldStartRuntime) {
661
616
  console.log('[openHermit] Starting bundled runtime service...');
662
617
  console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
663
618
  ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
@@ -745,7 +700,6 @@ const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderReg
745
700
  HOST: process.env.HOST || '127.0.0.1',
746
701
  NODE_ENV: 'production',
747
702
  HERMIT_HOME: hermitHome,
748
- HERMIT_RUNTIME_SETUP_MODE: runtimeSetupMode ? '1' : '0',
749
703
  CC_CONNECT_TOKEN: ccTokens.managementToken,
750
704
  CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
751
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.23",
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
  }