skilldex 0.1.0
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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/cli/index.js +701 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +510 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +148 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.js +458 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import * as p2 from "@clack/prompts";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import pc2 from "picocolors";
|
|
7
|
+
|
|
8
|
+
// src/lib/config.ts
|
|
9
|
+
import { writeFile } from "fs/promises";
|
|
10
|
+
import { join as join2 } from "path";
|
|
11
|
+
|
|
12
|
+
// src/lib/constants.ts
|
|
13
|
+
var START_TAG = "<!-- skilldex:start (auto-generated, do not edit) -->";
|
|
14
|
+
var END_TAG = "<!-- skilldex:end -->";
|
|
15
|
+
var TARGET_FILE = "AGENTS.md";
|
|
16
|
+
var CONFIG_FILENAME = "skilldex.config.json";
|
|
17
|
+
var SKILL_META_FILE = "SKILL.md";
|
|
18
|
+
var INDEX_HEADER = "[Skills Index]";
|
|
19
|
+
var INDEX_INSTRUCTION = "IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning for any tasks covered by indexed skills.";
|
|
20
|
+
var CONTEXT_BUDGET_WARN_KB = 20;
|
|
21
|
+
var CONTEXT_BUDGET_DANGER_KB = 40;
|
|
22
|
+
function compareByNameThenPath(a, b) {
|
|
23
|
+
return a.name.localeCompare(b.name) || a.path.localeCompare(b.path);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/lib/scanner.ts
|
|
27
|
+
import { lstat, readdir, readFile } from "fs/promises";
|
|
28
|
+
import { join, relative } from "path";
|
|
29
|
+
|
|
30
|
+
// src/lib/agents.ts
|
|
31
|
+
var AGENT_SOURCES = [
|
|
32
|
+
{ name: "universal", displayName: "Universal", skillsDir: ".agents/skills" },
|
|
33
|
+
{ name: "antigravity", displayName: "Antigravity", skillsDir: ".agent/skills" },
|
|
34
|
+
{ name: "claude-code", displayName: "Claude Code", skillsDir: ".claude/skills" },
|
|
35
|
+
{ name: "codex", displayName: "Codex", skillsDir: ".agents/skills" },
|
|
36
|
+
{ name: "cursor", displayName: "Cursor", skillsDir: ".cursor/skills" },
|
|
37
|
+
{ name: "github-copilot", displayName: "GitHub Copilot", skillsDir: ".agents/skills" },
|
|
38
|
+
{ name: "opencode", displayName: "OpenCode", skillsDir: ".agents/skills" },
|
|
39
|
+
{ name: "openclaw", displayName: "OpenClaw", skillsDir: "skills" },
|
|
40
|
+
{ name: "windsurf", displayName: "Windsurf", skillsDir: ".windsurf/skills" }
|
|
41
|
+
];
|
|
42
|
+
function getUniqueSkillsDirs() {
|
|
43
|
+
return [...new Set(AGENT_SOURCES.map((a) => a.skillsDir))];
|
|
44
|
+
}
|
|
45
|
+
function getAgentDisplayName(relativePath) {
|
|
46
|
+
for (const agent of AGENT_SOURCES) {
|
|
47
|
+
if (relativePath.startsWith(`${agent.skillsDir}/`) || relativePath === agent.skillsDir) {
|
|
48
|
+
return agent.displayName;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/lib/scanner.ts
|
|
55
|
+
function parseFrontmatter(content) {
|
|
56
|
+
const result = {};
|
|
57
|
+
if (!content.startsWith("---")) return result;
|
|
58
|
+
const endIndex = content.indexOf("\n---", 3);
|
|
59
|
+
if (endIndex === -1) return result;
|
|
60
|
+
const block = content.slice(4, endIndex);
|
|
61
|
+
for (const line of block.split("\n")) {
|
|
62
|
+
const colonIndex = line.indexOf(":");
|
|
63
|
+
if (colonIndex === -1) continue;
|
|
64
|
+
const key = line.slice(0, colonIndex).trim();
|
|
65
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
66
|
+
if (key) result[key] = value;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
async function safeReaddir(dir) {
|
|
71
|
+
try {
|
|
72
|
+
return await readdir(dir, { withFileTypes: true });
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function safeReadFile(path) {
|
|
78
|
+
try {
|
|
79
|
+
return await readFile(path, "utf-8");
|
|
80
|
+
} catch {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function collectMdFiles(dir, skillRoot) {
|
|
85
|
+
const entries = await safeReaddir(dir);
|
|
86
|
+
const files = [];
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const fullPath = join(dir, entry.name);
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
const nested = await collectMdFiles(fullPath, skillRoot);
|
|
91
|
+
files.push(...nested);
|
|
92
|
+
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== SKILL_META_FILE) {
|
|
93
|
+
files.push({
|
|
94
|
+
relativePath: relative(skillRoot, fullPath),
|
|
95
|
+
name: entry.name.replace(/\.md$/, "")
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return files;
|
|
100
|
+
}
|
|
101
|
+
async function isSymlink(path) {
|
|
102
|
+
try {
|
|
103
|
+
const stats = await lstat(path);
|
|
104
|
+
return stats.isSymbolicLink();
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function scanDirectory(dir, projectRoot) {
|
|
110
|
+
const entries = await safeReaddir(dir);
|
|
111
|
+
const skills = [];
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (!entry.isDirectory()) continue;
|
|
114
|
+
const skillPath = join(dir, entry.name);
|
|
115
|
+
if (await isSymlink(skillPath)) continue;
|
|
116
|
+
let description = "";
|
|
117
|
+
const skillMd = await safeReadFile(join(skillPath, SKILL_META_FILE));
|
|
118
|
+
if (skillMd !== void 0) {
|
|
119
|
+
const frontmatter = parseFrontmatter(skillMd);
|
|
120
|
+
description = frontmatter.description ?? "";
|
|
121
|
+
}
|
|
122
|
+
const files = await collectMdFiles(skillPath, skillPath);
|
|
123
|
+
skills.push({
|
|
124
|
+
name: entry.name,
|
|
125
|
+
description,
|
|
126
|
+
path: skillPath,
|
|
127
|
+
relativePath: relative(projectRoot, skillPath),
|
|
128
|
+
files
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return skills;
|
|
132
|
+
}
|
|
133
|
+
async function scanForSkills(projectRoot) {
|
|
134
|
+
const results = await Promise.all(
|
|
135
|
+
getUniqueSkillsDirs().map((d) => scanDirectory(join(projectRoot, d), projectRoot))
|
|
136
|
+
);
|
|
137
|
+
return results.flat().sort(compareByNameThenPath);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/lib/config.ts
|
|
141
|
+
function getDefaultConfig() {
|
|
142
|
+
return {
|
|
143
|
+
version: 1,
|
|
144
|
+
targets: [TARGET_FILE],
|
|
145
|
+
skills: []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function readConfig(projectRoot) {
|
|
149
|
+
const configPath = join2(projectRoot, CONFIG_FILENAME);
|
|
150
|
+
const content = await safeReadFile(configPath);
|
|
151
|
+
if (content === void 0) {
|
|
152
|
+
return getDefaultConfig();
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(content);
|
|
156
|
+
if (typeof parsed.version !== "number" || !Array.isArray(parsed.targets) || !Array.isArray(parsed.skills)) {
|
|
157
|
+
return getDefaultConfig();
|
|
158
|
+
}
|
|
159
|
+
return parsed;
|
|
160
|
+
} catch {
|
|
161
|
+
return getDefaultConfig();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function writeConfig(projectRoot, config) {
|
|
165
|
+
const configPath = join2(projectRoot, CONFIG_FILENAME);
|
|
166
|
+
const sorted = {
|
|
167
|
+
...config,
|
|
168
|
+
skills: [...config.skills].sort(compareByNameThenPath)
|
|
169
|
+
};
|
|
170
|
+
const content = JSON.stringify(sorted, null, 2);
|
|
171
|
+
await writeFile(configPath, content, "utf-8");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/lib/writer.ts
|
|
175
|
+
import { stat, writeFile as writeFile2 } from "fs/promises";
|
|
176
|
+
import { basename, join as join3 } from "path";
|
|
177
|
+
|
|
178
|
+
// src/lib/indexer.ts
|
|
179
|
+
import { dirname } from "path";
|
|
180
|
+
function groupFilesBySubdir(files) {
|
|
181
|
+
const groups = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
const dir = dirname(file.relativePath);
|
|
184
|
+
const key = dir === "." ? "" : dir;
|
|
185
|
+
const existing = groups.get(key);
|
|
186
|
+
if (existing) {
|
|
187
|
+
existing.push(`${file.name}.md`);
|
|
188
|
+
} else {
|
|
189
|
+
groups.set(key, [`${file.name}.md`]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return groups;
|
|
193
|
+
}
|
|
194
|
+
function generateIndex(skills) {
|
|
195
|
+
const segments = [INDEX_HEADER, INDEX_INSTRUCTION];
|
|
196
|
+
for (const skill of skills) {
|
|
197
|
+
segments.push(`[${skill.name}]`);
|
|
198
|
+
segments.push(`root:./${skill.relativePath}`);
|
|
199
|
+
if (skill.description) {
|
|
200
|
+
segments.push(`desc:${skill.description}`);
|
|
201
|
+
}
|
|
202
|
+
const groups = groupFilesBySubdir(skill.files);
|
|
203
|
+
for (const [subdir, fileNames] of groups) {
|
|
204
|
+
const fileList = `{${fileNames.join(",")}}`;
|
|
205
|
+
if (subdir) {
|
|
206
|
+
segments.push(`${subdir}:${fileList}`);
|
|
207
|
+
} else {
|
|
208
|
+
segments.push(fileList);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return segments.join("|");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/lib/writer.ts
|
|
216
|
+
async function getTargetInfos(projectRoot, targets) {
|
|
217
|
+
return Promise.all(
|
|
218
|
+
targets.map(async (t) => {
|
|
219
|
+
const p3 = join3(projectRoot, t);
|
|
220
|
+
const s = await stat(p3);
|
|
221
|
+
return { file: basename(t), path: p3, totalSize: s.size };
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
function buildManagedSection(indexContent) {
|
|
226
|
+
return `${START_TAG}
|
|
227
|
+
${indexContent}
|
|
228
|
+
${END_TAG}`;
|
|
229
|
+
}
|
|
230
|
+
async function writeTargetFile(projectRoot, indexContent, targetFile = TARGET_FILE) {
|
|
231
|
+
const targetPath = join3(projectRoot, targetFile);
|
|
232
|
+
const section = buildManagedSection(indexContent);
|
|
233
|
+
const existing = await safeReadFile(targetPath);
|
|
234
|
+
let output;
|
|
235
|
+
if (existing === void 0) {
|
|
236
|
+
output = `${section}
|
|
237
|
+
`;
|
|
238
|
+
} else if (existing.includes(START_TAG) && existing.includes(END_TAG)) {
|
|
239
|
+
const startIdx = existing.indexOf(START_TAG);
|
|
240
|
+
const endIdx = existing.indexOf(END_TAG) + END_TAG.length;
|
|
241
|
+
output = existing.slice(0, startIdx) + section + existing.slice(endIdx);
|
|
242
|
+
} else {
|
|
243
|
+
output = `${existing.trimEnd()}
|
|
244
|
+
|
|
245
|
+
${section}
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
await writeFile2(targetPath, output, "utf-8");
|
|
249
|
+
return output;
|
|
250
|
+
}
|
|
251
|
+
async function regenerateFromConfig(projectRoot) {
|
|
252
|
+
const config = await readConfig(projectRoot);
|
|
253
|
+
const allSkills = await scanForSkills(projectRoot);
|
|
254
|
+
const skillMap = new Map(allSkills.map((s) => [s.relativePath, s]));
|
|
255
|
+
const skills = config.skills.map((entry) => skillMap.get(entry.path)).filter((s) => s !== void 0);
|
|
256
|
+
const index = generateIndex(skills);
|
|
257
|
+
const writtenContent = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const target of config.targets) {
|
|
259
|
+
const content = await writeTargetFile(projectRoot, index, target);
|
|
260
|
+
writtenContent.set(join3(projectRoot, target), content);
|
|
261
|
+
}
|
|
262
|
+
const managedSize = Buffer.byteLength(buildManagedSection(index));
|
|
263
|
+
const targets = await getTargetInfos(projectRoot, config.targets);
|
|
264
|
+
return {
|
|
265
|
+
skillCount: skills.length,
|
|
266
|
+
managedSize,
|
|
267
|
+
targets,
|
|
268
|
+
writtenContent
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/lib/add.ts
|
|
273
|
+
async function addSkill(projectRoot, skillName) {
|
|
274
|
+
const config = await readConfig(projectRoot);
|
|
275
|
+
const allSkills = await scanForSkills(projectRoot);
|
|
276
|
+
const isPath = skillName.includes("/");
|
|
277
|
+
let targetSkill;
|
|
278
|
+
if (isPath) {
|
|
279
|
+
targetSkill = allSkills.find((s) => s.relativePath === skillName);
|
|
280
|
+
if (!targetSkill) {
|
|
281
|
+
throw new Error(`Skill "${skillName}" not found. Did you create the skill directory?`);
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
const matches = allSkills.filter((s) => s.name === skillName);
|
|
285
|
+
if (matches.length === 0) {
|
|
286
|
+
throw new Error(`Skill "${skillName}" not found. Did you create the skill directory?`);
|
|
287
|
+
}
|
|
288
|
+
if (matches.length > 1) {
|
|
289
|
+
const paths = matches.map((s) => ` ${s.relativePath}`).join("\n");
|
|
290
|
+
throw new Error(`Multiple skills named "${skillName}" found. Specify the path:
|
|
291
|
+
${paths}`);
|
|
292
|
+
}
|
|
293
|
+
targetSkill = matches[0];
|
|
294
|
+
}
|
|
295
|
+
if (config.skills.some((s) => s.path === targetSkill.relativePath)) {
|
|
296
|
+
throw new Error(`Skill "${targetSkill.relativePath}" is already indexed`);
|
|
297
|
+
}
|
|
298
|
+
config.skills.push({
|
|
299
|
+
name: targetSkill.name,
|
|
300
|
+
path: targetSkill.relativePath
|
|
301
|
+
});
|
|
302
|
+
await writeConfig(projectRoot, config);
|
|
303
|
+
const result = await regenerateFromConfig(projectRoot);
|
|
304
|
+
return {
|
|
305
|
+
skillName: targetSkill.name,
|
|
306
|
+
managedSize: result.managedSize,
|
|
307
|
+
targets: result.targets
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/lib/init.ts
|
|
312
|
+
async function initWithSkills(projectRoot, skills, targets = [TARGET_FILE]) {
|
|
313
|
+
const config = {
|
|
314
|
+
version: 1,
|
|
315
|
+
targets,
|
|
316
|
+
skills: skills.map((skill) => ({
|
|
317
|
+
name: skill.name,
|
|
318
|
+
path: skill.relativePath
|
|
319
|
+
}))
|
|
320
|
+
};
|
|
321
|
+
await writeConfig(projectRoot, config);
|
|
322
|
+
return regenerateFromConfig(projectRoot);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/lib/list.ts
|
|
326
|
+
async function listSkills(projectRoot) {
|
|
327
|
+
const config = await readConfig(projectRoot);
|
|
328
|
+
const allSkills = await scanForSkills(projectRoot);
|
|
329
|
+
const indexedPaths = new Set(config.skills.map((s) => s.path));
|
|
330
|
+
const skillMap = new Map(allSkills.map((s) => [s.relativePath, s]));
|
|
331
|
+
const indexed = config.skills.map((entry) => {
|
|
332
|
+
const discovered = skillMap.get(entry.path);
|
|
333
|
+
if (!discovered) return void 0;
|
|
334
|
+
return {
|
|
335
|
+
name: entry.name,
|
|
336
|
+
path: entry.path,
|
|
337
|
+
description: discovered.description
|
|
338
|
+
};
|
|
339
|
+
}).filter((s) => s !== void 0);
|
|
340
|
+
const available = allSkills.filter((s) => !indexedPaths.has(s.relativePath)).map((s) => ({
|
|
341
|
+
name: s.name,
|
|
342
|
+
description: s.description,
|
|
343
|
+
path: s.relativePath
|
|
344
|
+
}));
|
|
345
|
+
return { indexed, available };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/lib/remove.ts
|
|
349
|
+
import { rm } from "fs/promises";
|
|
350
|
+
import { join as join4 } from "path";
|
|
351
|
+
async function removeSkill(projectRoot, skillName, deleteFiles = false) {
|
|
352
|
+
const config = await readConfig(projectRoot);
|
|
353
|
+
const isPath = skillName.includes("/");
|
|
354
|
+
let skillIndex;
|
|
355
|
+
if (isPath) {
|
|
356
|
+
skillIndex = config.skills.findIndex((s) => s.path === skillName);
|
|
357
|
+
if (skillIndex === -1) {
|
|
358
|
+
throw new Error(`Skill "${skillName}" is not indexed`);
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
const matches = config.skills.map((s, i) => ({ entry: s, index: i })).filter(({ entry: entry2 }) => entry2.name === skillName);
|
|
362
|
+
if (matches.length === 0) {
|
|
363
|
+
throw new Error(`Skill "${skillName}" is not indexed`);
|
|
364
|
+
}
|
|
365
|
+
if (matches.length > 1) {
|
|
366
|
+
const paths = matches.map(({ entry: entry2 }) => ` ${entry2.path}`).join("\n");
|
|
367
|
+
throw new Error(`Multiple skills named "${skillName}" indexed. Specify the path:
|
|
368
|
+
${paths}`);
|
|
369
|
+
}
|
|
370
|
+
skillIndex = matches[0].index;
|
|
371
|
+
}
|
|
372
|
+
const entry = config.skills[skillIndex];
|
|
373
|
+
const skillPath = join4(projectRoot, entry.path);
|
|
374
|
+
config.skills.splice(skillIndex, 1);
|
|
375
|
+
await writeConfig(projectRoot, config);
|
|
376
|
+
if (deleteFiles) {
|
|
377
|
+
await rm(skillPath, { recursive: true, force: true });
|
|
378
|
+
}
|
|
379
|
+
const result = await regenerateFromConfig(projectRoot);
|
|
380
|
+
return {
|
|
381
|
+
skillName: entry.name,
|
|
382
|
+
wasDeleted: deleteFiles,
|
|
383
|
+
managedSize: result.managedSize,
|
|
384
|
+
targets: result.targets
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/lib/sync.ts
|
|
389
|
+
import { join as join5 } from "path";
|
|
390
|
+
async function syncSkills(projectRoot) {
|
|
391
|
+
const config = await readConfig(projectRoot);
|
|
392
|
+
const onDisk = await scanForSkills(projectRoot);
|
|
393
|
+
const diskPaths = new Set(onDisk.map((s) => s.relativePath));
|
|
394
|
+
const stale = config.skills.filter((s) => !diskPaths.has(s.path));
|
|
395
|
+
const removed = stale.map((s) => s.name);
|
|
396
|
+
if (stale.length > 0) {
|
|
397
|
+
const stalePaths = new Set(stale.map((s) => s.path));
|
|
398
|
+
config.skills = config.skills.filter((s) => !stalePaths.has(s.path));
|
|
399
|
+
await writeConfig(projectRoot, config);
|
|
400
|
+
}
|
|
401
|
+
const targetPaths = config.targets.map((t) => join5(projectRoot, t));
|
|
402
|
+
const beforeMap = /* @__PURE__ */ new Map();
|
|
403
|
+
for (const targetPath of targetPaths) {
|
|
404
|
+
beforeMap.set(targetPath, await safeReadFile(targetPath));
|
|
405
|
+
}
|
|
406
|
+
if (config.skills.length === 0 && [...beforeMap.values()].every((v) => v === void 0)) {
|
|
407
|
+
return { removed, changed: false, managedSize: 0, targets: [] };
|
|
408
|
+
}
|
|
409
|
+
const result = await regenerateFromConfig(projectRoot);
|
|
410
|
+
let changed = false;
|
|
411
|
+
for (const targetPath of targetPaths) {
|
|
412
|
+
const after = result.writtenContent.get(targetPath);
|
|
413
|
+
if (beforeMap.get(targetPath) !== after) {
|
|
414
|
+
changed = true;
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
removed,
|
|
420
|
+
changed,
|
|
421
|
+
managedSize: result.managedSize,
|
|
422
|
+
targets: result.targets
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/cli/format.ts
|
|
427
|
+
import * as p from "@clack/prompts";
|
|
428
|
+
import pc from "picocolors";
|
|
429
|
+
function pluralize(count, singular, plural) {
|
|
430
|
+
return count === 1 ? singular : plural;
|
|
431
|
+
}
|
|
432
|
+
function formatKb(bytes) {
|
|
433
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
434
|
+
}
|
|
435
|
+
function logContextSize(managedSize, targets) {
|
|
436
|
+
const sizeKb = managedSize / 1024;
|
|
437
|
+
const sizeFormatted = formatKb(managedSize);
|
|
438
|
+
let sizeLabel;
|
|
439
|
+
if (sizeKb < CONTEXT_BUDGET_WARN_KB) {
|
|
440
|
+
sizeLabel = pc.green(sizeFormatted);
|
|
441
|
+
} else if (sizeKb < CONTEXT_BUDGET_DANGER_KB) {
|
|
442
|
+
sizeLabel = pc.yellow(sizeFormatted);
|
|
443
|
+
} else {
|
|
444
|
+
sizeLabel = pc.red(`${sizeFormatted} \u2014 may degrade agent performance`);
|
|
445
|
+
}
|
|
446
|
+
const targetLines = targets.map(
|
|
447
|
+
(t) => ` ${t.file} ${pc.dim(`${formatKb(t.totalSize)} total`)}`
|
|
448
|
+
);
|
|
449
|
+
p.log.info(`Context: ${sizeLabel} (managed section)
|
|
450
|
+
${targetLines.join("\n")}`);
|
|
451
|
+
}
|
|
452
|
+
function formatSkillPath(relativePath) {
|
|
453
|
+
const agent = getAgentDisplayName(relativePath);
|
|
454
|
+
if (agent) return `${pc.cyan(agent)} ${pc.dim(relativePath)}`;
|
|
455
|
+
return pc.dim(relativePath);
|
|
456
|
+
}
|
|
457
|
+
function handleCommandError(error, spinner2) {
|
|
458
|
+
if (spinner2) {
|
|
459
|
+
spinner2.stop("\u2717 Failed");
|
|
460
|
+
}
|
|
461
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
462
|
+
p.outro(pc.red(message));
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/cli/index.ts
|
|
467
|
+
var program = new Command();
|
|
468
|
+
program.name("skilldex").description("Index AI agent skills into passive context (AGENTS.md)").version("0.1.0");
|
|
469
|
+
program.command("init").description("Initialize skilldex in the current project").option("-y, --yes", "Skip prompts and index all discovered skills").option("-t, --target <files...>", "Target file(s) to write index to").action(async (opts) => {
|
|
470
|
+
p2.intro(pc2.bgCyan(pc2.black(" skilldex init ")));
|
|
471
|
+
const projectRoot = process.cwd();
|
|
472
|
+
const skills = await scanForSkills(projectRoot);
|
|
473
|
+
if (skills.length === 0) {
|
|
474
|
+
p2.outro(pc2.yellow("No skills found in any agent skills directory."));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
let targets;
|
|
478
|
+
if (opts.target) {
|
|
479
|
+
targets = opts.target;
|
|
480
|
+
} else if (opts.yes) {
|
|
481
|
+
targets = [TARGET_FILE];
|
|
482
|
+
} else {
|
|
483
|
+
const selected = await p2.multiselect({
|
|
484
|
+
message: "Select target file(s)",
|
|
485
|
+
options: [
|
|
486
|
+
{ value: "AGENTS.md", label: "AGENTS.md", hint: "default" },
|
|
487
|
+
{ value: "CLAUDE.md", label: "CLAUDE.md" },
|
|
488
|
+
{ value: "__custom__", label: "Custom..." }
|
|
489
|
+
],
|
|
490
|
+
initialValues: ["AGENTS.md"],
|
|
491
|
+
required: true
|
|
492
|
+
});
|
|
493
|
+
if (p2.isCancel(selected)) {
|
|
494
|
+
p2.cancel("Init cancelled.");
|
|
495
|
+
process.exit(0);
|
|
496
|
+
}
|
|
497
|
+
targets = selected.filter((v) => v !== "__custom__");
|
|
498
|
+
if (selected.includes("__custom__")) {
|
|
499
|
+
const custom = await p2.text({
|
|
500
|
+
message: "Enter target filename",
|
|
501
|
+
placeholder: "AGENTS.md",
|
|
502
|
+
validate: (value) => {
|
|
503
|
+
if (!value?.trim()) return "Filename is required";
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
if (p2.isCancel(custom)) {
|
|
507
|
+
p2.cancel("Init cancelled.");
|
|
508
|
+
process.exit(0);
|
|
509
|
+
}
|
|
510
|
+
targets.push(custom);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const nameCount = /* @__PURE__ */ new Map();
|
|
514
|
+
for (const s of skills) {
|
|
515
|
+
nameCount.set(s.name, (nameCount.get(s.name) ?? 0) + 1);
|
|
516
|
+
}
|
|
517
|
+
let selectedSkillPaths;
|
|
518
|
+
if (opts.yes) {
|
|
519
|
+
p2.log.info(
|
|
520
|
+
`Found ${skills.length} ${pluralize(skills.length, "skill", "skills")}, indexing all (--yes)`
|
|
521
|
+
);
|
|
522
|
+
selectedSkillPaths = skills.map((s) => s.relativePath);
|
|
523
|
+
} else {
|
|
524
|
+
const selected = await p2.multiselect({
|
|
525
|
+
message: "Select skills to index",
|
|
526
|
+
options: skills.map((s) => {
|
|
527
|
+
const relPath = s.relativePath;
|
|
528
|
+
const hasCollision = (nameCount.get(s.name) ?? 0) > 1;
|
|
529
|
+
if (hasCollision) {
|
|
530
|
+
const agent = getAgentDisplayName(relPath);
|
|
531
|
+
const label = agent ? `${s.name} - ${agent}` : s.name;
|
|
532
|
+
return { value: relPath, label, hint: relPath };
|
|
533
|
+
}
|
|
534
|
+
return { value: relPath, label: s.name, hint: s.description || void 0 };
|
|
535
|
+
}),
|
|
536
|
+
initialValues: skills.map((s) => s.relativePath),
|
|
537
|
+
required: true
|
|
538
|
+
});
|
|
539
|
+
if (p2.isCancel(selected)) {
|
|
540
|
+
p2.cancel("Init cancelled.");
|
|
541
|
+
process.exit(0);
|
|
542
|
+
}
|
|
543
|
+
selectedSkillPaths = selected;
|
|
544
|
+
}
|
|
545
|
+
const selectedSkills = skills.filter((s) => selectedSkillPaths.includes(s.relativePath));
|
|
546
|
+
const result = await initWithSkills(projectRoot, selectedSkills, targets);
|
|
547
|
+
logContextSize(result.managedSize, result.targets);
|
|
548
|
+
p2.outro(
|
|
549
|
+
pc2.green(
|
|
550
|
+
`Indexed ${result.skillCount} ${pluralize(result.skillCount, "skill", "skills")} into ${result.targets.map((t) => t.file).join(", ")}`
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
program.command("add <skill>").description("Add a skill to the project").action(async (skillName) => {
|
|
555
|
+
p2.intro(pc2.bgCyan(pc2.black(" skilldex add ")));
|
|
556
|
+
const projectRoot = process.cwd();
|
|
557
|
+
const s = p2.spinner();
|
|
558
|
+
try {
|
|
559
|
+
let resolvedName = skillName;
|
|
560
|
+
if (!skillName.includes("/")) {
|
|
561
|
+
const allSkills = await scanForSkills(projectRoot);
|
|
562
|
+
const matches = allSkills.filter((sk) => sk.name === skillName);
|
|
563
|
+
if (matches.length > 1) {
|
|
564
|
+
const config = await readConfig(projectRoot);
|
|
565
|
+
const indexedPaths = new Set(config.skills.map((sk) => sk.path));
|
|
566
|
+
const available = matches.filter((sk) => !indexedPaths.has(sk.relativePath));
|
|
567
|
+
if (available.length === 0) {
|
|
568
|
+
p2.outro(pc2.yellow(`All skills named "${skillName}" are already indexed.`));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const selected = await p2.select({
|
|
572
|
+
message: `Multiple skills named "${skillName}" found. Which one?`,
|
|
573
|
+
options: matches.map((sk) => {
|
|
574
|
+
const indexed = indexedPaths.has(sk.relativePath);
|
|
575
|
+
const agent = getAgentDisplayName(sk.relativePath);
|
|
576
|
+
return {
|
|
577
|
+
value: sk.relativePath,
|
|
578
|
+
label: agent ?? sk.relativePath,
|
|
579
|
+
hint: indexed ? "already indexed" : sk.relativePath,
|
|
580
|
+
disabled: indexed
|
|
581
|
+
};
|
|
582
|
+
}).sort((a, b) => {
|
|
583
|
+
if (a.disabled !== b.disabled) return a.disabled ? -1 : 1;
|
|
584
|
+
return a.label.localeCompare(b.label);
|
|
585
|
+
})
|
|
586
|
+
});
|
|
587
|
+
if (p2.isCancel(selected)) {
|
|
588
|
+
p2.cancel("Add cancelled.");
|
|
589
|
+
process.exit(0);
|
|
590
|
+
}
|
|
591
|
+
resolvedName = selected;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
s.start("Adding skill...");
|
|
595
|
+
const result = await addSkill(projectRoot, resolvedName);
|
|
596
|
+
s.stop("\u2713 Skill added");
|
|
597
|
+
logContextSize(result.managedSize, result.targets);
|
|
598
|
+
p2.outro(
|
|
599
|
+
pc2.green(`Added "${result.skillName}" to ${result.targets.map((t) => t.file).join(", ")}`)
|
|
600
|
+
);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
handleCommandError(error, s);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
program.command("remove <skill>").description("Remove a skill from the project").option("--delete-files", "Delete skill files from disk").action(async (skillName, opts) => {
|
|
606
|
+
p2.intro(pc2.bgCyan(pc2.black(" skilldex remove ")));
|
|
607
|
+
const projectRoot = process.cwd();
|
|
608
|
+
const s = p2.spinner();
|
|
609
|
+
try {
|
|
610
|
+
let resolvedName = skillName;
|
|
611
|
+
if (!skillName.includes("/")) {
|
|
612
|
+
const config = await readConfig(projectRoot);
|
|
613
|
+
const matches = config.skills.filter((sk) => sk.name === skillName);
|
|
614
|
+
if (matches.length > 1) {
|
|
615
|
+
const selected = await p2.select({
|
|
616
|
+
message: `Multiple skills named "${skillName}" indexed. Which one?`,
|
|
617
|
+
options: matches.map((sk) => {
|
|
618
|
+
const agent = getAgentDisplayName(sk.path);
|
|
619
|
+
return { value: sk.path, label: agent ?? sk.path, hint: sk.path };
|
|
620
|
+
})
|
|
621
|
+
});
|
|
622
|
+
if (p2.isCancel(selected)) {
|
|
623
|
+
p2.cancel("Remove cancelled.");
|
|
624
|
+
process.exit(0);
|
|
625
|
+
}
|
|
626
|
+
resolvedName = selected;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
let deleteFiles = opts.deleteFiles ?? false;
|
|
630
|
+
if (!opts.deleteFiles) {
|
|
631
|
+
const shouldDelete = await p2.confirm({
|
|
632
|
+
message: `Delete skill files from disk (${resolvedName})?`,
|
|
633
|
+
initialValue: false
|
|
634
|
+
});
|
|
635
|
+
if (p2.isCancel(shouldDelete)) {
|
|
636
|
+
p2.cancel("Remove cancelled.");
|
|
637
|
+
process.exit(0);
|
|
638
|
+
}
|
|
639
|
+
deleteFiles = shouldDelete;
|
|
640
|
+
}
|
|
641
|
+
s.start("Removing skill...");
|
|
642
|
+
const result = await removeSkill(projectRoot, resolvedName, deleteFiles);
|
|
643
|
+
s.stop("\u2713 Skill removed");
|
|
644
|
+
logContextSize(result.managedSize, result.targets);
|
|
645
|
+
const suffix = result.wasDeleted ? " (files deleted)" : " (files kept on disk)";
|
|
646
|
+
p2.outro(pc2.green(`Removed "${result.skillName}"${suffix}`));
|
|
647
|
+
} catch (error) {
|
|
648
|
+
handleCommandError(error, s);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
program.command("list").description("List indexed and available skills").action(async () => {
|
|
652
|
+
p2.intro(pc2.bgCyan(pc2.black(" skilldex list ")));
|
|
653
|
+
const projectRoot = process.cwd();
|
|
654
|
+
try {
|
|
655
|
+
const result = await listSkills(projectRoot);
|
|
656
|
+
if (result.indexed.length > 0) {
|
|
657
|
+
p2.log.step(pc2.bold("Indexed skills"));
|
|
658
|
+
const indexedLines = result.indexed.map(
|
|
659
|
+
(skill) => ` ${pc2.green(skill.name)} ${formatSkillPath(skill.path)}`
|
|
660
|
+
);
|
|
661
|
+
p2.log.info(indexedLines.join("\n"));
|
|
662
|
+
} else {
|
|
663
|
+
p2.log.info(pc2.dim("No indexed skills."));
|
|
664
|
+
}
|
|
665
|
+
if (result.available.length > 0) {
|
|
666
|
+
p2.log.step(pc2.bold("Available skills (not indexed)"));
|
|
667
|
+
const availableLines = result.available.map(
|
|
668
|
+
(skill) => ` ${pc2.yellow(skill.name)} ${formatSkillPath(skill.path)}`
|
|
669
|
+
);
|
|
670
|
+
p2.log.info(availableLines.join("\n"));
|
|
671
|
+
}
|
|
672
|
+
p2.outro(pc2.green("Done"));
|
|
673
|
+
} catch (error) {
|
|
674
|
+
handleCommandError(error);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
program.command("sync").description("Sync skills and regenerate target file").action(async () => {
|
|
678
|
+
p2.intro(pc2.bgCyan(pc2.black(" skilldex sync ")));
|
|
679
|
+
const projectRoot = process.cwd();
|
|
680
|
+
const s = p2.spinner();
|
|
681
|
+
try {
|
|
682
|
+
s.start("Syncing...");
|
|
683
|
+
const result = await syncSkills(projectRoot);
|
|
684
|
+
s.stop("\u2713 Sync complete");
|
|
685
|
+
if (result.removed.length > 0) {
|
|
686
|
+
for (const name of result.removed) {
|
|
687
|
+
p2.log.warn(pc2.red(`Removed stale skill "${name}" (missing from disk)`));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
logContextSize(result.managedSize, result.targets);
|
|
691
|
+
if (result.changed) {
|
|
692
|
+
p2.outro(pc2.green("Index updated"));
|
|
693
|
+
} else {
|
|
694
|
+
p2.outro(pc2.green("Everything up to date"));
|
|
695
|
+
}
|
|
696
|
+
} catch (error) {
|
|
697
|
+
handleCommandError(error, s);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
program.parse();
|
|
701
|
+
//# sourceMappingURL=index.js.map
|