bfi-cli 0.1.29 → 0.1.35
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/_chunks/agents.mjs +733 -57
- package/dist/_chunks/agents2.mjs +66 -0
- package/dist/_chunks/libs/@clack/prompts.mjs +44 -1
- package/dist/_chunks/skills.mjs +445 -0
- package/dist/cli.mjs +613 -1631
- package/package.json +1 -1
- package/skills/skillhub-skills/SKILL.md +187 -0
- package/skills/bfi-skill/SKILL.md +0 -176
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { agents } from "./agents.mjs";
|
|
2
|
+
import { loadGlobalConfig } from "../cli.mjs";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
const CACHE_FILE = path.join(os.homedir(), ".bfi", "agents-cache.json");
|
|
7
|
+
const CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
8
|
+
function loadCache() {
|
|
9
|
+
try {
|
|
10
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
11
|
+
const raw = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8"));
|
|
12
|
+
if (Date.now() - raw.timestamp < CACHE_TTL_MS) return raw;
|
|
13
|
+
}
|
|
14
|
+
} catch {}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function saveCache(agents) {
|
|
18
|
+
try {
|
|
19
|
+
const dir = path.dirname(CACHE_FILE);
|
|
20
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
21
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify({
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
agents
|
|
24
|
+
}));
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
function mapToAgentConfig(a) {
|
|
28
|
+
return {
|
|
29
|
+
name: a.name,
|
|
30
|
+
displayName: a.displayName || a.name,
|
|
31
|
+
skillsDir: a.skillsDir,
|
|
32
|
+
globalSkillsDir: a.globalSkillsDir || void 0,
|
|
33
|
+
detectInstalled: async () => false
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function mapCustomEntry(name, entry) {
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
displayName: entry.displayName || name,
|
|
40
|
+
skillsDir: entry.skillsDir,
|
|
41
|
+
globalSkillsDir: entry.globalSkillsDir,
|
|
42
|
+
detectInstalled: async () => false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function resolveAgents(api) {
|
|
46
|
+
const merged = /* @__PURE__ */ new Map();
|
|
47
|
+
for (const [name, config] of Object.entries(agents)) merged.set(name, config);
|
|
48
|
+
if (api) {
|
|
49
|
+
let remoteAgents = null;
|
|
50
|
+
const cache = loadCache();
|
|
51
|
+
if (cache) remoteAgents = cache.agents;
|
|
52
|
+
else try {
|
|
53
|
+
const resp = await api.getAgents();
|
|
54
|
+
const list = resp.data || resp;
|
|
55
|
+
if (Array.isArray(list)) {
|
|
56
|
+
remoteAgents = list.map(mapToAgentConfig);
|
|
57
|
+
saveCache(remoteAgents);
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
if (remoteAgents) for (const agent of remoteAgents) merged.set(agent.name, agent);
|
|
61
|
+
}
|
|
62
|
+
const config = loadGlobalConfig();
|
|
63
|
+
if (config?.customAgents) for (const [name, entry] of Object.entries(config.customAgents)) merged.set(name, mapCustomEntry(name, entry));
|
|
64
|
+
return Object.fromEntries(merged);
|
|
65
|
+
}
|
|
66
|
+
export { resolveAgents };
|
|
@@ -1,6 +1,49 @@
|
|
|
1
|
+
import { __exportAll } from "../../rolldown-runtime.mjs";
|
|
1
2
|
import { a, block, fastStringWidth, getColumns, getRows, n$1, r, require_src, settings, u$2, wrapAnsi, wrapTextWithPrefix } from "./core.mjs";
|
|
2
3
|
import { styleText } from "node:util";
|
|
3
4
|
import process$1 from "node:process";
|
|
5
|
+
var dist_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
MULTISELECT_INSTRUCTIONS: () => MULTISELECT_INSTRUCTIONS,
|
|
7
|
+
SELECT_INSTRUCTIONS: () => SELECT_INSTRUCTIONS,
|
|
8
|
+
S_BAR: () => S_BAR,
|
|
9
|
+
S_BAR_END: () => S_BAR_END,
|
|
10
|
+
S_BAR_H: () => S_BAR_H,
|
|
11
|
+
S_BAR_START: () => S_BAR_START,
|
|
12
|
+
S_CHECKBOX_ACTIVE: () => S_CHECKBOX_ACTIVE,
|
|
13
|
+
S_CHECKBOX_INACTIVE: () => S_CHECKBOX_INACTIVE,
|
|
14
|
+
S_CHECKBOX_SELECTED: () => S_CHECKBOX_SELECTED,
|
|
15
|
+
S_CONNECT_LEFT: () => S_CONNECT_LEFT,
|
|
16
|
+
S_CORNER_BOTTOM_LEFT: () => S_CORNER_BOTTOM_LEFT,
|
|
17
|
+
S_CORNER_BOTTOM_RIGHT: () => S_CORNER_BOTTOM_RIGHT,
|
|
18
|
+
S_CORNER_TOP_RIGHT: () => S_CORNER_TOP_RIGHT,
|
|
19
|
+
S_ERROR: () => S_ERROR,
|
|
20
|
+
S_INFO: () => S_INFO,
|
|
21
|
+
S_RADIO_ACTIVE: () => S_RADIO_ACTIVE,
|
|
22
|
+
S_RADIO_INACTIVE: () => S_RADIO_INACTIVE,
|
|
23
|
+
S_STEP_ACTIVE: () => S_STEP_ACTIVE,
|
|
24
|
+
S_STEP_CANCEL: () => S_STEP_CANCEL,
|
|
25
|
+
S_STEP_ERROR: () => S_STEP_ERROR,
|
|
26
|
+
S_STEP_SUBMIT: () => S_STEP_SUBMIT,
|
|
27
|
+
S_SUCCESS: () => S_SUCCESS,
|
|
28
|
+
S_WARN: () => S_WARN,
|
|
29
|
+
cancel: () => cancel,
|
|
30
|
+
confirm: () => confirm,
|
|
31
|
+
formatInstructionFooter: () => formatInstructionFooter,
|
|
32
|
+
groupMultiselect: () => groupMultiselect,
|
|
33
|
+
intro: () => intro,
|
|
34
|
+
isCI: () => isCI,
|
|
35
|
+
limitOptions: () => limitOptions,
|
|
36
|
+
log: () => log,
|
|
37
|
+
multiselect: () => multiselect,
|
|
38
|
+
note: () => note,
|
|
39
|
+
outro: () => outro,
|
|
40
|
+
select: () => select,
|
|
41
|
+
spinner: () => spinner,
|
|
42
|
+
symbol: () => symbol,
|
|
43
|
+
symbolBar: () => symbolBar,
|
|
44
|
+
unicode: () => unicode,
|
|
45
|
+
unicodeOr: () => unicodeOr
|
|
46
|
+
});
|
|
4
47
|
var import_src = require_src();
|
|
5
48
|
function isUnicodeSupported() {
|
|
6
49
|
if (process$1.platform !== "win32") return process$1.env.TERM !== "linux";
|
|
@@ -536,4 +579,4 @@ ${b}
|
|
|
536
579
|
}).prompt();
|
|
537
580
|
};
|
|
538
581
|
`${styleText("gray", S_BAR)}`;
|
|
539
|
-
export { cancel, confirm, groupMultiselect, intro, log, multiselect, note, outro, select, spinner };
|
|
582
|
+
export { cancel, confirm, dist_exports, groupMultiselect, intro, log, multiselect, note, outro, select, spinner };
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { __exportAll, __toESM } from "./rolldown-runtime.mjs";
|
|
2
|
+
import { require_picocolors } from "./libs/picocolors.mjs";
|
|
3
|
+
import { basename, dirname, join, normalize, relative, resolve, sep } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
|
|
7
|
+
import { parse } from "yaml";
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
|
|
10
|
+
const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
|
|
11
|
+
const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
|
|
12
|
+
const SIMPLE_ESC_RE = /\x1b[\x20-\x7e]/g;
|
|
13
|
+
const C1_RE = /[\x80-\x9f]/g;
|
|
14
|
+
const CONTROL_RE = /[\x00-\x06\x07\x08\x0b\x0c\x0d-\x1a\x1c-\x1f\x7f]/g;
|
|
15
|
+
function stripTerminalEscapes(str) {
|
|
16
|
+
return str.replace(OSC_RE, "").replace(DCS_PM_APC_RE, "").replace(CSI_RE, "").replace(SIMPLE_ESC_RE, "").replace(C1_RE, "").replace(CONTROL_RE, "");
|
|
17
|
+
}
|
|
18
|
+
function sanitizeMetadata(str) {
|
|
19
|
+
return stripTerminalEscapes(str).replace(/[\r\n]+/g, " ").trim();
|
|
20
|
+
}
|
|
21
|
+
function parseFrontmatter(raw) {
|
|
22
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
23
|
+
if (!match) return {
|
|
24
|
+
data: {},
|
|
25
|
+
content: raw
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
data: parse(match[1]) ?? {},
|
|
29
|
+
content: match[2] ?? ""
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function isContainedIn(targetPath, basePath) {
|
|
33
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
34
|
+
const normalizedTarget = normalize(resolve(targetPath));
|
|
35
|
+
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
36
|
+
}
|
|
37
|
+
function isValidRelativePath(path) {
|
|
38
|
+
return path.startsWith("./");
|
|
39
|
+
}
|
|
40
|
+
async function getPluginSkillPaths(basePath) {
|
|
41
|
+
const searchDirs = [];
|
|
42
|
+
const addPluginSkillPaths = (pluginBase, skills) => {
|
|
43
|
+
if (!isContainedIn(pluginBase, basePath)) return;
|
|
44
|
+
if (skills && skills.length > 0) for (const skillPath of skills) {
|
|
45
|
+
if (!isValidRelativePath(skillPath)) continue;
|
|
46
|
+
const skillDir = dirname(join(pluginBase, skillPath));
|
|
47
|
+
if (isContainedIn(skillDir, basePath)) searchDirs.push(skillDir);
|
|
48
|
+
}
|
|
49
|
+
searchDirs.push(join(pluginBase, "skills"));
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8");
|
|
53
|
+
const manifest = JSON.parse(content);
|
|
54
|
+
const pluginRoot = manifest.metadata?.pluginRoot;
|
|
55
|
+
if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) {
|
|
56
|
+
if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
|
|
57
|
+
if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
|
|
58
|
+
addPluginSkillPaths(join(basePath, pluginRoot ?? "", plugin.source ?? ""), plugin.skills);
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
try {
|
|
62
|
+
const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8");
|
|
63
|
+
addPluginSkillPaths(basePath, JSON.parse(content).skills);
|
|
64
|
+
} catch {}
|
|
65
|
+
return searchDirs;
|
|
66
|
+
}
|
|
67
|
+
async function getPluginGroupings(basePath) {
|
|
68
|
+
const groupings = /* @__PURE__ */ new Map();
|
|
69
|
+
try {
|
|
70
|
+
const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8");
|
|
71
|
+
const manifest = JSON.parse(content);
|
|
72
|
+
const pluginRoot = manifest.metadata?.pluginRoot;
|
|
73
|
+
if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) {
|
|
74
|
+
if (!plugin.name) continue;
|
|
75
|
+
if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
|
|
76
|
+
if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
|
|
77
|
+
const pluginBase = join(basePath, pluginRoot ?? "", plugin.source ?? "");
|
|
78
|
+
if (!isContainedIn(pluginBase, basePath)) continue;
|
|
79
|
+
if (plugin.skills && plugin.skills.length > 0) for (const skillPath of plugin.skills) {
|
|
80
|
+
if (!isValidRelativePath(skillPath)) continue;
|
|
81
|
+
const skillDir = join(pluginBase, skillPath);
|
|
82
|
+
if (isContainedIn(skillDir, basePath)) groupings.set(resolve(skillDir), plugin.name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {}
|
|
86
|
+
try {
|
|
87
|
+
const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8");
|
|
88
|
+
const manifest = JSON.parse(content);
|
|
89
|
+
if (manifest.name && manifest.skills && manifest.skills.length > 0) for (const skillPath of manifest.skills) {
|
|
90
|
+
if (!isValidRelativePath(skillPath)) continue;
|
|
91
|
+
const skillDir = join(basePath, skillPath);
|
|
92
|
+
if (isContainedIn(skillDir, basePath)) groupings.set(resolve(skillDir), manifest.name);
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
return groupings;
|
|
96
|
+
}
|
|
97
|
+
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
98
|
+
const AGENTS_DIR = ".agents";
|
|
99
|
+
const SKILL_LOCK_FILE = ".skill-lock.json";
|
|
100
|
+
const SKILL_LOCK_CURRENT_VERSION = 3;
|
|
101
|
+
const LOCAL_LOCK_FILE = "skills-lock.json";
|
|
102
|
+
const LOCAL_LOCK_CURRENT_VERSION = 1;
|
|
103
|
+
function getSkillLockPath() {
|
|
104
|
+
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
105
|
+
if (xdgStateHome) return join(xdgStateHome, "skills", SKILL_LOCK_FILE);
|
|
106
|
+
return join(homedir(), AGENTS_DIR, SKILL_LOCK_FILE);
|
|
107
|
+
}
|
|
108
|
+
function createEmptySkillLockFile() {
|
|
109
|
+
return {
|
|
110
|
+
version: SKILL_LOCK_CURRENT_VERSION,
|
|
111
|
+
skills: {},
|
|
112
|
+
dismissed: {}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function readSkillLock() {
|
|
116
|
+
const lockPath = getSkillLockPath();
|
|
117
|
+
try {
|
|
118
|
+
const content = await readFile(lockPath, "utf-8");
|
|
119
|
+
const parsed = JSON.parse(content);
|
|
120
|
+
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptySkillLockFile();
|
|
121
|
+
if (parsed.version < SKILL_LOCK_CURRENT_VERSION) return createEmptySkillLockFile();
|
|
122
|
+
return parsed;
|
|
123
|
+
} catch {
|
|
124
|
+
return createEmptySkillLockFile();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function writeSkillLock(lock) {
|
|
128
|
+
const lockPath = getSkillLockPath();
|
|
129
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
130
|
+
await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
131
|
+
}
|
|
132
|
+
let _ghWarningShown = false;
|
|
133
|
+
function getGitHubToken() {
|
|
134
|
+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
135
|
+
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
|
|
136
|
+
if (!_ghWarningShown) {
|
|
137
|
+
process.stderr.write(`${import_picocolors.default.yellow("│")} ${import_picocolors.default.yellow("GitHub rate limit reached")} — using your ${import_picocolors.default.cyan("gh")} login to continue.\n${import_picocolors.default.yellow("│")} ${import_picocolors.default.dim(`Tip: set ${import_picocolors.default.cyan("GITHUB_TOKEN")} to avoid this prompt, or use ${import_picocolors.default.cyan("--full-depth")} to clone instead.\n`)}`);
|
|
138
|
+
_ghWarningShown = true;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const token = execSync("gh auth token", {
|
|
142
|
+
encoding: "utf-8",
|
|
143
|
+
stdio: [
|
|
144
|
+
"pipe",
|
|
145
|
+
"pipe",
|
|
146
|
+
"pipe"
|
|
147
|
+
]
|
|
148
|
+
}).trim();
|
|
149
|
+
if (token) return token;
|
|
150
|
+
} catch {}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
async function addSkillToLock(skillName, entry) {
|
|
154
|
+
const lock = await readSkillLock();
|
|
155
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
156
|
+
const existingEntry = lock.skills[skillName];
|
|
157
|
+
lock.skills[skillName] = {
|
|
158
|
+
...entry,
|
|
159
|
+
installedAt: existingEntry?.installedAt ?? now,
|
|
160
|
+
updatedAt: now
|
|
161
|
+
};
|
|
162
|
+
await writeSkillLock(lock);
|
|
163
|
+
}
|
|
164
|
+
async function removeSkillFromLock(skillName) {
|
|
165
|
+
const lock = await readSkillLock();
|
|
166
|
+
if (!(skillName in lock.skills)) return false;
|
|
167
|
+
delete lock.skills[skillName];
|
|
168
|
+
await writeSkillLock(lock);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
async function getSkillFromLock(skillName) {
|
|
172
|
+
return (await readSkillLock()).skills[skillName] ?? null;
|
|
173
|
+
}
|
|
174
|
+
async function getAllLockedSkills() {
|
|
175
|
+
return (await readSkillLock()).skills;
|
|
176
|
+
}
|
|
177
|
+
async function isPromptDismissed(promptKey) {
|
|
178
|
+
return (await readSkillLock()).dismissed?.[promptKey] === true;
|
|
179
|
+
}
|
|
180
|
+
async function dismissPrompt(promptKey) {
|
|
181
|
+
const lock = await readSkillLock();
|
|
182
|
+
if (!lock.dismissed) lock.dismissed = {};
|
|
183
|
+
lock.dismissed[promptKey] = true;
|
|
184
|
+
await writeSkillLock(lock);
|
|
185
|
+
}
|
|
186
|
+
async function getLastSelectedAgents() {
|
|
187
|
+
return (await readSkillLock()).lastSelectedAgents;
|
|
188
|
+
}
|
|
189
|
+
async function saveSelectedAgents(agents) {
|
|
190
|
+
const lock = await readSkillLock();
|
|
191
|
+
lock.lastSelectedAgents = agents;
|
|
192
|
+
await writeSkillLock(lock);
|
|
193
|
+
}
|
|
194
|
+
function getLocalLockPath(cwd) {
|
|
195
|
+
return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
|
|
196
|
+
}
|
|
197
|
+
function createEmptyLocalLock() {
|
|
198
|
+
return {
|
|
199
|
+
version: LOCAL_LOCK_CURRENT_VERSION,
|
|
200
|
+
skills: {}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function readLocalLock(cwd) {
|
|
204
|
+
const lockPath = getLocalLockPath(cwd);
|
|
205
|
+
try {
|
|
206
|
+
const content = await readFile(lockPath, "utf-8");
|
|
207
|
+
const parsed = JSON.parse(content);
|
|
208
|
+
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
|
|
209
|
+
if (parsed.version < LOCAL_LOCK_CURRENT_VERSION) return createEmptyLocalLock();
|
|
210
|
+
return parsed;
|
|
211
|
+
} catch {
|
|
212
|
+
return createEmptyLocalLock();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function writeLocalLock(lock, cwd) {
|
|
216
|
+
const lockPath = getLocalLockPath(cwd);
|
|
217
|
+
const sortedSkills = {};
|
|
218
|
+
for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
|
|
219
|
+
const sorted = {
|
|
220
|
+
version: lock.version,
|
|
221
|
+
skills: sortedSkills
|
|
222
|
+
};
|
|
223
|
+
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
224
|
+
}
|
|
225
|
+
async function computeSkillFolderHash(skillDir) {
|
|
226
|
+
const files = [];
|
|
227
|
+
await collectFiles(skillDir, skillDir, files);
|
|
228
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
229
|
+
const hash = createHash("sha256");
|
|
230
|
+
for (const file of files) {
|
|
231
|
+
hash.update(file.relativePath);
|
|
232
|
+
hash.update(file.content);
|
|
233
|
+
}
|
|
234
|
+
return hash.digest("hex");
|
|
235
|
+
}
|
|
236
|
+
async function collectFiles(baseDir, currentDir, results) {
|
|
237
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
238
|
+
await Promise.all(entries.map(async (entry) => {
|
|
239
|
+
const fullPath = join(currentDir, entry.name);
|
|
240
|
+
if (entry.isDirectory()) {
|
|
241
|
+
if (entry.name === ".git" || entry.name === "node_modules") return;
|
|
242
|
+
await collectFiles(baseDir, fullPath, results);
|
|
243
|
+
} else if (entry.isFile()) {
|
|
244
|
+
const content = await readFile(fullPath);
|
|
245
|
+
const relativePath = relative(baseDir, fullPath).split("\\").join("/");
|
|
246
|
+
results.push({
|
|
247
|
+
relativePath,
|
|
248
|
+
content
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
254
|
+
const lock = await readLocalLock(cwd);
|
|
255
|
+
lock.skills[skillName] = entry;
|
|
256
|
+
await writeLocalLock(lock, cwd);
|
|
257
|
+
}
|
|
258
|
+
var skills_exports = /* @__PURE__ */ __exportAll({
|
|
259
|
+
discoverSkills: () => discoverSkills,
|
|
260
|
+
filterSkills: () => filterSkills,
|
|
261
|
+
getSkillDisplayName: () => getSkillDisplayName,
|
|
262
|
+
isSubpathSafe: () => isSubpathSafe,
|
|
263
|
+
parseSkillMd: () => parseSkillMd,
|
|
264
|
+
shouldInstallInternalSkills: () => shouldInstallInternalSkills
|
|
265
|
+
});
|
|
266
|
+
const SKIP_DIRS = [
|
|
267
|
+
"node_modules",
|
|
268
|
+
".git",
|
|
269
|
+
"dist",
|
|
270
|
+
"build",
|
|
271
|
+
"__pycache__"
|
|
272
|
+
];
|
|
273
|
+
const AGENT_PROJECT_SKILL_DIRS = [
|
|
274
|
+
".agents/skills",
|
|
275
|
+
".claude/skills",
|
|
276
|
+
".cline/skills",
|
|
277
|
+
".codebuddy/skills",
|
|
278
|
+
".codex/skills",
|
|
279
|
+
".commandcode/skills",
|
|
280
|
+
".continue/skills",
|
|
281
|
+
".github/skills",
|
|
282
|
+
".goose/skills",
|
|
283
|
+
".iflow/skills",
|
|
284
|
+
".junie/skills",
|
|
285
|
+
".kilocode/skills",
|
|
286
|
+
".kiro/skills",
|
|
287
|
+
".mux/skills",
|
|
288
|
+
".neovate/skills",
|
|
289
|
+
".opencode/skills",
|
|
290
|
+
".openhands/skills",
|
|
291
|
+
".pi/skills",
|
|
292
|
+
".qoder/skills",
|
|
293
|
+
".roo/skills",
|
|
294
|
+
".trae/skills",
|
|
295
|
+
".windsurf/skills",
|
|
296
|
+
".zcode/skills",
|
|
297
|
+
".zencoder/skills"
|
|
298
|
+
];
|
|
299
|
+
function normalizeSkillName(name) {
|
|
300
|
+
return name.toLowerCase().replace(/[\s_]+/g, "-");
|
|
301
|
+
}
|
|
302
|
+
function normalizeRelativePath(path) {
|
|
303
|
+
return path.split(sep).join("/").replace(/\/+/g, "/");
|
|
304
|
+
}
|
|
305
|
+
function shouldInstallInternalSkills() {
|
|
306
|
+
const envValue = process.env.INSTALL_INTERNAL_SKILLS;
|
|
307
|
+
return envValue === "1" || envValue === "true";
|
|
308
|
+
}
|
|
309
|
+
async function hasSkillMd(dir) {
|
|
310
|
+
try {
|
|
311
|
+
return (await stat(join(dir, "SKILL.md"))).isFile();
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function parseSkillMd(skillMdPath, options) {
|
|
317
|
+
try {
|
|
318
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
319
|
+
const { data } = parseFrontmatter(content);
|
|
320
|
+
if (!data.name || !data.description) return null;
|
|
321
|
+
if (typeof data.name !== "string" || typeof data.description !== "string") return null;
|
|
322
|
+
if (data.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
|
|
323
|
+
return {
|
|
324
|
+
name: sanitizeMetadata(data.name),
|
|
325
|
+
description: sanitizeMetadata(data.description),
|
|
326
|
+
path: dirname(skillMdPath),
|
|
327
|
+
rawContent: content,
|
|
328
|
+
metadata: data.metadata
|
|
329
|
+
};
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
|
|
335
|
+
if (depth > maxDepth) return [];
|
|
336
|
+
try {
|
|
337
|
+
const [hasSkill, entries] = await Promise.all([hasSkillMd(dir), readdir(dir, { withFileTypes: true }).catch(() => [])]);
|
|
338
|
+
const currentDir = hasSkill ? [dir] : [];
|
|
339
|
+
const subDirResults = await Promise.all(entries.filter((entry) => entry.isDirectory() && !SKIP_DIRS.includes(entry.name)).map((entry) => findSkillDirs(join(dir, entry.name), depth + 1, maxDepth)));
|
|
340
|
+
return [...currentDir, ...subDirResults.flat()];
|
|
341
|
+
} catch {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function isSubpathSafe(basePath, subpath) {
|
|
346
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
347
|
+
const normalizedTarget = normalize(resolve(join(basePath, subpath)));
|
|
348
|
+
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
349
|
+
}
|
|
350
|
+
async function discoverSkills(basePath, subpath, options) {
|
|
351
|
+
const skills = [];
|
|
352
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
353
|
+
const localLock = await readLocalLock(basePath);
|
|
354
|
+
const lockedSkillNames = new Set(Object.keys(localLock.skills).map(normalizeSkillName));
|
|
355
|
+
if (subpath && !isSubpathSafe(basePath, subpath)) throw new Error(`Invalid subpath: "${subpath}" resolves outside the repository directory. Subpath must not contain ".." segments that escape the base path.`);
|
|
356
|
+
const searchPath = subpath ? join(basePath, subpath) : basePath;
|
|
357
|
+
const pluginGroupings = await getPluginGroupings(searchPath);
|
|
358
|
+
const enhanceSkill = (skill) => {
|
|
359
|
+
const resolvedPath = resolve(skill.path);
|
|
360
|
+
if (pluginGroupings.has(resolvedPath)) skill.pluginName = pluginGroupings.get(resolvedPath);
|
|
361
|
+
return skill;
|
|
362
|
+
};
|
|
363
|
+
const isInstalledProjectSkill = (skill) => {
|
|
364
|
+
if (lockedSkillNames.size === 0) return false;
|
|
365
|
+
const relativeDir = normalizeRelativePath(relative(basePath, skill.path));
|
|
366
|
+
if (!AGENT_PROJECT_SKILL_DIRS.some((dir) => relativeDir === dir || relativeDir.startsWith(`${dir}/`))) return false;
|
|
367
|
+
const skillName = normalizeSkillName(skill.name);
|
|
368
|
+
const directoryName = normalizeSkillName(basename(skill.path));
|
|
369
|
+
return lockedSkillNames.has(skillName) || lockedSkillNames.has(directoryName);
|
|
370
|
+
};
|
|
371
|
+
if (await hasSkillMd(searchPath)) {
|
|
372
|
+
let skill = await parseSkillMd(join(searchPath, "SKILL.md"), options);
|
|
373
|
+
if (skill) {
|
|
374
|
+
if (!isInstalledProjectSkill(skill)) {
|
|
375
|
+
skill = enhanceSkill(skill);
|
|
376
|
+
skills.push(skill);
|
|
377
|
+
seenNames.add(skill.name);
|
|
378
|
+
if (!options?.fullDepth) return skills;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const prioritySearchDirs = [
|
|
383
|
+
searchPath,
|
|
384
|
+
join(searchPath, "skills"),
|
|
385
|
+
join(searchPath, "skills/.curated"),
|
|
386
|
+
join(searchPath, "skills/.experimental"),
|
|
387
|
+
join(searchPath, "skills/.system"),
|
|
388
|
+
...AGENT_PROJECT_SKILL_DIRS.map((dir) => join(searchPath, dir))
|
|
389
|
+
];
|
|
390
|
+
const deepContainerDirs = new Set(prioritySearchDirs.slice(1));
|
|
391
|
+
prioritySearchDirs.push(...await getPluginSkillPaths(searchPath));
|
|
392
|
+
const tryAddSkillAt = async (skillDir) => {
|
|
393
|
+
if (!await hasSkillMd(skillDir)) return false;
|
|
394
|
+
let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
|
|
395
|
+
if (!skill || seenNames.has(skill.name)) return true;
|
|
396
|
+
if (isInstalledProjectSkill(skill)) return true;
|
|
397
|
+
skill = enhanceSkill(skill);
|
|
398
|
+
skills.push(skill);
|
|
399
|
+
seenNames.add(skill.name);
|
|
400
|
+
return true;
|
|
401
|
+
};
|
|
402
|
+
for (const dir of prioritySearchDirs) {
|
|
403
|
+
const walkDeep = deepContainerDirs.has(dir);
|
|
404
|
+
try {
|
|
405
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
406
|
+
for (const entry of entries) {
|
|
407
|
+
if (!entry.isDirectory()) continue;
|
|
408
|
+
const childDir = join(dir, entry.name);
|
|
409
|
+
if (await tryAddSkillAt(childDir) || !walkDeep) continue;
|
|
410
|
+
if (SKIP_DIRS.includes(entry.name)) continue;
|
|
411
|
+
try {
|
|
412
|
+
const grandEntries = await readdir(childDir, { withFileTypes: true });
|
|
413
|
+
for (const grand of grandEntries) {
|
|
414
|
+
if (!grand.isDirectory() || SKIP_DIRS.includes(grand.name)) continue;
|
|
415
|
+
await tryAddSkillAt(join(childDir, grand.name));
|
|
416
|
+
}
|
|
417
|
+
} catch {}
|
|
418
|
+
}
|
|
419
|
+
} catch {}
|
|
420
|
+
}
|
|
421
|
+
if (skills.length === 0 || options?.fullDepth) {
|
|
422
|
+
const allSkillDirs = await findSkillDirs(searchPath);
|
|
423
|
+
for (const skillDir of allSkillDirs) {
|
|
424
|
+
let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
|
|
425
|
+
if (skill && !seenNames.has(skill.name) && !isInstalledProjectSkill(skill)) {
|
|
426
|
+
skill = enhanceSkill(skill);
|
|
427
|
+
skills.push(skill);
|
|
428
|
+
seenNames.add(skill.name);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return skills;
|
|
433
|
+
}
|
|
434
|
+
function getSkillDisplayName(skill) {
|
|
435
|
+
return skill.name || basename(skill.path);
|
|
436
|
+
}
|
|
437
|
+
function filterSkills(skills, inputNames) {
|
|
438
|
+
const normalizedInputs = inputNames.map((n) => n.toLowerCase());
|
|
439
|
+
return skills.filter((skill) => {
|
|
440
|
+
const name = skill.name.toLowerCase();
|
|
441
|
+
const displayName = getSkillDisplayName(skill).toLowerCase();
|
|
442
|
+
return normalizedInputs.some((input) => input === name || input === displayName);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
export { addSkillToLocalLock, addSkillToLock, computeSkillFolderHash, discoverSkills, dismissPrompt, filterSkills, getAllLockedSkills, getGitHubToken, getLastSelectedAgents, getSkillDisplayName, getSkillFromLock, isPromptDismissed, parseFrontmatter, parseSkillMd, readLocalLock, readSkillLock, removeSkillFromLock, sanitizeMetadata, saveSelectedAgents, skills_exports, stripTerminalEscapes };
|