bfi-cli 0.1.102 → 0.1.103

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/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { __exportAll, __toESM } from "./_chunks/rolldown-runtime.mjs";
3
3
  import { isCancel } from "./_chunks/libs/@clack/core.mjs";
4
- import { cancel, confirm, groupMultiselect, intro, log, multiselect as multiselect$1, note, outro, select, spinner } from "./_chunks/libs/@clack/prompts.mjs";
4
+ import { cancel, confirm, groupMultiselect, intro, log, multiselect as multiselect$1, note, outro, password, select, spinner } from "./_chunks/libs/@clack/prompts.mjs";
5
5
  import { require_picocolors } from "./_chunks/libs/picocolors.mjs";
6
6
  import { addSkillToLocalLock, addSkillToLock, computeSkillFolderHash, discoverSkills, dismissPrompt, filterSkills, getAllLockedSkills, getGitHubToken, getLastSelectedAgents, getSkillDisplayName, getSkillFromLock, isPromptDismissed, parseFrontmatter, parseSkillMd, readLocalLock, readSkillLock, removeSkillFromLock, sanitizeMetadata, saveSelectedAgents, stripTerminalEscapes } from "./_chunks/skills.mjs";
7
7
  import { PluginRegistryClient, parsePackageIntent } from "./_chunks/intent.mjs";
@@ -9,25 +9,29 @@ import { loadGlobalConfig, loadLockfile, loadProjectConfig, resolveConfig, saveG
9
9
  import { esm_default } from "./_chunks/libs/simple-git.mjs";
10
10
  import { EVE_SUBAGENTS_DIR, agents, detectInstalledAgents, getEveSubagents, getNonUniversalAgents, getUniversalAgents, getVisibleUniversalAgents, isUniversalAgent } from "./_chunks/agents.mjs";
11
11
  import { packageSkillName } from "./_chunks/package-skill-identity.mjs";
12
+ import { entriesFromJson, mapStrings, normalizeConfig, object, resolveVariables, validName, variablePattern, variables } from "./_chunks/model.mjs";
12
13
  import { createRequire } from "node:module";
13
14
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
14
15
  import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
15
16
  import { fileURLToPath } from "url";
16
17
  import { createHash, randomUUID } from "node:crypto";
17
- import { stripVTControlCharacters } from "node:util";
18
- import fs from "node:fs";
19
- import path$1 from "node:path";
18
+ import { isDeepStrictEqual, stripVTControlCharacters } from "node:util";
19
+ import fs, { constants, existsSync as existsSync$1 } from "node:fs";
20
+ import path$1, { dirname as dirname$1, join as join$1, parse, resolve as resolve$1 } from "node:path";
20
21
  import { homedir, platform, tmpdir } from "os";
21
22
  import * as readline from "readline";
22
23
  import { Writable } from "stream";
23
24
  import https from "node:https";
24
25
  import http from "node:http";
26
+ import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
25
27
  import zlib, { gunzipSync, inflateRawSync } from "node:zlib";
26
- import os$1 from "node:os";
28
+ import os$1, { homedir as homedir$1 } from "node:os";
27
29
  import { promisify } from "util";
28
30
  import { execFile, spawn, spawnSync } from "child_process";
29
- import { access, chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, writeFile } from "fs/promises";
31
+ import { access as access$1, chmod, cp as cp$1, lstat as lstat$1, mkdir as mkdir$1, mkdtemp, readFile as readFile$1, readdir as readdir$1, readlink, realpath as realpath$1, rename as rename$1, rm as rm$1, stat, symlink, writeFile as writeFile$1 } from "fs/promises";
30
32
  import { determineAgent } from "@vercel/detect-agent";
33
+ import { applyEdits, getNodeValue, modify, parseTree } from "jsonc-parser";
34
+ import { parse as parse$1, stringify } from "smol-toml";
31
35
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
32
36
  const GIT_HOST = process.env.BFI_GIT_HOST || "git.yyrd.com";
33
37
  const GIT_URL = `https://${GIT_HOST}`;
@@ -1223,11 +1227,11 @@ function createGitClient(extraEnv) {
1223
1227
  }).env(env);
1224
1228
  }
