bfi-cli 0.1.102 → 0.1.104

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,33 +1,37 @@
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
- import { PluginRegistryClient, parsePackageIntent } from "./_chunks/intent.mjs";
7
+ import { PluginRegistryClient, normalizeRegistry, parsePackageIntent } from "./_chunks/intent.mjs";
8
8
  import { loadGlobalConfig, loadLockfile, loadProjectConfig, resolveConfig, saveGlobalConfig, saveLockfile, setConfigValue } from "./_chunks/config.mjs";
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
- import { packageSkillName } from "./_chunks/package-skill-identity.mjs";
11
+ import { entriesFromJson, mapStrings, normalizeConfig, object, resolveVariables, validName, variablePattern, variables } from "./_chunks/model.mjs";
12
+ import { hash, verifyPlan } from "./_chunks/digest.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, readFile, rename, unlink, writeFile } 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 {
@@ -3542,7 +3560,7 @@ async function installFromSkillHub(parsed, options, spinner) {
3542
3560
  process.exit(1);
3543
3561
  }
3544
3562
  if (options.packageSkill) {
3545
- const { scopePackageSkill } = await import("./_chunks/package-skill-identity.mjs").then((n) => n.package_skill_identity_exports);
3563
+ const { scopePackageSkill } = await Promise.resolve().then(() => package_skill_identity_exports);
3546
3564
  try {
3547
3565
  return {
3548
3566
  skills: scopePackageSkill(skills, namespace, slug),
@@ -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) {
@@ -5496,7 +5514,20 @@ async function runUpdate(args = []) {
5496
5514
  totalFail += failCount;
5497
5515
  totalFound += foundCount;
5498
5516
  }
5499
- if (options.skills && totalFound === 0) console.log(`${DIM$9}未找到匹配的已安装技能:${options.skills.join(", ")}${RESET$9}`);
5517
+ if (options.skills && totalFound === 0) {
5518
+ console.log(`${DIM$9}未找到安装记录,按首次安装重新获取:${options.skills.join(", ")}${RESET$9}`);
5519
+ const { runAdd } = await Promise.resolve().then(() => add_exports);
5520
+ for (const skill of options.skills) try {
5521
+ await runAdd([skill], {
5522
+ yes: true,
5523
+ global: options.global || scope === "global"
5524
+ });
5525
+ totalSuccess += 1;
5526
+ } catch (error) {
5527
+ totalFail += 1;
5528
+ console.log(`${DIM$9}✗ ${skill}:${error instanceof Error ? error.message : "安装失败"}${RESET$9}`);
5529
+ }
5530
+ }
5500
5531
  console.log();
5501
5532
  if (totalSuccess > 0) console.log(`${TEXT$7}✓ 已更新 ${totalSuccess} 个技能${RESET$9}`);
5502
5533
  if (totalFail > 0) {
@@ -5583,14 +5614,14 @@ async function materializeUseSkill(skill) {
5583
5614
  const tempRoot = await mkdtemp(join(tmpdir(), "skills-use-"));
5584
5615
  const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
5585
5616
  if (!isPathSafe(tempRoot, skillDir)) throw new Error("Invalid skill name: potential path traversal detected");
5586
- await mkdir(skillDir, { recursive: true });
5617
+ await mkdir$1(skillDir, { recursive: true });
5587
5618
  if (skill.kind === "blob") await writeSnapshotFiles(skillDir, skill.files);
5588
5619
  else if (skill.kind === "well-known") await writeMapFiles(skillDir, skill.files);
5589
5620
  else await copySkillDirectory(skill.path, skillDir);
5590
5621
  return {
5591
5622
  tempRoot,
5592
5623
  skillDir,
5593
- skillMd: skill.rawContent ?? await readFile(join(skillDir, "SKILL.md"), "utf-8"),
5624
+ skillMd: skill.rawContent ?? await readFile$1(join(skillDir, "SKILL.md"), "utf-8"),
5594
5625
  hasSupportingFiles: await containsSupportingFiles(skillDir, skillDir)
5595
5626
  };
5596
5627
  }
@@ -5806,13 +5837,13 @@ async function writeMapFiles(targetDir, files) {
5806
5837
  async function writeSafeFile(targetDir, filePath, contents) {
5807
5838
  const fullPath = join(targetDir, filePath);
5808
5839
  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);
5840
+ await mkdir$1(dirname(fullPath), { recursive: true });
5841
+ if (typeof contents === "string") await writeFile$1(fullPath, contents, "utf-8");
5842
+ else await writeFile$1(fullPath, contents);
5812
5843
  }
5813
5844
  async function copySkillDirectory(src, dest) {
5814
- await mkdir(dest, { recursive: true });
5815
- const entries = await readdir(src, { withFileTypes: true });
5845
+ await mkdir$1(dest, { recursive: true });
5846
+ const entries = await readdir$1(src, { withFileTypes: true });
5816
5847
  await Promise.all(entries.filter((entry) => !isExcluded(entry.name, entry.isDirectory())).map(async (entry) => {
5817
5848
  const srcPath = join(src, entry.name);
5818
5849
  const destPath = join(dest, entry.name);
@@ -5822,7 +5853,7 @@ async function copySkillDirectory(src, dest) {
5822
5853
  return;
5823
5854
  }
5824
5855
  try {
5825
- await cp(srcPath, destPath, {
5856
+ await cp$1(srcPath, destPath, {
5826
5857
  dereference: true,
5827
5858
  recursive: true
5828
5859
  });
@@ -5836,7 +5867,7 @@ async function copySkillDirectory(src, dest) {
5836
5867
  }));
5837
5868
  }
5838
5869
  async function containsSupportingFiles(rootDir, currentDir) {
5839
- const entries = await readdir(currentDir, { withFileTypes: true });
5870
+ const entries = await readdir$1(currentDir, { withFileTypes: true });
5840
5871
  for (const entry of entries) {
5841
5872
  const entryPath = join(currentDir, entry.name);
5842
5873
  const relPath = relative(rootDir, entryPath).split(sep).join("/");
@@ -6966,6 +6997,673 @@ function maybePrintAnalyticsNotice(env = process.env) {
6966
6997
  console.log();
6967
6998
  } catch {}
6968
6999
  }
7000
+ function registryUrl(value) {
7001
+ const url = new URL(value);
7002
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("无效的 registry 地址。");
7003
+ return url.toString().replace(/\/$/, "");
7004
+ }
7005
+ function credentialForRegistry(registry, config) {
7006
+ return config?.token && registryUrl(registry) === registryUrl(config.registry ?? "https://bfi.yyuap.com:811") ? config.token : void 0;
7007
+ }
7008
+ async function readResponse(response, limit) {
7009
+ const chunks = [];
7010
+ let size = 0;
7011
+ if (!response.body) throw new Error("服务返回空响应。");
7012
+ const reader = response.body.getReader();
7013
+ try {
7014
+ while (true) {
7015
+ const chunk = await reader.read();
7016
+ if (chunk.done) break;
7017
+ size += chunk.value.length;
7018
+ if (size > limit) throw new Error("响应超过安全大小上限。");
7019
+ chunks.push(chunk.value);
7020
+ }
7021
+ } finally {
7022
+ await reader.cancel().catch(() => {});
7023
+ }
7024
+ return Buffer.concat(chunks);
7025
+ }
7026
+ var InstallationClient = class {
7027
+ registry;
7028
+ token;
7029
+ fetcher;
7030
+ constructor(registry, token, fetcher = fetch) {
7031
+ this.registry = registryUrl(registry);
7032
+ this.token = token;
7033
+ this.fetcher = fetcher;
7034
+ }
7035
+ async request(path, method = "GET", body) {
7036
+ const headers = { Accept: "application/json" };
7037
+ if (this.token) headers.Authorization = `Bearer ${this.token}`;
7038
+ if (method === "POST") {
7039
+ headers["Content-Type"] = "application/json";
7040
+ if (!this.token) {
7041
+ const initial = await this.request("/api/web/mcps?size=1");
7042
+ const cookie = initial.headers.get("set-cookie")?.match(/(?:^|,\s*)XSRF-TOKEN=([^;]+)/)?.[1];
7043
+ await initial.body?.cancel();
7044
+ if (!cookie) throw new Error("无法获取匿名 CSRF 凭据,请检查服务配置。");
7045
+ headers.Cookie = `XSRF-TOKEN=${cookie}`;
7046
+ headers["X-XSRF-TOKEN"] = decodeURIComponent(cookie);
7047
+ }
7048
+ }
7049
+ let response;
7050
+ try {
7051
+ response = await this.fetcher(`${this.registry}${path}`, {
7052
+ method,
7053
+ headers,
7054
+ body: body === void 0 ? void 0 : JSON.stringify(body),
7055
+ redirect: "error",
7056
+ signal: AbortSignal.timeout(12e4)
7057
+ });
7058
+ } catch {
7059
+ throw new Error("网络连接失败或服务重定向;未转发登录凭据。");
7060
+ }
7061
+ if (response.status === 401) throw new Error("登录无效或需要登录,请运行 bfi auth login 后重试。");
7062
+ if (response.status === 403) throw new Error("权限不足或 CSRF 校验失败;不会覆盖现有登录。");
7063
+ if (response.status >= 300 && response.status < 400) throw new Error("不允许服务重定向。");
7064
+ if (!response.ok) throw new Error(`安装服务请求失败(HTTP ${response.status}),请检查权限、计划有效期和服务版本。`);
7065
+ return response;
7066
+ }
7067
+ async plan(response, kind, ref, id) {
7068
+ const envelope = JSON.parse((await readResponse(response, 12 * 1024 ** 2)).toString("utf8"));
7069
+ if (envelope.code !== 0) throw new Error("安装服务拒绝请求。");
7070
+ const plan = verifyPlan(envelope.data);
7071
+ if (registryUrl(plan.registry) !== this.registry || plan.kind !== kind || plan.ref !== ref || id && plan.id !== id) throw new Error("计划与请求来源或资源不匹配。");
7072
+ if (Date.parse(plan.expiresAt) <= Date.now()) throw new Error("内容计划已过期,请重新确认最新内容。");
7073
+ return plan;
7074
+ }
7075
+ async resolve(kind, ref) {
7076
+ return this.plan(await this.request("/api/v1/installation-content-plans", "POST", {
7077
+ kind,
7078
+ ref
7079
+ }), kind, ref);
7080
+ }
7081
+ async get(id, kind, ref) {
7082
+ return this.plan(await this.request(`/api/v1/installation-content-plans/${encodeURIComponent(id)}`), kind, ref, id);
7083
+ }
7084
+ async artifact(plan) {
7085
+ const response = await this.request(`/api/v1/installation-content-plans/${encodeURIComponent(plan.id)}/artifact`);
7086
+ const bytes = await readResponse(response, 510 * 1024 ** 2);
7087
+ if (response.headers.get("X-Archive-SHA256") !== hash(bytes)) throw new Error("下载归档摘要不匹配。");
7088
+ return bytes;
7089
+ }
7090
+ async verifyAccount() {
7091
+ if (!this.token) throw new Error("BFI MCP 需要本机登录态,请先运行 bfi auth login;不会把 Token 写进 MCP 配置。");
7092
+ const response = await this.request("/api/v1/whoami");
7093
+ let envelope;
7094
+ try {
7095
+ envelope = JSON.parse((await readResponse(response, 1048576)).toString("utf8"));
7096
+ } catch {
7097
+ throw new Error("本机 BFI 登录响应无效;未覆盖现有登录。");
7098
+ }
7099
+ if (!envelope || typeof envelope !== "object") throw new Error("本机 BFI 登录响应无效。");
7100
+ if (envelope.code !== void 0 && envelope.code !== 0) throw new Error("本机 BFI 登录态验证失败。");
7101
+ const data = envelope.data ?? envelope;
7102
+ if (!data?.id && !data?.user?.id) throw new Error("本机 BFI 登录响应无效。");
7103
+ }
7104
+ };
7105
+ function printArchiveHeaders(globalOpts) {
7106
+ const { registry } = resolveConfig(globalOpts);
7107
+ const token = typeof globalOpts.token === "string" ? globalOpts.token : credentialForRegistry(registry, loadGlobalConfig());
7108
+ if (!token) throw new Error("BFI_TOKEN_REQUIRED: run bfi auth login first");
7109
+ console.log(JSON.stringify({ Authorization: `Bearer ${token}` }));
7110
+ }
7111
+ async function addClaudeMarketplace(namespace, globalOpts) {
7112
+ const { registry } = resolveConfig(globalOpts);
7113
+ const base = normalizeRegistry(String(registry));
7114
+ const url = `${base}/api/web/namespaces/${encodeURIComponent(namespace)}/plugin-marketplace.json`;
7115
+ const market = `skillhub-${namespace}`;
7116
+ const path = join$1(homedir$1(), ".claude", "settings.json");
7117
+ const existing = await readFile(path, "utf8").then(JSON.parse).catch(() => ({}));
7118
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) throw new Error("CLAUDE_SETTINGS_INVALID");
7119
+ const known = { ...existing.extraKnownMarketplaces };
7120
+ known[market] = { source: {
7121
+ source: "url",
7122
+ url,
7123
+ headersHelper: `bfi skills package archive-headers --registry ${base}`
7124
+ } };
7125
+ existing.extraKnownMarketplaces = known;
7126
+ await mkdir(dirname$1(path), {
7127
+ recursive: true,
7128
+ mode: 448
7129
+ });
7130
+ const temporary = `${path}.${process.pid}.tmp`;
7131
+ await writeFile(temporary, JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
7132
+ await rename(temporary, path);
7133
+ return {
7134
+ market,
7135
+ url,
7136
+ settings: path,
7137
+ next: `claude plugin install <plugin>@${market} --scope user`,
7138
+ message: "Claude Code 将通过 headersHelper 使用 bfi 当前登录态读取市场和归档;安装后可用 claude plugin update 更新。"
7139
+ };
7140
+ }
7141
+ var package_skill_identity_exports = /* @__PURE__ */ __exportAll({
7142
+ packageSkillName: () => packageSkillName,
7143
+ scopePackageSkill: () => scopePackageSkill
7144
+ });
7145
+ function packageSkillName(namespace, slug) {
7146
+ if (!namespace || !slug) throw new Error("Namespace and slug are required");
7147
+ return `bfi-skill-${createHash("sha256").update(JSON.stringify([namespace, slug])).digest("hex")}`;
7148
+ }
7149
+ function scopePackageSkill(skills, namespace, slug) {
7150
+ if (skills.length !== 1) throw new Error("A package reference must resolve to exactly one skill");
7151
+ return [{
7152
+ ...skills[0],
7153
+ name: packageSkillName(namespace, slug)
7154
+ }];
7155
+ }
7156
+ function checkNode(node) {
7157
+ if (node.type === "object") {
7158
+ const keys = (node.children ?? []).map((child) => String(child.children?.[0]?.value));
7159
+ if (keys.length !== new Set(keys).size || keys.some((key) => [
7160
+ "__proto__",
7161
+ "prototype",
7162
+ "constructor"
7163
+ ].includes(key))) throw new Error("配置包含重复键或不安全键,拒绝修改。");
7164
+ }
7165
+ node.children?.forEach(checkNode);
7166
+ }
7167
+ function readDocument(text, format) {
7168
+ if (!text.trim()) return {};
7169
+ try {
7170
+ if (format === "toml") return object(parse$1(text));
7171
+ const errors = [];
7172
+ const tree = parseTree(text, errors, {
7173
+ allowTrailingComma: true,
7174
+ disallowComments: false
7175
+ });
7176
+ if (!tree || errors.length) throw new Error();
7177
+ checkNode(tree);
7178
+ return object(JSON.parse(JSON.stringify(getNodeValue(tree))));
7179
+ } catch {
7180
+ throw new Error("现有配置解析失败,未修改任何内容。请先修复配置格式或重复键。");
7181
+ }
7182
+ }
7183
+ function serversIn(document, path) {
7184
+ let current = document;
7185
+ for (const key of path) {
7186
+ if (current[key] === void 0) return {};
7187
+ current = object(current[key], "MCP 配置节点");
7188
+ }
7189
+ return current;
7190
+ }
7191
+ function tablePath(line) {
7192
+ if (!/^\s*\[[^[]/.test(line)) return;
7193
+ try {
7194
+ let value = parse$1(`${line}\n__bfi_table_marker__ = true\n`);
7195
+ const path = [];
7196
+ while (value && typeof value === "object") {
7197
+ const entries = Object.entries(value);
7198
+ if (entries.length !== 1) return;
7199
+ const [key, next] = entries[0];
7200
+ if (key === "__bfi_table_marker__") return path;
7201
+ path.push(key);
7202
+ value = next;
7203
+ }
7204
+ } catch {
7205
+ return;
7206
+ }
7207
+ }
7208
+ function editToml(text, replacements) {
7209
+ const lines = text.split(/(?<=\n)/);
7210
+ const starts = [];
7211
+ let offset = 0;
7212
+ for (const line of lines) {
7213
+ if (/^\s*\[/.test(line)) starts.push({
7214
+ offset,
7215
+ path: tablePath(line.trim())
7216
+ });
7217
+ offset += line.length;
7218
+ }
7219
+ const existing = serversIn(readDocument(text, "toml"), ["mcp_servers"]);
7220
+ const ranges = [];
7221
+ for (const name of Object.keys(replacements)) {
7222
+ const matches = starts.flatMap((start, index) => start.path?.[0] === "mcp_servers" && start.path[1] === name ? [[start.offset, starts[index + 1]?.offset ?? text.length]] : []);
7223
+ if (existing[name] !== void 0 && !matches.length) throw new Error("目标 MCP 使用内联或复杂 TOML 定义,无法安全增量更新;请先改为独立 [mcp_servers.\"name\"] 表。");
7224
+ ranges.push(...matches);
7225
+ }
7226
+ let result = text;
7227
+ for (const [start, end] of ranges.sort((left, right) => right[0] - left[0])) result = result.slice(0, start) + result.slice(end);
7228
+ 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, "")}`;
7229
+ if (!readDocument(result, "toml").mcp_servers) result += "\n[mcp_servers]\n";
7230
+ return result;
7231
+ }
7232
+ function editDocument(text, format, path, replacements) {
7233
+ serversIn(readDocument(text, format), path);
7234
+ const expected = readDocument(text, format);
7235
+ let parent = expected;
7236
+ for (const key of path) parent = object(parent[key] ?? (parent[key] = {}));
7237
+ for (const [name, value] of Object.entries(replacements)) if (value === void 0) delete parent[name];
7238
+ else parent[name] = value;
7239
+ let result = text.trim() ? text : format === "jsonc" ? "{}\n" : "";
7240
+ if (format === "toml") result = editToml(result, replacements);
7241
+ else for (const [name, value] of Object.entries(replacements)) result = applyEdits(result, modify(result, [...path, name], value, { formattingOptions: {
7242
+ insertSpaces: true,
7243
+ tabSize: 2,
7244
+ eol: text.includes("\r\n") ? "\r\n" : "\n"
7245
+ } }));
7246
+ if (!isDeepStrictEqual(readDocument(result, format), expected)) throw new Error("配置保留校验失败,未写入;请使用客户端原生配置处理复杂结构。");
7247
+ return result;
7248
+ }
7249
+ const agentIds = [
7250
+ "claude-code",
7251
+ "codex",
7252
+ "cursor",
7253
+ "qwen-code",
7254
+ "gemini-cli",
7255
+ "zcode",
7256
+ "codebuddy",
7257
+ "kimi-code",
7258
+ "opencode"
7259
+ ];
7260
+ const aliases = {
7261
+ claude: "claude-code",
7262
+ qwen: "qwen-code",
7263
+ gemini: "gemini-cli",
7264
+ kimi: "kimi-code",
7265
+ "kimi-cli": "kimi-code",
7266
+ "codebuddy-code": "codebuddy"
7267
+ };
7268
+ function preferred(paths) {
7269
+ return paths.find((filename) => existsSync$1(filename)) ?? paths[0];
7270
+ }
7271
+ function targetFor(name, scope = "user", options = {}) {
7272
+ const agent = aliases[name] ?? name;
7273
+ if (!agentIds.includes(agent)) throw new Error(`尚未支持该 Agent 的自动配置。支持:${agentIds.join(", ")}。WorkBuddy/IDE 专有配置请使用原生导入,不猜测路径。`);
7274
+ const home = options.home ?? homedir$1();
7275
+ const cwd = options.cwd ?? process.cwd();
7276
+ const env = options.env ?? process.env;
7277
+ const user = scope === "user";
7278
+ const paths = {
7279
+ "claude-code": [user ? join$1(home, ".claude.json") : join$1(cwd, ".mcp.json")],
7280
+ codex: [user ? join$1(env.CODEX_HOME ?? join$1(home, ".codex"), "config.toml") : join$1(cwd, ".codex/config.toml")],
7281
+ cursor: [join$1(user ? home : cwd, ".cursor/mcp.json")],
7282
+ "qwen-code": [join$1(user ? home : cwd, ".qwen/settings.json")],
7283
+ "gemini-cli": [join$1(user ? home : cwd, ".gemini/settings.json")],
7284
+ zcode: [join$1(user ? home : cwd, user ? ".zcode/cli/config.json" : ".zcode/config.json")],
7285
+ codebuddy: user ? [
7286
+ join$1(home, ".codebuddy/.mcp.json"),
7287
+ join$1(home, ".codebuddy/mcp.json"),
7288
+ join$1(home, ".codebuddy.json")
7289
+ ] : [join$1(cwd, ".mcp.json"), join$1(cwd, "mcp.json")],
7290
+ "kimi-code": [join$1(user ? env.KIMI_CODE_HOME ?? join$1(home, ".kimi-code") : join$1(cwd, ".kimi-code"), "mcp.json")],
7291
+ 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")]
7292
+ };
7293
+ if (agent === "claude-code" && env.CLAUDE_CONFIG_DIR && !options.config) throw new Error("设置了 CLAUDE_CONFIG_DIR;请用 --config 明确 MCP 文件,避免改错作用域。");
7294
+ 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 布局。");
7295
+ return {
7296
+ agent,
7297
+ scope,
7298
+ path: resolve$1(options.config ?? preferred(paths[agent])),
7299
+ format: agent === "codex" ? "toml" : "jsonc",
7300
+ key: agent === "codex" ? ["mcp_servers"] : agent === "opencode" ? ["mcp"] : agent === "zcode" ? ["mcp", "servers"] : ["mcpServers"]
7301
+ };
7302
+ }
7303
+ function hasLiteralSecrets(config) {
7304
+ 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());
7305
+ const sensitive = /token|secret|password|api.?key|authorization|credential|private.?key|x-pm-(ak|sk)/i;
7306
+ 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;
7307
+ if (Object.entries(config.env ?? {}).some(([key, value]) => sensitive.test(key) && literal(value))) return true;
7308
+ 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;
7309
+ if (config.url) {
7310
+ if ([...new URL(config.url.replace(variablePattern, "__BFI_VARIABLE__")).searchParams].some(([key, value]) => sensitive.test(key) && value.replaceAll("__BFI_VARIABLE__", ""))) return true;
7311
+ }
7312
+ return false;
7313
+ }
7314
+ function encodeServer(config, target, bindings = {}, materialize = false, environment = process.env) {
7315
+ if (config.cwd && ![
7316
+ "codex",
7317
+ "qwen-code",
7318
+ "gemini-cli",
7319
+ "kimi-code"
7320
+ ].includes(target.agent)) throw new Error("此宿主的 MCP cwd 字段未支持或未确证;请提供不依赖 cwd 的启动配置。");
7321
+ if (Object.values(bindings).some((name) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) throw new Error("环境变量绑定名称无效。");
7322
+ if (materialize && target.scope !== "user") throw new Error("凭据物化仅允许用户级配置,项目级配置只能保存引用。");
7323
+ if (hasLiteralSecrets(config) && !materialize) throw new Error("源配置可能含明文凭据;请改为环境变量占位符,或显式选择用户级 --materialize-inputs。");
7324
+ let value = materialize ? resolveVariables(config, bindings, environment) : config;
7325
+ if (variables(value).length && ["zcode", "kimi-code"].includes(target.agent)) throw new Error("该宿主的通用变量插值未确证。请使用用户级 --materialize-inputs,或原生秘密配置;不会保存无效占位符。");
7326
+ if (!materialize && target.agent !== "codex") value = mapStrings(value, (text) => text.replace(variablePattern, (_whole, key, fallback) => {
7327
+ const name = bindings[key] ?? key;
7328
+ if (fallback !== void 0 && !["claude-code", "codebuddy"].includes(target.agent)) throw new Error("此宿主不支持带默认值的变量引用。");
7329
+ if (target.agent === "cursor") return `\${env:${name}}`;
7330
+ if (target.agent === "opencode") return `{env:${name}}`;
7331
+ return `\${${name}${fallback === void 0 ? "" : `:-${fallback}`}}`;
7332
+ }));
7333
+ const { type, ...rest } = value;
7334
+ if (target.agent === "codex") {
7335
+ if (type === "sse") throw new Error("Codex 不支持旧 SSE 配置;请提供 Streamable HTTP 服务。");
7336
+ const result = {};
7337
+ if (type === "stdio") {
7338
+ result.command = value.command;
7339
+ if (value.args) result.args = value.args;
7340
+ if (value.cwd) result.cwd = value.cwd;
7341
+ const env = {};
7342
+ const inherited = [];
7343
+ for (const [name, text] of Object.entries(value.env ?? {})) {
7344
+ const match = /^\$\{(?:user_config\.)?([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(text);
7345
+ if (match && (bindings[match[1]] ?? match[1]) === name) inherited.push(name);
7346
+ else if (text.includes("${")) throw new Error("Codex env_vars 不能重命名或拼接变量;请使用同名环境变量或用户级物化。");
7347
+ else env[name] = text;
7348
+ }
7349
+ if (Object.keys(env).length) result.env = env;
7350
+ if (inherited.length) result.env_vars = inherited;
7351
+ if ([
7352
+ value.command,
7353
+ value.cwd,
7354
+ ...value.args ?? []
7355
+ ].some((item) => item?.includes("${"))) throw new Error("Codex command/args/cwd 不支持变量插值。");
7356
+ } else {
7357
+ result.url = value.url;
7358
+ if (value.url?.includes("${")) throw new Error("Codex URL 不支持变量插值。");
7359
+ const literal = {};
7360
+ const inherited = {};
7361
+ for (const [header, text] of Object.entries(value.headers ?? {})) {
7362
+ const match = /^\$\{(?:user_config\.)?([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(text);
7363
+ const bearer = /^Bearer \$\{(?:user_config\.)?([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(text);
7364
+ if (match) inherited[header] = bindings[match[1]] ?? match[1];
7365
+ else if (header.toLowerCase() === "authorization" && bearer) result.bearer_token_env_var = bindings[bearer[1]] ?? bearer[1];
7366
+ else if (text.includes("${")) throw new Error("Codex header 只支持整值环境引用或 Bearer 引用,不支持拼接。");
7367
+ else literal[header] = text;
7368
+ }
7369
+ if (Object.keys(literal).length) result.http_headers = literal;
7370
+ if (Object.keys(inherited).length) result.env_http_headers = inherited;
7371
+ }
7372
+ return result;
7373
+ }
7374
+ if (target.agent === "opencode") {
7375
+ if (value.cwd) throw new Error("OpenCode MCP 无独立 cwd 字段;请提供绝对启动路径。");
7376
+ return type === "stdio" ? {
7377
+ type: "local",
7378
+ command: [value.command, ...value.args ?? []],
7379
+ ...value.env ? { environment: value.env } : {}
7380
+ } : {
7381
+ type: "remote",
7382
+ url: value.url,
7383
+ ...value.headers ? { headers: value.headers } : {}
7384
+ };
7385
+ }
7386
+ if (["qwen-code", "gemini-cli"].includes(target.agent)) {
7387
+ const { url, ...fields } = rest;
7388
+ return {
7389
+ ...fields,
7390
+ ...url ? { [type === "http" ? "httpUrl" : "url"]: url } : {}
7391
+ };
7392
+ }
7393
+ if (target.agent === "cursor") return rest;
7394
+ if (target.agent === "kimi-code") return {
7395
+ ...rest,
7396
+ ...type === "sse" ? { transport: "sse" } : {}
7397
+ };
7398
+ return {
7399
+ type,
7400
+ ...rest
7401
+ };
7402
+ }
7403
+ const managedKeys = /* @__PURE__ */ new Set([
7404
+ "type",
7405
+ "transport",
7406
+ "command",
7407
+ "args",
7408
+ "env",
7409
+ "environment",
7410
+ "cwd",
7411
+ "url",
7412
+ "httpUrl",
7413
+ "headers",
7414
+ "http_headers",
7415
+ "env_http_headers",
7416
+ "env_vars",
7417
+ "bearer_token_env_var"
7418
+ ]);
7419
+ function mergeServer(existing, desired) {
7420
+ if (existing === void 0) return desired;
7421
+ const previous = object(existing, "已有 MCP");
7422
+ const extras = Object.fromEntries(Object.entries(previous).filter(([key]) => !managedKeys.has(key)));
7423
+ if ([
7424
+ "oauth",
7425
+ "auth",
7426
+ "headersHelper",
7427
+ "http_headers_helper",
7428
+ "bearerTokenEnvVar",
7429
+ "bearer_token"
7430
+ ].some((key) => extras[key] !== void 0) && [
7431
+ "url",
7432
+ "httpUrl",
7433
+ "command",
7434
+ "type",
7435
+ "transport"
7436
+ ].some((key) => previous[key] !== desired[key])) throw new Error("已有服务器包含宿主专有认证设置,不能安全更换启动目标;请在客户端中处理。");
7437
+ return {
7438
+ ...extras,
7439
+ ...desired
7440
+ };
7441
+ }
7442
+ function decodeServer(raw, target) {
7443
+ const source = object(raw, "MCP");
7444
+ if (source.cwd && ![
7445
+ "codex",
7446
+ "qwen-code",
7447
+ "gemini-cli",
7448
+ "kimi-code"
7449
+ ].includes(target.agent)) throw new Error("此宿主 cwd 语义未确证,请使用客户端原生测试。");
7450
+ if (source.enabled === false || source.disabled === true) throw new Error("此 MCP 已在客户端停用;测试不会绕过停用设置。");
7451
+ if ([
7452
+ "oauth",
7453
+ "auth",
7454
+ "headersHelper",
7455
+ "http_headers_helper",
7456
+ "bearer_token",
7457
+ "bearerTokenEnvVar"
7458
+ ].some((key) => source[key] !== void 0)) throw new Error("此服务器使用宿主专有认证;请用客户端原生连接测试。");
7459
+ let result = Object.fromEntries(Object.entries(source).filter(([key]) => managedKeys.has(key)));
7460
+ if (target.agent === "codex") {
7461
+ const { env_vars, env_http_headers, http_headers, bearer_token_env_var, ...rest } = result;
7462
+ result = rest;
7463
+ if (source.command) {
7464
+ const env = { ...object(source.env ?? {}) };
7465
+ if (env_vars !== void 0 && (!Array.isArray(env_vars) || !env_vars.every((item) => typeof item === "string"))) throw new Error("不支持的 Codex env_vars 结构。");
7466
+ for (const name of env_vars ?? []) env[name] = `\${${name}}`;
7467
+ if (Object.keys(env).length) result.env = env;
7468
+ } else {
7469
+ const headers = { ...object(http_headers ?? {}) };
7470
+ for (const [name, variable] of Object.entries(object(env_http_headers ?? {}))) {
7471
+ if (typeof variable !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable)) throw new Error("无效环境变量引用。");
7472
+ headers[name] = `\${${variable}}`;
7473
+ }
7474
+ if (typeof bearer_token_env_var === "string") headers.Authorization = `Bearer \${${bearer_token_env_var}}`;
7475
+ if (Object.keys(headers).length) result.headers = headers;
7476
+ }
7477
+ } else if (target.agent === "opencode") {
7478
+ if (source.type === "local") {
7479
+ if (!Array.isArray(source.command) || !source.command.length) throw new Error("OpenCode command 必须为非空数组。");
7480
+ result = {
7481
+ type: "stdio",
7482
+ command: source.command[0],
7483
+ args: source.command.slice(1),
7484
+ ...source.environment ? { env: source.environment } : {}
7485
+ };
7486
+ } else result = {
7487
+ type: "http",
7488
+ url: source.url,
7489
+ ...source.headers ? { headers: source.headers } : {}
7490
+ };
7491
+ result = mapStrings(result, (text) => text.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, "${$1}"));
7492
+ } else if (["qwen-code", "gemini-cli"].includes(target.agent)) {
7493
+ const { httpUrl, ...rest } = result;
7494
+ result = {
7495
+ ...rest,
7496
+ ...httpUrl ? {
7497
+ type: "http",
7498
+ url: httpUrl
7499
+ } : source.url ? { type: "sse" } : {}
7500
+ };
7501
+ } else if (target.agent === "cursor") result = mapStrings(result, (text) => text.replace(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, "${$1}"));
7502
+ return normalizeConfig(result);
7503
+ }
7504
+ async function checkPath(filename) {
7505
+ let current = resolve$1(filename);
7506
+ while (current !== parse(current).root) {
7507
+ try {
7508
+ const stat = await lstat(current);
7509
+ if (stat.isSymbolicLink() || current === resolve$1(filename) && (!stat.isFile() || stat.nlink > 1)) throw new Error("配置路径含符号链接、特殊文件或硬链接,拒绝写入。");
7510
+ } catch (error) {
7511
+ if (error.code !== "ENOENT") throw error;
7512
+ }
7513
+ current = dirname$1(current);
7514
+ }
7515
+ }
7516
+ async function readConfig(filename) {
7517
+ await checkPath(filename);
7518
+ try {
7519
+ const handle = await open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
7520
+ try {
7521
+ const stat = await handle.stat();
7522
+ if (!stat.isFile() || stat.size > 2097152) throw new Error("配置文件不是普通文件或超过 2 MiB。");
7523
+ return await handle.readFile("utf8");
7524
+ } finally {
7525
+ await handle.close();
7526
+ }
7527
+ } catch (error) {
7528
+ if (error.code === "ENOENT") return;
7529
+ throw error;
7530
+ }
7531
+ }
7532
+ async function identity(filename) {
7533
+ try {
7534
+ const stat = await lstat(filename, { bigint: true });
7535
+ return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.mtimeNs}:${stat.size}`;
7536
+ } catch (error) {
7537
+ if (error.code === "ENOENT") return;
7538
+ throw error;
7539
+ }
7540
+ }
7541
+ async function writeConfig(filename, previous, next, expectedIdentity) {
7542
+ if (previous === next) return;
7543
+ await checkPath(filename);
7544
+ const originalIdentity = expectedIdentity ?? await identity(filename);
7545
+ if (previous !== void 0 && ((await lstat(filename)).mode & 128) === 0) throw new Error("配置文件只读,不放宽其权限。");
7546
+ await mkdir(dirname$1(filename), {
7547
+ recursive: true,
7548
+ mode: 448
7549
+ });
7550
+ const lockPath = `${filename}.bfi-mcp.lock`;
7551
+ const temporary = `${filename}.bfi-mcp-${randomUUID()}.tmp`;
7552
+ const backup = previous === void 0 ? void 0 : `${filename}.bfi-mcp-${Date.now()}-${randomUUID()}.bak`;
7553
+ let lock;
7554
+ try {
7555
+ lock = await open(lockPath, "wx", 384);
7556
+ } catch {
7557
+ throw new Error("无法取得配置写入锁。可能有其它安装正在进行;请核对残留 .bfi-mcp.lock 后重试。");
7558
+ }
7559
+ try {
7560
+ await checkPath(filename);
7561
+ if (await readConfig(filename) !== previous || await identity(filename) !== originalIdentity) throw new Error("配置已被其它进程修改,请重新预览并重试。");
7562
+ if (backup) {
7563
+ const handle = await open(backup, "wx", 384);
7564
+ try {
7565
+ await handle.writeFile(previous, "utf8");
7566
+ await handle.sync();
7567
+ } finally {
7568
+ await handle.close();
7569
+ }
7570
+ }
7571
+ const handle = await open(temporary, "wx", 384);
7572
+ try {
7573
+ await handle.writeFile(next, "utf8");
7574
+ await handle.sync();
7575
+ } finally {
7576
+ await handle.close();
7577
+ }
7578
+ await checkPath(filename);
7579
+ if (await readConfig(filename) !== previous || await identity(filename) !== originalIdentity) throw new Error("写入前检测到并发修改,已取消。");
7580
+ await rename(temporary, filename);
7581
+ return backup;
7582
+ } finally {
7583
+ await unlink(temporary).catch(() => {});
7584
+ await lock.close().catch(() => {});
7585
+ await unlink(lockPath).catch(() => {});
7586
+ }
7587
+ }
7588
+ async function loadTarget(target) {
7589
+ const fileIdentity = await identity(target.path);
7590
+ const text = await readConfig(target.path);
7591
+ if (await identity(target.path) !== fileIdentity) throw new Error("读取时配置发生变化,请重试。");
7592
+ return {
7593
+ text,
7594
+ fileIdentity,
7595
+ servers: serversIn(readDocument(text ?? "", target.format), target.key)
7596
+ };
7597
+ }
7598
+ async function planInstall(target, entries, options = {}) {
7599
+ if (!entries.length || entries.length > 100) throw new Error("每次需要选择 1–100 个 MCP。");
7600
+ const names = entries.map((entry) => validName(entry.registrationName));
7601
+ if (new Set(names).size !== names.length) throw new Error("安装列表包含重复名称。");
7602
+ const knownVariables = new Set(entries.flatMap((entry) => variables(entry.config)));
7603
+ if (Object.keys(options.bindings ?? {}).some((key) => !knownVariables.has(key))) throw new Error("环境绑定包含未使用的变量名,请检查 --input-env。");
7604
+ for (const [server, bindings] of Object.entries(options.bindingsByServer ?? {})) {
7605
+ const entry = entries.find((candidate) => candidate.registrationName === server);
7606
+ if (!entry || Object.keys(bindings).some((key) => !variables(entry.config).includes(key))) throw new Error("服务器环境绑定包含未使用的名称。");
7607
+ }
7608
+ const { text, fileIdentity, servers } = await loadTarget(target);
7609
+ const replacements = {};
7610
+ const changes = [];
7611
+ for (const entry of entries) {
7612
+ if (entry.runtime?.source === "upload") throw new Error(`MCP ${entry.registrationName} 需要上传运行包。请先下载检查并准备本地启动路径,再用 --file 安装本地配置。`);
7613
+ const name = entry.registrationName;
7614
+ const bindings = {
7615
+ ...options.bindings,
7616
+ ...options.bindingsByServer?.[name]
7617
+ };
7618
+ const desired = encodeServer(entry.config, target, bindings, options.materialize, options.environment);
7619
+ const merged = mergeServer(servers[name], desired);
7620
+ const action = servers[name] === void 0 ? "add" : isDeepStrictEqual(servers[name], merged) ? "unchanged" : "replace";
7621
+ if (action === "replace" && !options.replace) throw new Error(`同名 MCP ${name} 配置不同;未修改任何服务器。确认要替换其连接配置时使用 --replace。`);
7622
+ if (action !== "unchanged") replacements[name] = merged;
7623
+ changes.push({
7624
+ name,
7625
+ action,
7626
+ transport: entry.config.type,
7627
+ variables: variables(entry.config).map((key) => bindings[key] ?? key)
7628
+ });
7629
+ }
7630
+ return {
7631
+ target,
7632
+ previous: text,
7633
+ next: Object.keys(replacements).length ? editDocument(text ?? "", target.format, target.key, replacements) : text,
7634
+ fileIdentity,
7635
+ changes
7636
+ };
7637
+ }
7638
+ async function planRemove(target, names) {
7639
+ if (!names.length || names.length > 100) throw new Error("请明确指定 1–100 个要移除的 MCP。");
7640
+ const { text, fileIdentity, servers } = await loadTarget(target);
7641
+ const replacements = {};
7642
+ const changes = [...new Set(names)].map((name) => {
7643
+ validName(name);
7644
+ const action = Object.hasOwn(servers, name) ? "remove" : "absent";
7645
+ if (action === "remove") replacements[name] = void 0;
7646
+ return {
7647
+ name,
7648
+ action
7649
+ };
7650
+ });
7651
+ return {
7652
+ target,
7653
+ previous: text,
7654
+ next: Object.keys(replacements).length ? editDocument(text ?? "", target.format, target.key, replacements) : text,
7655
+ fileIdentity,
7656
+ changes
7657
+ };
7658
+ }
7659
+ async function applyPlan(plan) {
7660
+ if (plan.previous === plan.next || plan.next === void 0) return { changed: false };
7661
+ if (await identity(plan.target.path) !== plan.fileIdentity) throw new Error("配置在预览后发生变化,请重新运行;未写入。");
7662
+ return {
7663
+ changed: true,
7664
+ backup: await writeConfig(plan.target.path, plan.previous, plan.next, plan.fileIdentity)
7665
+ };
7666
+ }
6969
7667
  var package_exports = /* @__PURE__ */ __exportAll({ runPackage: () => runPackage });
6970
7668
  const RESET$3 = "\x1B[0m";
6971
7669
  const BOLD$1 = "\x1B[1m";
@@ -7007,6 +7705,12 @@ async function dispatchPackage(args, globalOpts) {
7007
7705
  case "up":
7008
7706
  await handleUpdate(args.slice(1), api, globalOpts);
7009
7707
  break;
7708
+ case "marketplace":
7709
+ await handleMarketplace(args.slice(1), globalOpts);
7710
+ break;
7711
+ case "archive-headers":
7712
+ printArchiveHeaders(globalOpts);
7713
+ break;
7010
7714
  case "search":
7011
7715
  await handleSearch(args.slice(1), api, globalOpts);
7012
7716
  break;
@@ -7136,7 +7840,7 @@ function buildTargetSummaryLines(targets, installMode) {
7136
7840
  const subagentNames = targets.filter((t) => t.subagent).map(targetDisplayName);
7137
7841
  const { universal, symlinked } = splitAgentsByType(rootAgents);
7138
7842
  if (installMode === "symlink") {
7139
- if (universal.length > 0) lines.push(` ${import_picocolors.default.green("共享目录:")} ${formatList(universal, 3)}`);
7843
+ if (universal.length > 0) lines.push(` ${import_picocolors.default.green("共享目录(共用 .agents/skills):")} ${formatList(universal, 3)}`);
7140
7844
  if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("符号链接 →")} ${formatList(symlinked)}`);
7141
7845
  if (subagentNames.length > 0) lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList(subagentNames)}`);
7142
7846
  } else {
@@ -7193,8 +7897,47 @@ async function handleInstall(args, api, globalOpts) {
7193
7897
  console.log(` ${DIM$3}• ${skill.displayName || skill.slug}${RESET$3} ${DIM$3}${ref}${RESET$3}`);
7194
7898
  }
7195
7899
  console.log();
7900
+ let mcpNames = [];
7901
+ let mcpEntries = [];
7902
+ try {
7903
+ mcpNames = ((await api.getPackageMcps(namespace, slug))?.mcps ?? []).filter((entry) => entry?.mcpEntryId).map((entry) => entry.lastKnownName).filter((name) => typeof name === "string" && name.length > 0);
7904
+ } catch {}
7905
+ if (mcpNames.length > 0) {
7906
+ try {
7907
+ const { catalogEntries } = await import("./_chunks/registry.mjs");
7908
+ const { registry } = resolveConfig(globalOpts);
7909
+ mcpEntries = await catalogEntries(registry, mcpNames);
7910
+ } catch {}
7911
+ console.log(`${BOLD$1}关联 MCP(${mcpNames.length}):${RESET$3}`);
7912
+ for (const name of mcpNames) {
7913
+ const entry = mcpEntries.find((item) => item.registrationName === name);
7914
+ const keys = entry ? variables(entry.config) : [];
7915
+ console.log(` ${DIM$3}• ${name}${keys.length > 0 ? ` 变量:${keys.join("、")}` : ""}${RESET$3}`);
7916
+ }
7917
+ if (mcpEntries.length === 0) console.log(` ${YELLOW$1}变量信息暂不可用,安装时会读取组合包内容。${RESET$3}`);
7918
+ console.log();
7919
+ }
7920
+ const bindingsByServer = {};
7921
+ const envNames = [];
7922
+ const interactive = !isYes && !!process.stdin.isTTY && !!process.stdout.isTTY;
7923
+ const promptBinding = async (entry, key) => {
7924
+ let value;
7925
+ do {
7926
+ value = await password({ message: `${entry.registrationName}.${key}(输入不回显)` });
7927
+ if (isCancel(value)) {
7928
+ cancel("安装已取消");
7929
+ process.exit(0);
7930
+ }
7931
+ } while (!String(value).trim());
7932
+ const envName = `${entry.registrationName}_${key}`.replace(/[^A-Za-z0-9_]/g, "_").toUpperCase().replace(/^(?=[0-9])/, "_");
7933
+ process.env[envName] = String(value);
7934
+ (bindingsByServer[entry.registrationName] ??= {})[key] = envName;
7935
+ if (!envNames.includes(envName)) envNames.push(envName);
7936
+ };
7937
+ if (interactive) for (const entry of mcpEntries) for (const key of variables(entry.config)) await promptBinding(entry, key);
7196
7938
  let targetAgents;
7197
7939
  let installGlobally;
7940
+ let mcpGloballyOverride;
7198
7941
  let installMode;
7199
7942
  let eveSubagentTargets = [void 0];
7200
7943
  if (requestedAgents) {
@@ -7240,7 +7983,11 @@ async function handleInstall(args, api, globalOpts) {
7240
7983
  targetAgents = ["eve"];
7241
7984
  console.log(`${DIM$3}安装到:eve agent${RESET$3}`);
7242
7985
  } else {
7243
- const selected = await selectAgentsInteractive({ global: false });
7986
+ const selected = await selectAgentsInteractive({
7987
+ global: false,
7988
+ useMemory: false,
7989
+ preselectDetected: false
7990
+ });
7244
7991
  if (isCancel(selected)) {
7245
7992
  cancel("安装已取消");
7246
7993
  process.exit(0);
@@ -7262,7 +8009,11 @@ async function handleInstall(args, api, globalOpts) {
7262
8009
  const firstAgent = installedAgents[0];
7263
8010
  console.log(`${DIM$3}安装到:${agents[firstAgent].displayName}${RESET$3}`);
7264
8011
  } else {
7265
- const selected = await selectAgentsInteractive({ global: false });
8012
+ const selected = await selectAgentsInteractive({
8013
+ global: false,
8014
+ useMemory: false,
8015
+ preselectDetected: false
8016
+ });
7266
8017
  if (isCancel(selected)) {
7267
8018
  cancel("安装已取消");
7268
8019
  process.exit(0);
@@ -7339,7 +8090,7 @@ async function handleInstall(args, api, globalOpts) {
7339
8090
  const overwriteChecks = await Promise.all(skills.flatMap((skill) => installTargets.map(async (target) => ({
7340
8091
  skillName: skill.slug,
7341
8092
  target,
7342
- installed: await isSkillInstalled(packageSkillName(skill.namespace || namespace, skill.slug), target.agent, {
8093
+ installed: await isSkillInstalled(skill.slug, target.agent, {
7343
8094
  global: installGlobally,
7344
8095
  eveSubagent: target.subagent
7345
8096
  })
@@ -7349,6 +8100,17 @@ async function handleInstall(args, api, globalOpts) {
7349
8100
  if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
7350
8101
  overwriteStatus.get(skillName).set(targetKey(target), installed);
7351
8102
  }
8103
+ if (envNames.length > 0 && !installGlobally) {
8104
+ const choice = await confirm({
8105
+ message: "项目级配置不能保存明文变量。是否将关联 MCP 写入用户级配置并保存输入值?",
8106
+ initialValue: true
8107
+ });
8108
+ if (isCancel(choice)) {
8109
+ cancel("安装已取消");
8110
+ process.exit(0);
8111
+ }
8112
+ mcpGloballyOverride = !!choice;
8113
+ }
7352
8114
  const summaryLines = [];
7353
8115
  for (const skill of skills) {
7354
8116
  if (summaryLines.length > 0) summaryLines.push("");
@@ -7366,41 +8128,128 @@ async function handleInstall(args, api, globalOpts) {
7366
8128
  process.exit(0);
7367
8129
  }
7368
8130
  }
7369
- const { runAdd } = await Promise.resolve().then(() => add_exports);
8131
+ const mcpGlobally = mcpGloballyOverride ?? installGlobally;
8132
+ const { mkdtemp, readdir, realpath, rm } = await import("node:fs/promises");
8133
+ const { join } = await import("node:path");
8134
+ const { extractZip } = await Promise.resolve().then(() => zip_exports);
8135
+ const { discoverSkills } = await import("./_chunks/skills.mjs").then((n) => n.skills_exports);
8136
+ const { installSkillForAgent } = await Promise.resolve().then(() => installer_exports);
7370
8137
  const installedSkills = [];
7371
8138
  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}`);
8139
+ const universalTarget = targetAgents.find((agent) => isUniversalAgent(agent));
8140
+ const installTargetsForInstall = [
8141
+ ...universalTarget ? [{ agent: universalTarget }] : [],
8142
+ ...targetAgents.filter((agent) => agent !== "eve" && !isUniversalAgent(agent)).map((agent) => ({ agent })),
8143
+ ...targetAgents.includes("eve") ? eveSubagentTargets.map((subagent) => ({
8144
+ agent: "eve",
8145
+ subagent
8146
+ })) : []
8147
+ ];
8148
+ const work = await mkdtemp(join(await realpath(os$1.tmpdir()), "bfi-package-"));
8149
+ let mcpFile;
8150
+ try {
8151
+ const archive = join(work, "package.zip");
8152
+ const unpack = join(work, "content");
8153
+ const spinner$2 = spinner();
8154
+ spinner$2.start("正在下载组合包...");
8155
+ await api.downloadPackage(namespace, slug, archive);
8156
+ spinner$2.message("正在解压组合包...");
8157
+ await extractZip(archive, unpack);
8158
+ spinner$2.stop("组合包已就绪,开始安装技能");
8159
+ const entries = await readdir(unpack, { withFileTypes: true });
8160
+ if (entries.some((entry) => entry.isFile() && entry.name === "mcp.json")) mcpFile = join(unpack, "mcp.json");
8161
+ const directories = entries.filter((entry) => entry.isDirectory());
8162
+ if (directories.length === 0) throw new Error("组合包中没有技能目录");
8163
+ const installSpinner = spinner();
8164
+ installSpinner.start("正在安装技能...");
8165
+ const failures = [];
8166
+ for (const directory of directories) {
8167
+ const skill = skills.find((entry) => entry.slug === directory.name);
8168
+ const skillRef = skill ? `@${skill.namespace || namespace}/${skill.slug}` : directory.name;
8169
+ installSpinner.message(`正在安装 ${directory.name}`);
8170
+ try {
8171
+ const found = (await discoverSkills(join(unpack, directory.name)))[0];
8172
+ if (!found) throw new Error("未找到有效的 SKILL.md");
8173
+ const results = [];
8174
+ for (const target of installTargetsForInstall) results.push(await installSkillForAgent(found, target.agent, {
8175
+ global: installGlobally,
8176
+ cwd: process.cwd(),
8177
+ mode: installMode,
8178
+ eveSubagent: target.subagent
8179
+ }));
8180
+ const failed = results.filter((result) => !result.success);
8181
+ if (failed.length > 0) throw new Error(failed.map((result) => result.error).filter(Boolean).join(";") || "安装失败");
8182
+ installedSkills.push({
8183
+ ref: skillRef,
8184
+ name: found.name || directory.name
8185
+ });
8186
+ } catch (error) {
8187
+ failures.push(`${directory.name}:${error.message}`);
8188
+ }
8189
+ }
8190
+ installSpinner.stop(`已安装 ${installedSkills.length}/${directories.length} 个技能`);
8191
+ for (const failure of failures) log.error(failure);
8192
+ } catch (error) {
8193
+ await rm(work, {
8194
+ recursive: true,
8195
+ force: true
8196
+ });
8197
+ throw error;
8198
+ }
8199
+ let mcpJsonText;
8200
+ if (mcpFile) try {
8201
+ mcpJsonText = await readConfig(mcpFile);
8202
+ } catch {
8203
+ mcpJsonText = void 0;
8204
+ }
8205
+ await rm(work, {
8206
+ recursive: true,
8207
+ force: true
8208
+ });
8209
+ if (installedSkills.length > 0) {
8210
+ let entries = [];
7385
8211
  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}`);
8212
+ if (mcpJsonText) entries = entriesFromJson(readDocument(mcpJsonText, "jsonc"));
8213
+ } catch {
8214
+ entries = [];
7403
8215
  }
8216
+ if (entries.length === 0) entries = mcpEntries;
8217
+ if (entries.length > 0) {
8218
+ const scope = mcpGlobally ? "user" : "project";
8219
+ if (interactive) {
8220
+ for (const entry of entries) for (const key of variables(entry.config)) if (!bindingsByServer[entry.registrationName]?.[key]) await promptBinding(entry, key);
8221
+ }
8222
+ const supported = nonEveAgents.filter((agent) => agentIds.includes(agent));
8223
+ const skipped = nonEveAgents.filter((agent) => !agentIds.includes(agent));
8224
+ const lines = [];
8225
+ let installedMcps = 0;
8226
+ for (const agent of supported) try {
8227
+ const plan = await planInstall(targetFor(agent, scope), entries, {
8228
+ bindingsByServer,
8229
+ materialize: scope === "user",
8230
+ environment: process.env
8231
+ });
8232
+ const result = await applyPlan(plan);
8233
+ const added = plan.changes.filter((change) => change.action === "add").length;
8234
+ const replaced = plan.changes.filter((change) => change.action === "replace").length;
8235
+ const unchanged = plan.changes.filter((change) => change.action === "unchanged").length;
8236
+ lines.push(`${agent.padEnd(14)} ${[
8237
+ added > 0 ? `新增 ${added}` : "",
8238
+ replaced > 0 ? `更新 ${replaced}` : "",
8239
+ unchanged > 0 ? `未变 ${unchanged}` : "",
8240
+ result.changed ? "" : "无变化"
8241
+ ].filter(Boolean).join(" · ")}`);
8242
+ installedMcps++;
8243
+ } catch (error) {
8244
+ lines.push(`${agent.padEnd(14)} ✗ ${error.message}`);
8245
+ }
8246
+ note(lines.join("\n"), `关联 MCP(${installedMcps}/${supported.length} 个 Agent)`);
8247
+ if (skipped.length > 0) log.info(`未支持自动写入 MCP 的 Agent:${skipped.join(", ")}`);
8248
+ if (scope === "project") {
8249
+ const projectVariables = [...new Set(entries.flatMap((entry) => variables(entry.config).map((key) => bindingsByServer[entry.registrationName]?.[key] ?? key)))];
8250
+ if (projectVariables.length > 0) log.info(`项目级 MCP 配置只保存变量引用:${projectVariables.map((name) => "${" + name + "}").join("、")}。请在启动 Agent 前导出这些环境变量;需要保存输入值时可在提示中选择写入用户级配置。`);
8251
+ }
8252
+ } else if (mcpNames.length > 0) log.warn("组合声明的 MCP 未能加载,请稍后重试或使用 bfi mcp install 单独安装。");
7404
8253
  }
7405
8254
  if (installedSkills.length === 0) {
7406
8255
  console.log(`${YELLOW$1}没有确认安装成功的技能,未创建包安装记录${RESET$3}`);
@@ -7417,14 +8266,14 @@ async function handleInstall(args, api, globalOpts) {
7417
8266
  version: pkgInfo.version || "latest",
7418
8267
  skillCount: installedSkills.length
7419
8268
  };
7420
- for (const skillRef of installedSkills) {
8269
+ for (const { ref: skillRef, name } of installedSkills) {
7421
8270
  const identity = parseSkillRef(skillRef);
7422
8271
  const existing = lock.skills[skillRef];
7423
8272
  lock.skills[skillRef] = {
7424
8273
  ...existing,
7425
8274
  namespace: identity.namespace,
7426
8275
  slug: identity.slug,
7427
- installedName: packageSkillName(identity.namespace, identity.slug),
8276
+ installedName: name,
7428
8277
  version: existing?.version || "latest",
7429
8278
  package: {
7430
8279
  namespace,
@@ -7433,15 +8282,7 @@ async function handleInstall(args, api, globalOpts) {
7433
8282
  };
7434
8283
  }
7435
8284
  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, {
8285
+ for (const { ref: skillRef } of installedSkills) await reportInstallEventForSkill(api, {
7445
8286
  skillId: skills.find((entry) => `@${entry.namespace || namespace}/${entry.slug}` === skillRef)?.skillId,
7446
8287
  agentTarget: targetAgents[0]
7447
8288
  });
@@ -7492,10 +8333,7 @@ async function handleUpdate(args, api, globalOpts) {
7492
8333
  const pkgs = lock.packages || {};
7493
8334
  if (pkgRef) {
7494
8335
  const { namespace, slug } = parseSkillRef(pkgRef, globalOpts.namespace);
7495
- if (!pkgs[`@${namespace}/${slug}`]) {
7496
- console.log(`${YELLOW$1}包未安装:@${namespace}/${slug}${RESET$3}`);
7497
- return;
7498
- }
8336
+ if (!pkgs[`@${namespace}/${slug}`]) console.log(`${DIM$3}未找到安装记录,按首次安装重新执行:@${namespace}/${slug}${RESET$3}`);
7499
8337
  packagesToUpdate.push({
7500
8338
  namespace,
7501
8339
  slug
@@ -7530,6 +8368,12 @@ async function handleUpdate(args, api, globalOpts) {
7530
8368
  }
7531
8369
  console.log(`${GREEN$2}✓ 包更新完成${RESET$3}`);
7532
8370
  }
8371
+ async function handleMarketplace(args, globalOpts) {
8372
+ const [action, namespace] = args;
8373
+ if (action !== "add" || !namespace) throw new Error("用法:bfi skills package marketplace add <namespace>");
8374
+ const result = await addClaudeMarketplace(namespace, globalOpts);
8375
+ console.log(JSON.stringify(result, null, 2));
8376
+ }
7533
8377
  async function handleSearch(args, api, globalOpts) {
7534
8378
  const query = args.filter((a) => !a.startsWith("--")).join(" ") || void 0;
7535
8379
  const json = args.includes("--json");
@@ -9822,6 +10666,11 @@ ${BOLD}━━━ 技能包管理 ━━━${RESET}
9822
10666
  --prepare-native <dir> 准备完整原生目录和市场索引,审阅后交给宿主安装;不注册、不转技能
9823
10667
  --agent <a> 目标 agent(如 claude-code、codex、zcode)
9824
10668
  --global 安装到主目录(跨项目可用)
10669
+ skills package download <ref> --as plugin [--output <file>] [--format zip|tgz|tar.gz] 下载并校验插件归档(不安装、不需要 --agent)
10670
+ skills package prepare <ref> --as plugin --agent <a> [--dir <dir>] 生成版本化原生目录并更新市场索引
10671
+ skills package update <ref> --as plugin [--agent <a>] 按默认安装版本重新下载/交接,不依赖安装记录
10672
+ skills package marketplace add <namespace> 将命名空间注册为 Claude Code 市场(settings.json)
10673
+ skills package archive-headers 输出 marketplace/归档所需的认证头(供 headersHelper 调用)
9825
10674
 
9826
10675
  ${BOLD}━━━ 文档 ━━━${RESET}
9827
10676
  docs upload <file> 上传文档
@@ -10288,4 +11137,4 @@ main().catch((e) => {
10288
11137
  console.error(`✗ ${e?.message || e}`);
10289
11138
  process.exit(1);
10290
11139
  });
10291
- export { SkillHubAPI, getAgentBaseDir };
11140
+ export { InstallationClient, SkillHubAPI, agentIds, applyPlan, checkPath, credentialForRegistry, decodeServer, getAgentBaseDir, loadTarget, planInstall, planRemove, readConfig, readDocument, registryUrl, serversIn, targetFor, writeConfig };