@hasna/skills 0.1.14 → 0.1.15

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