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/README.md +2 -2
- package/dist/_chunks/command.mjs +1 -3
- package/dist/_chunks/libs/@clack/core.mjs +28 -2
- package/dist/_chunks/libs/@clack/prompts.mjs +32 -2
- package/dist/_chunks/mcp.mjs +19 -87
- package/dist/_chunks/receipt.mjs +1 -1
- package/dist/_chunks/registry.mjs +83 -0
- package/dist/cli.mjs +799 -133
- package/package.json +1 -1
- package/dist/_chunks/files.mjs +0 -89
- package/dist/_chunks/manager.mjs +0 -436
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: [
|
|
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$
|
|
3604
|
-
spinner$
|
|
3621
|
+
const spinner$4 = spinner();
|
|
3622
|
+
spinner$4.start("正在解析源...");
|
|
3605
3623
|
const parsed = parseSource(source);
|
|
3606
|
-
spinner$
|
|
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$
|
|
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$
|
|
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$
|
|
3648
|
+
spinner$4.start("正在验证本地路径...");
|
|
3631
3649
|
if (!existsSync(parsed.localPath)) {
|
|
3632
|
-
spinner$
|
|
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$
|
|
3637
|
-
spinner$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
3667
|
-
spinner$
|
|
3668
|
-
spinner$
|
|
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$
|
|
3676
|
-
spinner$
|
|
3677
|
-
spinner$
|
|
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$
|
|
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$
|
|
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$
|
|
3821
|
+
spinner$4.start("正在加载 Agent...");
|
|
3804
3822
|
const installedAgents = await detectInstalledAgents();
|
|
3805
3823
|
const totalAgents = Object.keys(agents).length;
|
|
3806
|
-
spinner$
|
|
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$
|
|
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$
|
|
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$
|
|
4716
|
-
spinner$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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("
|
|
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$
|
|
7224
|
-
spinner$
|
|
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$
|
|
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({
|
|
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({
|
|
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(
|
|
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
|
|
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
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
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
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
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:
|
|
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
|
-
|
|
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 };
|