@hasna/skills 0.1.14 → 0.1.16
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/bin/index.js +1212 -220
- package/bin/mcp.js +516 -101
- package/dist/index.d.ts +2 -1
- package/dist/index.js +391 -105
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/installer.d.ts +11 -2
- package/dist/lib/registry.d.ts +13 -0
- package/dist/lib/scheduler.d.ts +47 -0
- package/package.json +2 -1
- package/skills/_common/index.ts +4 -0
- package/skills/_common/vision.ts +374 -0
- package/skills/skill-colorextract/SKILL.md +35 -0
- package/skills/skill-colorextract/bun.lock +102 -0
- package/skills/skill-colorextract/package.json +13 -0
- package/skills/skill-colorextract/src/index.ts +405 -0
- package/skills/skill-siteanalyze/SKILL.md +25 -0
- package/skills/skill-siteanalyze/package.json +13 -0
- package/skills/skill-siteanalyze/src/index.ts +592 -0
package/bin/mcp.js
CHANGED
|
@@ -44,6 +44,7 @@ var __export = (target, all) => {
|
|
|
44
44
|
set: __exportSetter.bind(all, name)
|
|
45
45
|
});
|
|
46
46
|
};
|
|
47
|
+
var __require = import.meta.require;
|
|
47
48
|
|
|
48
49
|
// node_modules/ajv/dist/compile/codegen/code.js
|
|
49
50
|
var require_code = __commonJS((exports) => {
|
|
@@ -28560,13 +28561,12 @@ class StdioServerTransport {
|
|
|
28560
28561
|
}
|
|
28561
28562
|
|
|
28562
28563
|
// src/mcp/index.ts
|
|
28563
|
-
import { existsSync as
|
|
28564
|
-
import { join as
|
|
28565
|
-
import { homedir as homedir2 } from "os";
|
|
28564
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
28565
|
+
import { join as join5 } from "path";
|
|
28566
28566
|
// package.json
|
|
28567
28567
|
var package_default = {
|
|
28568
28568
|
name: "@hasna/skills",
|
|
28569
|
-
version: "0.1.
|
|
28569
|
+
version: "0.1.16",
|
|
28570
28570
|
description: "Skills library for AI coding agents",
|
|
28571
28571
|
type: "module",
|
|
28572
28572
|
bin: {
|
|
@@ -28626,6 +28626,7 @@ var package_default = {
|
|
|
28626
28626
|
typescript: "^5"
|
|
28627
28627
|
},
|
|
28628
28628
|
dependencies: {
|
|
28629
|
+
"@hasna/cloud": "^0.1.0",
|
|
28629
28630
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
28630
28631
|
chalk: "^5.3.0",
|
|
28631
28632
|
commander: "^12.1.0",
|
|
@@ -28650,6 +28651,9 @@ var package_default = {
|
|
|
28650
28651
|
};
|
|
28651
28652
|
|
|
28652
28653
|
// src/lib/registry.ts
|
|
28654
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
28655
|
+
import { join } from "path";
|
|
28656
|
+
import { homedir } from "os";
|
|
28653
28657
|
var CATEGORIES = [
|
|
28654
28658
|
"Development Tools",
|
|
28655
28659
|
"Business & Marketing",
|
|
@@ -29686,6 +29690,20 @@ var SKILLS = [
|
|
|
29686
29690
|
category: "Design & Branding",
|
|
29687
29691
|
tags: ["testimonials", "graphics", "social-proof", "marketing"]
|
|
29688
29692
|
},
|
|
29693
|
+
{
|
|
29694
|
+
name: "colorextract",
|
|
29695
|
+
displayName: "Color Extract",
|
|
29696
|
+
description: "Extract complete color palettes from screenshots and images using Claude Vision. Outputs open-styles compatible profiles.",
|
|
29697
|
+
category: "Design & Branding",
|
|
29698
|
+
tags: ["colors", "palette", "design", "vision", "screenshot", "extract", "open-styles"]
|
|
29699
|
+
},
|
|
29700
|
+
{
|
|
29701
|
+
name: "siteanalyze",
|
|
29702
|
+
displayName: "Site Analyze",
|
|
29703
|
+
description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
|
|
29704
|
+
category: "Design & Branding",
|
|
29705
|
+
tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "open-styles"]
|
|
29706
|
+
},
|
|
29689
29707
|
{
|
|
29690
29708
|
name: "browse",
|
|
29691
29709
|
displayName: "Browse",
|
|
@@ -30086,8 +30104,90 @@ var SKILLS = [
|
|
|
30086
30104
|
tags: ["seating", "chart", "events", "venues"]
|
|
30087
30105
|
}
|
|
30088
30106
|
];
|
|
30107
|
+
function parseSkillMdFrontmatter(content) {
|
|
30108
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
30109
|
+
if (!match)
|
|
30110
|
+
return null;
|
|
30111
|
+
const result = {};
|
|
30112
|
+
for (const line of match[1].split(`
|
|
30113
|
+
`)) {
|
|
30114
|
+
const colon = line.indexOf(":");
|
|
30115
|
+
if (colon === -1)
|
|
30116
|
+
continue;
|
|
30117
|
+
const key = line.slice(0, colon).trim();
|
|
30118
|
+
const value = line.slice(colon + 1).trim();
|
|
30119
|
+
if (!key || !value)
|
|
30120
|
+
continue;
|
|
30121
|
+
if (key === "name")
|
|
30122
|
+
result.name = value;
|
|
30123
|
+
else if (key === "description")
|
|
30124
|
+
result.description = value;
|
|
30125
|
+
else if (key === "displayName" || key === "display_name")
|
|
30126
|
+
result.displayName = value;
|
|
30127
|
+
else if (key === "category")
|
|
30128
|
+
result.category = value;
|
|
30129
|
+
else if (key === "tags") {
|
|
30130
|
+
result.tags = value.replace(/[\[\]]/g, "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
30131
|
+
}
|
|
30132
|
+
}
|
|
30133
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
30134
|
+
}
|
|
30135
|
+
function discoverSkillsInDir(dir) {
|
|
30136
|
+
if (!existsSync(dir))
|
|
30137
|
+
return [];
|
|
30138
|
+
const result = [];
|
|
30139
|
+
try {
|
|
30140
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
30141
|
+
for (const entry of entries) {
|
|
30142
|
+
if (!entry.isDirectory())
|
|
30143
|
+
continue;
|
|
30144
|
+
const skillMdPath = join(dir, entry.name, "SKILL.md");
|
|
30145
|
+
if (!existsSync(skillMdPath))
|
|
30146
|
+
continue;
|
|
30147
|
+
let content;
|
|
30148
|
+
try {
|
|
30149
|
+
content = readFileSync(skillMdPath, "utf-8");
|
|
30150
|
+
} catch {
|
|
30151
|
+
continue;
|
|
30152
|
+
}
|
|
30153
|
+
const fm = parseSkillMdFrontmatter(content);
|
|
30154
|
+
if (!fm?.name)
|
|
30155
|
+
continue;
|
|
30156
|
+
const name = fm.name.replace(/^skill-/, "");
|
|
30157
|
+
result.push({
|
|
30158
|
+
name,
|
|
30159
|
+
displayName: fm.displayName || name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
30160
|
+
description: fm.description || "",
|
|
30161
|
+
category: fm.category || "Development Tools",
|
|
30162
|
+
tags: fm.tags || [],
|
|
30163
|
+
source: "custom"
|
|
30164
|
+
});
|
|
30165
|
+
}
|
|
30166
|
+
} catch {}
|
|
30167
|
+
return result;
|
|
30168
|
+
}
|
|
30169
|
+
var _registryCache = null;
|
|
30170
|
+
var _registryCacheTime = 0;
|
|
30171
|
+
var REGISTRY_CACHE_TTL = 5000;
|
|
30172
|
+
function loadRegistry(cwd) {
|
|
30173
|
+
const now = Date.now();
|
|
30174
|
+
if (_registryCache && now - _registryCacheTime < REGISTRY_CACHE_TTL) {
|
|
30175
|
+
return _registryCache;
|
|
30176
|
+
}
|
|
30177
|
+
const official = SKILLS.map((s) => ({ ...s, source: "official" }));
|
|
30178
|
+
const globalCustomNew = discoverSkillsInDir(join(homedir(), ".hasna", "skills", "custom"));
|
|
30179
|
+
const globalCustomOld = discoverSkillsInDir(join(homedir(), ".skills"));
|
|
30180
|
+
const oldNames = new Set(globalCustomNew.map((s) => s.name));
|
|
30181
|
+
const globalCustom = [...globalCustomNew, ...globalCustomOld.filter((s) => !oldNames.has(s.name))];
|
|
30182
|
+
const projectCustom = discoverSkillsInDir(join(cwd || process.cwd(), ".skills", "custom-skills"));
|
|
30183
|
+
const customNames = new Set([...globalCustom, ...projectCustom].map((s) => s.name));
|
|
30184
|
+
const filtered = official.filter((s) => !customNames.has(s.name));
|
|
30185
|
+
_registryCache = [...filtered, ...globalCustom, ...projectCustom];
|
|
30186
|
+
_registryCacheTime = now;
|
|
30187
|
+
return _registryCache;
|
|
30188
|
+
}
|
|
30089
30189
|
function getSkillsByCategory(category) {
|
|
30090
|
-
return
|
|
30190
|
+
return loadRegistry().filter((s) => s.category === category);
|
|
30091
30191
|
}
|
|
30092
30192
|
function editDistance(a, b) {
|
|
30093
30193
|
if (a === b)
|
|
@@ -30133,7 +30233,7 @@ function searchSkills(query) {
|
|
|
30133
30233
|
if (words.length === 0)
|
|
30134
30234
|
return [];
|
|
30135
30235
|
const scored = [];
|
|
30136
|
-
for (const skill of
|
|
30236
|
+
for (const skill of loadRegistry()) {
|
|
30137
30237
|
const nameLower = skill.name.toLowerCase();
|
|
30138
30238
|
const displayNameLower = skill.displayName.toLowerCase();
|
|
30139
30239
|
const descriptionLower = skill.description.toLowerCase();
|
|
@@ -30169,7 +30269,7 @@ function searchSkills(query) {
|
|
|
30169
30269
|
return scored.map((s) => s.skill);
|
|
30170
30270
|
}
|
|
30171
30271
|
function getSkill(name) {
|
|
30172
|
-
return
|
|
30272
|
+
return loadRegistry().find((s) => s.name === name);
|
|
30173
30273
|
}
|
|
30174
30274
|
function levenshtein(a, b) {
|
|
30175
30275
|
const m = a.length, n = b.length;
|
|
@@ -30183,14 +30283,14 @@ function levenshtein(a, b) {
|
|
|
30183
30283
|
}
|
|
30184
30284
|
function findSimilarSkills(query, maxResults = 3) {
|
|
30185
30285
|
const q = query.toLowerCase();
|
|
30186
|
-
const scored =
|
|
30286
|
+
const scored = loadRegistry().map((s) => ({ name: s.name, dist: levenshtein(q, s.name.toLowerCase()) })).filter((s) => s.dist <= Math.max(3, Math.floor(q.length / 2))).sort((a, b) => a.dist - b.dist);
|
|
30187
30287
|
return scored.slice(0, maxResults).map((s) => s.name);
|
|
30188
30288
|
}
|
|
30189
30289
|
|
|
30190
30290
|
// src/lib/installer.ts
|
|
30191
|
-
import { existsSync, cpSync, mkdirSync, writeFileSync, rmSync, readdirSync, statSync, readFileSync, accessSync, constants } from "fs";
|
|
30192
|
-
import { join, dirname } from "path";
|
|
30193
|
-
import { homedir } from "os";
|
|
30291
|
+
import { existsSync as existsSync2, cpSync, mkdirSync, writeFileSync, rmSync, readdirSync as readdirSync2, statSync, readFileSync as readFileSync2, accessSync, constants } from "fs";
|
|
30292
|
+
import { join as join2, dirname } from "path";
|
|
30293
|
+
import { homedir as homedir2 } from "os";
|
|
30194
30294
|
import { fileURLToPath } from "url";
|
|
30195
30295
|
|
|
30196
30296
|
// src/lib/utils.ts
|
|
@@ -30203,33 +30303,33 @@ var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
|
30203
30303
|
function findSkillsDir() {
|
|
30204
30304
|
let dir = __dirname2;
|
|
30205
30305
|
for (let i = 0;i < 5; i++) {
|
|
30206
|
-
const candidate =
|
|
30207
|
-
if (
|
|
30306
|
+
const candidate = join2(dir, "skills");
|
|
30307
|
+
if (existsSync2(candidate) && !dir.includes(".skills")) {
|
|
30208
30308
|
return candidate;
|
|
30209
30309
|
}
|
|
30210
30310
|
dir = dirname(dir);
|
|
30211
30311
|
}
|
|
30212
|
-
return
|
|
30312
|
+
return join2(__dirname2, "..", "skills");
|
|
30213
30313
|
}
|
|
30214
30314
|
var SKILLS_DIR = findSkillsDir();
|
|
30215
30315
|
function getSkillPath(name) {
|
|
30216
30316
|
const skillName = normalizeSkillName(name);
|
|
30217
|
-
return
|
|
30317
|
+
return join2(SKILLS_DIR, skillName);
|
|
30218
30318
|
}
|
|
30219
30319
|
function installSkill(name, options = {}) {
|
|
30220
30320
|
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
30221
30321
|
const skillName = normalizeSkillName(name);
|
|
30222
30322
|
const sourcePath = getSkillPath(name);
|
|
30223
|
-
const destDir =
|
|
30224
|
-
const destPath =
|
|
30225
|
-
if (!
|
|
30323
|
+
const destDir = join2(targetDir, ".skills");
|
|
30324
|
+
const destPath = join2(destDir, skillName);
|
|
30325
|
+
if (!existsSync2(sourcePath)) {
|
|
30226
30326
|
return {
|
|
30227
30327
|
skill: name,
|
|
30228
30328
|
success: false,
|
|
30229
30329
|
error: `Skill '${name}' not found`
|
|
30230
30330
|
};
|
|
30231
30331
|
}
|
|
30232
|
-
if (
|
|
30332
|
+
if (existsSync2(destPath) && !overwrite) {
|
|
30233
30333
|
return {
|
|
30234
30334
|
skill: name,
|
|
30235
30335
|
success: false,
|
|
@@ -30238,10 +30338,10 @@ function installSkill(name, options = {}) {
|
|
|
30238
30338
|
};
|
|
30239
30339
|
}
|
|
30240
30340
|
try {
|
|
30241
|
-
if (!
|
|
30341
|
+
if (!existsSync2(destDir)) {
|
|
30242
30342
|
mkdirSync(destDir, { recursive: true });
|
|
30243
30343
|
}
|
|
30244
|
-
if (
|
|
30344
|
+
if (existsSync2(destPath) && overwrite) {
|
|
30245
30345
|
rmSync(destPath, { recursive: true, force: true });
|
|
30246
30346
|
}
|
|
30247
30347
|
cpSync(sourcePath, destPath, {
|
|
@@ -30277,10 +30377,10 @@ function installSkill(name, options = {}) {
|
|
|
30277
30377
|
}
|
|
30278
30378
|
}
|
|
30279
30379
|
function updateSkillsIndex(skillsDir) {
|
|
30280
|
-
const indexPath =
|
|
30380
|
+
const indexPath = join2(skillsDir, "index.ts");
|
|
30281
30381
|
const meta3 = loadMeta(skillsDir);
|
|
30282
30382
|
const disabledSet = new Set(meta3.disabled || []);
|
|
30283
|
-
const skills =
|
|
30383
|
+
const skills = readdirSync2(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
|
|
30284
30384
|
const exports = skills.map((s) => {
|
|
30285
30385
|
const name = s.replace("skill-", "").replace(/-/g, "_");
|
|
30286
30386
|
return `export * as ${name} from './${s}/src/index.js';`;
|
|
@@ -30296,13 +30396,13 @@ ${exports}
|
|
|
30296
30396
|
writeFileSync(indexPath, content);
|
|
30297
30397
|
}
|
|
30298
30398
|
function getMetaPath(skillsDir) {
|
|
30299
|
-
return
|
|
30399
|
+
return join2(skillsDir, ".meta.json");
|
|
30300
30400
|
}
|
|
30301
30401
|
function loadMeta(skillsDir) {
|
|
30302
30402
|
const metaPath = getMetaPath(skillsDir);
|
|
30303
|
-
if (
|
|
30403
|
+
if (existsSync2(metaPath)) {
|
|
30304
30404
|
try {
|
|
30305
|
-
return JSON.parse(
|
|
30405
|
+
return JSON.parse(readFileSync2(metaPath, "utf-8"));
|
|
30306
30406
|
} catch {}
|
|
30307
30407
|
}
|
|
30308
30408
|
return { skills: {} };
|
|
@@ -30315,9 +30415,9 @@ function recordInstall(skillsDir, name) {
|
|
|
30315
30415
|
const skillName = normalizeSkillName(name);
|
|
30316
30416
|
let version2 = "unknown";
|
|
30317
30417
|
try {
|
|
30318
|
-
const pkgPath =
|
|
30319
|
-
if (
|
|
30320
|
-
const pkg = JSON.parse(
|
|
30418
|
+
const pkgPath = join2(skillsDir, skillName, "package.json");
|
|
30419
|
+
if (existsSync2(pkgPath)) {
|
|
30420
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
30321
30421
|
version2 = pkg.version || "unknown";
|
|
30322
30422
|
}
|
|
30323
30423
|
} catch {}
|
|
@@ -30330,20 +30430,20 @@ function recordRemove(skillsDir, name) {
|
|
|
30330
30430
|
saveMeta(skillsDir, meta3);
|
|
30331
30431
|
}
|
|
30332
30432
|
function getInstalledSkills(targetDir = process.cwd()) {
|
|
30333
|
-
const skillsDir =
|
|
30334
|
-
if (!
|
|
30433
|
+
const skillsDir = join2(targetDir, ".skills");
|
|
30434
|
+
if (!existsSync2(skillsDir)) {
|
|
30335
30435
|
return [];
|
|
30336
30436
|
}
|
|
30337
|
-
return
|
|
30338
|
-
const fullPath =
|
|
30437
|
+
return readdirSync2(skillsDir).filter((f) => {
|
|
30438
|
+
const fullPath = join2(skillsDir, f);
|
|
30339
30439
|
return f.startsWith("skill-") && statSync(fullPath).isDirectory();
|
|
30340
30440
|
}).map((f) => f.replace("skill-", ""));
|
|
30341
30441
|
}
|
|
30342
30442
|
function removeSkill(name, targetDir = process.cwd()) {
|
|
30343
30443
|
const skillName = normalizeSkillName(name);
|
|
30344
|
-
const skillsDir =
|
|
30345
|
-
const skillPath =
|
|
30346
|
-
if (!
|
|
30444
|
+
const skillsDir = join2(targetDir, ".skills");
|
|
30445
|
+
const skillPath = join2(skillsDir, skillName);
|
|
30446
|
+
if (!existsSync2(skillPath)) {
|
|
30347
30447
|
return false;
|
|
30348
30448
|
}
|
|
30349
30449
|
rmSync(skillPath, { recursive: true, force: true });
|
|
@@ -30351,7 +30451,14 @@ function removeSkill(name, targetDir = process.cwd()) {
|
|
|
30351
30451
|
recordRemove(skillsDir, name);
|
|
30352
30452
|
return true;
|
|
30353
30453
|
}
|
|
30354
|
-
var AGENT_TARGETS = ["claude", "codex", "gemini"];
|
|
30454
|
+
var AGENT_TARGETS = ["claude", "codex", "gemini", "pi", "opencode"];
|
|
30455
|
+
var AGENT_LABELS = {
|
|
30456
|
+
claude: "Claude Code",
|
|
30457
|
+
codex: "Codex CLI",
|
|
30458
|
+
gemini: "Gemini CLI",
|
|
30459
|
+
pi: "pi.dev",
|
|
30460
|
+
opencode: "OpenCode"
|
|
30461
|
+
};
|
|
30355
30462
|
function resolveAgents(agentArg) {
|
|
30356
30463
|
if (agentArg === "all")
|
|
30357
30464
|
return [...AGENT_TARGETS];
|
|
@@ -30362,27 +30469,31 @@ function resolveAgents(agentArg) {
|
|
|
30362
30469
|
return [agent];
|
|
30363
30470
|
}
|
|
30364
30471
|
function getAgentSkillsDir(agent, scope = "global", projectDir) {
|
|
30365
|
-
const
|
|
30366
|
-
|
|
30367
|
-
|
|
30472
|
+
const base = projectDir || process.cwd();
|
|
30473
|
+
switch (agent) {
|
|
30474
|
+
case "pi":
|
|
30475
|
+
return scope === "project" ? join2(base, ".pi", "skills") : join2(homedir2(), ".pi", "agent", "skills");
|
|
30476
|
+
case "opencode":
|
|
30477
|
+
return scope === "project" ? join2(base, ".opencode", "skills") : join2(homedir2(), ".opencode", "skills");
|
|
30478
|
+
default:
|
|
30479
|
+
return scope === "project" ? join2(base, `.${agent}`, "skills") : join2(homedir2(), `.${agent}`, "skills");
|
|
30368
30480
|
}
|
|
30369
|
-
return join(homedir(), agentDir, "skills");
|
|
30370
30481
|
}
|
|
30371
30482
|
function getAgentSkillPath(name, agent, scope = "global", projectDir) {
|
|
30372
30483
|
const skillName = normalizeSkillName(name);
|
|
30373
|
-
return
|
|
30484
|
+
return join2(getAgentSkillsDir(agent, scope, projectDir), skillName);
|
|
30374
30485
|
}
|
|
30375
30486
|
function installSkillForAgent(name, options, generateSkillMd) {
|
|
30376
30487
|
const { agent, scope = "global", projectDir } = options;
|
|
30377
30488
|
const skillName = normalizeSkillName(name);
|
|
30378
30489
|
const sourcePath = getSkillPath(name);
|
|
30379
|
-
if (!
|
|
30490
|
+
if (!existsSync2(sourcePath)) {
|
|
30380
30491
|
return { skill: name, success: false, error: `Skill '${name}' not found` };
|
|
30381
30492
|
}
|
|
30382
30493
|
let skillMdContent = null;
|
|
30383
|
-
const skillMdPath =
|
|
30384
|
-
if (
|
|
30385
|
-
skillMdContent =
|
|
30494
|
+
const skillMdPath = join2(sourcePath, "SKILL.md");
|
|
30495
|
+
if (existsSync2(skillMdPath)) {
|
|
30496
|
+
skillMdContent = readFileSync2(skillMdPath, "utf-8");
|
|
30386
30497
|
} else if (generateSkillMd) {
|
|
30387
30498
|
skillMdContent = generateSkillMd(name);
|
|
30388
30499
|
}
|
|
@@ -30391,17 +30502,12 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
30391
30502
|
}
|
|
30392
30503
|
const destDir = getAgentSkillPath(name, agent, scope, projectDir);
|
|
30393
30504
|
if (scope === "global") {
|
|
30394
|
-
const agentBaseDir =
|
|
30395
|
-
if (!
|
|
30396
|
-
const agentLabels = {
|
|
30397
|
-
claude: "Claude Code",
|
|
30398
|
-
codex: "Codex CLI",
|
|
30399
|
-
gemini: "Gemini CLI"
|
|
30400
|
-
};
|
|
30505
|
+
const agentBaseDir = agent === "pi" ? join2(homedir2(), ".pi", "agent") : join2(homedir2(), `.${agent}`);
|
|
30506
|
+
if (!existsSync2(agentBaseDir)) {
|
|
30401
30507
|
return {
|
|
30402
30508
|
skill: name,
|
|
30403
30509
|
success: false,
|
|
30404
|
-
error: `Agent directory ${agentBaseDir} does not exist. Is ${
|
|
30510
|
+
error: `Agent directory ${agentBaseDir} does not exist. Is ${AGENT_LABELS[agent]} installed?`
|
|
30405
30511
|
};
|
|
30406
30512
|
}
|
|
30407
30513
|
try {
|
|
@@ -30416,7 +30522,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
30416
30522
|
}
|
|
30417
30523
|
try {
|
|
30418
30524
|
mkdirSync(destDir, { recursive: true });
|
|
30419
|
-
writeFileSync(
|
|
30525
|
+
writeFileSync(join2(destDir, "SKILL.md"), skillMdContent);
|
|
30420
30526
|
return { skill: name, success: true, path: destDir };
|
|
30421
30527
|
} catch (error48) {
|
|
30422
30528
|
return {
|
|
@@ -30429,7 +30535,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
30429
30535
|
function removeSkillForAgent(name, options) {
|
|
30430
30536
|
const { agent, scope = "global", projectDir } = options;
|
|
30431
30537
|
const destDir = getAgentSkillPath(name, agent, scope, projectDir);
|
|
30432
|
-
if (!
|
|
30538
|
+
if (!existsSync2(destDir)) {
|
|
30433
30539
|
return false;
|
|
30434
30540
|
}
|
|
30435
30541
|
rmSync(destDir, { recursive: true, force: true });
|
|
@@ -30437,16 +30543,16 @@ function removeSkillForAgent(name, options) {
|
|
|
30437
30543
|
}
|
|
30438
30544
|
|
|
30439
30545
|
// src/lib/skillinfo.ts
|
|
30440
|
-
import { existsSync as
|
|
30441
|
-
import { join as
|
|
30546
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "fs";
|
|
30547
|
+
import { join as join3 } from "path";
|
|
30442
30548
|
function getSkillDocs(name) {
|
|
30443
30549
|
const skillPath = getSkillPath(name);
|
|
30444
|
-
if (!
|
|
30550
|
+
if (!existsSync3(skillPath))
|
|
30445
30551
|
return null;
|
|
30446
30552
|
return {
|
|
30447
|
-
skillMd: readIfExists(
|
|
30448
|
-
readme: readIfExists(
|
|
30449
|
-
claudeMd: readIfExists(
|
|
30553
|
+
skillMd: readIfExists(join3(skillPath, "SKILL.md")),
|
|
30554
|
+
readme: readIfExists(join3(skillPath, "README.md")),
|
|
30555
|
+
claudeMd: readIfExists(join3(skillPath, "CLAUDE.md"))
|
|
30450
30556
|
};
|
|
30451
30557
|
}
|
|
30452
30558
|
function getSkillBestDoc(name) {
|
|
@@ -30457,11 +30563,11 @@ function getSkillBestDoc(name) {
|
|
|
30457
30563
|
}
|
|
30458
30564
|
function getSkillRequirements(name) {
|
|
30459
30565
|
const skillPath = getSkillPath(name);
|
|
30460
|
-
if (!
|
|
30566
|
+
if (!existsSync3(skillPath))
|
|
30461
30567
|
return null;
|
|
30462
30568
|
const texts = [];
|
|
30463
30569
|
for (const file2 of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example", ".env.local.example"]) {
|
|
30464
|
-
const content = readIfExists(
|
|
30570
|
+
const content = readIfExists(join3(skillPath, file2));
|
|
30465
30571
|
if (content)
|
|
30466
30572
|
texts.push(content);
|
|
30467
30573
|
}
|
|
@@ -30488,10 +30594,10 @@ function getSkillRequirements(name) {
|
|
|
30488
30594
|
}
|
|
30489
30595
|
let cliCommand = null;
|
|
30490
30596
|
let dependencies = {};
|
|
30491
|
-
const pkgPath =
|
|
30492
|
-
if (
|
|
30597
|
+
const pkgPath = join3(skillPath, "package.json");
|
|
30598
|
+
if (existsSync3(pkgPath)) {
|
|
30493
30599
|
try {
|
|
30494
|
-
const pkg = JSON.parse(
|
|
30600
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
30495
30601
|
if (pkg.bin) {
|
|
30496
30602
|
const binKeys = Object.keys(pkg.bin);
|
|
30497
30603
|
if (binKeys.length > 0)
|
|
@@ -30511,25 +30617,25 @@ async function runSkill(name, args, options = {}) {
|
|
|
30511
30617
|
const skillName = normalizeSkillName(name);
|
|
30512
30618
|
let skillPath;
|
|
30513
30619
|
if (options.installed) {
|
|
30514
|
-
skillPath =
|
|
30620
|
+
skillPath = join3(process.cwd(), ".skills", skillName);
|
|
30515
30621
|
} else {
|
|
30516
|
-
const installedPath =
|
|
30517
|
-
if (
|
|
30622
|
+
const installedPath = join3(process.cwd(), ".skills", skillName);
|
|
30623
|
+
if (existsSync3(installedPath)) {
|
|
30518
30624
|
skillPath = installedPath;
|
|
30519
30625
|
} else {
|
|
30520
30626
|
skillPath = getSkillPath(name);
|
|
30521
30627
|
}
|
|
30522
30628
|
}
|
|
30523
|
-
if (!
|
|
30629
|
+
if (!existsSync3(skillPath)) {
|
|
30524
30630
|
return { exitCode: 1, error: `Skill '${name}' not found` };
|
|
30525
30631
|
}
|
|
30526
|
-
const pkgPath =
|
|
30527
|
-
if (!
|
|
30632
|
+
const pkgPath = join3(skillPath, "package.json");
|
|
30633
|
+
if (!existsSync3(pkgPath)) {
|
|
30528
30634
|
return { exitCode: 1, error: `No package.json in skill '${name}'` };
|
|
30529
30635
|
}
|
|
30530
30636
|
let entryPoint;
|
|
30531
30637
|
try {
|
|
30532
|
-
const pkg = JSON.parse(
|
|
30638
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
30533
30639
|
if (pkg.bin) {
|
|
30534
30640
|
const binValues = Object.values(pkg.bin);
|
|
30535
30641
|
entryPoint = binValues[0];
|
|
@@ -30543,12 +30649,12 @@ async function runSkill(name, args, options = {}) {
|
|
|
30543
30649
|
} catch {
|
|
30544
30650
|
return { exitCode: 1, error: `Failed to parse package.json for skill '${name}'` };
|
|
30545
30651
|
}
|
|
30546
|
-
const entryPath =
|
|
30547
|
-
if (!
|
|
30652
|
+
const entryPath = join3(skillPath, entryPoint);
|
|
30653
|
+
if (!existsSync3(entryPath)) {
|
|
30548
30654
|
return { exitCode: 1, error: `Entry point '${entryPoint}' not found in skill '${name}'` };
|
|
30549
30655
|
}
|
|
30550
|
-
const nodeModules =
|
|
30551
|
-
if (!
|
|
30656
|
+
const nodeModules = join3(skillPath, "node_modules");
|
|
30657
|
+
if (!existsSync3(nodeModules)) {
|
|
30552
30658
|
const install = Bun.spawn(["bun", "install", "--no-save"], {
|
|
30553
30659
|
cwd: skillPath,
|
|
30554
30660
|
stdout: "pipe",
|
|
@@ -30565,12 +30671,99 @@ async function runSkill(name, args, options = {}) {
|
|
|
30565
30671
|
const exitCode = await proc.exited;
|
|
30566
30672
|
return { exitCode };
|
|
30567
30673
|
}
|
|
30674
|
+
function detectProjectSkills(cwd = process.cwd()) {
|
|
30675
|
+
const pkgPath = join3(cwd, "package.json");
|
|
30676
|
+
if (!existsSync3(pkgPath)) {
|
|
30677
|
+
const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
|
|
30678
|
+
const recommended2 = alwaysRecommend.map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
|
|
30679
|
+
return { detected: [], recommended: recommended2 };
|
|
30680
|
+
}
|
|
30681
|
+
let pkg;
|
|
30682
|
+
try {
|
|
30683
|
+
pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
30684
|
+
} catch {
|
|
30685
|
+
const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
|
|
30686
|
+
const recommended2 = alwaysRecommend.map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
|
|
30687
|
+
return { detected: [], recommended: recommended2 };
|
|
30688
|
+
}
|
|
30689
|
+
const allDeps = {
|
|
30690
|
+
...pkg.dependencies,
|
|
30691
|
+
...pkg.devDependencies
|
|
30692
|
+
};
|
|
30693
|
+
const depNames = Object.keys(allDeps);
|
|
30694
|
+
const detected = [];
|
|
30695
|
+
const recommendedNames = new Set;
|
|
30696
|
+
for (const name of ["implementation-plan", "write", "deepresearch"]) {
|
|
30697
|
+
recommendedNames.add(name);
|
|
30698
|
+
}
|
|
30699
|
+
const frontendDeps = ["next", "react", "vue", "svelte", "nuxt", "@nuxtjs/nuxt"];
|
|
30700
|
+
for (const dep of frontendDeps) {
|
|
30701
|
+
if (depNames.some((d) => d === dep || d.startsWith(`${dep}/`))) {
|
|
30702
|
+
detected.push(dep);
|
|
30703
|
+
for (const name of ["image", "generate-favicon", "seo-brief-builder"]) {
|
|
30704
|
+
recommendedNames.add(name);
|
|
30705
|
+
}
|
|
30706
|
+
break;
|
|
30707
|
+
}
|
|
30708
|
+
}
|
|
30709
|
+
const backendDeps = ["express", "fastify", "hono", "koa", "@hono/hono"];
|
|
30710
|
+
for (const dep of backendDeps) {
|
|
30711
|
+
if (depNames.some((d) => d === dep || d.startsWith(`${dep}/`))) {
|
|
30712
|
+
detected.push(dep);
|
|
30713
|
+
for (const name of ["api-test-suite", "apidocs"]) {
|
|
30714
|
+
recommendedNames.add(name);
|
|
30715
|
+
}
|
|
30716
|
+
break;
|
|
30717
|
+
}
|
|
30718
|
+
}
|
|
30719
|
+
const aiDeps = ["@anthropic-ai/sdk", "openai", "@openai/openai", "anthropic"];
|
|
30720
|
+
for (const dep of aiDeps) {
|
|
30721
|
+
if (depNames.includes(dep)) {
|
|
30722
|
+
detected.push(dep);
|
|
30723
|
+
for (const name of ["deepresearch", "webcrawling"]) {
|
|
30724
|
+
recommendedNames.add(name);
|
|
30725
|
+
}
|
|
30726
|
+
break;
|
|
30727
|
+
}
|
|
30728
|
+
}
|
|
30729
|
+
if (depNames.includes("stripe")) {
|
|
30730
|
+
detected.push("stripe");
|
|
30731
|
+
recommendedNames.add("invoice");
|
|
30732
|
+
}
|
|
30733
|
+
const emailDeps = ["nodemailer", "@sendgrid/mail", "@sendgrid/client"];
|
|
30734
|
+
for (const dep of emailDeps) {
|
|
30735
|
+
if (depNames.includes(dep)) {
|
|
30736
|
+
detected.push(dep);
|
|
30737
|
+
for (const name of ["gmail", "email-campaign"]) {
|
|
30738
|
+
recommendedNames.add(name);
|
|
30739
|
+
}
|
|
30740
|
+
break;
|
|
30741
|
+
}
|
|
30742
|
+
}
|
|
30743
|
+
const testDeps = ["vitest", "jest", "mocha", "@jest/core"];
|
|
30744
|
+
for (const dep of testDeps) {
|
|
30745
|
+
if (depNames.includes(dep)) {
|
|
30746
|
+
detected.push(dep);
|
|
30747
|
+
recommendedNames.add("api-test-suite");
|
|
30748
|
+
break;
|
|
30749
|
+
}
|
|
30750
|
+
}
|
|
30751
|
+
if (depNames.includes("typescript")) {
|
|
30752
|
+
detected.push("typescript");
|
|
30753
|
+
for (const name of ["scaffold-project", "deploy"]) {
|
|
30754
|
+
recommendedNames.add(name);
|
|
30755
|
+
}
|
|
30756
|
+
}
|
|
30757
|
+
const uniqueDetected = Array.from(new Set(detected));
|
|
30758
|
+
const recommended = Array.from(recommendedNames).map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
|
|
30759
|
+
return { detected: uniqueDetected, recommended };
|
|
30760
|
+
}
|
|
30568
30761
|
function generateSkillMd(name) {
|
|
30569
30762
|
const meta3 = getSkill(name);
|
|
30570
30763
|
if (!meta3)
|
|
30571
30764
|
return null;
|
|
30572
30765
|
const skillPath = getSkillPath(name);
|
|
30573
|
-
if (!
|
|
30766
|
+
if (!existsSync3(skillPath))
|
|
30574
30767
|
return null;
|
|
30575
30768
|
const frontmatter = [
|
|
30576
30769
|
"---",
|
|
@@ -30579,13 +30772,13 @@ function generateSkillMd(name) {
|
|
|
30579
30772
|
"---"
|
|
30580
30773
|
].join(`
|
|
30581
30774
|
`);
|
|
30582
|
-
const readme = readIfExists(
|
|
30583
|
-
const claudeMd = readIfExists(
|
|
30775
|
+
const readme = readIfExists(join3(skillPath, "README.md"));
|
|
30776
|
+
const claudeMd = readIfExists(join3(skillPath, "CLAUDE.md"));
|
|
30584
30777
|
let cliCommand = null;
|
|
30585
|
-
const pkgPath =
|
|
30586
|
-
if (
|
|
30778
|
+
const pkgPath = join3(skillPath, "package.json");
|
|
30779
|
+
if (existsSync3(pkgPath)) {
|
|
30587
30780
|
try {
|
|
30588
|
-
const pkg = JSON.parse(
|
|
30781
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
30589
30782
|
if (pkg.bin) {
|
|
30590
30783
|
const binKeys = Object.keys(pkg.bin);
|
|
30591
30784
|
if (binKeys.length > 0)
|
|
@@ -30660,13 +30853,141 @@ function extractEnvVars(text) {
|
|
|
30660
30853
|
}
|
|
30661
30854
|
function readIfExists(path) {
|
|
30662
30855
|
try {
|
|
30663
|
-
if (
|
|
30664
|
-
return
|
|
30856
|
+
if (existsSync3(path)) {
|
|
30857
|
+
return readFileSync3(path, "utf-8");
|
|
30665
30858
|
}
|
|
30666
30859
|
} catch {}
|
|
30667
30860
|
return null;
|
|
30668
30861
|
}
|
|
30669
30862
|
|
|
30863
|
+
// src/lib/scheduler.ts
|
|
30864
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
30865
|
+
import { join as join4 } from "path";
|
|
30866
|
+
function getSchedulesPath(targetDir = process.cwd()) {
|
|
30867
|
+
return join4(targetDir, ".skills", "schedules.json");
|
|
30868
|
+
}
|
|
30869
|
+
function loadSchedules(targetDir = process.cwd()) {
|
|
30870
|
+
const path = getSchedulesPath(targetDir);
|
|
30871
|
+
if (existsSync4(path)) {
|
|
30872
|
+
try {
|
|
30873
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
30874
|
+
} catch {}
|
|
30875
|
+
}
|
|
30876
|
+
return { version: 1, schedules: [] };
|
|
30877
|
+
}
|
|
30878
|
+
function saveSchedules(data, targetDir = process.cwd()) {
|
|
30879
|
+
const path = getSchedulesPath(targetDir);
|
|
30880
|
+
const dir = join4(targetDir, ".skills");
|
|
30881
|
+
if (!existsSync4(dir))
|
|
30882
|
+
mkdirSync2(dir, { recursive: true });
|
|
30883
|
+
writeFileSync2(path, JSON.stringify(data, null, 2));
|
|
30884
|
+
}
|
|
30885
|
+
function validateCron(expr) {
|
|
30886
|
+
const fields = expr.trim().split(/\s+/);
|
|
30887
|
+
if (fields.length !== 5) {
|
|
30888
|
+
return { valid: false, error: `Expected 5 fields, got ${fields.length}. Format: "minute hour day-of-month month day-of-week"` };
|
|
30889
|
+
}
|
|
30890
|
+
return { valid: true };
|
|
30891
|
+
}
|
|
30892
|
+
function getNextRun(cron, from = new Date) {
|
|
30893
|
+
const { valid } = validateCron(cron);
|
|
30894
|
+
if (!valid)
|
|
30895
|
+
return null;
|
|
30896
|
+
const [minuteF, hourF, domF, monthF, dowF] = cron.trim().split(/\s+/);
|
|
30897
|
+
function parseField(f, min, max) {
|
|
30898
|
+
if (f === "*")
|
|
30899
|
+
return Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
|
30900
|
+
if (f.startsWith("*/")) {
|
|
30901
|
+
const step = parseInt(f.slice(2));
|
|
30902
|
+
if (isNaN(step))
|
|
30903
|
+
return [];
|
|
30904
|
+
const vals = [];
|
|
30905
|
+
for (let i = min;i <= max; i += step)
|
|
30906
|
+
vals.push(i);
|
|
30907
|
+
return vals;
|
|
30908
|
+
}
|
|
30909
|
+
return f.split(",").flatMap((part) => {
|
|
30910
|
+
if (part.includes("-")) {
|
|
30911
|
+
const [lo, hi] = part.split("-").map(Number);
|
|
30912
|
+
return Array.from({ length: hi - lo + 1 }, (_, i) => i + lo);
|
|
30913
|
+
}
|
|
30914
|
+
const n = parseInt(part);
|
|
30915
|
+
return isNaN(n) ? [] : [n];
|
|
30916
|
+
});
|
|
30917
|
+
}
|
|
30918
|
+
const minutes = parseField(minuteF, 0, 59);
|
|
30919
|
+
const hours = parseField(hourF, 0, 23);
|
|
30920
|
+
const doms = parseField(domF, 1, 31);
|
|
30921
|
+
const months = parseField(monthF, 1, 12);
|
|
30922
|
+
const dows = parseField(dowF, 0, 6);
|
|
30923
|
+
const candidate = new Date(from);
|
|
30924
|
+
candidate.setSeconds(0, 0);
|
|
30925
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
30926
|
+
const limit = new Date(from);
|
|
30927
|
+
limit.setFullYear(limit.getFullYear() + 1);
|
|
30928
|
+
while (candidate < limit) {
|
|
30929
|
+
const month = candidate.getMonth() + 1;
|
|
30930
|
+
const dom = candidate.getDate();
|
|
30931
|
+
const dow = candidate.getDay();
|
|
30932
|
+
const hour = candidate.getHours();
|
|
30933
|
+
const minute = candidate.getMinutes();
|
|
30934
|
+
if (!months.includes(month)) {
|
|
30935
|
+
candidate.setMonth(candidate.getMonth() + 1, 1);
|
|
30936
|
+
candidate.setHours(0, 0, 0, 0);
|
|
30937
|
+
continue;
|
|
30938
|
+
}
|
|
30939
|
+
if (!doms.includes(dom) || !dows.includes(dow)) {
|
|
30940
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
30941
|
+
candidate.setHours(0, 0, 0, 0);
|
|
30942
|
+
continue;
|
|
30943
|
+
}
|
|
30944
|
+
if (!hours.includes(hour)) {
|
|
30945
|
+
candidate.setHours(candidate.getHours() + 1, 0, 0, 0);
|
|
30946
|
+
continue;
|
|
30947
|
+
}
|
|
30948
|
+
if (!minutes.includes(minute)) {
|
|
30949
|
+
candidate.setMinutes(candidate.getMinutes() + 1, 0, 0);
|
|
30950
|
+
continue;
|
|
30951
|
+
}
|
|
30952
|
+
return new Date(candidate);
|
|
30953
|
+
}
|
|
30954
|
+
return null;
|
|
30955
|
+
}
|
|
30956
|
+
function addSchedule(skill, cron, options = {}) {
|
|
30957
|
+
const { valid, error: error48 } = validateCron(cron);
|
|
30958
|
+
if (!valid)
|
|
30959
|
+
return { schedule: null, error: error48 };
|
|
30960
|
+
const data = loadSchedules(options.targetDir);
|
|
30961
|
+
const id = `${skill}-${Date.now()}`;
|
|
30962
|
+
const now = new Date;
|
|
30963
|
+
const nextRun = getNextRun(cron, now);
|
|
30964
|
+
const schedule = {
|
|
30965
|
+
id,
|
|
30966
|
+
name: options.name || `${skill} (${cron})`,
|
|
30967
|
+
skill,
|
|
30968
|
+
cron,
|
|
30969
|
+
args: options.args,
|
|
30970
|
+
enabled: true,
|
|
30971
|
+
createdAt: now.toISOString(),
|
|
30972
|
+
nextRun: nextRun?.toISOString()
|
|
30973
|
+
};
|
|
30974
|
+
data.schedules.push(schedule);
|
|
30975
|
+
saveSchedules(data, options.targetDir);
|
|
30976
|
+
return { schedule };
|
|
30977
|
+
}
|
|
30978
|
+
function listSchedules(targetDir) {
|
|
30979
|
+
return loadSchedules(targetDir).schedules;
|
|
30980
|
+
}
|
|
30981
|
+
function removeSchedule(idOrName, targetDir) {
|
|
30982
|
+
const data = loadSchedules(targetDir);
|
|
30983
|
+
const before = data.schedules.length;
|
|
30984
|
+
data.schedules = data.schedules.filter((s) => s.id !== idOrName && s.name !== idOrName);
|
|
30985
|
+
if (data.schedules.length === before)
|
|
30986
|
+
return false;
|
|
30987
|
+
saveSchedules(data, targetDir);
|
|
30988
|
+
return true;
|
|
30989
|
+
}
|
|
30990
|
+
|
|
30670
30991
|
// src/mcp/index.ts
|
|
30671
30992
|
var server = new McpServer({
|
|
30672
30993
|
name: "skills",
|
|
@@ -30710,7 +31031,7 @@ server.registerTool("list_skills", {
|
|
|
30710
31031
|
offset: exports_external.number().optional()
|
|
30711
31032
|
}
|
|
30712
31033
|
}, async ({ category, detail, limit, offset }) => {
|
|
30713
|
-
const skills = category ? getSkillsByCategory(category) :
|
|
31034
|
+
const skills = category ? getSkillsByCategory(category) : loadRegistry();
|
|
30714
31035
|
const mapped = detail ? skills : skills.map((s) => ({ name: s.name, category: s.category }));
|
|
30715
31036
|
if (limit !== undefined || offset !== undefined) {
|
|
30716
31037
|
const start = offset || 0;
|
|
@@ -30795,7 +31116,7 @@ server.registerTool("get_skill_docs", {
|
|
|
30795
31116
|
});
|
|
30796
31117
|
server.registerTool("install_skill", {
|
|
30797
31118
|
title: "Install Skill",
|
|
30798
|
-
description: "Install a skill to .skills/ or to an agent dir (for: claude|codex|gemini|all).",
|
|
31119
|
+
description: "Install a skill to .skills/ or to an agent dir (for: claude|codex|gemini|pi|opencode|all).",
|
|
30799
31120
|
inputSchema: {
|
|
30800
31121
|
name: exports_external.string(),
|
|
30801
31122
|
for: exports_external.string().optional(),
|
|
@@ -30807,7 +31128,7 @@ server.registerTool("install_skill", {
|
|
|
30807
31128
|
try {
|
|
30808
31129
|
agents = resolveAgents(agentArg);
|
|
30809
31130
|
} catch (err) {
|
|
30810
|
-
return mcpError("INVALID_AGENT", err.message, [
|
|
31131
|
+
return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
|
|
30811
31132
|
}
|
|
30812
31133
|
const results = agents.map((a) => installSkillForAgent(name, { agent: a, scope: scope || "global" }, generateSkillMd));
|
|
30813
31134
|
return {
|
|
@@ -30845,7 +31166,7 @@ server.registerTool("install_category", {
|
|
|
30845
31166
|
try {
|
|
30846
31167
|
agents = resolveAgents(agentArg);
|
|
30847
31168
|
} catch (err) {
|
|
30848
|
-
return mcpError("INVALID_AGENT", err.message, [
|
|
31169
|
+
return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
|
|
30849
31170
|
}
|
|
30850
31171
|
const results2 = [];
|
|
30851
31172
|
for (const name of names) {
|
|
@@ -30879,7 +31200,7 @@ server.registerTool("remove_skill", {
|
|
|
30879
31200
|
try {
|
|
30880
31201
|
agents = resolveAgents(agentArg);
|
|
30881
31202
|
} catch (err) {
|
|
30882
|
-
return mcpError("INVALID_AGENT", err.message, [
|
|
31203
|
+
return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
|
|
30883
31204
|
}
|
|
30884
31205
|
const results = agents.map((a) => ({
|
|
30885
31206
|
skill: name,
|
|
@@ -30912,7 +31233,7 @@ server.registerTool("list_tags", {
|
|
|
30912
31233
|
description: "List all unique skill tags with occurrence counts."
|
|
30913
31234
|
}, async () => {
|
|
30914
31235
|
const tagCounts = new Map;
|
|
30915
|
-
for (const skill of
|
|
31236
|
+
for (const skill of loadRegistry()) {
|
|
30916
31237
|
for (const tag of skill.tags) {
|
|
30917
31238
|
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
|
|
30918
31239
|
}
|
|
@@ -30986,7 +31307,7 @@ server.registerTool("import_skills", {
|
|
|
30986
31307
|
try {
|
|
30987
31308
|
agents = resolveAgents(agentArg);
|
|
30988
31309
|
} catch (err) {
|
|
30989
|
-
return mcpError("INVALID_AGENT", err.message, [
|
|
31310
|
+
return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
|
|
30990
31311
|
}
|
|
30991
31312
|
for (const name of skillList) {
|
|
30992
31313
|
const agentResults = agents.map((a) => installSkillForAgent(name, { agent: a, scope: scope || "global" }, generateSkillMd));
|
|
@@ -31014,21 +31335,20 @@ server.registerTool("whoami", {
|
|
|
31014
31335
|
const version2 = package_default.version;
|
|
31015
31336
|
const cwd = process.cwd();
|
|
31016
31337
|
const installed = getInstalledSkills();
|
|
31017
|
-
const agentNames = ["claude", "codex", "gemini"];
|
|
31018
31338
|
const agents = [];
|
|
31019
|
-
for (const agent of
|
|
31020
|
-
const agentSkillsPath =
|
|
31021
|
-
const exists =
|
|
31339
|
+
for (const agent of AGENT_TARGETS) {
|
|
31340
|
+
const agentSkillsPath = getAgentSkillsDir(agent, "global");
|
|
31341
|
+
const exists = existsSync5(agentSkillsPath);
|
|
31022
31342
|
let skillCount = 0;
|
|
31023
31343
|
if (exists) {
|
|
31024
31344
|
try {
|
|
31025
|
-
skillCount =
|
|
31026
|
-
const full =
|
|
31345
|
+
skillCount = readdirSync4(agentSkillsPath).filter((f) => {
|
|
31346
|
+
const full = join5(agentSkillsPath, f);
|
|
31027
31347
|
return f.startsWith("skill-") && statSync2(full).isDirectory();
|
|
31028
31348
|
}).length;
|
|
31029
31349
|
} catch {}
|
|
31030
31350
|
}
|
|
31031
|
-
agents.push({ agent, path: agentSkillsPath, exists, skillCount });
|
|
31351
|
+
agents.push({ agent, label: AGENT_LABELS[agent], path: agentSkillsPath, exists, skillCount });
|
|
31032
31352
|
}
|
|
31033
31353
|
const skillsDir = getSkillPath("image").replace(/[/\\][^/\\]*$/, "");
|
|
31034
31354
|
const result = {
|
|
@@ -31041,12 +31361,107 @@ server.registerTool("whoami", {
|
|
|
31041
31361
|
};
|
|
31042
31362
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
31043
31363
|
});
|
|
31364
|
+
server.registerTool("schedule_skill", {
|
|
31365
|
+
title: "Schedule Skill",
|
|
31366
|
+
description: "Add a cron schedule to run a skill at a recurring time. Cron format: 'minute hour dom month dow' (e.g. '0 9 * * *' = daily at 9am).",
|
|
31367
|
+
inputSchema: {
|
|
31368
|
+
skill: exports_external.string(),
|
|
31369
|
+
cron: exports_external.string(),
|
|
31370
|
+
name: exports_external.string().optional(),
|
|
31371
|
+
args: exports_external.array(exports_external.string()).optional()
|
|
31372
|
+
}
|
|
31373
|
+
}, async ({ skill, cron, name, args }) => {
|
|
31374
|
+
const { schedule, error: error48 } = addSchedule(skill, cron, { name, args });
|
|
31375
|
+
if (error48 || !schedule) {
|
|
31376
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: error48 || "Failed to add schedule" }) }] };
|
|
31377
|
+
}
|
|
31378
|
+
return { content: [{ type: "text", text: JSON.stringify(schedule, null, 2) }] };
|
|
31379
|
+
});
|
|
31380
|
+
server.registerTool("list_schedules", {
|
|
31381
|
+
title: "List Schedules",
|
|
31382
|
+
description: "List all scheduled skill runs.",
|
|
31383
|
+
inputSchema: {}
|
|
31384
|
+
}, async () => {
|
|
31385
|
+
const schedules = listSchedules();
|
|
31386
|
+
return { content: [{ type: "text", text: JSON.stringify(schedules, null, 2) }] };
|
|
31387
|
+
});
|
|
31388
|
+
server.registerTool("remove_schedule", {
|
|
31389
|
+
title: "Remove Schedule",
|
|
31390
|
+
description: "Remove a schedule by its ID or name.",
|
|
31391
|
+
inputSchema: {
|
|
31392
|
+
id_or_name: exports_external.string()
|
|
31393
|
+
}
|
|
31394
|
+
}, async ({ id_or_name }) => {
|
|
31395
|
+
const removed = removeSchedule(id_or_name);
|
|
31396
|
+
return { content: [{ type: "text", text: JSON.stringify({ removed, id_or_name }) }] };
|
|
31397
|
+
});
|
|
31398
|
+
server.registerTool("detect_project_skills", {
|
|
31399
|
+
title: "Detect Project Skills",
|
|
31400
|
+
description: "Detect project type from package.json and return recommended skills based on dependencies.",
|
|
31401
|
+
inputSchema: {
|
|
31402
|
+
directory: exports_external.string().optional()
|
|
31403
|
+
}
|
|
31404
|
+
}, async ({ directory }) => {
|
|
31405
|
+
const cwd = directory || process.cwd();
|
|
31406
|
+
const { detected, recommended } = detectProjectSkills(cwd);
|
|
31407
|
+
return {
|
|
31408
|
+
content: [{
|
|
31409
|
+
type: "text",
|
|
31410
|
+
text: JSON.stringify({
|
|
31411
|
+
directory: cwd,
|
|
31412
|
+
detected,
|
|
31413
|
+
recommended: recommended.map((s) => ({ name: s.name, displayName: s.displayName, description: s.description, category: s.category }))
|
|
31414
|
+
}, null, 2)
|
|
31415
|
+
}]
|
|
31416
|
+
};
|
|
31417
|
+
});
|
|
31418
|
+
server.registerTool("validate_skill", {
|
|
31419
|
+
title: "Validate Skill",
|
|
31420
|
+
description: "Check a skill's structure: SKILL.md, package.json with bin entry, tsconfig.json, src/index.ts. Returns validation result with list of issues.",
|
|
31421
|
+
inputSchema: {
|
|
31422
|
+
name: exports_external.string()
|
|
31423
|
+
}
|
|
31424
|
+
}, async ({ name }) => {
|
|
31425
|
+
const skillPath = getSkillPath(name);
|
|
31426
|
+
const issues = [];
|
|
31427
|
+
if (!existsSync5(skillPath)) {
|
|
31428
|
+
return {
|
|
31429
|
+
content: [{ type: "text", text: JSON.stringify({ name, valid: false, issues: [`Skill directory not found: ${skillPath}`] }) }]
|
|
31430
|
+
};
|
|
31431
|
+
}
|
|
31432
|
+
if (!existsSync5(join5(skillPath, "SKILL.md")))
|
|
31433
|
+
issues.push("Missing SKILL.md");
|
|
31434
|
+
if (!existsSync5(join5(skillPath, "tsconfig.json")))
|
|
31435
|
+
issues.push("Missing tsconfig.json");
|
|
31436
|
+
const pkgPath = join5(skillPath, "package.json");
|
|
31437
|
+
if (!existsSync5(pkgPath)) {
|
|
31438
|
+
issues.push("Missing package.json");
|
|
31439
|
+
} else {
|
|
31440
|
+
try {
|
|
31441
|
+
const pkg = JSON.parse(__require("fs").readFileSync(pkgPath, "utf-8"));
|
|
31442
|
+
if (!pkg.bin || Object.keys(pkg.bin).length === 0)
|
|
31443
|
+
issues.push("package.json missing 'bin' entry");
|
|
31444
|
+
} catch {
|
|
31445
|
+
issues.push("package.json is invalid JSON");
|
|
31446
|
+
}
|
|
31447
|
+
}
|
|
31448
|
+
const srcDir = join5(skillPath, "src");
|
|
31449
|
+
if (!existsSync5(srcDir)) {
|
|
31450
|
+
issues.push("Missing src/ directory");
|
|
31451
|
+
} else if (!existsSync5(join5(srcDir, "index.ts"))) {
|
|
31452
|
+
issues.push("Missing src/index.ts");
|
|
31453
|
+
}
|
|
31454
|
+
const valid = issues.length === 0;
|
|
31455
|
+
return {
|
|
31456
|
+
content: [{ type: "text", text: JSON.stringify({ name, valid, path: skillPath, issues }) }]
|
|
31457
|
+
};
|
|
31458
|
+
});
|
|
31044
31459
|
server.registerResource("Skills Registry", "skills://registry", {
|
|
31045
31460
|
description: "Compact skill list [{name,category}]. Use skills://{name} for detail."
|
|
31046
31461
|
}, async () => ({
|
|
31047
31462
|
contents: [{
|
|
31048
31463
|
uri: "skills://registry",
|
|
31049
|
-
text: JSON.stringify(
|
|
31464
|
+
text: JSON.stringify(loadRegistry().map((s) => ({ name: s.name, category: s.category }))),
|
|
31050
31465
|
mimeType: "application/json"
|
|
31051
31466
|
}]
|
|
31052
31467
|
}));
|