@simonyea/holysheep-cli 1.7.54 → 1.7.55

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.
@@ -9,7 +9,7 @@ const fs = require('fs')
9
9
  const path = require('path')
10
10
  const { execSync, spawn } = require('child_process')
11
11
  const { loadConfig, saveConfig, getApiKey, BASE_URL_ANTHROPIC, BASE_URL_OPENAI, BASE_URL_CLAUDE_RELAY, SHOP_URL, CONFIG_FILE } = require('../utils/config')
12
- const { removeEnvFromShell } = require('../utils/shell')
12
+ const { removeEnvFromShell, writeEnvToShell, getShellRcFiles } = require('../utils/shell')
13
13
  const { commandExists } = require('../utils/which')
14
14
  const TOOLS = require('../tools')
15
15
  const pkg = require('../../package.json')
@@ -441,6 +441,104 @@ async function handleToolInstall(req, res) {
441
441
  res.end()
442
442
  }
443
443
 
444
+ // ── Single-tool configure (SSE) ──────────────────────────────────────────────
445
+
446
+ async function handleToolConfigure(req, res) {
447
+ const body = await parseBody(req)
448
+ const { toolId } = body
449
+ const apiKey = getApiKey()
450
+ if (!apiKey) return json(res, { error: '未登录' }, 401)
451
+
452
+ const tool = TOOLS.find(t => t.id === toolId)
453
+ if (!tool) return json(res, { error: '未知工具' }, 400)
454
+
455
+ sseStart(res)
456
+
457
+ if (!tool.checkInstalled()) {
458
+ sseEmit(res, { type: 'error', message: `${tool.name} 未安装` })
459
+ sseEmit(res, { type: 'done', success: false })
460
+ return res.end()
461
+ }
462
+
463
+ const allModelIds = ['gpt-5.4', 'gpt-5.3-codex-spark', 'claude-sonnet-4-6', 'claude-opus-4-6', 'MiniMax-M2.7-highspeed', 'claude-haiku-4-5']
464
+ const primaryModel = 'claude-sonnet-4-6'
465
+
466
+ sseEmit(res, { type: 'progress', message: `正在配置 ${tool.name}...` })
467
+
468
+ try {
469
+ const result = tool.configure(apiKey, BASE_URL_ANTHROPIC, BASE_URL_OPENAI, primaryModel, allModelIds)
470
+
471
+ if (result.envVars && Object.keys(result.envVars).length > 0) {
472
+ writeEnvToShell(result.envVars)
473
+ sseEmit(res, { type: 'progress', message: '已写入环境变量到 shell 配置' })
474
+ }
475
+
476
+ try { removeEnvFromShell(['ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_BASE_URL']) } catch {}
477
+
478
+ if (result.manual) {
479
+ sseEmit(res, { type: 'result', status: 'manual', message: `${tool.name} 需要手动完成配置`, steps: result.steps })
480
+ } else if (result.warning) {
481
+ sseEmit(res, { type: 'result', status: 'warning', message: result.warning, file: result.file })
482
+ } else {
483
+ sseEmit(res, { type: 'result', status: 'ok', message: `${tool.name} 配置成功`, file: result.file, hot: result.hot })
484
+ }
485
+
486
+ sseEmit(res, { type: 'done', success: true, file: result.file, hot: result.hot })
487
+ } catch (e) {
488
+ sseEmit(res, { type: 'error', message: e.message })
489
+ sseEmit(res, { type: 'done', success: false })
490
+ }
491
+ res.end()
492
+ }
493
+
494
+ // ── Single-tool reset ────────────────────────────────────────────────────────
495
+
496
+ async function handleToolReset(req, res) {
497
+ const body = await parseBody(req)
498
+ const { toolId } = body
499
+ const tool = TOOLS.find(t => t.id === toolId)
500
+ if (!tool) return json(res, { error: '未知工具' }, 400)
501
+ try {
502
+ tool.reset()
503
+ json(res, { success: true })
504
+ } catch (e) {
505
+ json(res, { success: false, error: e.message }, 500)
506
+ }
507
+ }
508
+
509
+ // ── Environment variables ────────────────────────────────────────────────────
510
+
511
+ const HS_ENV_KEYS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_BASE_URL']
512
+ const MARKER_START = '# >>> holysheep-cli managed >>>'
513
+
514
+ function handleEnv(_req, res) {
515
+ const vars = {}
516
+ for (const k of HS_ENV_KEYS) {
517
+ const v = process.env[k]
518
+ vars[k] = v ? (k.includes('KEY') || k.includes('TOKEN') ? maskKey(v) : v) : null
519
+ }
520
+
521
+ const rcFiles = []
522
+ try {
523
+ for (const f of getShellRcFiles()) {
524
+ let has = false
525
+ try { has = fs.readFileSync(f, 'utf8').includes(MARKER_START) } catch {}
526
+ rcFiles.push({ path: f.replace(require('os').homedir(), '~'), hasManagedBlock: has })
527
+ }
528
+ } catch {}
529
+
530
+ json(res, { vars, rcFiles })
531
+ }
532
+
533
+ function handleEnvClean(_req, res) {
534
+ try {
535
+ const cleaned = removeEnvFromShell()
536
+ json(res, { success: true, cleaned })
537
+ } catch (e) {
538
+ json(res, { success: false, error: e.message }, 500)
539
+ }
540
+ }
541
+
444
542
  // ── Models list ──────────────────────────────────────────────────────────────