1225
1229
  async function resetTempDir(dir) {
1226
- await rm(dir, {
1230
+ await rm$1(dir, {
1227
1231
  recursive: true,
1228
1232
  force: true
1229
1233
  }).catch(() => {});
1230
- await mkdir(dir, { recursive: true });
1234
+ await mkdir$1(dir, { recursive: true });
1231
1235
  }
1232
1236
  async function tryGhClone(repo, tempDir, ref) {
1233
1237
  let cloneTarget = repo.slug;
@@ -1292,7 +1296,7 @@ async function cloneRepo(url, ref) {
1292
1296
  const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
1293
1297
  const isAuthError = isAuthFailure(errorMessage);
1294
1298
  if (isTimeout) {
1295
- await rm(tempDir, {
1299
+ await rm$1(tempDir, {
1296
1300
  recursive: true,
1297
1301
  force: true
1298
1302
  }).catch(() => {});
@@ -1309,7 +1313,7 @@ async function cloneRepo(url, ref) {
1309
1313
  return tempDir;
1310
1314
  } catch {}
1311
1315
  }
1312
- await rm(tempDir, {
1316
+ await rm$1(tempDir, {
1313
1317
  recursive: true,
1314
1318
  force: true
1315
1319
  }).catch(() => {});
@@ -1321,13 +1325,26 @@ async function cleanupTempDir(dir) {
1321
1325
  const normalizedDir = normalize(resolve(dir));
1322
1326
  const normalizedTmpDir = normalize(resolve(tmpdir()));
1323
1327
  if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
1324
- await rm(dir, {
1328
+ await rm$1(dir, {
1325
1329
  recursive: true,
1326
1330
  force: true
1327
1331
  });
1328
1332
  }
1329
1333
  const AGENTS_DIR = ".agents";
1330
1334
  const SKILLS_SUBDIR = "skills";
1335
+ var installer_exports = /* @__PURE__ */ __exportAll({
1336
+ getAgentBaseDir: () => getAgentBaseDir,
1337
+ getCanonicalPath: () => getCanonicalPath,
1338
+ getCanonicalSkillsDir: () => getCanonicalSkillsDir,
1339
+ getEveSubagentSkillsDir: () => getEveSubagentSkillsDir,
1340
+ getInstallPath: () => getInstallPath,
1341
+ installBlobSkillForAgent: () => installBlobSkillForAgent,
1342
+ installSkillForAgent: () => installSkillForAgent,
1343
+ installWellKnownSkillForAgent: () => installWellKnownSkillForAgent,
1344
+ isSkillInstalled: () => isSkillInstalled,
1345
+ listInstalledSkills: () => listInstalledSkills,
1346
+ sanitizeName: () => sanitizeName
1347
+ });
1331
1348
  function sanitizeName(name) {
1332
1349
  return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
1333
1350
  }
@@ -1372,7 +1389,7 @@ function resolveSymlinkTarget(linkPath, linkTarget) {
1372
1389
  }
1373
1390
  async function cleanAndCreateDirectory(path) {
1374
1391
  try {
1375
- await rm(path, {
1392
+ await rm$1(path, {
1376
1393
  recursive: true,
1377
1394
  force: true
1378
1395
  });
@@ -1387,7 +1404,7 @@ async function classifyPathEntry(path) {
1387
1404
  return (await stat(path)).isDirectory() ? "dir" : "blocker";
1388
1405
  } catch {
1389
1406
  try {
1390
- await lstat(path);
1407
+ await lstat$1(path);
1391
1408
  return "blocker";
1392
1409
  } catch {
1393
1410
  return "missing";
@@ -1407,32 +1424,32 @@ async function findBlockingEntry(path) {
1407
1424
  }
1408
1425
  async function ensureDirectory(path) {
1409
1426
  try {
1410
- await mkdir(path, { recursive: true });
1427
+ await mkdir$1(path, { recursive: true });
1411
1428
  return;
1412
1429
  } catch (err) {
1413
1430
  if (!isErrnoException(err) || err.code !== "ENOTDIR" && err.code !== "EEXIST" && err.code !== "ELOOP" && err.code !== "ENOENT") throw err;
1414
1431
  }
1415
1432
  const blockingEntry = await findBlockingEntry(path);
1416
1433
  if (!blockingEntry) {
1417
- await mkdir(path, { recursive: true });
1434
+ await mkdir$1(path, { recursive: true });
1418
1435
  return;
1419
1436
  }
1420
1437
  const backupPath = `${blockingEntry}.bak-${Date.now()}`;
1421
1438
  try {
1422
- await rename(blockingEntry, backupPath);
1439
+ await rename$1(blockingEntry, backupPath);
1423
1440
  } catch (renameErr) {
1424
1441
  const reason = renameErr instanceof Error ? renameErr.message : String(renameErr);
1425
1442
  throw new Error(`无法创建目录 ${path}:${blockingEntry} 存在但不是可用目录(可能是文件或失效的链接),且自动移开失败(${reason})。请手动删除或重命名该条目后重试。`);
1426
1443
  }
1427
1444
  console.warn(`检测到 ${blockingEntry} 不是可用目录(可能是文件或失效的链接),已重命名为 ${backupPath}`);
1428
- await mkdir(path, { recursive: true });
1445
+ await mkdir$1(path, { recursive: true });
1429
1446
  }
1430
1447
  async function resolveParentSymlinks(path) {
1431
1448
  const resolved = resolve(path);
1432
1449
  const dir = dirname(resolved);
1433
1450
  const base = basename(resolved);
1434
1451
  try {
1435
- const realDir = await realpath(dir);
1452
+ const realDir = await realpath$1(dir);
1436
1453
  return join(realDir, base);
1437
1454
  } catch {
1438
1455
  return resolved;
@@ -1442,21 +1459,21 @@ async function createSymlink(target, linkPath) {
1442
1459
  try {
1443
1460
  const resolvedTarget = resolve(target);
1444
1461
  const resolvedLinkPath = resolve(linkPath);
1445
- const [realTarget, realLinkPath] = await Promise.all([realpath(resolvedTarget).catch(() => resolvedTarget), realpath(resolvedLinkPath).catch(() => resolvedLinkPath)]);
1462
+ const [realTarget, realLinkPath] = await Promise.all([realpath$1(resolvedTarget).catch(() => resolvedTarget), realpath$1(resolvedLinkPath).catch(() => resolvedLinkPath)]);
1446
1463
  if (realTarget === realLinkPath) return true;
1447
1464
  if (await resolveParentSymlinks(target) === await resolveParentSymlinks(linkPath)) return true;
1448
1465
  try {
1449
- if ((await lstat(linkPath)).isSymbolicLink()) {
1466
+ if ((await lstat$1(linkPath)).isSymbolicLink()) {
1450
1467
  if (resolveSymlinkTarget(linkPath, await readlink(linkPath)) === resolvedTarget) return true;
1451
- await rm(linkPath);
1452
- } else await rm(linkPath, { recursive: true });
1468
+ await rm$1(linkPath);
1469
+ } else await rm$1(linkPath, { recursive: true });
1453
1470
  } catch (err) {
1454
1471
  if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") try {
1455
- await rm(linkPath, { force: true });
1472
+ await rm$1(linkPath, { force: true });
1456
1473
  } catch {}
1457
1474
  }
1458
1475
  const linkDir = dirname(linkPath);
1459
- await mkdir(linkDir, { recursive: true });
1476
+ await mkdir$1(linkDir, { recursive: true });
1460
1477
  const realLinkDir = await resolveParentSymlinks(linkDir);
1461
1478
  const relativePath = relative(realLinkDir, target);
1462
1479
  const symlinkType = platform() === "win32" ? "junction" : void 0;
@@ -1589,18 +1606,18 @@ function stripIgnoredEveFrontmatter(raw) {
1589
1606
  return `---\n${keys.map((key) => `${key}: ${JSON.stringify(eveData[key])}`).join("\n")}\n---\n${content.replace(/^\r?\n/u, "")}`;
1590
1607
  }
1591
1608
  async function copyDirectory(src, dest, agentType) {
1592
- await mkdir(dest, { recursive: true });
1593
- const entries = await readdir(src, { withFileTypes: true });
1609
+ await mkdir$1(dest, { recursive: true });
1610
+ const entries = await readdir$1(src, { withFileTypes: true });
1594
1611
  await Promise.all(entries.filter((entry) => !isExcluded$1(entry.name, entry.isDirectory())).map(async (entry) => {
1595
1612
  const srcPath = join(src, entry.name);
1596
1613
  const destPath = join(dest, entry.name);
1597
1614
  if (entry.isDirectory()) await copyDirectory(srcPath, destPath, agentType);
1598
1615
  else try {
1599
1616
  if (agentType === "eve" && entry.name.toLowerCase() === "skill.md") {
1600
- await writeFile(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
1617
+ await writeFile$1(destPath, stripIgnoredEveFrontmatter(await readFile$1(srcPath, "utf-8")));
1601
1618
  return;
1602
1619
  }
1603
- await cp(srcPath, destPath, {
1620
+ await cp$1(srcPath, destPath, {
1604
1621
  dereference: true,
1605
1622
  recursive: true
1606
1623
  });
@@ -1632,7 +1649,7 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
1632
1649
  const skillDir = join(targetBase, sanitized);
1633
1650
  if (!isPathSafe$1(targetBase, skillDir)) return false;
1634
1651
  try {
1635
- await access(skillDir);
1652
+ await access$1(skillDir);
1636
1653
  return true;
1637
1654
  } catch {
1638
1655
  return false;
@@ -1688,8 +1705,8 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1688
1705
  const fullPath = join(targetDir, filePath);
1689
1706
  if (!isPathSafe$1(targetDir, fullPath)) continue;
1690
1707
  const parentDir = dirname(fullPath);
1691
- if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
1692
- await writeFile(fullPath, agentType === "eve" && basename(filePath).toLowerCase() === "skill.md" && typeof content === "string" ? stripIgnoredEveFrontmatter(content) : content);
1708
+ if (parentDir !== targetDir) await mkdir$1(parentDir, { recursive: true });
1709
+ await writeFile$1(fullPath, agentType === "eve" && basename(filePath).toLowerCase() === "skill.md" && typeof content === "string" ? stripIgnoredEveFrontmatter(content) : content);
1693
1710
  }
1694
1711
  }
1695
1712
  try {
@@ -1760,11 +1777,11 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1760
1777
  };
1761
1778
  try {
1762
1779
  await ensureDirectory(agentBase);
1763
- await rm(flatSkillPath, {
1780
+ await rm$1(flatSkillPath, {
1764
1781
  recursive: true,
1765
1782
  force: true
1766
1783
  });
1767
- await writeFile(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
1784
+ await writeFile$1(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
1768
1785
  return {
1769
1786
  success: true,
1770
1787
  path: flatSkillPath,
@@ -1799,8 +1816,8 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1799
1816
  const fullPath = join(targetDir, file.path);
1800
1817
  if (!isPathSafe$1(targetDir, fullPath)) continue;
1801
1818
  const parentDir = dirname(fullPath);
1802
- if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
1803
- await writeFile(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
1819
+ if (parentDir !== targetDir) await mkdir$1(parentDir, { recursive: true });
1820
+ await writeFile$1(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
1804
1821
  }
1805
1822
  }
1806
1823
  try {
@@ -1905,7 +1922,7 @@ async function listInstalledSkills(options = {}) {
1905
1922
  }
1906
1923
  }
1907
1924
  for (const scope of scopes) try {
1908
- const entries = await readdir(scope.path, { withFileTypes: true });
1925
+ const entries = await readdir$1(scope.path, { withFileTypes: true });
1909
1926
  for (const entry of entries) {
1910
1927
  const skillDir = join(scope.path, entry.name);
1911
1928
  if (!await isDirEntryOrSymlinkToDir(entry, skillDir)) continue;
@@ -1949,13 +1966,13 @@ async function listInstalledSkills(options = {}) {
1949
1966
  const agentSkillDir = join(agentBase, possibleName);
1950
1967
  if (!isPathSafe$1(agentBase, agentSkillDir)) continue;
1951
1968
  try {
1952
- await access(agentSkillDir);
1969
+ await access$1(agentSkillDir);
1953
1970
  found = true;
1954
1971
  break;
1955
1972
  } catch {}
1956
1973
  }
1957
1974
  if (!found) try {
1958
- const agentEntries = await readdir(agentBase, { withFileTypes: true });
1975
+ const agentEntries = await readdir$1(agentBase, { withFileTypes: true });
1959
1976
  for (const agentEntry of agentEntries) {
1960
1977
  const candidateDir = join(agentBase, agentEntry.name);
1961
1978
  if (!await isDirEntryOrSymlinkToDir(agentEntry, candidateDir)) continue;
@@ -3111,13 +3128,14 @@ async function selectAgentsInteractive(options) {
3111
3128
  hint: options.global ? agents[a].globalSkillsDir : agents[a].skillsDir
3112
3129
  }));
3113
3130
  let lastSelected = [];
3114
- try {
3131
+ if (options.useMemory !== false) try {
3115
3132
  lastSelected = await getLastSelectedAgents() || [];
3116
3133
  } catch {}
3134
+ const baseSelection = lastSelected.length > 0 ? lastSelected : options.preselectDetected === false ? [] : installedAgents;
3117
3135
  const selected = await searchMultiselect({
3118
3136
  message: "请选择要安装到的 Agent:",
3119
3137
  items: otherChoices,
3120
- initialSelected: [.../* @__PURE__ */ new Set([...installedAgents, ...lastSelected])].filter((a) => otherAgents.includes(a) && !universalAgents.includes(a)),
3138
+ initialSelected: [...new Set(baseSelection)].filter((a) => otherAgents.includes(a) && !universalAgents.includes(a)),
3121
3139
  lockedSection: universalSection
3122
3140
  });
3123
3141
  if (!isCancelled(selected)) try {
@@ -3600,10 +3618,10 @@ async function runAdd(args, options = {}) {
3600
3618
  else if (!process.stdin.isTTY) showInstallTip();
3601
3619
  let tempDir = null;
3602
3620
  try {
3603
- const spinner$3 = spinner();
3604
- spinner$3.start("正在解析源...");
3621
+ const spinner$4 = spinner();
3622
+ spinner$4.start("正在解析源...");
3605
3623
  const parsed = parseSource(source);
3606
- spinner$3.stop(`源:${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
3624
+ spinner$4.stop(`源:${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
3607
3625
  const ownerRepoRaw = getOwnerRepo(parsed);
3608
3626
  const repoPrivacyPromise = (() => {
3609
3627
  if (!ownerRepoRaw) return Promise.resolve(null);
@@ -3612,7 +3630,7 @@ async function runAdd(args, options = {}) {
3612
3630
  return isRepoPrivate(ownerRepo.owner, ownerRepo.repo).catch(() => null);
3613
3631
  })();
3614
3632
  if (parsed.type === "well-known") {
3615
- await handleWellKnownSkills(source, parsed.url, options, spinner$3);
3633
+ await handleWellKnownSkills(source, parsed.url, options, spinner$4);
3616
3634
  return;
3617
3635
  }
3618
3636
  if (parsed.skillFilter) {
@@ -3623,18 +3641,18 @@ async function runAdd(args, options = {}) {
3623
3641
  let skills;
3624
3642
  let blobResult = null;
3625
3643
  if (parsed.type === "skillhub") {
3626
- const result = await installFromSkillHub(parsed, options, spinner$3);
3644
+ const result = await installFromSkillHub(parsed, options, spinner$4);
3627
3645
  skills = result.skills;
3628
3646
  tempDir = result.tempDir;
3629
3647
  } else if (parsed.type === "local") {
3630
- spinner$3.start("正在验证本地路径...");
3648
+ spinner$4.start("正在验证本地路径...");
3631
3649
  if (!existsSync(parsed.localPath)) {
3632
- spinner$3.stop(import_picocolors.default.red("路径未找到"));
3650
+ spinner$4.stop(import_picocolors.default.red("路径未找到"));
3633
3651
  outro(import_picocolors.default.red(`本地路径不存在:${parsed.localPath}`));
3634
3652
  process.exit(1);
3635
3653
  }
3636
- spinner$3.stop("本地路径验证通过");
3637
- spinner$3.start("正在发现技能...");
3654
+ spinner$4.stop("本地路径验证通过");
3655
+ spinner$4.start("正在发现技能...");
3638
3656
  skills = await discoverSkills(parsed.localPath, parsed.subpath, {
3639
3657
  includeInternal,
3640
3658
  fullDepth: options.fullDepth
@@ -3649,7 +3667,7 @@ async function runAdd(args, options = {}) {
3649
3667
  const owner = ownerRepo?.split("/")[0]?.toLowerCase();
3650
3668
  const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
3651
3669
  if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
3652
- spinner$3.start("正在获取技能...");
3670
+ spinner$4.start("正在获取技能...");
3653
3671
  blobResult = await tryBlobInstall(ownerRepo, {
3654
3672
  subpath: parsed.subpath,
3655
3673
  skillFilter: parsed.skillFilter,
@@ -3657,36 +3675,36 @@ async function runAdd(args, options = {}) {
3657
3675
  getToken: getGitHubToken,
3658
3676
  includeInternal
3659
3677
  });
3660
- if (!blobResult) spinner$3.stop(import_picocolors.default.dim("Falling back to clone..."));
3678
+ if (!blobResult) spinner$4.stop(import_picocolors.default.dim("Falling back to clone..."));
3661
3679
  }
3662
3680
  if (blobResult) {
3663
3681
  skills = blobResult.skills;
3664
- spinner$3.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3682
+ spinner$4.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3665
3683
  } else {
3666
- tempDir = await cloneWithGitHubFallback(parsed.url, options.branch || parsed.ref, spinner$3);
3667
- spinner$3.stop("仓库克隆完成");
3668
- spinner$3.start("正在发现技能...");
3684
+ tempDir = await cloneWithGitHubFallback(parsed.url, options.branch || parsed.ref, spinner$4);
3685
+ spinner$4.stop("仓库克隆完成");
3686
+ spinner$4.start("正在发现技能...");
3669
3687
  skills = await discoverSkills(tempDir, parsed.subpath, {
3670
3688
  includeInternal,
3671
3689
  fullDepth: options.fullDepth
3672
3690
  });
3673
3691
  }
3674
3692
  } else {
3675
- tempDir = await cloneWithGitHubFallback(parsed.url, options.branch || parsed.ref, spinner$3);
3676
- spinner$3.stop("仓库克隆完成");
3677
- spinner$3.start("正在发现技能...");
3693
+ tempDir = await cloneWithGitHubFallback(parsed.url, options.branch || parsed.ref, spinner$4);
3694
+ spinner$4.stop("仓库克隆完成");
3695
+ spinner$4.start("正在发现技能...");
3678
3696
  skills = await discoverSkills(tempDir, parsed.subpath, {
3679
3697
  includeInternal,
3680
3698
  fullDepth: options.fullDepth
3681
3699
  });
3682
3700
  }
3683
3701
  if (skills.length === 0) {
3684
- spinner$3.stop(import_picocolors.default.red("未找到技能"));
3702
+ spinner$4.stop(import_picocolors.default.red("未找到技能"));
3685
3703
  outro(import_picocolors.default.red("未找到有效技能。技能需要包含 name 和 description 的 SKILL.md 文件。"));
3686
3704
  await cleanup(tempDir);
3687
3705
  process.exit(1);
3688
3706
  }
3689
- if (!blobResult) spinner$3.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3707
+ if (!blobResult) spinner$4.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3690
3708
  if (options.list) {
3691
3709
  console.log();
3692
3710
  log.step(import_picocolors.default.bold("可用技能"));
@@ -3800,10 +3818,10 @@ async function runAdd(args, options = {}) {
3800
3818
  }
3801
3819
  targetAgents = options.agent;
3802
3820
  } else {
3803
- spinner$3.start("正在加载 Agent...");
3821
+ spinner$4.start("正在加载 Agent...");
3804
3822
  const installedAgents = await detectInstalledAgents();
3805
3823
  const totalAgents = Object.keys(agents).length;
3806
- spinner$3.stop(`${totalAgents} agents`);
3824
+ spinner$4.stop(`${totalAgents} agents`);
3807
3825
  if (installedAgents.includes("eve") && (options.yes || !agentResult.isAgent)) {
3808
3826
  const useEve = options.yes ? true : await confirm({
3809
3827
  message: formatEveInstallPromptMessage(selectedSkills),
@@ -4005,7 +4023,7 @@ async function runAdd(args, options = {}) {
4005
4023
  process.exit(0);
4006
4024
  }
4007
4025
  }
4008
- spinner$3.start("正在安装技能...");
4026
+ spinner$4.start("正在安装技能...");
4009
4027
  const results = [];
4010
4028
  for (const skill of selectedSkills) for (const target of installTargets) {
4011
4029
  const { agent, subagent } = target;
@@ -4042,7 +4060,7 @@ async function runAdd(args, options = {}) {
4042
4060
  ...result
4043
4061
  });
4044
4062
  }
4045
- spinner$3.stop("安装完成");
4063
+ spinner$4.stop("安装完成");
4046
4064
  console.log();
4047
4065
  const successful = results.filter((r) => r.success);
4048
4066
  const failed = results.filter((r) => !r.success);
@@ -4712,12 +4730,12 @@ async function removeCommand(skillNames, options) {
4712
4730
  }
4713
4731
  const isGlobal = options.global ?? false;
4714
4732
  const cwd = process.cwd();
4715
- const spinner$2 = spinner();
4716
- spinner$2.start("正在扫描已安装的技能...");
4733
+ const spinner$3 = spinner();
4734
+ spinner$3.start("正在扫描已安装的技能...");
4717
4735
  const skillNamesSet = /* @__PURE__ */ new Set();
4718
4736
  const scanDir = async (dir) => {
4719
4737
  try {
4720
- const entries = await readdir(dir, { withFileTypes: true });
4738
+ const entries = await readdir$1(dir, { withFileTypes: true });
4721
4739
  for (const entry of entries) if (entry.isDirectory()) skillNamesSet.add(entry.name);
4722
4740
  } catch (err) {
4723
4741
  if (err instanceof Error && err.code !== "ENOENT") log.warn(`Could not scan directory ${dir}: ${err.message}`);
@@ -4732,7 +4750,7 @@ async function removeCommand(skillNames, options) {
4732
4750
  for (const subagent of getEveSubagents(cwd)) await scanDir(getEveSubagentSkillsDir(subagent, cwd));
4733
4751
  }
4734
4752
  const installedSkills = Array.from(skillNamesSet).sort();
4735
- spinner$2.stop(`发现 ${installedSkills.length} 个已安装技能`);
4753
+ spinner$3.stop(`发现 ${installedSkills.length} 个已安装技能`);
4736
4754
  if (installedSkills.length === 0) {
4737
4755
  outro(import_picocolors.default.yellow("没有可卸载的技能。"));
4738
4756
  return { removed: [] };
@@ -4774,7 +4792,7 @@ async function removeCommand(skillNames, options) {
4774
4792
  if (options.agent && options.agent.length > 0) targetAgents = options.agent;
4775
4793
  else {
4776
4794
  targetAgents = Object.keys(agents);
4777
- spinner$2.stop(`Targeting ${targetAgents.length} potential agent(s)`);
4795
+ spinner$3.stop(`Targeting ${targetAgents.length} potential agent(s)`);
4778
4796
  }
4779
4797
  const { assertLegacyUnmanaged } = await import("./_chunks/ownership.mjs");
4780
4798
  await assertLegacyUnmanaged(selectedSkills.flatMap((skill) => [getCanonicalPath(skill, {
@@ -4795,7 +4813,7 @@ async function removeCommand(skillNames, options) {
4795
4813
  process.exit(0);
4796
4814
  }
4797
4815
  }
4798
- spinner$2.start("正在卸载技能...");
4816
+ spinner$3.start("正在卸载技能...");
4799
4817
  const results = [];
4800
4818
  for (const skillName of selectedSkills) try {
4801
4819
  const canonicalPath = getCanonicalPath(skillName, {
@@ -4818,7 +4836,7 @@ async function removeCommand(skillNames, options) {
4818
4836
  for (const pathToCleanup of pathsToCleanup) {
4819
4837
  if (pathToCleanup === canonicalPath) continue;
4820
4838
  try {
4821
- if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
4839
+ if (await lstat$1(pathToCleanup).catch(() => null)) await rm$1(pathToCleanup, {
4822
4840
  recursive: true,
4823
4841
  force: true
4824
4842
  });
@@ -4835,12 +4853,12 @@ async function removeCommand(skillNames, options) {
4835
4853
  global: isGlobal,
4836
4854
  cwd
4837
4855
  });
4838
- if (await lstat(path).catch(() => null)) {
4856
+ if (await lstat$1(path).catch(() => null)) {
4839
4857
  isStillUsed = true;
4840
4858
  break;
4841
4859
  }
4842
4860
  }
4843
- if (!isStillUsed) await rm(canonicalPath, {
4861
+ if (!isStillUsed) await rm$1(canonicalPath, {
4844
4862
  recursive: true,
4845
4863
  force: true
4846
4864
  });
@@ -4861,7 +4879,7 @@ async function removeCommand(skillNames, options) {
4861
4879
  error: err instanceof Error ? err.message : String(err)
4862
4880
  });
4863
4881
  }
4864
- spinner$2.stop("卸载完成");
4882
+ spinner$3.stop("卸载完成");
4865
4883
  const successful = results.filter((r) => r.success);
4866
4884
  const failed = results.filter((r) => !r.success);
4867
4885
  if (successful.length > 0) {
@@ -5583,14 +5601,14 @@ async function materializeUseSkill(skill) {
5583
5601
  const tempRoot = await mkdtemp(join(tmpdir(), "skills-use-"));
5584
5602
  const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
5585
5603
  if (!isPathSafe(tempRoot, skillDir)) throw new Error("Invalid skill name: potential path traversal detected");
5586
- await mkdir(skillDir, { recursive: true });
5604
+ await mkdir$1(skillDir, { recursive: true });
5587
5605
  if (skill.kind === "blob") await writeSnapshotFiles(skillDir, skill.files);
5588
5606
  else if (skill.kind === "well-known") await writeMapFiles(skillDir, skill.files);
5589
5607
  else await copySkillDirectory(skill.path, skillDir);
5590
5608
  return {
5591
5609
  tempRoot,
5592
5610
  skillDir,
5593
- skillMd: skill.rawContent ?? await readFile(join(skillDir, "SKILL.md"), "utf-8"),
5611
+ skillMd: skill.rawContent ?? await readFile$1(join(skillDir, "SKILL.md"), "utf-8"),
5594
5612
  hasSupportingFiles: await containsSupportingFiles(skillDir, skillDir)
5595
5613
  };
5596
5614
  }
@@ -5806,13 +5824,13 @@ async function writeMapFiles(targetDir, files) {
5806
5824
  async function writeSafeFile(targetDir, filePath, contents) {
5807
5825
  const fullPath = join(targetDir, filePath);
5808
5826
  if (!isPathSafe(targetDir, fullPath)) return;
5809
- await mkdir(dirname(fullPath), { recursive: true });
5810
- if (typeof contents === "string") await writeFile(fullPath, contents, "utf-8");
5811
- else await writeFile(fullPath, contents);
5827
+ await mkdir$1(dirname(fullPath), { recursive: true });
5828
+ if (typeof contents === "string") await writeFile$1(fullPath, contents, "utf-8");
5829
+ else await writeFile$1(fullPath, contents);
5812
5830
  }
5813
5831
  async function copySkillDirectory(src, dest) {
5814
- await mkdir(dest, { recursive: true });
5815
- const entries = await readdir(src, { withFileTypes: true });
5832
+ await mkdir$1(dest, { recursive: true });
5833
+ const entries = await readdir$1(src, { withFileTypes: true });
5816
5834
  await Promise.all(entries.filter((entry) => !isExcluded(entry.name, entry.isDirectory())).map(async (entry) => {
5817
5835
  const srcPath = join(src, entry.name);
5818
5836
  const destPath = join(dest, entry.name);
@@ -5822,7 +5840,7 @@ async function copySkillDirectory(src, dest) {
5822
5840
  return;
5823
5841
  }
5824
5842
  try {
5825
- await cp(srcPath, destPath, {
5843
+ await cp$1(srcPath, destPath, {
5826
5844
  dereference: true,
5827
5845
  recursive: true
5828
5846
  });
@@ -5836,7 +5854,7 @@ async function copySkillDirectory(src, dest) {
5836
5854
  }));
5837
5855
  }
5838
5856
  async function containsSupportingFiles(rootDir, currentDir) {
5839
- const entries = await readdir(currentDir, { withFileTypes: true });
5857
+ const entries = await readdir$1(currentDir, { withFileTypes: true });
5840
5858
  for (const entry of entries) {
5841
5859
  const entryPath = join(currentDir, entry.name);
5842
5860
  const relPath = relative(rootDir, entryPath).split(sep).join("/");
@@ -6966,6 +6984,517 @@ function maybePrintAnalyticsNotice(env = process.env) {
6966
6984
  console.log();
6967
6985
  } catch {}
6968
6986
  }
6987
+ function checkNode(node) {
6988
+ if (node.type === "object") {
6989
+ const keys = (node.children ?? []).map((child) => String(child.children?.[0]?.value));
6990
+ if (keys.length !== new Set(keys).size || keys.some((key) => [
6991
+ "__proto__",
6992
+ "prototype",
6993
+ "constructor"
6994
+ ].includes(key))) throw new Error("配置包含重复键或不安全键,拒绝修改。");
6995
+ }
6996
+ node.children?.forEach(checkNode);
6997
+ }
6998
+ function readDocument(text, format) {
6999
+ if (!text.trim()) return {};
7000
+ try {
7001
+ if (format === "toml") return object(parse$1(text));
7002
+ const errors = [];
7003
+ const tree = parseTree(text, errors, {
7004
+ allowTrailingComma: true,
7005
+ disallowComments: false
7006
+ });
7007
+ if (!tree || errors.length) throw new Error();
7008
+ checkNode(tree);
7009
+ return object(JSON.parse(JSON.stringify(getNodeValue(tree))));
7010
+ } catch {
7011
+ throw new Error("现有配置解析失败,未修改任何内容。请先修复配置格式或重复键。");
7012
+ }
7013
+ }
7014
+ function serversIn(document, path) {
7015
+ let current = document;
7016
+ for (const key of path) {
7017
+ if (current[key] === void 0) return {};
7018
+ current = object(current[key], "MCP 配置节点");
7019
+ }
7020
+ return current;
7021
+ }
7022
+ function tablePath(line) {
7023
+ if (!/^\s*\[[^[]/.test(line)) return;
7024
+ try {
7025
+ let value = parse$1(`${line}\n__bfi_table_marker__ = true\n`);
7026
+ const path = [];
7027
+ while (value && typeof value === "object") {
7028
+ const entries = Object.entries(value);
7029
+ if (entries.length !== 1) return;
7030
+ const [key, next] = entries[0];
7031
+ if (key === "__bfi_table_marker__") return path;
7032
+ path.push(key);
7033
+ value = next;
7034
+ }
7035
+ } catch {
7036
+ return;
7037
+ }
7038
+ }
7039
+ function editToml(text, replacements) {
7040
+ const lines = text.split(/(?<=\n)/);
7041
+ const starts = [];
7042
+ let offset = 0;
7043
+ for (const line of lines) {
7044
+ if (/^\s*\[/.test(line)) starts.push({
7045
+ offset,
7046
+ path: tablePath(line.trim())
7047
+ });
7048
+ offset += line.length;
7049
+ }
7050
+ const existing = serversIn(readDocument(text, "toml"), ["mcp_servers"]);
7051
+ const ranges = [];
7052
+ for (const name of Object.keys(replacements)) {
7053
+ const matches = starts.flatMap((start, index) => start.path?.[0] === "mcp_servers" && start.path[1] === name ? [[start.offset, starts[index + 1]?.offset ?? text.length]] : []);
7054
+ if (existing[name] !== void 0 && !matches.length) throw new Error("目标 MCP 使用内联或复杂 TOML 定义,无法安全增量更新;请先改为独立 [mcp_servers.\"name\"] 表。");
7055
+ ranges.push(...matches);
7056
+ }
7057
+ let result = text;
7058
+ for (const [start, end] of ranges.sort((left, right) => right[0] - left[0])) result = result.slice(0, start) + result.slice(end);
7059
+ for (const [name, config] of Object.entries(replacements)) if (config !== void 0) result += `${result.endsWith("\n") || !result ? "" : "\n"}\n${stringify({ mcp_servers: { [name]: config } }).replace(/^\[mcp_servers\]\r?\n/m, "")}`;
7060
+ if (!readDocument(result, "toml").mcp_servers) result += "\n[mcp_servers]\n";
7061
+ return result;
7062
+ }
7063
+ function editDocument(text, format, path, replacements) {
7064
+ serversIn(readDocument(text, format), path);
7065
+ const expected = readDocument(text, format);
7066
+ let parent = expected;
7067
+ for (const key of path) parent = object(parent[key] ?? (parent[key] = {}));
7068
+ for (const [name, value] of Object.entries(replacements)) if (value === void 0) delete parent[name];
7069
+ else parent[name] = value;
7070
+ let result = text.trim() ? text : format === "jsonc" ? "{}\n" : "";
7071
+ if (format === "toml") result = editToml(result, replacements);
7072
+ else for (const [name, value] of Object.entries(replacements)) result = applyEdits(result, modify(result, [...path, name], value, { formattingOptions: {
7073
+ insertSpaces: true,
7074
+ tabSize: 2,
7075
+ eol: text.includes("\r\n") ? "\r\n" : "\n"
7076
+ } }));
7077
+ if (!isDeepStrictEqual(readDocument(result, format), expected)) throw new Error("配置保留校验失败,未写入;请使用客户端原生配置处理复杂结构。");
7078
+ return result;
7079
+ }
7080
+ const agentIds = [
7081
+ "claude-code",
7082
+ "codex",
7083
+ "cursor",
7084
+ "qwen-code",
7085
+ "gemini-cli",
7086
+ "zcode",
7087
+ "codebuddy",
7088
+ "kimi-code",
7089
+ "opencode"
7090
+ ];
7091
+ const aliases = {
7092
+ claude: "claude-code",
7093
+ qwen: "qwen-code",
7094
+ gemini: "gemini-cli",
7095
+ kimi: "kimi-code",
7096
+ "kimi-cli": "kimi-code",
7097
+ "codebuddy-code": "codebuddy"
7098
+ };
7099
+ function preferred(paths) {
7100
+ return paths.find((filename) => existsSync$1(filename)) ?? paths[0];
7101
+ }
7102
+ function targetFor(name, scope = "user", options = {}) {
7103
+ const agent = aliases[name] ?? name;
7104
+ if (!agentIds.includes(agent)) throw new Error(`尚未支持该 Agent 的自动配置。支持:${agentIds.join(", ")}。WorkBuddy/IDE 专有配置请使用原生导入,不猜测路径。`);
7105
+ const home = options.home ?? homedir$1();
7106
+ const cwd = options.cwd ?? process.cwd();
7107
+ const env = options.env ?? process.env;
7108
+ const user = scope === "user";
7109
+ const paths = {
7110
+ "claude-code": [user ? join$1(home, ".claude.json") : join$1(cwd, ".mcp.json")],
7111
+ codex: [user ? join$1(env.CODEX_HOME ?? join$1(home, ".codex"), "config.toml") : join$1(cwd, ".codex/config.toml")],
7112
+ cursor: [join$1(user ? home : cwd, ".cursor/mcp.json")],
7113
+ "qwen-code": [join$1(user ? home : cwd, ".qwen/settings.json")],
7114
+ "gemini-cli": [join$1(user ? home : cwd, ".gemini/settings.json")],
7115
+ zcode: [join$1(user ? home : cwd, user ? ".zcode/cli/config.json" : ".zcode/config.json")],
7116
+ codebuddy: user ? [
7117
+ join$1(home, ".codebuddy/.mcp.json"),
7118
+ join$1(home, ".codebuddy/mcp.json"),
7119
+ join$1(home, ".codebuddy.json")
7120
+ ] : [join$1(cwd, ".mcp.json"), join$1(cwd, "mcp.json")],
7121
+ "kimi-code": [join$1(user ? env.KIMI_CODE_HOME ?? join$1(home, ".kimi-code") : join$1(cwd, ".kimi-code"), "mcp.json")],
7122
+ opencode: [join$1(user ? env.XDG_CONFIG_HOME ? join$1(env.XDG_CONFIG_HOME, "opencode") : join$1(home, ".config/opencode") : cwd, "opencode.jsonc"), join$1(user ? env.XDG_CONFIG_HOME ? join$1(env.XDG_CONFIG_HOME, "opencode") : join$1(home, ".config/opencode") : cwd, "opencode.json")]
7123
+ };
7124
+ if (agent === "claude-code" && env.CLAUDE_CONFIG_DIR && !options.config) throw new Error("设置了 CLAUDE_CONFIG_DIR;请用 --config 明确 MCP 文件,避免改错作用域。");
7125
+ if (agent === "kimi-code" && user && !env.KIMI_CODE_HOME && !existsSync$1(join$1(home, ".kimi-code")) && existsSync$1(join$1(home, ".kimi/mcp.json")) && !options.config) throw new Error("检测到旧版 Kimi 配置,请核对版本后用 --config 明确文件;本适配器面向 .kimi-code 布局。");
7126
+ return {
7127
+ agent,
7128
+ scope,
7129
+ path: resolve$1(options.config ?? preferred(paths[agent])),
7130
+ format: agent === "codex" ? "toml" : "jsonc",
7131
+ key: agent === "codex" ? ["mcp_servers"] : agent === "opencode" ? ["mcp"] : agent === "zcode" ? ["mcp", "servers"] : ["mcpServers"]
7132
+ };
7133
+ }
7134
+ function hasLiteralSecrets(config) {
7135
+ const literal = (value) => !/^Bearer \$\{(?:user_config\.)?[A-Za-z_][A-Za-z0-9_]*\}$/i.test(value) && Boolean(value.replace(variablePattern, (_whole, _name, fallback) => fallback ?? "").trim());
7136
+ const sensitive = /token|secret|password|api.?key|authorization|credential|private.?key|x-pm-(ak|sk)/i;
7137
+ if (Object.entries(config.headers ?? {}).some(([key, value]) => !/^(accept|content-type|user-agent|x-pm-env|x-pm-product-line)$/i.test(key) && literal(value))) return true;
7138
+ if (Object.entries(config.env ?? {}).some(([key, value]) => sensitive.test(key) && literal(value))) return true;
7139
+ if ((config.args ?? []).some((value, index, args) => /^--?(token|password|secret|api-key)(=|$)/i.test(value) && literal(value.includes("=") ? value.split("=").slice(1).join("=") : args[index + 1] ?? ""))) return true;
7140
+ if (config.url) {
7141
+ if ([...new URL(config.url.replace(variablePattern, "__BFI_VARIABLE__")).searchParams].some(([key, value]) => sensitive.test(key) && value.replaceAll("__BFI_VARIABLE__", ""))) return true;
7142
+ }
7143
+ return false;
7144
+ }
7145
+ function encodeServer(config, target, bindings = {}, materialize = false, environment = process.env) {
7146
+ if (config.cwd && ![
7147
+ "codex",
7148
+ "qwen-code",
7149
+ "gemini-cli",
7150
+ "kimi-code"
7151
+ ].includes(target.agent)) throw new Error("此宿主的 MCP cwd 字段未支持或未确证;请提供不依赖 cwd 的启动配置。");
7152
+ if (Object.values(bindings).some((name) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) throw new Error("环境变量绑定名称无效。");
7153
+ if (materialize && target.scope !== "user") throw new Error("凭据物化仅允许用户级配置,项目级配置只能保存引用。");
7154
+ if (hasLiteralSecrets(config) && !materialize) throw new Error("源配置可能含明文凭据;请改为环境变量占位符,或显式选择用户级 --materialize-inputs。");
7155
+ let value = materialize ? resolveVariables(config, bindings, environment) : config;
7156
+ if (variables(value).length && ["zcode", "kimi-code"].includes(target.agent)) throw new Error("该宿主的通用变量插值未确证。请使用用户级 --materialize-inputs,或原生秘密配置;不会保存无效占位符。");
7157
+ if (!materialize && target.agent !== "codex") value = mapStrings(value, (text) => text.replace(variablePattern, (_whole, key, fallback) => {
7158
+ const name = bindings[key] ?? key;
7159
+ if (fallback !== void 0 && !["claude-code", "codebuddy"].includes(target.agent)) throw new Error("此宿主不支持带默认值的变量引用。");
7160
+ if (target.agent === "cursor") return `\${env:${name}}`;
7161
+ if (target.agent === "opencode") return `{env:${name}}`;
7162
+ return `\${${name}${fallback === void 0 ? "" : `:-${fallback}`}}`;
7163
+ }));
7164
+ const { type, ...rest } = value;
7165
+ if (target.agent === "codex") {
7166
+ if (type === "sse") throw new Error("Codex 不支持旧 SSE 配置;请提供 Streamable HTTP 服务。");
7167
+ const result = {};
7168
+ if (type === "stdio") {
7169
+ result.command = value.command;
7170
+ if (value.args) result.args = value.args;
7171
+ if (value.cwd) result.cwd = value.cwd;
7172
+ const env = {};
7173
+ const inherited = [];
7174
+ for (const [name, text] of Object.entries(value.env ?? {})) {
7175
+ const match = /^\$\{(?:user_config\.)?([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(text);
7176
+ if (match && (bindings[match[1]] ?? match[1]) === name) inherited.push(name);
7177
+ else if (text.includes("${")) throw new Error("Codex env_vars 不能重命名或拼接变量;请使用同名环境变量或用户级物化。");
7178
+ else env[name] = text;
7179
+ }
7180
+ if (Object.keys(env).length) result.env = env;
7181
+ if (inherited.length) result.env_vars = inherited;
7182
+ if ([
7183
+ value.command,
7184
+ value.cwd,
7185
+ ...value.args ?? []
7186
+ ].some((item) => item?.includes("${"))) throw new Error("Codex command/args/cwd 不支持变量插值。");
7187
+ } else {
7188
+ result.url = value.url;
7189
+ if (value.url?.includes("${")) throw new Error("Codex URL 不支持变量插值。");
7190
+ const literal = {};
7191
+ const inherited = {};
7192
+ for (const [header, text] of Object.entries(value.headers ?? {})) {
7193
+ const match = /^\$\{(?:user_config\.)?([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(text);
7194
+ const bearer = /^Bearer \$\{(?:user_config\.)?([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(text);
7195
+ if (match) inherited[header] = bindings[match[1]] ?? match[1];
7196
+ else if (header.toLowerCase() === "authorization" && bearer) result.bearer_token_env_var = bindings[bearer[1]] ?? bearer[1];
7197
+ else if (text.includes("${")) throw new Error("Codex header 只支持整值环境引用或 Bearer 引用,不支持拼接。");
7198
+ else literal[header] = text;
7199
+ }
7200
+ if (Object.keys(literal).length) result.http_headers = literal;
7201
+ if (Object.keys(inherited).length) result.env_http_headers = inherited;
7202
+ }
7203
+ return result;
7204
+ }
7205
+ if (target.agent === "opencode") {
7206
+ if (value.cwd) throw new Error("OpenCode MCP 无独立 cwd 字段;请提供绝对启动路径。");
7207
+ return type === "stdio" ? {
7208
+ type: "local",
7209
+ command: [value.command, ...value.args ?? []],
7210
+ ...value.env ? { environment: value.env } : {}
7211
+ } : {
7212
+ type: "remote",
7213
+ url: value.url,
7214
+ ...value.headers ? { headers: value.headers } : {}
7215
+ };
7216
+ }
7217
+ if (["qwen-code", "gemini-cli"].includes(target.agent)) {
7218
+ const { url, ...fields } = rest;
7219
+ return {
7220
+ ...fields,
7221
+ ...url ? { [type === "http" ? "httpUrl" : "url"]: url } : {}
7222
+ };
7223
+ }
7224
+ if (target.agent === "cursor") return rest;
7225
+ if (target.agent === "kimi-code") return {
7226
+ ...rest,
7227
+ ...type === "sse" ? { transport: "sse" } : {}
7228
+ };
7229
+ return {
7230
+ type,
7231
+ ...rest
7232
+ };
7233
+ }
7234
+ const managedKeys = /* @__PURE__ */ new Set([
7235
+ "type",
7236
+ "transport",
7237
+ "command",
7238
+ "args",
7239
+ "env",
7240
+ "environment",
7241
+ "cwd",
7242
+ "url",
7243
+ "httpUrl",
7244
+ "headers",
7245
+ "http_headers",
7246
+ "env_http_headers",
7247
+ "env_vars",
7248
+ "bearer_token_env_var"
7249
+ ]);
7250
+ function mergeServer(existing, desired) {
7251
+ if (existing === void 0) return desired;
7252
+ const previous = object(existing, "已有 MCP");
7253
+ const extras = Object.fromEntries(Object.entries(previous).filter(([key]) => !managedKeys.has(key)));
7254
+ if ([
7255
+ "oauth",
7256
+ "auth",
7257
+ "headersHelper",
7258
+ "http_headers_helper",
7259
+ "bearerTokenEnvVar",
7260
+ "bearer_token"
7261
+ ].some((key) => extras[key] !== void 0) && [
7262
+ "url",
7263
+ "httpUrl",
7264
+ "command",
7265
+ "type",
7266
+ "transport"
7267
+ ].some((key) => previous[key] !== desired[key])) throw new Error("已有服务器包含宿主专有认证设置,不能安全更换启动目标;请在客户端中处理。");
7268
+ return {
7269
+ ...extras,
7270
+ ...desired
7271
+ };
7272
+ }
7273
+ function decodeServer(raw, target) {
7274
+ const source = object(raw, "MCP");
7275
+ if (source.cwd && ![
7276
+ "codex",
7277
+ "qwen-code",
7278
+ "gemini-cli",
7279
+ "kimi-code"
7280
+ ].includes(target.agent)) throw new Error("此宿主 cwd 语义未确证,请使用客户端原生测试。");
7281
+ if (source.enabled === false || source.disabled === true) throw new Error("此 MCP 已在客户端停用;测试不会绕过停用设置。");
7282
+ if ([
7283
+ "oauth",
7284
+ "auth",
7285
+ "headersHelper",
7286
+ "http_headers_helper",
7287
+ "bearer_token",
7288
+ "bearerTokenEnvVar"
7289
+ ].some((key) => source[key] !== void 0)) throw new Error("此服务器使用宿主专有认证;请用客户端原生连接测试。");
7290
+ let result = Object.fromEntries(Object.entries(source).filter(([key]) => managedKeys.has(key)));
7291
+ if (target.agent === "codex") {
7292
+ const { env_vars, env_http_headers, http_headers, bearer_token_env_var, ...rest } = result;
7293
+ result = rest;
7294
+ if (source.command) {
7295
+ const env = { ...object(source.env ?? {}) };
7296
+ if (env_vars !== void 0 && (!Array.isArray(env_vars) || !env_vars.every((item) => typeof item === "string"))) throw new Error("不支持的 Codex env_vars 结构。");
7297
+ for (const name of env_vars ?? []) env[name] = `\${${name}}`;
7298
+ if (Object.keys(env).length) result.env = env;
7299
+ } else {
7300
+ const headers = { ...object(http_headers ?? {}) };
7301
+ for (const [name, variable] of Object.entries(object(env_http_headers ?? {}))) {
7302
+ if (typeof variable !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable)) throw new Error("无效环境变量引用。");
7303
+ headers[name] = `\${${variable}}`;
7304
+ }
7305
+ if (typeof bearer_token_env_var === "string") headers.Authorization = `Bearer \${${bearer_token_env_var}}`;
7306
+ if (Object.keys(headers).length) result.headers = headers;
7307
+ }
7308
+ } else if (target.agent === "opencode") {
7309
+ if (source.type === "local") {
7310
+ if (!Array.isArray(source.command) || !source.command.length) throw new Error("OpenCode command 必须为非空数组。");
7311
+ result = {
7312
+ type: "stdio",
7313
+ command: source.command[0],
7314
+ args: source.command.slice(1),
7315
+ ...source.environment ? { env: source.environment } : {}
7316
+ };
7317
+ } else result = {
7318
+ type: "http",
7319
+ url: source.url,
7320
+ ...source.headers ? { headers: source.headers } : {}
7321
+ };
7322
+ result = mapStrings(result, (text) => text.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, "${$1}"));
7323
+ } else if (["qwen-code", "gemini-cli"].includes(target.agent)) {
7324
+ const { httpUrl, ...rest } = result;
7325
+ result = {
7326
+ ...rest,
7327
+ ...httpUrl ? {
7328
+ type: "http",
7329
+ url: httpUrl
7330
+ } : source.url ? { type: "sse" } : {}
7331
+ };
7332
+ } else if (target.agent === "cursor") result = mapStrings(result, (text) => text.replace(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, "${$1}"));
7333
+ return normalizeConfig(result);
7334
+ }
7335
+ async function checkPath(filename) {
7336
+ let current = resolve$1(filename);
7337
+ while (current !== parse(current).root) {
7338
+ try {
7339
+ const stat = await lstat(current);
7340
+ if (stat.isSymbolicLink() || current === resolve$1(filename) && (!stat.isFile() || stat.nlink > 1)) throw new Error("配置路径含符号链接、特殊文件或硬链接,拒绝写入。");
7341
+ } catch (error) {
7342
+ if (error.code !== "ENOENT") throw error;
7343
+ }
7344
+ current = dirname$1(current);
7345
+ }
7346
+ }
7347
+ async function readConfig(filename) {
7348
+ await checkPath(filename);
7349
+ try {
7350
+ const handle = await open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
7351
+ try {
7352
+ const stat = await handle.stat();
7353
+ if (!stat.isFile() || stat.size > 2097152) throw new Error("配置文件不是普通文件或超过 2 MiB。");
7354
+ return await handle.readFile("utf8");
7355
+ } finally {
7356
+ await handle.close();
7357
+ }
7358
+ } catch (error) {
7359
+ if (error.code === "ENOENT") return;
7360
+ throw error;
7361
+ }
7362
+ }
7363
+ async function identity(filename) {
7364
+ try {
7365
+ const stat = await lstat(filename, { bigint: true });
7366
+ return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.mtimeNs}:${stat.size}`;
7367
+ } catch (error) {
7368
+ if (error.code === "ENOENT") return;
7369
+ throw error;
7370
+ }
7371
+ }
7372
+ async function writeConfig(filename, previous, next, expectedIdentity) {
7373
+ if (previous === next) return;
7374
+ await checkPath(filename);
7375
+ const originalIdentity = expectedIdentity ?? await identity(filename);
7376
+ if (previous !== void 0 && ((await lstat(filename)).mode & 128) === 0) throw new Error("配置文件只读,不放宽其权限。");
7377
+ await mkdir(dirname$1(filename), {
7378
+ recursive: true,
7379
+ mode: 448
7380
+ });
7381
+ const lockPath = `${filename}.bfi-mcp.lock`;
7382
+ const temporary = `${filename}.bfi-mcp-${randomUUID()}.tmp`;
7383
+ const backup = previous === void 0 ? void 0 : `${filename}.bfi-mcp-${Date.now()}-${randomUUID()}.bak`;
7384
+ let lock;
7385
+ try {
7386
+ lock = await open(lockPath, "wx", 384);
7387
+ } catch {
7388
+ throw new Error("无法取得配置写入锁。可能有其它安装正在进行;请核对残留 .bfi-mcp.lock 后重试。");
7389
+ }
7390
+ try {
7391
+ await checkPath(filename);
7392
+ if (await readConfig(filename) !== previous || await identity(filename) !== originalIdentity) throw new Error("配置已被其它进程修改,请重新预览并重试。");
7393
+ if (backup) {
7394
+ const handle = await open(backup, "wx", 384);
7395
+ try {
7396
+ await handle.writeFile(previous, "utf8");
7397
+ await handle.sync();
7398
+ } finally {
7399
+ await handle.close();
7400
+ }
7401
+ }
7402
+ const handle = await open(temporary, "wx", 384);
7403
+ try {
7404
+ await handle.writeFile(next, "utf8");
7405
+ await handle.sync();
7406
+ } finally {
7407
+ await handle.close();
7408
+ }
7409
+ await checkPath(filename);
7410
+ if (await readConfig(filename) !== previous || await identity(filename) !== originalIdentity) throw new Error("写入前检测到并发修改,已取消。");
7411
+ await rename(temporary, filename);
7412
+ return backup;
7413
+ } finally {
7414
+ await unlink(temporary).catch(() => {});
7415
+ await lock.close().catch(() => {});
7416
+ await unlink(lockPath).catch(() => {});
7417
+ }
7418
+ }
7419
+ async function loadTarget(target) {
7420
+ const fileIdentity = await identity(target.path);
7421
+ const text = await readConfig(target.path);
7422
+ if (await identity(target.path) !== fileIdentity) throw new Error("读取时配置发生变化,请重试。");
7423
+ return {
7424
+ text,
7425
+ fileIdentity,
7426
+ servers: serversIn(readDocument(text ?? "", target.format), target.key)
7427
+ };
7428
+ }
7429
+ async function planInstall(target, entries, options = {}) {
7430
+ if (!entries.length || entries.length > 100) throw new Error("每次需要选择 1–100 个 MCP。");
7431
+ const names = entries.map((entry) => validName(entry.registrationName));
7432
+ if (new Set(names).size !== names.length) throw new Error("安装列表包含重复名称。");
7433
+ const knownVariables = new Set(entries.flatMap((entry) => variables(entry.config)));
7434
+ if (Object.keys(options.bindings ?? {}).some((key) => !knownVariables.has(key))) throw new Error("环境绑定包含未使用的变量名,请检查 --input-env。");
7435
+ for (const [server, bindings] of Object.entries(options.bindingsByServer ?? {})) {
7436
+ const entry = entries.find((candidate) => candidate.registrationName === server);
7437
+ if (!entry || Object.keys(bindings).some((key) => !variables(entry.config).includes(key))) throw new Error("服务器环境绑定包含未使用的名称。");
7438
+ }
7439
+ const { text, fileIdentity, servers } = await loadTarget(target);
7440
+ const replacements = {};
7441
+ const changes = [];
7442
+ for (const entry of entries) {
7443
+ if (entry.runtime?.source === "upload") throw new Error(`MCP ${entry.registrationName} 需要上传运行包。请先下载检查并准备本地启动路径,再用 --file 安装本地配置。`);
7444
+ const name = entry.registrationName;
7445
+ const bindings = {
7446
+ ...options.bindings,
7447
+ ...options.bindingsByServer?.[name]
7448
+ };
7449
+ const desired = encodeServer(entry.config, target, bindings, options.materialize, options.environment);
7450
+ const merged = mergeServer(servers[name], desired);
7451
+ const action = servers[name] === void 0 ? "add" : isDeepStrictEqual(servers[name], merged) ? "unchanged" : "replace";
7452
+ if (action === "replace" && !options.replace) throw new Error(`同名 MCP ${name} 配置不同;未修改任何服务器。确认要替换其连接配置时使用 --replace。`);
7453
+ if (action !== "unchanged") replacements[name] = merged;
7454
+ changes.push({
7455
+ name,
7456
+ action,
7457
+ transport: entry.config.type,
7458
+ variables: variables(entry.config).map((key) => bindings[key] ?? key)
7459
+ });
7460
+ }
7461
+ return {
7462
+ target,
7463
+ previous: text,
7464
+ next: Object.keys(replacements).length ? editDocument(text ?? "", target.format, target.key, replacements) : text,
7465
+ fileIdentity,
7466
+ changes
7467
+ };
7468
+ }
7469
+ async function planRemove(target, names) {
7470
+ if (!names.length || names.length > 100) throw new Error("请明确指定 1–100 个要移除的 MCP。");
7471
+ const { text, fileIdentity, servers } = await loadTarget(target);
7472
+ const replacements = {};
7473
+ const changes = [...new Set(names)].map((name) => {
7474
+ validName(name);
7475
+ const action = Object.hasOwn(servers, name) ? "remove" : "absent";
7476
+ if (action === "remove") replacements[name] = void 0;
7477
+ return {
7478
+ name,
7479
+ action
7480
+ };
7481
+ });
7482
+ return {
7483
+ target,
7484
+ previous: text,
7485
+ next: Object.keys(replacements).length ? editDocument(text ?? "", target.format, target.key, replacements) : text,
7486
+ fileIdentity,
7487
+ changes
7488
+ };
7489
+ }
7490
+ async function applyPlan(plan) {
7491
+ if (plan.previous === plan.next || plan.next === void 0) return { changed: false };
7492
+ if (await identity(plan.target.path) !== plan.fileIdentity) throw new Error("配置在预览后发生变化,请重新运行;未写入。");
7493
+ return {
7494
+ changed: true,
7495
+ backup: await writeConfig(plan.target.path, plan.previous, plan.next, plan.fileIdentity)
7496
+ };
7497
+ }
6969
7498
  var package_exports = /* @__PURE__ */ __exportAll({ runPackage: () => runPackage });
6970
7499
  const RESET$3 = "\x1B[0m";
6971
7500
  const BOLD$1 = "\x1B[1m";
@@ -7136,7 +7665,7 @@ function buildTargetSummaryLines(targets, installMode) {
7136
7665
  const subagentNames = targets.filter((t) => t.subagent).map(targetDisplayName);
7137
7666
  const { universal, symlinked } = splitAgentsByType(rootAgents);
7138
7667
  if (installMode === "symlink") {
7139
- if (universal.length > 0) lines.push(` ${import_picocolors.default.green("共享目录:")} ${formatList(universal, 3)}`);
7668
+ if (universal.length > 0) lines.push(` ${import_picocolors.default.green("共享目录(共用 .agents/skills):")} ${formatList(universal, 3)}`);
7140
7669
  if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("符号链接 →")} ${formatList(symlinked)}`);
7141
7670
  if (subagentNames.length > 0) lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList(subagentNames)}`);
7142
7671
  } else {
@@ -7193,8 +7722,47 @@ async function handleInstall(args, api, globalOpts) {
7193
7722
  console.log(` ${DIM$3}• ${skill.displayName || skill.slug}${RESET$3} ${DIM$3}${ref}${RESET$3}`);
7194
7723
  }
7195
7724
  console.log();
7725
+ let mcpNames = [];
7726
+ let mcpEntries = [];
7727
+ try {
7728
+ mcpNames = ((await api.getPackageMcps(namespace, slug))?.mcps ?? []).filter((entry) => entry?.mcpEntryId).map((entry) => entry.lastKnownName).filter((name) => typeof name === "string" && name.length > 0);
7729
+ } catch {}
7730
+ if (mcpNames.length > 0) {
7731
+ try {
7732
+ const { catalogEntries } = await import("./_chunks/registry.mjs");
7733
+ const { registry } = resolveConfig(globalOpts);
7734
+ mcpEntries = await catalogEntries(registry, mcpNames);
7735
+ } catch {}
7736
+ console.log(`${BOLD$1}关联 MCP(${mcpNames.length}):${RESET$3}`);
7737
+ for (const name of mcpNames) {
7738
+ const entry = mcpEntries.find((item) => item.registrationName === name);
7739
+ const keys = entry ? variables(entry.config) : [];
7740
+ console.log(` ${DIM$3}• ${name}${keys.length > 0 ? ` 变量:${keys.join("、")}` : ""}${RESET$3}`);
7741
+ }
7742
+ if (mcpEntries.length === 0) console.log(` ${YELLOW$1}变量信息暂不可用,安装时会读取组合包内容。${RESET$3}`);
7743
+ console.log();
7744
+ }
7745
+ const bindingsByServer = {};
7746
+ const envNames = [];
7747
+ const interactive = !isYes && !!process.stdin.isTTY && !!process.stdout.isTTY;
7748
+ const promptBinding = async (entry, key) => {
7749
+ let value;
7750
+ do {
7751
+ value = await password({ message: `${entry.registrationName}.${key}(输入不回显)` });
7752
+ if (isCancel(value)) {
7753
+ cancel("安装已取消");
7754
+ process.exit(0);
7755
+ }
7756
+ } while (!String(value).trim());
7757
+ const envName = `${entry.registrationName}_${key}`.replace(/[^A-Za-z0-9_]/g, "_").toUpperCase().replace(/^(?=[0-9])/, "_");
7758
+ process.env[envName] = String(value);
7759
+ (bindingsByServer[entry.registrationName] ??= {})[key] = envName;
7760
+ if (!envNames.includes(envName)) envNames.push(envName);
7761
+ };
7762
+ if (interactive) for (const entry of mcpEntries) for (const key of variables(entry.config)) await promptBinding(entry, key);
7196
7763
  let targetAgents;
7197
7764
  let installGlobally;
7765
+ let mcpGloballyOverride;
7198
7766
  let installMode;
7199
7767
  let eveSubagentTargets = [void 0];
7200
7768
  if (requestedAgents) {
@@ -7220,11 +7788,11 @@ async function handleInstall(args, api, globalOpts) {
7220
7788
  console.log(`${DIM$3}范围:${installGlobally ? "全局" : "项目"} | 方式:${installMode === "symlink" ? "符号链接" : "复制"}${RESET$3}`);
7221
7789
  console.log();
7222
7790
  } else {
7223
- const spinner$1 = spinner();
7224
- spinner$1.start("正在加载 Agent...");
7791
+ const spinner$2 = spinner();
7792
+ spinner$2.start("正在加载 Agent...");
7225
7793
  const installedAgents = await detectInstalledAgents();
7226
7794
  const totalAgents = Object.keys(agents).length;
7227
- spinner$1.stop(`${totalAgents} agents`);
7795
+ spinner$2.stop(`${totalAgents} agents`);
7228
7796
  const skillNames = skills.map((s) => s.displayName || s.slug);
7229
7797
  const skillSubject = skillNames.length <= 3 ? skillNames.map((n) => import_picocolors.default.cyan(n)).join(", ") : `${skills.length} 个技能`;
7230
7798
  if (installedAgents.includes("eve")) {
@@ -7240,7 +7808,11 @@ async function handleInstall(args, api, globalOpts) {
7240
7808
  targetAgents = ["eve"];
7241
7809
  console.log(`${DIM$3}安装到:eve agent${RESET$3}`);
7242
7810
  } else {
7243
- const selected = await selectAgentsInteractive({ global: false });
7811
+ const selected = await selectAgentsInteractive({
7812
+ global: false,
7813
+ useMemory: false,
7814
+ preselectDetected: false
7815
+ });
7244
7816
  if (isCancel(selected)) {
7245
7817
  cancel("安装已取消");
7246
7818
  process.exit(0);
@@ -7262,7 +7834,11 @@ async function handleInstall(args, api, globalOpts) {
7262
7834
  const firstAgent = installedAgents[0];
7263
7835
  console.log(`${DIM$3}安装到:${agents[firstAgent].displayName}${RESET$3}`);
7264
7836
  } else {
7265
- const selected = await selectAgentsInteractive({ global: false });
7837
+ const selected = await selectAgentsInteractive({
7838
+ global: false,
7839
+ useMemory: false,
7840
+ preselectDetected: false
7841
+ });
7266
7842
  if (isCancel(selected)) {
7267
7843
  cancel("安装已取消");
7268
7844
  process.exit(0);
@@ -7339,7 +7915,7 @@ async function handleInstall(args, api, globalOpts) {
7339
7915
  const overwriteChecks = await Promise.all(skills.flatMap((skill) => installTargets.map(async (target) => ({
7340
7916
  skillName: skill.slug,
7341
7917
  target,
7342
- installed: await isSkillInstalled(packageSkillName(skill.namespace || namespace, skill.slug), target.agent, {
7918
+ installed: await isSkillInstalled(skill.slug, target.agent, {
7343
7919
  global: installGlobally,
7344
7920
  eveSubagent: target.subagent
7345
7921
  })
@@ -7349,6 +7925,17 @@ async function handleInstall(args, api, globalOpts) {
7349
7925
  if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
7350
7926
  overwriteStatus.get(skillName).set(targetKey(target), installed);
7351
7927
  }
7928
+ if (envNames.length > 0 && !installGlobally) {
7929
+ const choice = await confirm({
7930
+ message: "项目级配置不能保存明文变量。是否将关联 MCP 写入用户级配置并保存输入值?",
7931
+ initialValue: true
7932
+ });
7933
+ if (isCancel(choice)) {
7934
+ cancel("安装已取消");
7935
+ process.exit(0);
7936
+ }
7937
+ mcpGloballyOverride = !!choice;
7938
+ }
7352
7939
  const summaryLines = [];
7353
7940
  for (const skill of skills) {
7354
7941
  if (summaryLines.length > 0) summaryLines.push("");
@@ -7366,41 +7953,128 @@ async function handleInstall(args, api, globalOpts) {
7366
7953
  process.exit(0);
7367
7954
  }
7368
7955
  }
7369
- const { runAdd } = await Promise.resolve().then(() => add_exports);
7956
+ const mcpGlobally = mcpGloballyOverride ?? installGlobally;
7957
+ const { mkdtemp, readdir, realpath, rm } = await import("node:fs/promises");
7958
+ const { join } = await import("node:path");
7959
+ const { extractZip } = await Promise.resolve().then(() => zip_exports);
7960
+ const { discoverSkills } = await import("./_chunks/skills.mjs").then((n) => n.skills_exports);
7961
+ const { installSkillForAgent } = await Promise.resolve().then(() => installer_exports);
7370
7962
  const installedSkills = [];
7371
7963
  const nonEveAgents = targetAgents.filter((a) => a !== "eve");
7372
- const runAddBaseOpts = {
7373
- registry: globalOpts.registry,
7374
- token: globalOpts.token,
7375
- packageSkill: true,
7376
- global: installGlobally,
7377
- copy: installMode === "copy",
7378
- yes: true
7379
- };
7380
- if (nonEveAgents.length > 0) runAddBaseOpts.agent = nonEveAgents;
7381
- if (targetAgents.includes("eve")) runAddBaseOpts.subagent = eveSubagentTargets.map((s) => s ?? "root");
7382
- for (const skill of skills) {
7383
- const skillRef = `@${skill.namespace || namespace}/${skill.slug}`;
7384
- console.log(` ${DIM$3}正在安装 ${skillRef}...${RESET$3}`);
7964
+ const universalTarget = targetAgents.find((agent) => isUniversalAgent(agent));
7965
+ const installTargetsForInstall = [
7966
+ ...universalTarget ? [{ agent: universalTarget }] : [],
7967
+ ...targetAgents.filter((agent) => agent !== "eve" && !isUniversalAgent(agent)).map((agent) => ({ agent })),
7968
+ ...targetAgents.includes("eve") ? eveSubagentTargets.map((subagent) => ({
7969
+ agent: "eve",
7970
+ subagent
7971
+ })) : []
7972
+ ];
7973
+ const work = await mkdtemp(join(await realpath(os$1.tmpdir()), "bfi-package-"));
7974
+ let mcpFile;
7975
+ try {
7976
+ const archive = join(work, "package.zip");
7977
+ const unpack = join(work, "content");
7978
+ const spinner$1 = spinner();
7979
+ spinner$1.start("正在下载组合包...");
7980
+ await api.downloadPackage(namespace, slug, archive);
7981
+ spinner$1.message("正在解压组合包...");
7982
+ await extractZip(archive, unpack);
7983
+ spinner$1.stop("组合包已就绪,开始安装技能");
7984
+ const entries = await readdir(unpack, { withFileTypes: true });
7985
+ if (entries.some((entry) => entry.isFile() && entry.name === "mcp.json")) mcpFile = join(unpack, "mcp.json");
7986
+ const directories = entries.filter((entry) => entry.isDirectory());
7987
+ if (directories.length === 0) throw new Error("组合包中没有技能目录");
7988
+ const installSpinner = spinner();
7989
+ installSpinner.start("正在安装技能...");
7990
+ const failures = [];
7991
+ for (const directory of directories) {
7992
+ const skill = skills.find((entry) => entry.slug === directory.name);
7993
+ const skillRef = skill ? `@${skill.namespace || namespace}/${skill.slug}` : directory.name;
7994
+ installSpinner.message(`正在安装 ${directory.name}`);
7995
+ try {
7996
+ const found = (await discoverSkills(join(unpack, directory.name)))[0];
7997
+ if (!found) throw new Error("未找到有效的 SKILL.md");
7998
+ const results = [];
7999
+ for (const target of installTargetsForInstall) results.push(await installSkillForAgent(found, target.agent, {
8000
+ global: installGlobally,
8001
+ cwd: process.cwd(),
8002
+ mode: installMode,
8003
+ eveSubagent: target.subagent
8004
+ }));
8005
+ const failed = results.filter((result) => !result.success);
8006
+ if (failed.length > 0) throw new Error(failed.map((result) => result.error).filter(Boolean).join(";") || "安装失败");
8007
+ installedSkills.push({
8008
+ ref: skillRef,
8009
+ name: found.name || directory.name
8010
+ });
8011
+ } catch (error) {
8012
+ failures.push(`${directory.name}:${error.message}`);
8013
+ }
8014
+ }
8015
+ installSpinner.stop(`已安装 ${installedSkills.length}/${directories.length} 个技能`);
8016
+ for (const failure of failures) log.error(failure);
8017
+ } catch (error) {
8018
+ await rm(work, {
8019
+ recursive: true,
8020
+ force: true
8021
+ });
8022
+ throw error;
8023
+ }
8024
+ let mcpJsonText;
8025
+ if (mcpFile) try {
8026
+ mcpJsonText = await readConfig(mcpFile);
8027
+ } catch {
8028
+ mcpJsonText = void 0;
8029
+ }
8030
+ await rm(work, {
8031
+ recursive: true,
8032
+ force: true
8033
+ });
8034
+ if (installedSkills.length > 0) {
8035
+ let entries = [];
7385
8036
  try {
7386
- await runAdd([skillRef], runAddBaseOpts);
7387
- const targets = targetAgents.flatMap((agent) => agent === "eve" ? eveSubagentTargets.map((subagent) => ({
7388
- agent,
7389
- subagent
7390
- })) : [{
7391
- agent,
7392
- subagent: void 0
7393
- }]);
7394
- const verified = await Promise.all(targets.map((target) => isSkillInstalled(packageSkillName(skill.namespace || namespace, skill.slug), target.agent, {
7395
- global: installGlobally,
7396
- eveSubagent: target.subagent
7397
- })));
7398
- if (verified.length === 0 || verified.some((result) => !result)) throw new Error("Skill installation could not be verified");
7399
- console.log(` ${GREEN$2}✓ ${skill.slug}${RESET$3}`);
7400
- installedSkills.push(skillRef);
7401
- } catch (error) {
7402
- console.log(` ${RED$1}✗ ${skill.slug}:${error.message}${RESET$3}`);
8037
+ if (mcpJsonText) entries = entriesFromJson(readDocument(mcpJsonText, "jsonc"));
8038
+ } catch {
8039
+ entries = [];
7403
8040
  }
8041
+ if (entries.length === 0) entries = mcpEntries;
8042
+ if (entries.length > 0) {
8043
+ const scope = mcpGlobally ? "user" : "project";
8044
+ if (interactive) {
8045
+ for (const entry of entries) for (const key of variables(entry.config)) if (!bindingsByServer[entry.registrationName]?.[key]) await promptBinding(entry, key);
8046
+ }
8047
+ const supported = nonEveAgents.filter((agent) => agentIds.includes(agent));
8048
+ const skipped = nonEveAgents.filter((agent) => !agentIds.includes(agent));
8049
+ const lines = [];
8050
+ let installedMcps = 0;
8051
+ for (const agent of supported) try {
8052
+ const plan = await planInstall(targetFor(agent, scope), entries, {
8053
+ bindingsByServer,
8054
+ materialize: scope === "user",
8055
+ environment: process.env
8056
+ });
8057
+ const result = await applyPlan(plan);
8058
+ const added = plan.changes.filter((change) => change.action === "add").length;
8059
+ const replaced = plan.changes.filter((change) => change.action === "replace").length;
8060
+ const unchanged = plan.changes.filter((change) => change.action === "unchanged").length;
8061
+ lines.push(`${agent.padEnd(14)} ${[
8062
+ added > 0 ? `新增 ${added}` : "",
8063
+ replaced > 0 ? `更新 ${replaced}` : "",
8064
+ unchanged > 0 ? `未变 ${unchanged}` : "",
8065
+ result.changed ? "" : "无变化"
8066
+ ].filter(Boolean).join(" · ")}`);
8067
+ installedMcps++;
8068
+ } catch (error) {
8069
+ lines.push(`${agent.padEnd(14)} ✗ ${error.message}`);
8070
+ }
8071
+ note(lines.join("\n"), `关联 MCP(${installedMcps}/${supported.length} 个 Agent)`);
8072
+ if (skipped.length > 0) log.info(`未支持自动写入 MCP 的 Agent:${skipped.join(", ")}`);
8073
+ if (scope === "project") {
8074
+ const projectVariables = [...new Set(entries.flatMap((entry) => variables(entry.config).map((key) => bindingsByServer[entry.registrationName]?.[key] ?? key)))];
8075
+ if (projectVariables.length > 0) log.info(`项目级 MCP 配置只保存变量引用:${projectVariables.map((name) => "${" + name + "}").join("、")}。请在启动 Agent 前导出这些环境变量;需要保存输入值时可在提示中选择写入用户级配置。`);
8076
+ }
8077
+ } else if (mcpNames.length > 0) log.warn("组合声明的 MCP 未能加载,请稍后重试或使用 bfi mcp install 单独安装。");
7404
8078
  }
7405
8079
  if (installedSkills.length === 0) {
7406
8080
  console.log(`${YELLOW$1}没有确认安装成功的技能,未创建包安装记录${RESET$3}`);
@@ -7417,14 +8091,14 @@ async function handleInstall(args, api, globalOpts) {
7417
8091
  version: pkgInfo.version || "latest",
7418
8092
  skillCount: installedSkills.length
7419
8093
  };
7420
- for (const skillRef of installedSkills) {
8094
+ for (const { ref: skillRef, name } of installedSkills) {
7421
8095
  const identity = parseSkillRef(skillRef);
7422
8096
  const existing = lock.skills[skillRef];
7423
8097
  lock.skills[skillRef] = {
7424
8098
  ...existing,
7425
8099
  namespace: identity.namespace,
7426
8100
  slug: identity.slug,
7427
- installedName: packageSkillName(identity.namespace, identity.slug),
8101
+ installedName: name,
7428
8102
  version: existing?.version || "latest",
7429
8103
  package: {
7430
8104
  namespace,
@@ -7433,15 +8107,7 @@ async function handleInstall(args, api, globalOpts) {
7433
8107
  };
7434
8108
  }
7435
8109
  saveLockfile(cwd, lock);
7436
- try {
7437
- const mcpNames = ((await api.getPackageMcps(namespace, slug))?.mcps ?? []).filter((entry) => entry?.mcpEntryId).map((entry) => entry.lastKnownName).filter((name) => typeof name === "string" && name.length > 0);
7438
- if (mcpNames.length > 0) {
7439
- console.log();
7440
- console.log(`${DIM$3}该组合关联 MCP:${mcpNames.join("、")}${RESET$3}`);
7441
- console.log(`${DIM$3}安装 MCP:bfi mcp install ${mcpNames.join(" ")} --agent <agent>${RESET$3}`);
7442
- }
7443
- } catch {}
7444
- for (const skillRef of installedSkills) await reportInstallEventForSkill(api, {
8110
+ for (const { ref: skillRef } of installedSkills) await reportInstallEventForSkill(api, {
7445
8111
  skillId: skills.find((entry) => `@${entry.namespace || namespace}/${entry.slug}` === skillRef)?.skillId,
7446
8112
  agentTarget: targetAgents[0]
7447
8113
  });
@@ -10288,4 +10954,4 @@ main().catch((e) => {
10288
10954
  console.error(`✗ ${e?.message || e}`);
10289
10955
  process.exit(1);
10290
10956
  });
10291
- export { SkillHubAPI, getAgentBaseDir };
10957
+ export { SkillHubAPI, agentIds, applyPlan, checkPath, decodeServer, getAgentBaseDir, loadTarget, planInstall, planRemove, readConfig, readDocument, serversIn, targetFor, writeConfig };