445
543
 
446
544
  function handleModels(_req, res) {
@@ -478,15 +576,16 @@ async function handleRequest(req, res) {
478
576
  if (route === '/api/status' && req.method === 'GET') return await handleStatus(req, res)
479
577
  if (route === '/api/login' && req.method === 'POST') return await handleLogin(req, res)
480
578
  if (route === '/api/logout' && req.method === 'POST') return await handleLogout(req, res)
481
- if (route === '/api/whoami' && req.method === 'GET') return await handleWhoami(req, res)
482
579
  if (route === '/api/balance' && req.method === 'GET') return await handleBalance(req, res)
483
580
  if (route === '/api/doctor' && req.method === 'GET') return await handleDoctor(req, res)
484
581
  if (route === '/api/tools' && req.method === 'GET') return await handleTools(req, res)
485
582
  if (route === '/api/models' && req.method === 'GET') return await handleModels(req, res)
486
- if (route === '/api/setup' && req.method === 'POST') return await handleSetup(req, res)
487
- if (route === '/api/reset' && req.method === 'POST') return await handleReset(req, res)
488
583
  if (route === '/api/upgrade' && req.method === 'POST') return await handleUpgrade(req, res)
489
- if (route === '/api/tool/install' && req.method === 'POST') return await handleToolInstall(req, res)
584
+ if (route === '/api/tool/install' && req.method === 'POST') return await handleToolInstall(req, res)
585
+ if (route === '/api/tool/configure' && req.method === 'POST') return await handleToolConfigure(req, res)
586
+ if (route === '/api/tool/reset' && req.method === 'POST') return await handleToolReset(req, res)
587
+ if (route === '/api/env' && req.method === 'GET') return handleEnv(req, res)
588
+ if (route === '/api/env/clean' && req.method === 'POST') return handleEnvClean(req, res)
490
589
 
491
590
  res.writeHead(404)
492
591
  res.end('Not Found')
@@ -498,9 +597,25 @@ async function handleRequest(req, res) {
498
597
  }
499
598
 
500
599
  function startServer(port) {
501
- const server = http.createServer(handleRequest)
502
- server.listen(port, '127.0.0.1')
503
- return server
600
+ return new Promise((resolve, reject) => {
601
+ const server = http.createServer(handleRequest)
602
+ server.on('error', (err) => {
603
+ if (err.code === 'EADDRINUSE') {
604
+ // Try to kill stale process and retry once
605
+ try {
606
+ execSync(`lsof -ti:${port} | xargs kill -9`, { stdio: 'ignore' })
607
+ } catch {}
608
+ setTimeout(() => {
609
+ const retry = http.createServer(handleRequest)
610
+ retry.on('error', (err2) => reject(err2))
611
+ retry.listen(port, '127.0.0.1', () => resolve(retry))
612
+ }, 500)
613
+ } else {
614
+ reject(err)
615
+ }
616
+ })
617
+ server.listen(port, '127.0.0.1', () => resolve(server))
618
+ })
504
619
  }
505
620
 
506
621
  module.exports = { startServer }