@hasna/skills 0.1.43 → 0.1.44

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
@@ -6509,6 +6509,100 @@ var require_dist = __commonJS((exports, module) => {
6509
6509
  exports.default = formatsPlugin;
6510
6510
  });
6511
6511
 
6512
+ // src/lib/config.ts
6513
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
6514
+ import { join, dirname } from "path";
6515
+ import { homedir } from "os";
6516
+ function validKeys() {
6517
+ return ["mode", ...Object.keys(ENUM_KEYS), ...STRING_KEYS];
6518
+ }
6519
+ function allowedValues(key) {
6520
+ if (key === "mode")
6521
+ return MODE_VALUES;
6522
+ return ENUM_KEYS[key];
6523
+ }
6524
+ function normalizeConfigValue(key, value) {
6525
+ if (typeof value !== "string")
6526
+ return;
6527
+ if (key === "mode")
6528
+ return MODE_ALIASES[value.trim().toLowerCase()];
6529
+ const allowed = allowedValues(key);
6530
+ if (allowed)
6531
+ return allowed.includes(value) ? value : undefined;
6532
+ if (key === "apiUrl") {
6533
+ try {
6534
+ const url2 = new URL(value);
6535
+ if (url2.protocol !== "http:" && url2.protocol !== "https:")
6536
+ return;
6537
+ return value.replace(/\/+$/, "");
6538
+ } catch {
6539
+ return;
6540
+ }
6541
+ }
6542
+ return;
6543
+ }
6544
+ function getDataDir() {
6545
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
6546
+ const newDir = join(home, ".hasna", "skills");
6547
+ const oldConfigFile = join(home, ".skillsrc");
6548
+ if (existsSync(oldConfigFile) && !existsSync(join(newDir, "config.json"))) {
6549
+ mkdirSync(newDir, { recursive: true });
6550
+ try {
6551
+ copyFileSync(oldConfigFile, join(newDir, "config.json"));
6552
+ } catch {}
6553
+ }
6554
+ mkdirSync(newDir, { recursive: true });
6555
+ return newDir;
6556
+ }
6557
+ function getConfigPath(scope) {
6558
+ if (scope === "global") {
6559
+ return join(getDataDir(), "config.json");
6560
+ }
6561
+ return join(process.cwd(), "skills.config.json");
6562
+ }
6563
+ function readConfigFile(path) {
6564
+ if (!existsSync(path))
6565
+ return {};
6566
+ try {
6567
+ const raw = readFileSync(path, "utf-8");
6568
+ const parsed = JSON.parse(raw);
6569
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
6570
+ return {};
6571
+ const config2 = {};
6572
+ for (const key of validKeys()) {
6573
+ const value = normalizeConfigValue(key, parsed[key]);
6574
+ if (value !== undefined)
6575
+ config2[key] = value;
6576
+ }
6577
+ return config2;
6578
+ } catch {
6579
+ return {};
6580
+ }
6581
+ }
6582
+ function loadConfig() {
6583
+ const globalConfig2 = readConfigFile(getConfigPath("global"));
6584
+ const projectConfig = readConfigFile(getConfigPath("project"));
6585
+ return { ...globalConfig2, ...projectConfig };
6586
+ }
6587
+ var ENUM_KEYS, STRING_KEYS, MODE_VALUES, MODE_ALIASES;
6588
+ var init_config = __esm(() => {
6589
+ ENUM_KEYS = {
6590
+ defaultAgent: ["claude", "codex", "gemini", "pi", "opencode", "all"],
6591
+ defaultScope: ["global", "project"],
6592
+ format: ["compact", "json", "csv"]
6593
+ };
6594
+ STRING_KEYS = ["apiUrl"];
6595
+ MODE_VALUES = ["local", "hosted"];
6596
+ MODE_ALIASES = {
6597
+ local: "local",
6598
+ offline: "local",
6599
+ hosted: "hosted",
6600
+ remote: "hosted",
6601
+ "skills.md": "hosted",
6602
+ skillsmd: "hosted"
6603
+ };
6604
+ });
6605
+
6512
6606
  // src/lib/skill-aliases.ts
6513
6607
  function normalizeSkillSlug(name) {
6514
6608
  return name.trim();
@@ -7016,7 +7110,7 @@ var init_pricing = __esm(() => {
7016
7110
 
7017
7111
  // src/lib/remote-run-contract.ts
7018
7112
  function normalizeRemoteSkillRunContract(payload, fallbackSkill) {
7019
- const record3 = isRecord(payload) ? payload : {};
7113
+ const record3 = isRecord2(payload) ? payload : {};
7020
7114
  return {
7021
7115
  contractVersion: REMOTE_SKILL_RUN_CONTRACT_VERSION,
7022
7116
  ...pickString(record3, "id"),
@@ -7044,7 +7138,7 @@ function normalizeRemoteSkillRunContract(payload, fallbackSkill) {
7044
7138
  ...pickNumber(record3, "balanceCents")
7045
7139
  };
7046
7140
  }
7047
- function isRecord(value) {
7141
+ function isRecord2(value) {
7048
7142
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
7049
7143
  }
7050
7144
  function hasOwn(record3, key) {
@@ -7063,104 +7157,10 @@ function pickNumber(record3, key) {
7063
7157
  return typeof value === "number" && Number.isFinite(value) ? { [key]: value } : {};
7064
7158
  }
7065
7159
  function pickPricing(record3) {
7066
- return isRecord(record3.pricing) ? { pricing: record3.pricing } : {};
7160
+ return isRecord2(record3.pricing) ? { pricing: record3.pricing } : {};
7067
7161
  }
7068
7162
  var REMOTE_SKILL_RUN_CONTRACT_VERSION = 1;
7069
7163
 
7070
- // src/lib/config.ts
7071
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, copyFileSync } from "fs";
7072
- import { join as join6, dirname as dirname2 } from "path";
7073
- import { homedir as homedir3 } from "os";
7074
- function validKeys() {
7075
- return ["mode", ...Object.keys(ENUM_KEYS), ...STRING_KEYS];
7076
- }
7077
- function allowedValues(key) {
7078
- if (key === "mode")
7079
- return MODE_VALUES;
7080
- return ENUM_KEYS[key];
7081
- }
7082
- function normalizeConfigValue(key, value) {
7083
- if (typeof value !== "string")
7084
- return;
7085
- if (key === "mode")
7086
- return MODE_ALIASES[value.trim().toLowerCase()];
7087
- const allowed = allowedValues(key);
7088
- if (allowed)
7089
- return allowed.includes(value) ? value : undefined;
7090
- if (key === "apiUrl") {
7091
- try {
7092
- const url2 = new URL(value);
7093
- if (url2.protocol !== "http:" && url2.protocol !== "https:")
7094
- return;
7095
- return value.replace(/\/+$/, "");
7096
- } catch {
7097
- return;
7098
- }
7099
- }
7100
- return;
7101
- }
7102
- function getDataDir() {
7103
- const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
7104
- const newDir = join6(home, ".hasna", "skills");
7105
- const oldConfigFile = join6(home, ".skillsrc");
7106
- if (existsSync6(oldConfigFile) && !existsSync6(join6(newDir, "config.json"))) {
7107
- mkdirSync3(newDir, { recursive: true });
7108
- try {
7109
- copyFileSync(oldConfigFile, join6(newDir, "config.json"));
7110
- } catch {}
7111
- }
7112
- mkdirSync3(newDir, { recursive: true });
7113
- return newDir;
7114
- }
7115
- function getConfigPath(scope) {
7116
- if (scope === "global") {
7117
- return join6(getDataDir(), "config.json");
7118
- }
7119
- return join6(process.cwd(), "skills.config.json");
7120
- }
7121
- function readConfigFile(path) {
7122
- if (!existsSync6(path))
7123
- return {};
7124
- try {
7125
- const raw = readFileSync6(path, "utf-8");
7126
- const parsed = JSON.parse(raw);
7127
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
7128
- return {};
7129
- const config2 = {};
7130
- for (const key of validKeys()) {
7131
- const value = normalizeConfigValue(key, parsed[key]);
7132
- if (value !== undefined)
7133
- config2[key] = value;
7134
- }
7135
- return config2;
7136
- } catch {
7137
- return {};
7138
- }
7139
- }
7140
- function loadConfig() {
7141
- const globalConfig2 = readConfigFile(getConfigPath("global"));
7142
- const projectConfig = readConfigFile(getConfigPath("project"));
7143
- return { ...globalConfig2, ...projectConfig };
7144
- }
7145
- var ENUM_KEYS, STRING_KEYS, MODE_VALUES, MODE_ALIASES;
7146
- var init_config = __esm(() => {
7147
- ENUM_KEYS = {
7148
- defaultAgent: ["claude", "codex", "gemini", "pi", "opencode", "all"],
7149
- defaultScope: ["global", "project"],
7150
- format: ["compact", "json", "csv"]
7151
- };
7152
- STRING_KEYS = ["apiUrl"];
7153
- MODE_VALUES = ["local", "hosted"];
7154
- MODE_ALIASES = {
7155
- local: "local",
7156
- offline: "local",
7157
- hosted: "hosted",
7158
- remote: "hosted",
7159
- "skills.md": "hosted",
7160
- skillsmd: "hosted"
7161
- };
7162
- });
7163
-
7164
7164
  // src/lib/auth-store.ts
7165
7165
  var exports_auth_store = {};
7166
7166
  __export(exports_auth_store, {
@@ -7171,14 +7171,14 @@ __export(exports_auth_store, {
7171
7171
  getApiKey: () => getApiKey,
7172
7172
  clearAuthConfig: () => clearAuthConfig
7173
7173
  });
7174
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync } from "fs";
7175
- import { join as join7 } from "path";
7176
- import { homedir as homedir4 } from "os";
7174
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5, unlinkSync } from "fs";
7175
+ import { join as join9 } from "path";
7176
+ import { homedir as homedir3 } from "os";
7177
7177
  function getAuthConfig() {
7178
7178
  if (cachedConfig !== undefined)
7179
7179
  return cachedConfig;
7180
7180
  try {
7181
- const raw = readFileSync7(existsSync7(AUTH_FILE) ? AUTH_FILE : LEGACY_AUTH_FILE, "utf-8");
7181
+ const raw = readFileSync9(existsSync9(AUTH_FILE) ? AUTH_FILE : LEGACY_AUTH_FILE, "utf-8");
7182
7182
  const config2 = JSON.parse(raw);
7183
7183
  if (!config2.apiKey || !config2.email) {
7184
7184
  cachedConfig = null;
@@ -7192,8 +7192,8 @@ function getAuthConfig() {
7192
7192
  }
7193
7193
  }
7194
7194
  function saveAuthConfig(config2) {
7195
- mkdirSync4(AUTH_DIR, { recursive: true, mode: 448 });
7196
- writeFileSync4(AUTH_FILE, JSON.stringify(config2, null, 2) + `
7195
+ mkdirSync5(AUTH_DIR, { recursive: true, mode: 448 });
7196
+ writeFileSync5(AUTH_FILE, JSON.stringify(config2, null, 2) + `
7197
7197
  `, { mode: 384 });
7198
7198
  cachedConfig = config2;
7199
7199
  }
@@ -7231,9 +7231,9 @@ function getApiUrl() {
7231
7231
  var AUTH_DIR, AUTH_FILE, LEGACY_AUTH_FILE, cachedConfig;
7232
7232
  var init_auth_store = __esm(() => {
7233
7233
  init_config();
7234
- AUTH_DIR = join7(homedir4(), ".hasna", "skills");
7235
- AUTH_FILE = join7(AUTH_DIR, "auth.json");
7236
- LEGACY_AUTH_FILE = join7(homedir4(), ".skills", "auth.json");
7234
+ AUTH_DIR = join9(homedir3(), ".hasna", "skills");
7235
+ AUTH_FILE = join9(AUTH_DIR, "auth.json");
7236
+ LEGACY_AUTH_FILE = join9(homedir3(), ".skills", "auth.json");
7237
7237
  });
7238
7238
 
7239
7239
  // src/lib/remote-client.ts
@@ -21797,7 +21797,7 @@ class StdioServerTransport {
21797
21797
  // package.json
21798
21798
  var package_default = {
21799
21799
  name: "@hasna/skills",
21800
- version: "0.1.43",
21800
+ version: "0.1.44",
21801
21801
  description: "Skills library for AI coding agents",
21802
21802
  type: "module",
21803
21803
  bin: {
@@ -21816,6 +21816,7 @@ var package_default = {
21816
21816
  "!dist/platform",
21817
21817
  "!dist/server",
21818
21818
  "bin/",
21819
+ "docs/skill-standard.md",
21819
21820
  "skills/",
21820
21821
  "!skills/**/node_modules",
21821
21822
  "!skills/scaffold-project/my-app",
@@ -21860,7 +21861,6 @@ var package_default = {
21860
21861
  typescript: "^5"
21861
21862
  },
21862
21863
  dependencies: {
21863
- "@hasna/events": "^0.1.7",
21864
21864
  "@modelcontextprotocol/sdk": "^1.26.0",
21865
21865
  chalk: "^5.3.0",
21866
21866
  commander: "^12.1.0",
@@ -29473,98 +29473,990 @@ var EMPTY_COMPLETION_RESULT = {
29473
29473
  };
29474
29474
 
29475
29475
  // src/lib/registry.ts
29476
- init_skill_aliases();
29477
- import { existsSync, readFileSync, readdirSync } from "fs";
29478
- import { homedir } from "os";
29479
- import { join } from "path";
29476
+ init_config();
29477
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
29478
+ import { join as join4 } from "path";
29480
29479
 
29481
- // src/lib/registry-data/development-tools.ts
29482
- var DEVELOPMENT_TOOLS_SKILLS = [
29483
- {
29484
- name: "api-test-suite",
29485
- displayName: "API Test Suite",
29486
- description: "Generate and run API test suites with comprehensive endpoint coverage",
29487
- category: "Development Tools",
29488
- tags: ["api", "testing", "automation", "qa"]
29489
- },
29490
- {
29491
- name: "apidocs",
29492
- displayName: "API Docs",
29493
- description: "Agentic web crawler for API documentation indexing and semantic search",
29494
- category: "Development Tools",
29495
- tags: ["api", "documentation", "search", "indexing"]
29496
- },
29497
- {
29498
- name: "api-docs-portal",
29499
- displayName: "API Docs Portal",
29500
- description: "Generate premium static API documentation portals from OpenAPI specs, route lists, and endpoint examples",
29501
- category: "Development Tools",
29502
- tags: ["api", "documentation", "openapi", "portal", "premium", "remote"]
29503
- },
29504
- {
29505
- name: "sdk-generator",
29506
- displayName: "SDK Generator",
29507
- description: "Generate hosted TypeScript SDK scaffolds with client code, types, package files, tests, README, examples, and API summaries",
29508
- category: "Development Tools",
29509
- tags: ["sdk", "api", "typescript", "developer-tools", "premium", "remote"]
29510
- },
29511
- {
29512
- name: "repo-onboarding-report",
29513
- displayName: "Repo Onboarding Report",
29514
- description: "Generate hosted repository onboarding packages with architecture maps, setup guides, risk registers, and first-week plans",
29515
- category: "Development Tools",
29516
- tags: ["repository", "onboarding", "architecture", "developer-tools", "premium", "remote"]
29517
- },
29518
- {
29519
- name: "codefix",
29520
- displayName: "Code Fix",
29521
- description: "Code quality CLI for auto-linting, formatting, fixing, and style enforcement",
29522
- category: "Development Tools",
29523
- tags: ["code", "linting", "formatting", "quality"]
29524
- },
29525
- {
29526
- name: "commitpush",
29527
- displayName: "Commit Push",
29528
- description: "Create logical commits from repo changes and push directly to the main branch",
29529
- category: "Development Tools",
29530
- tags: ["git", "commit", "push", "automation"]
29531
- },
29532
- {
29533
- name: "commitpushpr",
29534
- displayName: "Commit Push PR",
29535
- description: "Create logical commits, push a feature branch, and open a GitHub pull request",
29536
- category: "Development Tools",
29537
- tags: ["git", "commit", "pull-request", "github", "automation"],
29538
- dependencies: ["commitpush"]
29539
- },
29540
- {
29541
- name: "consolelog",
29542
- displayName: "Console Log",
29543
- description: "Monitor console logs from web applications using Playwright headless browser",
29544
- category: "Development Tools",
29545
- tags: ["console", "monitoring", "debugging", "logs"]
29546
- },
29547
- {
29548
- name: "database-explorer",
29549
- displayName: "Database Explorer",
29550
- description: "Explore and query databases with an interactive interface",
29551
- category: "Development Tools",
29552
- tags: ["database", "explorer", "sql", "query"]
29553
- },
29554
- {
29555
- name: "deploy",
29556
- displayName: "Deploy",
29557
- description: "Deployment CLI for managing EC2 deployments with automated health checks",
29558
- category: "Development Tools",
29559
- tags: ["deployment", "ec2", "aws", "ci-cd"]
29560
- },
29561
- {
29562
- name: "diff-viewer",
29563
- displayName: "Diff Viewer",
29564
- description: "View and analyze file differences with visual diff representation",
29565
- category: "Development Tools",
29566
- tags: ["diff", "comparison", "files", "code-review"]
29567
- },
29480
+ // src/lib/portable-skills.ts
29481
+ init_config();
29482
+ import {
29483
+ cpSync,
29484
+ existsSync as existsSync3,
29485
+ lstatSync as lstatSync2,
29486
+ mkdirSync as mkdirSync2,
29487
+ readFileSync as readFileSync3,
29488
+ readdirSync as readdirSync2,
29489
+ rmSync,
29490
+ statSync as statSync2,
29491
+ writeFileSync as writeFileSync2
29492
+ } from "fs";
29493
+ import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join3, normalize as normalize2, relative } from "path";
29494
+
29495
+ // src/lib/skill-validation.ts
29496
+ init_pricing();
29497
+ import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
29498
+ import { isAbsolute, join as join2, normalize } from "path";
29499
+ var DOC_FILES = ["SKILL.md", "README.md", "CLAUDE.md"];
29500
+ var RESERVED_SKILL_ENTRIES = new Set([
29501
+ ".env",
29502
+ ".npmrc",
29503
+ ".pypirc",
29504
+ ".netrc",
29505
+ "id_rsa",
29506
+ "id_ed25519"
29507
+ ]);
29508
+ var KNOWN_TOP_LEVEL_ENTRIES = new Set([
29509
+ ".claude",
29510
+ ".env.example",
29511
+ ".gitignore",
29512
+ ".skills",
29513
+ "CLAUDE.md",
29514
+ "AGENTS.md",
29515
+ "LICENSE",
29516
+ "PROJECT_OVERVIEW.md",
29517
+ "QUICKSTART.md",
29518
+ "README.md",
29519
+ "SKILL.md",
29520
+ "api-docs-list.json",
29521
+ "auth.ts",
29522
+ "bun.lock",
29523
+ "bunfig.toml",
29524
+ "data",
29525
+ "dist",
29526
+ "examples",
29527
+ "exports",
29528
+ "http-client.ts",
29529
+ "index.ts",
29530
+ "install.sh",
29531
+ "installer.ts",
29532
+ "logs",
29533
+ "node_modules",
29534
+ "package.json",
29535
+ "scripts",
29536
+ "skill-install.ts",
29537
+ "skill.json",
29538
+ "src",
29539
+ "tests",
29540
+ "references",
29541
+ "assets",
29542
+ "tsconfig.json",
29543
+ "vision.ts"
29544
+ ]);
29545
+ var VALID_PROVENANCE_SOURCES = new Set(["official", "custom", "remote", "private", "private-hosted", "upstream"]);
29546
+ var VALID_BIN_COMMAND = /^[a-z0-9][a-z0-9._-]*$/;
29547
+ function add(target, code, message) {
29548
+ target.push({ code, message });
29549
+ }
29550
+ function readJsonFile(path) {
29551
+ return JSON.parse(readFileSync2(path, "utf-8"));
29552
+ }
29553
+ function asRecord(value) {
29554
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
29555
+ }
29556
+ function sortMessages(messages) {
29557
+ return [...messages].sort((a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
29558
+ }
29559
+ function isSafeRelativePath(value) {
29560
+ if (!value.trim() || isAbsolute(value))
29561
+ return false;
29562
+ const normalized = normalize(value).replace(/\\/g, "/");
29563
+ return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
29564
+ }
29565
+ function isHostedPackageMetadata(pkg) {
29566
+ const skills = asRecord(pkg.skills);
29567
+ if (!skills)
29568
+ return false;
29569
+ const runtime = typeof skills.runtime === "string" ? skills.runtime.trim().toLowerCase() : "";
29570
+ const source = typeof skills.source === "string" ? skills.source.trim().toLowerCase() : "";
29571
+ return runtime === "hosted" || source === "remote" || source === "private-hosted";
29572
+ }
29573
+ function isHostedMetadataSkill(skillName, frontmatter, registryMeta, packageDeclaresHosted) {
29574
+ if (packageDeclaresHosted)
29575
+ return true;
29576
+ if (isPremiumSkill(skillName))
29577
+ return true;
29578
+ if (frontmatter?.source === "private-hosted")
29579
+ return true;
29580
+ if (frontmatter?.source === "remote" && !registryMeta?.tags.includes("local"))
29581
+ return true;
29582
+ return false;
29583
+ }
29584
+ function parseSkillFrontmatter(content) {
29585
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
29586
+ if (!match)
29587
+ return null;
29588
+ const result = {};
29589
+ const lines = match[1].split(/\r?\n/);
29590
+ for (let i = 0;i < lines.length; i++) {
29591
+ const line = lines[i];
29592
+ const colon = line.indexOf(":");
29593
+ if (colon === -1)
29594
+ continue;
29595
+ const key = line.slice(0, colon).trim();
29596
+ const rawValue = line.slice(colon + 1).trim();
29597
+ if (!key)
29598
+ continue;
29599
+ if (key === "tags" && rawValue === "") {
29600
+ const tags = [];
29601
+ while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
29602
+ i++;
29603
+ tags.push(lines[i].replace(/^\s+-\s+/, "").trim());
29604
+ }
29605
+ result.tags = tags;
29606
+ continue;
29607
+ }
29608
+ const value = rawValue.replace(/^["']|["']$/g, "");
29609
+ if (!value)
29610
+ continue;
29611
+ if (key === "name")
29612
+ result.name = value;
29613
+ else if (key === "description")
29614
+ result.description = value;
29615
+ else if (key === "displayName" || key === "display_name")
29616
+ result.displayName = value;
29617
+ else if (key === "category")
29618
+ result.category = value;
29619
+ else if (key === "version")
29620
+ result.version = value;
29621
+ else if (key === "source")
29622
+ result.source = value;
29623
+ else if (key === "tags") {
29624
+ result.tags = value.replace(/[\[\]]/g, "").split(",").map((tag) => tag.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
29625
+ }
29626
+ }
29627
+ return Object.keys(result).length > 0 ? result : null;
29628
+ }
29629
+ function validateSkillDirectory(name, skillPath, registryMeta) {
29630
+ const bareName = name;
29631
+ const issues = [];
29632
+ const warnings = [];
29633
+ let packageDeclaresHosted = false;
29634
+ let packageSkillSource;
29635
+ const metadata = {
29636
+ binCommands: [],
29637
+ docFiles: []
29638
+ };
29639
+ if (!existsSync2(skillPath)) {
29640
+ add(issues, "skill.dir_missing", `Skill directory not found: ${skillPath}`);
29641
+ return {
29642
+ name: bareName,
29643
+ path: skillPath,
29644
+ valid: false,
29645
+ issues: sortMessages(issues),
29646
+ warnings: sortMessages(warnings),
29647
+ metadata
29648
+ };
29649
+ }
29650
+ if (!VALID_BIN_COMMAND.test(bareName)) {
29651
+ add(issues, "skill.name_invalid", `Skill name '${bareName}' must use lowercase letters, numbers, dots, underscores, or hyphens`);
29652
+ }
29653
+ for (const entry of readdirSync(skillPath).sort()) {
29654
+ const entryPath = join2(skillPath, entry);
29655
+ if (RESERVED_SKILL_ENTRIES.has(entry)) {
29656
+ add(issues, "skill.reserved_file", `Reserved file '${entry}' is not allowed in skill packages`);
29657
+ }
29658
+ if (lstatSync(entryPath).isSymbolicLink()) {
29659
+ add(issues, "skill.symlink_forbidden", `Symlink '${entry}' is not allowed in skill packages`);
29660
+ }
29661
+ if (!KNOWN_TOP_LEVEL_ENTRIES.has(entry)) {
29662
+ add(warnings, "skill.file_unrecognized", `Unrecognized top-level skill entry '${entry}'`);
29663
+ }
29664
+ }
29665
+ for (const docFile of DOC_FILES) {
29666
+ if (existsSync2(join2(skillPath, docFile)))
29667
+ metadata.docFiles.push(docFile);
29668
+ }
29669
+ if (metadata.docFiles.length === 0) {
29670
+ add(issues, "skill.docs_missing", "Missing documentation file: expected SKILL.md, README.md, or CLAUDE.md");
29671
+ }
29672
+ const skillMdPath = join2(skillPath, "SKILL.md");
29673
+ if (existsSync2(skillMdPath)) {
29674
+ const frontmatter = parseSkillFrontmatter(readFileSync2(skillMdPath, "utf-8"));
29675
+ if (!frontmatter) {
29676
+ add(warnings, "skill.frontmatter_missing", "SKILL.md has no YAML frontmatter");
29677
+ } else {
29678
+ metadata.skillMdFrontmatter = frontmatter;
29679
+ metadata.provenance = {
29680
+ ...metadata.provenance ?? { directoryName: bareName },
29681
+ ...frontmatter.source ? { frontmatterSource: frontmatter.source } : {},
29682
+ ...registryMeta?.source ? { registrySource: registryMeta.source } : {}
29683
+ };
29684
+ if (!frontmatter.name)
29685
+ add(issues, "skill.frontmatter_name_missing", "SKILL.md frontmatter missing name");
29686
+ if (!frontmatter.description)
29687
+ add(issues, "skill.frontmatter_description_missing", "SKILL.md frontmatter missing description");
29688
+ if (frontmatter.name && frontmatter.name !== bareName) {
29689
+ add(issues, "skill.frontmatter_name_mismatch", `SKILL.md name '${frontmatter.name}' does not match '${bareName}'`);
29690
+ }
29691
+ if (frontmatter.source && !VALID_PROVENANCE_SOURCES.has(frontmatter.source)) {
29692
+ add(issues, "skill.frontmatter_source_invalid", `SKILL.md source '${frontmatter.source}' is not one of: ${[...VALID_PROVENANCE_SOURCES].join(", ")}`);
29693
+ }
29694
+ if (frontmatter.tags && frontmatter.tags.some((tag) => !tag.trim())) {
29695
+ add(issues, "skill.frontmatter_tags_invalid", "SKILL.md tags must be non-empty strings");
29696
+ }
29697
+ if (registryMeta?.description && frontmatter.description && frontmatter.description.length < 8) {
29698
+ add(warnings, "skill.frontmatter_description_short", "SKILL.md description is very short");
29699
+ }
29700
+ if (registryMeta?.category && frontmatter.category && frontmatter.category !== registryMeta.category) {
29701
+ add(warnings, "skill.frontmatter_category_mismatch", `SKILL.md category '${frontmatter.category}' does not match registry category '${registryMeta.category}'`);
29702
+ }
29703
+ }
29704
+ } else {
29705
+ add(warnings, "skill.skill_md_missing", "Missing SKILL.md; registry docs may need generated agent-facing instructions");
29706
+ }
29707
+ const pkgPath = join2(skillPath, "package.json");
29708
+ if (!existsSync2(pkgPath)) {
29709
+ add(issues, "package.missing", "Missing package.json");
29710
+ } else {
29711
+ try {
29712
+ const pkg = readJsonFile(pkgPath);
29713
+ const packageRecord = asRecord(pkg);
29714
+ if (!packageRecord) {
29715
+ add(issues, "package.invalid_shape", "package.json must be an object");
29716
+ } else {
29717
+ packageDeclaresHosted = isHostedPackageMetadata(pkg);
29718
+ const skillsRecord = asRecord(pkg.skills);
29719
+ if (skillsRecord && typeof skillsRecord.source === "string") {
29720
+ packageSkillSource = skillsRecord.source;
29721
+ }
29722
+ const hostedMetadata2 = isHostedMetadataSkill(bareName, metadata.skillMdFrontmatter, registryMeta, packageDeclaresHosted);
29723
+ metadata.runtime = hostedMetadata2 ? "hosted" : "local";
29724
+ if (typeof pkg.name === "string") {
29725
+ metadata.packageName = pkg.name;
29726
+ if (pkg.name !== bareName) {
29727
+ add(issues, "package.name_mismatch", `package.json name '${pkg.name}' does not match '${bareName}'`);
29728
+ }
29729
+ } else {
29730
+ add(issues, "package.name_missing", "package.json missing string name");
29731
+ }
29732
+ if (typeof pkg.version === "string" && pkg.version.trim())
29733
+ metadata.version = pkg.version;
29734
+ else
29735
+ add(warnings, "package.version_missing", "package.json missing string version");
29736
+ metadata.provenance = {
29737
+ ...metadata.provenance ?? { directoryName: bareName },
29738
+ ...typeof pkg.name === "string" ? { packageName: pkg.name } : {},
29739
+ ...typeof pkg.version === "string" && pkg.version.trim() ? { packageVersion: pkg.version } : {},
29740
+ ...registryMeta?.source ? { registrySource: registryMeta.source } : {},
29741
+ ...packageSkillSource ? { packageSkillSource } : {}
29742
+ };
29743
+ const binRecord = asRecord(pkg.bin);
29744
+ if (!binRecord || Object.keys(binRecord).length === 0) {
29745
+ if (!hostedMetadata2) {
29746
+ add(issues, "package.bin_missing", "package.json missing non-empty bin object");
29747
+ }
29748
+ } else {
29749
+ if (hostedMetadata2) {
29750
+ add(issues, "package.hosted_bin_forbidden", "Hosted metadata packages must not expose a local bin entry");
29751
+ }
29752
+ for (const [command, target] of Object.entries(binRecord)) {
29753
+ if (!VALID_BIN_COMMAND.test(command)) {
29754
+ add(issues, "package.bin_command_invalid", `package.json bin command '${command}' must use lowercase letters, numbers, dots, underscores, or hyphens`);
29755
+ }
29756
+ if (typeof target !== "string" || !target.trim()) {
29757
+ add(issues, "package.bin_invalid", `package.json bin '${command}' must point to a file`);
29758
+ continue;
29759
+ }
29760
+ metadata.binCommands.push(command);
29761
+ if (!isSafeRelativePath(target)) {
29762
+ add(issues, "package.bin_target_unsafe", `package.json bin '${command}' target '${target}' must stay inside the skill directory`);
29763
+ continue;
29764
+ }
29765
+ const targetPath = join2(skillPath, target);
29766
+ if (!existsSync2(targetPath)) {
29767
+ add(warnings, "package.bin_target_missing", `package.json bin '${command}' target '${target}' is not present before build`);
29768
+ } else if (statSync(targetPath).isDirectory()) {
29769
+ add(issues, "package.bin_target_directory", `package.json bin '${command}' target '${target}' must point to a file, not a directory`);
29770
+ }
29771
+ }
29772
+ }
29773
+ }
29774
+ } catch (error48) {
29775
+ add(issues, "package.invalid_json", `package.json is invalid JSON: ${error48.message}`);
29776
+ }
29777
+ }
29778
+ const hostedMetadata = isHostedMetadataSkill(bareName, metadata.skillMdFrontmatter, registryMeta, packageDeclaresHosted);
29779
+ metadata.runtime = hostedMetadata ? "hosted" : "local";
29780
+ const srcDir = join2(skillPath, "src");
29781
+ if (hostedMetadata) {
29782
+ if (existsSync2(srcDir)) {
29783
+ add(issues, "skill.hosted_source_forbidden", "Hosted metadata skills must not include local implementation source");
29784
+ }
29785
+ } else if (!existsSync2(srcDir)) {
29786
+ add(issues, "skill.src_missing", "Missing src/ directory");
29787
+ } else if (!existsSync2(join2(srcDir, "index.ts")) && !existsSync2(join2(srcDir, "index.js"))) {
29788
+ add(issues, "skill.src_index_missing", "Missing src/index.ts or src/index.js");
29789
+ } else {
29790
+ const indexPath = existsSync2(join2(srcDir, "index.ts")) ? join2(srcDir, "index.ts") : join2(srcDir, "index.js");
29791
+ const size = statSync(indexPath).size;
29792
+ if (size < 50)
29793
+ add(warnings, "skill.src_index_minimal", `Source entry point is very small (${size}B)`);
29794
+ }
29795
+ return {
29796
+ name: bareName,
29797
+ path: skillPath,
29798
+ valid: issues.length === 0,
29799
+ issues: sortMessages(issues),
29800
+ warnings: sortMessages(warnings),
29801
+ metadata
29802
+ };
29803
+ }
29804
+
29805
+ // src/lib/portable-skills.ts
29806
+ var PORTABLE_SKILL_STANDARD = "hasna.skill.v1";
29807
+ var PORTABLE_SKILL_SCHEMA = "https://hasna.dev/schemas/skill.v1.json";
29808
+ var PORTABLE_SKILL_DEFAULT_VERSION = "0.1.0";
29809
+ var DATA_DIR_NON_SKILL_ENTRIES = new Set([
29810
+ "auth.json",
29811
+ "config.json",
29812
+ "custom",
29813
+ "skills.db"
29814
+ ]);
29815
+ var COPY_EXCLUDES = new Set([
29816
+ ".git",
29817
+ ".DS_Store",
29818
+ "node_modules",
29819
+ "dist",
29820
+ "build",
29821
+ ".turbo"
29822
+ ]);
29823
+ var DEFAULT_INPUTS = [
29824
+ {
29825
+ name: "args",
29826
+ type: "string[]",
29827
+ required: false,
29828
+ description: "Arguments passed after `skills run <name>`."
29829
+ }
29830
+ ];
29831
+ function normalizePortableSkillName(name) {
29832
+ const normalized = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
29833
+ if (!normalized || !/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) {
29834
+ throw new Error(`Invalid skill name '${name}'. Use letters, numbers, dots, underscores, or hyphens.`);
29835
+ }
29836
+ return normalized;
29837
+ }
29838
+ function getPortableSkillsRoot(options = {}) {
29839
+ if (options.rootDir)
29840
+ return options.rootDir;
29841
+ if (process.env["HASNA_SKILLS_DIR"])
29842
+ return process.env["HASNA_SKILLS_DIR"];
29843
+ if (options.homeDir)
29844
+ return join3(options.homeDir, ".hasna", "skills");
29845
+ return getDataDir();
29846
+ }
29847
+ function getPortableSkillPath(name, options = {}) {
29848
+ return join3(getPortableSkillsRoot(options), normalizePortableSkillName(name));
29849
+ }
29850
+ function findPortableSkill(name, options = {}) {
29851
+ let normalized;
29852
+ try {
29853
+ normalized = normalizePortableSkillName(name);
29854
+ } catch {
29855
+ return null;
29856
+ }
29857
+ const path = getPortableSkillPath(normalized, options);
29858
+ if (!existsSync3(path) || !statSync2(path).isDirectory())
29859
+ return null;
29860
+ try {
29861
+ return summarizePortableSkill(path, normalized);
29862
+ } catch {
29863
+ return null;
29864
+ }
29865
+ }
29866
+ function listPortableSkills(options = {}) {
29867
+ const root = getPortableSkillsRoot(options);
29868
+ if (!existsSync3(root))
29869
+ return [];
29870
+ const skills = [];
29871
+ for (const entry of readdirSync2(root).sort()) {
29872
+ if (entry.startsWith(".") || DATA_DIR_NON_SKILL_ENTRIES.has(entry))
29873
+ continue;
29874
+ const path = join3(root, entry);
29875
+ if (!safeIsDirectory(path))
29876
+ continue;
29877
+ try {
29878
+ skills.push(summarizePortableSkill(path, entry));
29879
+ } catch {
29880
+ continue;
29881
+ }
29882
+ }
29883
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
29884
+ }
29885
+ function listPortableSkillMetas(options = {}) {
29886
+ return listPortableSkills(options).map((skill) => ({
29887
+ name: skill.name,
29888
+ displayName: skill.displayName,
29889
+ description: skill.description,
29890
+ category: readPortableSkillManifest(skill.path).category || "Development Tools",
29891
+ tags: readPortableSkillManifest(skill.path).tags || ["custom"],
29892
+ version: skill.version,
29893
+ source: "custom",
29894
+ pricing: {
29895
+ tier: "free",
29896
+ billingUnit: "run",
29897
+ costCents: 0,
29898
+ formattedCost: "free",
29899
+ estimated: false,
29900
+ quoteDependsOnInput: false,
29901
+ quoteRequired: false,
29902
+ description: "Local portable skill"
29903
+ }
29904
+ }));
29905
+ }
29906
+ function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)) {
29907
+ const skillJsonPath = join3(skillPath, "skill.json");
29908
+ const skillMdPath = join3(skillPath, "SKILL.md");
29909
+ const pkgPath = join3(skillPath, "package.json");
29910
+ const jsonManifest = existsSync3(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
29911
+ const frontmatter = existsSync3(skillMdPath) ? parseSkillFrontmatter(readFileSync3(skillMdPath, "utf-8")) ?? undefined : undefined;
29912
+ const pkg = existsSync3(pkgPath) ? readJsonObject(pkgPath) : undefined;
29913
+ const name = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
29914
+ const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
29915
+ const version2 = stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version) ?? PORTABLE_SKILL_DEFAULT_VERSION;
29916
+ const commands = parseManifestCommands(jsonManifest) ?? inferPackageCommands(pkg, name) ?? [];
29917
+ return {
29918
+ $schema: stringField(jsonManifest, "$schema") ?? PORTABLE_SKILL_SCHEMA,
29919
+ standard: stringField(jsonManifest, "standard") ?? PORTABLE_SKILL_STANDARD,
29920
+ name,
29921
+ description,
29922
+ version: version2,
29923
+ displayName: stringField(jsonManifest, "displayName") ?? frontmatter?.displayName ?? displayName(name),
29924
+ category: stringField(jsonManifest, "category") ?? frontmatter?.category ?? "Development Tools",
29925
+ tags: stringArrayField(jsonManifest, "tags") ?? frontmatter?.tags ?? ["custom"],
29926
+ inputs: parseManifestInputs(jsonManifest) ?? DEFAULT_INPUTS,
29927
+ commands
29928
+ };
29929
+ }
29930
+ function scaffoldPortableSkill(name, options = {}) {
29931
+ const skillName = normalizePortableSkillName(name);
29932
+ const root = getPortableSkillsRoot(options);
29933
+ const skillPath = join3(root, skillName);
29934
+ if (existsSync3(skillPath)) {
29935
+ if (!options.overwrite)
29936
+ throw new Error(`Skill '${skillName}' already exists at ${skillPath}`);
29937
+ rmSync(skillPath, { recursive: true, force: true });
29938
+ }
29939
+ const manifest = createPortableManifest(skillName, {
29940
+ description: options.description ?? `${displayName(skillName)} skill`
29941
+ });
29942
+ writePortableSkillTemplate(skillPath, manifest);
29943
+ return { name: skillName, path: skillPath, manifest, created: true };
29944
+ }
29945
+ function portPortableSkill(sourcePath, options = {}) {
29946
+ const absoluteSource = normalize2(sourcePath);
29947
+ if (!existsSync3(absoluteSource) || !statSync2(absoluteSource).isDirectory()) {
29948
+ throw new Error(`Skill source directory not found: ${sourcePath}`);
29949
+ }
29950
+ const inferred = readPortableSkillManifest(absoluteSource, basename(absoluteSource));
29951
+ const skillName = normalizePortableSkillName(options.name ?? inferred.name);
29952
+ const root = getPortableSkillsRoot(options);
29953
+ const destination = join3(root, skillName);
29954
+ if (existsSync3(destination)) {
29955
+ if (!options.overwrite)
29956
+ throw new Error(`Skill '${skillName}' already exists at ${destination}`);
29957
+ rmSync(destination, { recursive: true, force: true });
29958
+ }
29959
+ mkdirSync2(dirname2(destination), { recursive: true });
29960
+ copySkillDirectory(absoluteSource, destination);
29961
+ const manifest = ensurePortableSkillFiles(destination, {
29962
+ ...inferred,
29963
+ name: skillName,
29964
+ displayName: inferred.displayName ?? displayName(skillName)
29965
+ });
29966
+ return { name: skillName, path: destination, manifest, created: true };
29967
+ }
29968
+ function validatePortableSkillDirectory(name, skillPath) {
29969
+ const normalizedName = normalizePortableSkillName(name);
29970
+ const base = validateSkillDirectory(normalizedName, skillPath);
29971
+ const issues = [...base.issues];
29972
+ const warnings = [...base.warnings];
29973
+ let manifest;
29974
+ if (existsSync3(skillPath)) {
29975
+ const skillJsonPath = join3(skillPath, "skill.json");
29976
+ const skillMdPath = join3(skillPath, "SKILL.md");
29977
+ if (!existsSync3(skillJsonPath) && !existsSync3(skillMdPath)) {
29978
+ add2(issues, "portable.manifest_missing", "Missing portable manifest: expected SKILL.md frontmatter and/or skill.json");
29979
+ }
29980
+ try {
29981
+ manifest = readPortableSkillManifest(skillPath, normalizedName);
29982
+ if (manifest.name !== normalizedName) {
29983
+ add2(issues, "portable.name_mismatch", `Portable manifest name '${manifest.name}' does not match '${normalizedName}'`);
29984
+ }
29985
+ if (manifest.standard !== PORTABLE_SKILL_STANDARD) {
29986
+ add2(issues, "portable.standard_invalid", `Portable manifest standard must be '${PORTABLE_SKILL_STANDARD}'`);
29987
+ }
29988
+ if (!manifest.description.trim()) {
29989
+ add2(issues, "portable.description_missing", "Portable manifest missing description");
29990
+ }
29991
+ if (!manifest.version.trim()) {
29992
+ add2(issues, "portable.version_missing", "Portable manifest missing version");
29993
+ }
29994
+ if (!Array.isArray(manifest.inputs) || manifest.inputs.length === 0) {
29995
+ add2(issues, "portable.inputs_missing", "Portable manifest must declare inputs");
29996
+ }
29997
+ if (!Array.isArray(manifest.commands) || manifest.commands.length === 0) {
29998
+ add2(issues, "portable.commands_missing", "Portable manifest must declare at least one command");
29999
+ } else {
30000
+ for (const command of manifest.commands) {
30001
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(command.name)) {
30002
+ add2(issues, "portable.command_name_invalid", `Command '${command.name}' must use lowercase letters, numbers, dots, underscores, or hyphens`);
30003
+ }
30004
+ if (!command.entry && !command.command) {
30005
+ add2(issues, "portable.command_target_missing", `Command '${command.name}' must declare entry or command`);
30006
+ continue;
30007
+ }
30008
+ if (command.entry) {
30009
+ if (!isSafeRelativePath2(command.entry)) {
30010
+ add2(issues, "portable.command_entry_unsafe", `Command '${command.name}' entry '${command.entry}' must stay inside the skill directory`);
30011
+ continue;
30012
+ }
30013
+ const entryPath = join3(skillPath, command.entry);
30014
+ if (!existsSync3(entryPath))
30015
+ add2(issues, "portable.command_entry_missing", `Command '${command.name}' entry '${command.entry}' is missing`);
30016
+ else if (statSync2(entryPath).isDirectory())
30017
+ add2(issues, "portable.command_entry_directory", `Command '${command.name}' entry '${command.entry}' must be a file`);
30018
+ }
30019
+ }
30020
+ }
30021
+ } catch (error48) {
30022
+ add2(issues, "portable.manifest_invalid", error48.message);
30023
+ }
30024
+ if (!existsSync3(join3(skillPath, "AGENTS.md"))) {
30025
+ add2(issues, "portable.agents_missing", "Missing AGENTS.md with build-out instructions for coding agents");
30026
+ }
30027
+ }
30028
+ const sortedIssues = sortMessages2(issues);
30029
+ const sortedWarnings = sortMessages2(warnings);
30030
+ return {
30031
+ ...base,
30032
+ valid: sortedIssues.length === 0,
30033
+ issues: sortedIssues,
30034
+ warnings: sortedWarnings,
30035
+ metadata: {
30036
+ ...base.metadata,
30037
+ portableManifest: manifest
30038
+ }
30039
+ };
30040
+ }
30041
+ function summarizePortableSkill(skillPath, fallbackName) {
30042
+ const manifest = readPortableSkillManifest(skillPath, fallbackName);
30043
+ return {
30044
+ name: manifest.name,
30045
+ displayName: manifest.displayName ?? displayName(manifest.name),
30046
+ description: manifest.description,
30047
+ version: manifest.version,
30048
+ path: skillPath,
30049
+ commands: manifest.commands,
30050
+ source: "custom",
30051
+ standard: manifest.standard
30052
+ };
30053
+ }
30054
+ function createPortableManifest(name, options) {
30055
+ return {
30056
+ $schema: PORTABLE_SKILL_SCHEMA,
30057
+ standard: PORTABLE_SKILL_STANDARD,
30058
+ name,
30059
+ description: options.description,
30060
+ version: PORTABLE_SKILL_DEFAULT_VERSION,
30061
+ displayName: displayName(name),
30062
+ category: "Development Tools",
30063
+ tags: ["custom", name],
30064
+ inputs: DEFAULT_INPUTS,
30065
+ commands: [{
30066
+ name,
30067
+ description: `Run ${displayName(name)}.`,
30068
+ entry: "src/index.ts",
30069
+ args: ["...args"]
30070
+ }]
30071
+ };
30072
+ }
30073
+ function writePortableSkillTemplate(skillPath, manifest) {
30074
+ mkdirSync2(join3(skillPath, "src"), { recursive: true });
30075
+ writeFileSync2(join3(skillPath, "SKILL.md"), renderSkillMd(manifest));
30076
+ writeFileSync2(join3(skillPath, "skill.json"), renderSkillJson(manifest));
30077
+ writeFileSync2(join3(skillPath, "AGENTS.md"), renderAgentsMd(manifest));
30078
+ writeFileSync2(join3(skillPath, "package.json"), renderPackageJson(manifest));
30079
+ writeFileSync2(join3(skillPath, "tsconfig.json"), renderTsconfig());
30080
+ writeFileSync2(join3(skillPath, "src", "index.ts"), renderEntrypoint(manifest));
30081
+ }
30082
+ function ensurePortableSkillFiles(skillPath, manifest) {
30083
+ let next = manifest;
30084
+ if (!next.commands.length) {
30085
+ next = {
30086
+ ...next,
30087
+ commands: [{
30088
+ name: next.name,
30089
+ description: `Run ${displayName(next.name)}.`,
30090
+ entry: "src/index.ts",
30091
+ args: ["...args"]
30092
+ }]
30093
+ };
30094
+ }
30095
+ if (!next.inputs.length)
30096
+ next = { ...next, inputs: DEFAULT_INPUTS };
30097
+ next = {
30098
+ ...next,
30099
+ standard: PORTABLE_SKILL_STANDARD,
30100
+ $schema: next.$schema ?? PORTABLE_SKILL_SCHEMA,
30101
+ displayName: next.displayName ?? displayName(next.name),
30102
+ category: next.category ?? "Development Tools",
30103
+ tags: next.tags?.length ? next.tags : ["custom", next.name]
30104
+ };
30105
+ const entry = next.commands[0]?.entry ?? "src/index.ts";
30106
+ if (entry && !existsSync3(join3(skillPath, entry))) {
30107
+ mkdirSync2(dirname2(join3(skillPath, entry)), { recursive: true });
30108
+ writeFileSync2(join3(skillPath, entry), renderEntrypoint(next));
30109
+ }
30110
+ if (!existsSync3(join3(skillPath, "SKILL.md")))
30111
+ writeFileSync2(join3(skillPath, "SKILL.md"), renderSkillMd(next));
30112
+ else
30113
+ writeFileSync2(join3(skillPath, "SKILL.md"), ensureSkillMdFrontmatter(readFileSync3(join3(skillPath, "SKILL.md"), "utf-8"), next));
30114
+ if (!existsSync3(join3(skillPath, "skill.json")))
30115
+ writeFileSync2(join3(skillPath, "skill.json"), renderSkillJson(next));
30116
+ if (!existsSync3(join3(skillPath, "AGENTS.md")))
30117
+ writeFileSync2(join3(skillPath, "AGENTS.md"), renderAgentsMd(next));
30118
+ if (!existsSync3(join3(skillPath, "package.json")))
30119
+ writeFileSync2(join3(skillPath, "package.json"), renderPackageJson(next));
30120
+ if (!existsSync3(join3(skillPath, "tsconfig.json")))
30121
+ writeFileSync2(join3(skillPath, "tsconfig.json"), renderTsconfig());
30122
+ return readPortableSkillManifest(skillPath, next.name);
30123
+ }
30124
+ function copySkillDirectory(source, destination) {
30125
+ mkdirSync2(destination, { recursive: true });
30126
+ cpSync(source, destination, {
30127
+ recursive: true,
30128
+ filter: (src) => {
30129
+ const rel = relative(source, src);
30130
+ if (!rel)
30131
+ return true;
30132
+ const first = rel.split(/[\\/]/)[0];
30133
+ if (COPY_EXCLUDES.has(first))
30134
+ return false;
30135
+ if (lstatSync2(src).isSymbolicLink())
30136
+ return false;
30137
+ return true;
30138
+ }
30139
+ });
30140
+ }
30141
+ function renderSkillMd(manifest) {
30142
+ const tags = manifest.tags?.length ? `tags:
30143
+ ${manifest.tags.map((tag) => ` - ${tag}`).join(`
30144
+ `)}
30145
+ ` : "";
30146
+ return `---
30147
+ name: ${manifest.name}
30148
+ description: ${manifest.description}
30149
+ version: ${manifest.version}
30150
+ source: custom
30151
+ category: ${manifest.category ?? "Development Tools"}
30152
+ ${tags}---
30153
+
30154
+ # ${manifest.displayName ?? displayName(manifest.name)}
30155
+
30156
+ ${manifest.description}
30157
+
30158
+ ## Usage
30159
+
30160
+ \`\`\`bash
30161
+ skills run ${manifest.name} --help
30162
+ \`\`\`
30163
+ `;
30164
+ }
30165
+ function renderSkillJson(manifest) {
30166
+ return `${JSON.stringify({
30167
+ $schema: manifest.$schema ?? PORTABLE_SKILL_SCHEMA,
30168
+ standard: PORTABLE_SKILL_STANDARD,
30169
+ name: manifest.name,
30170
+ description: manifest.description,
30171
+ version: manifest.version,
30172
+ displayName: manifest.displayName ?? displayName(manifest.name),
30173
+ category: manifest.category ?? "Development Tools",
30174
+ tags: manifest.tags ?? ["custom", manifest.name],
30175
+ inputs: manifest.inputs,
30176
+ commands: manifest.commands
30177
+ }, null, 2)}
30178
+ `;
30179
+ }
30180
+ function renderPackageJson(manifest) {
30181
+ const first = manifest.commands[0] ?? { name: manifest.name, entry: "src/index.ts" };
30182
+ return `${JSON.stringify({
30183
+ name: manifest.name,
30184
+ version: manifest.version,
30185
+ description: manifest.description,
30186
+ type: "module",
30187
+ bin: { [first.name]: first.entry ?? "src/index.ts" },
30188
+ scripts: { dev: `bun run ${first.entry ?? "src/index.ts"}` },
30189
+ dependencies: {}
30190
+ }, null, 2)}
30191
+ `;
30192
+ }
30193
+ function renderTsconfig() {
30194
+ return `${JSON.stringify({
30195
+ compilerOptions: {
30196
+ target: "ES2022",
30197
+ module: "ESNext",
30198
+ moduleResolution: "bundler",
30199
+ strict: true,
30200
+ outDir: "dist"
30201
+ },
30202
+ include: ["src/**/*.ts"]
30203
+ }, null, 2)}
30204
+ `;
30205
+ }
30206
+ function renderEntrypoint(manifest) {
30207
+ return `#!/usr/bin/env bun
30208
+
30209
+ const args = process.argv.slice(2);
30210
+
30211
+ if (args.includes("--help") || args.includes("-h")) {
30212
+ console.log("${manifest.name}");
30213
+ console.log("");
30214
+ console.log("${escapeJsString(manifest.description)}");
30215
+ console.log("");
30216
+ console.log("Usage: skills run ${manifest.name} [args...]");
30217
+ process.exit(0);
30218
+ }
30219
+
30220
+ console.log(JSON.stringify({
30221
+ skill: "${manifest.name}",
30222
+ args,
30223
+ }, null, 2));
30224
+ `;
30225
+ }
30226
+ function renderAgentsMd(manifest) {
30227
+ const command = manifest.commands[0];
30228
+ const entry = command?.entry ?? "src/index.ts";
30229
+ return `# Agent Build Instructions: ${manifest.name}
30230
+
30231
+ This folder is a portable @hasna/skills skill. Build it in place and keep it valid against the portable skill standard.
30232
+
30233
+ ## Contract
30234
+
30235
+ - Skill name: \`${manifest.name}\`
30236
+ - Description: ${manifest.description}
30237
+ - Manifest files: \`SKILL.md\` frontmatter and \`skill.json\`
30238
+ - Runtime entrypoint: \`${entry}\`
30239
+ - User command: \`skills run ${manifest.name} [args]\`
30240
+
30241
+ ## Build Rules
30242
+
30243
+ 1. Put executable logic in \`${entry}\` or files imported by it.
30244
+ 2. Keep \`skill.json\` updated when inputs, commands, or version change.
30245
+ 3. Keep \`SKILL.md\` concise and compatible with Codewith-style skill frontmatter: \`name\`, \`description\`, \`version\`, optional \`category\`, and optional \`tags\`.
30246
+ 4. Add tests under \`tests/\` when behavior is non-trivial, then run \`bun test\` from this folder if tests exist.
30247
+ 5. Verify with \`skills validate ${manifest.name}\` and smoke-test with \`skills run ${manifest.name} --help\`.
30248
+ 6. Do not commit secrets, generated credentials, \`.env\`, \`node_modules\`, or build output.
30249
+ `;
30250
+ }
30251
+ function ensureSkillMdFrontmatter(content, manifest) {
30252
+ const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trimStart();
30253
+ const generated = renderSkillMd(manifest);
30254
+ const frontmatter = generated.match(/^---\r?\n[\s\S]*?\r?\n---/)?.[0] ?? "";
30255
+ return `${frontmatter}
30256
+
30257
+ ${body || `# ${manifest.displayName ?? displayName(manifest.name)}
30258
+
30259
+ ${manifest.description}
30260
+ `}`;
30261
+ }
30262
+ function parseManifestCommands(value) {
30263
+ const raw = value?.commands;
30264
+ if (!Array.isArray(raw))
30265
+ return;
30266
+ const commands = raw.map((item) => {
30267
+ if (!isRecord(item))
30268
+ return null;
30269
+ const name = stringValue(item.name);
30270
+ if (!name)
30271
+ return null;
30272
+ return {
30273
+ name: normalizePortableSkillName(name),
30274
+ ...stringValue(item.description) ? { description: stringValue(item.description) } : {},
30275
+ ...stringValue(item.entry) ? { entry: stringValue(item.entry) } : {},
30276
+ ...stringValue(item.command) ? { command: stringValue(item.command) } : {},
30277
+ ...Array.isArray(item.args) ? { args: item.args.filter((arg) => typeof arg === "string") } : {}
30278
+ };
30279
+ }).filter((item) => item !== null);
30280
+ return commands.length ? commands : undefined;
30281
+ }
30282
+ function parseManifestInputs(value) {
30283
+ const raw = value?.inputs;
30284
+ if (!Array.isArray(raw))
30285
+ return;
30286
+ const inputs = raw.map((item) => {
30287
+ if (!isRecord(item))
30288
+ return null;
30289
+ const name = stringValue(item.name);
30290
+ const type = stringValue(item.type);
30291
+ if (!name || !type)
30292
+ return null;
30293
+ return {
30294
+ name,
30295
+ type,
30296
+ ...typeof item.required === "boolean" ? { required: item.required } : {},
30297
+ ...stringValue(item.description) ? { description: stringValue(item.description) } : {}
30298
+ };
30299
+ }).filter((item) => item !== null);
30300
+ return inputs.length ? inputs : undefined;
30301
+ }
30302
+ function inferPackageCommands(pkg, fallbackName) {
30303
+ if (!pkg)
30304
+ return;
30305
+ if (isRecord(pkg.bin)) {
30306
+ const commands = Object.entries(pkg.bin).filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0).map(([name, entry]) => ({
30307
+ name: normalizePortableSkillName(name),
30308
+ entry: entry.replace(/^\.\//, ""),
30309
+ description: `Run ${displayName(fallbackName)}.`
30310
+ }));
30311
+ if (commands.length)
30312
+ return commands;
30313
+ }
30314
+ const scripts = isRecord(pkg.scripts) ? pkg.scripts : undefined;
30315
+ const dev = stringValue(scripts?.dev);
30316
+ const match = dev?.match(/(?:bun\s+run\s+|bun\s+)([^ ]+)/);
30317
+ if (match?.[1]) {
30318
+ return [{ name: fallbackName, entry: match[1].replace(/^\.\//, ""), description: `Run ${displayName(fallbackName)}.` }];
30319
+ }
30320
+ return;
30321
+ }
30322
+ function readJsonObject(path) {
30323
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
30324
+ if (!isRecord(parsed))
30325
+ throw new Error(`${basename(path)} must contain a JSON object`);
30326
+ return parsed;
30327
+ }
30328
+ function isRecord(value) {
30329
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30330
+ }
30331
+ function stringField(value, key) {
30332
+ return stringValue(value?.[key]);
30333
+ }
30334
+ function stringArrayField(value, key) {
30335
+ const raw = value?.[key];
30336
+ if (!Array.isArray(raw))
30337
+ return;
30338
+ const strings = raw.filter((item) => typeof item === "string" && item.trim().length > 0);
30339
+ return strings.length ? strings : undefined;
30340
+ }
30341
+ function stringValue(value) {
30342
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
30343
+ }
30344
+ function displayName(name) {
30345
+ return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
30346
+ }
30347
+ function safeIsDirectory(path) {
30348
+ try {
30349
+ return statSync2(path).isDirectory();
30350
+ } catch {
30351
+ return false;
30352
+ }
30353
+ }
30354
+ function isSafeRelativePath2(value) {
30355
+ if (!value.trim() || isAbsolute2(value))
30356
+ return false;
30357
+ const normalized = normalize2(value).replace(/\\/g, "/");
30358
+ return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
30359
+ }
30360
+ function add2(target, code, message) {
30361
+ target.push({ code, message });
30362
+ }
30363
+ function sortMessages2(messages) {
30364
+ return [...messages].sort((a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
30365
+ }
30366
+ function escapeJsString(value) {
30367
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\r?\n/g, " ");
30368
+ }
30369
+
30370
+ // src/lib/registry.ts
30371
+ init_skill_aliases();
30372
+
30373
+ // src/lib/registry-data/development-tools.ts
30374
+ var DEVELOPMENT_TOOLS_SKILLS = [
30375
+ {
30376
+ name: "api-test-suite",
30377
+ displayName: "API Test Suite",
30378
+ description: "Generate and run API test suites with comprehensive endpoint coverage",
30379
+ category: "Development Tools",
30380
+ tags: ["api", "testing", "automation", "qa"]
30381
+ },
30382
+ {
30383
+ name: "apidocs",
30384
+ displayName: "API Docs",
30385
+ description: "Agentic web crawler for API documentation indexing and semantic search",
30386
+ category: "Development Tools",
30387
+ tags: ["api", "documentation", "search", "indexing"]
30388
+ },
30389
+ {
30390
+ name: "api-docs-portal",
30391
+ displayName: "API Docs Portal",
30392
+ description: "Generate premium static API documentation portals from OpenAPI specs, route lists, and endpoint examples",
30393
+ category: "Development Tools",
30394
+ tags: ["api", "documentation", "openapi", "portal", "premium", "remote"]
30395
+ },
30396
+ {
30397
+ name: "sdk-generator",
30398
+ displayName: "SDK Generator",
30399
+ description: "Generate hosted TypeScript SDK scaffolds with client code, types, package files, tests, README, examples, and API summaries",
30400
+ category: "Development Tools",
30401
+ tags: ["sdk", "api", "typescript", "developer-tools", "premium", "remote"]
30402
+ },
30403
+ {
30404
+ name: "repo-onboarding-report",
30405
+ displayName: "Repo Onboarding Report",
30406
+ description: "Generate hosted repository onboarding packages with architecture maps, setup guides, risk registers, and first-week plans",
30407
+ category: "Development Tools",
30408
+ tags: ["repository", "onboarding", "architecture", "developer-tools", "premium", "remote"]
30409
+ },
30410
+ {
30411
+ name: "codefix",
30412
+ displayName: "Code Fix",
30413
+ description: "Code quality CLI for auto-linting, formatting, fixing, and style enforcement",
30414
+ category: "Development Tools",
30415
+ tags: ["code", "linting", "formatting", "quality"]
30416
+ },
30417
+ {
30418
+ name: "commitpush",
30419
+ displayName: "Commit Push",
30420
+ description: "Create logical commits from repo changes and push directly to the main branch",
30421
+ category: "Development Tools",
30422
+ tags: ["git", "commit", "push", "automation"]
30423
+ },
30424
+ {
30425
+ name: "commitpushpr",
30426
+ displayName: "Commit Push PR",
30427
+ description: "Create logical commits, push a feature branch, and open a GitHub pull request",
30428
+ category: "Development Tools",
30429
+ tags: ["git", "commit", "pull-request", "github", "automation"],
30430
+ dependencies: ["commitpush"]
30431
+ },
30432
+ {
30433
+ name: "consolelog",
30434
+ displayName: "Console Log",
30435
+ description: "Monitor console logs from web applications using Playwright headless browser",
30436
+ category: "Development Tools",
30437
+ tags: ["console", "monitoring", "debugging", "logs"]
30438
+ },
30439
+ {
30440
+ name: "database-explorer",
30441
+ displayName: "Database Explorer",
30442
+ description: "Explore and query databases with an interactive interface",
30443
+ category: "Development Tools",
30444
+ tags: ["database", "explorer", "sql", "query"]
30445
+ },
30446
+ {
30447
+ name: "deploy",
30448
+ displayName: "Deploy",
30449
+ description: "Deployment CLI for managing EC2 deployments with automated health checks",
30450
+ category: "Development Tools",
30451
+ tags: ["deployment", "ec2", "aws", "ci-cd"]
30452
+ },
30453
+ {
30454
+ name: "diff-viewer",
30455
+ displayName: "Diff Viewer",
30456
+ description: "View and analyze file differences with visual diff representation",
30457
+ category: "Development Tools",
30458
+ tags: ["diff", "comparison", "files", "code-review"]
30459
+ },
29568
30460
  {
29569
30461
  name: "generate-api-client",
29570
30462
  displayName: "Generate API Client",
@@ -31446,20 +32338,20 @@ function parseSkillMdFrontmatter(content) {
31446
32338
  return Object.keys(result).length > 0 ? result : null;
31447
32339
  }
31448
32340
  function discoverSkillsInDir(dir) {
31449
- if (!existsSync(dir))
32341
+ if (!existsSync4(dir))
31450
32342
  return [];
31451
32343
  const result = [];
31452
32344
  try {
31453
- const entries = readdirSync(dir, { withFileTypes: true });
32345
+ const entries = readdirSync3(dir, { withFileTypes: true });
31454
32346
  for (const entry of entries) {
31455
32347
  if (!entry.isDirectory())
31456
32348
  continue;
31457
- const skillMdPath = join(dir, entry.name, "SKILL.md");
31458
- if (!existsSync(skillMdPath))
32349
+ const skillMdPath = join4(dir, entry.name, "SKILL.md");
32350
+ if (!existsSync4(skillMdPath))
31459
32351
  continue;
31460
32352
  let content;
31461
32353
  try {
31462
- content = readFileSync(skillMdPath, "utf-8");
32354
+ content = readFileSync4(skillMdPath, "utf-8");
31463
32355
  } catch {
31464
32356
  continue;
31465
32357
  }
@@ -31488,7 +32380,10 @@ function loadRegistry(cwd) {
31488
32380
  return registryCache;
31489
32381
  }
31490
32382
  const official = SKILLS.map((s) => ({ ...s, source: "official" }));
31491
- const globalCustom = discoverSkillsInDir(join(homedir(), ".hasna", "skills", "custom"));
32383
+ const dataDir = getDataDir();
32384
+ const portableCustom = listPortableSkillMetas({ rootDir: dataDir });
32385
+ const legacyCustom = discoverSkillsInDir(join4(dataDir, "custom"));
32386
+ const globalCustom = mergeCustomSkills([...legacyCustom, ...portableCustom]);
31492
32387
  const customNames = new Set(globalCustom.map((s) => s.name));
31493
32388
  const filtered = official.filter((s) => !customNames.has(s.name));
31494
32389
  registryCache = [...filtered, ...globalCustom];
@@ -31498,11 +32393,17 @@ function loadRegistry(cwd) {
31498
32393
  function loadBasicRegistry(cwd) {
31499
32394
  const registry2 = loadRegistry(cwd);
31500
32395
  const byName = new Map(registry2.map((skill) => [skill.name, skill]));
31501
- return BASIC_SKILL_NAMES.map((name) => byName.get(name)).filter((skill) => skill !== undefined);
32396
+ const basic = BASIC_SKILL_NAMES.map((name) => byName.get(name)).filter((skill) => skill !== undefined);
32397
+ const custom2 = registry2.filter((skill) => skill.source === "custom" && !BASIC_SKILL_NAMES.includes(skill.name));
32398
+ return [...basic, ...custom2];
31502
32399
  }
31503
32400
  function loadRegistryProfile(profile = "basic", cwd) {
31504
32401
  return profile === "all" ? loadRegistry(cwd) : loadBasicRegistry(cwd);
31505
32402
  }
32403
+ function clearRegistryCache() {
32404
+ registryCache = null;
32405
+ registryCacheTime = 0;
32406
+ }
31506
32407
  function getSkillsByCategory(category) {
31507
32408
  return loadRegistry().filter((s) => s.category === category);
31508
32409
  }
@@ -31511,10 +32412,16 @@ function getSkill(name) {
31511
32412
  const slug = normalizeSkillSlug(name);
31512
32413
  return registry2.find((s) => s.name === slug) ?? registry2.find((s) => s.name === resolveSkillAlias(slug));
31513
32414
  }
32415
+ function mergeCustomSkills(skills) {
32416
+ const byName = new Map;
32417
+ for (const skill of skills)
32418
+ byName.set(skill.name, skill);
32419
+ return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
32420
+ }
31514
32421
 
31515
32422
  // src/lib/installer.ts
31516
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31517
- import { dirname, join as join3 } from "path";
32423
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
32424
+ import { dirname as dirname3, join as join6 } from "path";
31518
32425
  import { homedir as homedir2 } from "os";
31519
32426
  import { fileURLToPath } from "url";
31520
32427
 
@@ -31524,26 +32431,27 @@ function normalizeSkillName(name) {
31524
32431
  }
31525
32432
 
31526
32433
  // src/lib/installer.ts
32434
+ init_config();
31527
32435
  init_skill_aliases();
31528
32436
 
31529
32437
  // src/lib/project-state.ts
31530
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
31531
- import { join as join2 } from "path";
32438
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
32439
+ import { join as join5 } from "path";
31532
32440
  var SKILLS_PROJECT_DIR = ".skills";
31533
32441
  var PROJECT_CONFIG_FILE = "project.json";
31534
32442
  var DEFAULT_EXPORT_DIR = ".skills/exports";
31535
32443
  function getProjectStateDir(targetDir = process.cwd()) {
31536
- return join2(targetDir, SKILLS_PROJECT_DIR);
32444
+ return join5(targetDir, SKILLS_PROJECT_DIR);
31537
32445
  }
31538
32446
  function getProjectConfigPath(targetDir = process.cwd()) {
31539
- return join2(getProjectStateDir(targetDir), PROJECT_CONFIG_FILE);
32447
+ return join5(getProjectStateDir(targetDir), PROJECT_CONFIG_FILE);
31540
32448
  }
31541
32449
  function loadProjectConfig(targetDir = process.cwd()) {
31542
32450
  const path = getProjectConfigPath(targetDir);
31543
- if (!existsSync2(path))
32451
+ if (!existsSync5(path))
31544
32452
  return null;
31545
32453
  try {
31546
- return normalizeProjectConfig(JSON.parse(readFileSync2(path, "utf-8")));
32454
+ return normalizeProjectConfig(JSON.parse(readFileSync5(path, "utf-8")));
31547
32455
  } catch {
31548
32456
  return null;
31549
32457
  }
@@ -31564,9 +32472,9 @@ function ensureProjectConfig(targetDir = process.cwd()) {
31564
32472
  }
31565
32473
  function saveProjectConfig(config2, targetDir = process.cwd()) {
31566
32474
  const dir = getProjectStateDir(targetDir);
31567
- mkdirSync(dir, { recursive: true });
32475
+ mkdirSync3(dir, { recursive: true });
31568
32476
  const normalized = normalizeProjectConfig({ ...config2, updatedAt: new Date().toISOString() });
31569
- writeFileSync(getProjectConfigPath(targetDir), JSON.stringify(normalized, null, 2) + `
32477
+ writeFileSync3(getProjectConfigPath(targetDir), JSON.stringify(normalized, null, 2) + `
31570
32478
  `);
31571
32479
  }
31572
32480
  function pinProjectSkill(name, details = {}, targetDir = process.cwd()) {
@@ -31638,21 +32546,27 @@ function isPinSource(value) {
31638
32546
  }
31639
32547
 
31640
32548
  // src/lib/installer.ts
31641
- var __dirname2 = dirname(fileURLToPath(import.meta.url));
32549
+ var __dirname2 = dirname3(fileURLToPath(import.meta.url));
31642
32550
  function findSkillsDir() {
31643
32551
  let dir = __dirname2;
31644
32552
  for (let i = 0;i < 5; i++) {
31645
- const candidate = join3(dir, "skills");
31646
- if (existsSync3(candidate) && !dir.includes(".skills"))
32553
+ const candidate = join6(dir, "skills");
32554
+ if (existsSync6(candidate) && !dir.includes(".skills"))
31647
32555
  return candidate;
31648
- dir = dirname(dir);
32556
+ dir = dirname3(dir);
31649
32557
  }
31650
- return join3(__dirname2, "..", "skills");
32558
+ return join6(__dirname2, "..", "skills");
31651
32559
  }
31652
32560
  var SKILLS_DIR = findSkillsDir();
31653
32561
  function getSkillPath(name) {
31654
32562
  const skillName = normalizeSkillName(getCanonicalSkillName(name));
31655
- return join3(SKILLS_DIR, skillName);
32563
+ const portable = findPortableSkill(skillName);
32564
+ if (portable)
32565
+ return portable.path;
32566
+ const legacyCustomPath = join6(getDataDir(), "custom", skillName);
32567
+ if (existsSync6(legacyCustomPath))
32568
+ return legacyCustomPath;
32569
+ return join6(SKILLS_DIR, skillName);
31656
32570
  }
31657
32571
  function getCanonicalSkillName(name) {
31658
32572
  return getSkill(name)?.name ?? resolveSkillAlias(normalizeSkillSlug(name));
@@ -31661,7 +32575,7 @@ function installSkill(name, options = {}) {
31661
32575
  const { targetDir = process.cwd(), overwrite = false } = options;
31662
32576
  const canonicalName = getCanonicalSkillName(name);
31663
32577
  const skillName = normalizeSkillName(canonicalName);
31664
- if (!existsSync3(getSkillPath(name))) {
32578
+ if (!existsSync6(getSkillPath(name))) {
31665
32579
  return { skill: canonicalName, success: false, error: `Skill '${name}' not found`, mode: "pin" };
31666
32580
  }
31667
32581
  const existing = new Set(listPinnedSkills(targetDir));
@@ -31710,11 +32624,11 @@ function getAgentSkillsDir(agent, scope = "global", projectDir) {
31710
32624
  const base = projectDir || process.cwd();
31711
32625
  switch (agent) {
31712
32626
  case "pi":
31713
- return scope === "project" ? join3(base, ".pi", "skills") : join3(homedir2(), ".pi", "agent", "skills");
32627
+ return scope === "project" ? join6(base, ".pi", "skills") : join6(homedir2(), ".pi", "agent", "skills");
31714
32628
  case "opencode":
31715
- return scope === "project" ? join3(base, ".opencode", "skills") : join3(homedir2(), ".config", "opencode", "skills");
32629
+ return scope === "project" ? join6(base, ".opencode", "skills") : join6(homedir2(), ".config", "opencode", "skills");
31716
32630
  default:
31717
- return scope === "project" ? join3(base, `.${agent}`, "skills") : join3(homedir2(), `.${agent}`, "skills");
32631
+ return scope === "project" ? join6(base, `.${agent}`, "skills") : join6(homedir2(), `.${agent}`, "skills");
31718
32632
  }
31719
32633
  }
31720
32634
  function warnMissingDependencies(name, targetDir) {
@@ -31729,11 +32643,11 @@ function warnMissingDependencies(name, targetDir) {
31729
32643
  }
31730
32644
  }
31731
32645
  function readBundledSkillVersion(name) {
31732
- const pkgPath = join3(getSkillPath(name), "package.json");
31733
- if (!existsSync3(pkgPath))
32646
+ const pkgPath = join6(getSkillPath(name), "package.json");
32647
+ if (!existsSync6(pkgPath))
31734
32648
  return "unknown";
31735
32649
  try {
31736
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
32650
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
31737
32651
  return pkg.version || "unknown";
31738
32652
  } catch {
31739
32653
  return "unknown";
@@ -31741,8 +32655,8 @@ function readBundledSkillVersion(name) {
31741
32655
  }
31742
32656
 
31743
32657
  // src/lib/skillinfo.ts
31744
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
31745
- import { join as join4 } from "path";
32658
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
32659
+ import { join as join7 } from "path";
31746
32660
  init_pricing();
31747
32661
  var HOSTED_PROVIDER_ENV_PREFIXES = [
31748
32662
  "OPENAI_",
@@ -31762,12 +32676,12 @@ var HOSTED_PROVIDER_ENV_PREFIXES = [
31762
32676
  ];
31763
32677
  function getSkillDocs(name) {
31764
32678
  const skillPath = getSkillPath(name);
31765
- if (!existsSync4(skillPath))
32679
+ if (!existsSync7(skillPath))
31766
32680
  return null;
31767
32681
  return {
31768
- skillMd: readIfExists(join4(skillPath, "SKILL.md")),
31769
- readme: readIfExists(join4(skillPath, "README.md")),
31770
- claudeMd: readIfExists(join4(skillPath, "CLAUDE.md"))
32682
+ skillMd: readIfExists(join7(skillPath, "SKILL.md")),
32683
+ readme: readIfExists(join7(skillPath, "README.md")),
32684
+ claudeMd: readIfExists(join7(skillPath, "CLAUDE.md"))
31771
32685
  };
31772
32686
  }
31773
32687
  function getSkillBestDoc(name) {
@@ -31778,11 +32692,11 @@ function getSkillBestDoc(name) {
31778
32692
  }
31779
32693
  function getSkillRequirements(name) {
31780
32694
  const skillPath = getSkillPath(name);
31781
- if (!existsSync4(skillPath))
32695
+ if (!existsSync7(skillPath))
31782
32696
  return null;
31783
32697
  const texts = [];
31784
32698
  for (const file2 of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example", ".env.local.example"]) {
31785
- const content = readIfExists(join4(skillPath, file2));
32699
+ const content = readIfExists(join7(skillPath, file2));
31786
32700
  if (content)
31787
32701
  texts.push(content);
31788
32702
  }
@@ -31821,10 +32735,10 @@ function getSkillRequirements(name) {
31821
32735
  const skillName = normalizeSkillName(name);
31822
32736
  let cliCommand = `skills run ${skillName}`;
31823
32737
  let dependencies = {};
31824
- const pkgPath = join4(skillPath, "package.json");
31825
- if (existsSync4(pkgPath)) {
32738
+ const pkgPath = join7(skillPath, "package.json");
32739
+ if (existsSync7(pkgPath)) {
31826
32740
  try {
31827
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
32741
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
31828
32742
  dependencies = pkg.dependencies || {};
31829
32743
  } catch {}
31830
32744
  }
@@ -31841,16 +32755,16 @@ function isHostedPremiumSkill(skillName, meta3) {
31841
32755
  async function runSkill(name, args, options = {}) {
31842
32756
  const canonicalName = getSkill(name)?.name ?? name;
31843
32757
  const skillPath = getSkillPath(canonicalName);
31844
- if (!existsSync4(skillPath)) {
32758
+ if (!existsSync7(skillPath)) {
31845
32759
  return { exitCode: 1, error: `Skill '${name}' not found` };
31846
32760
  }
31847
- const pkgPath = join4(skillPath, "package.json");
31848
- if (!existsSync4(pkgPath)) {
32761
+ const pkgPath = join7(skillPath, "package.json");
32762
+ if (!existsSync7(pkgPath)) {
31849
32763
  return { exitCode: 1, error: `No package.json in skill '${name}'` };
31850
32764
  }
31851
32765
  let entryPoint;
31852
32766
  try {
31853
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
32767
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
31854
32768
  if (pkg.bin) {
31855
32769
  const binValues = Object.values(pkg.bin);
31856
32770
  entryPoint = binValues[0];
@@ -31864,12 +32778,12 @@ async function runSkill(name, args, options = {}) {
31864
32778
  } catch {
31865
32779
  return { exitCode: 1, error: `Failed to parse package.json for skill '${name}'` };
31866
32780
  }
31867
- const entryPath = join4(skillPath, entryPoint);
31868
- if (!existsSync4(entryPath)) {
32781
+ const entryPath = join7(skillPath, entryPoint);
32782
+ if (!existsSync7(entryPath)) {
31869
32783
  return { exitCode: 1, error: `Entry point '${entryPoint}' not found in skill '${name}'` };
31870
32784
  }
31871
- const nodeModules = join4(skillPath, "node_modules");
31872
- if (!existsSync4(nodeModules)) {
32785
+ const nodeModules = join7(skillPath, "node_modules");
32786
+ if (!existsSync7(nodeModules)) {
31873
32787
  const install = Bun.spawn(["bun", "install", "--no-save"], {
31874
32788
  cwd: skillPath,
31875
32789
  stdout: "pipe",
@@ -31896,15 +32810,15 @@ async function runSkill(name, args, options = {}) {
31896
32810
  return { exitCode };
31897
32811
  }
31898
32812
  function detectProjectSkills(cwd = process.cwd()) {
31899
- const pkgPath = join4(cwd, "package.json");
31900
- if (!existsSync4(pkgPath)) {
32813
+ const pkgPath = join7(cwd, "package.json");
32814
+ if (!existsSync7(pkgPath)) {
31901
32815
  const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
31902
32816
  const recommended2 = alwaysRecommend.map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
31903
32817
  return { detected: [], recommended: recommended2 };
31904
32818
  }
31905
32819
  let pkg;
31906
32820
  try {
31907
- pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
32821
+ pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
31908
32822
  } catch {
31909
32823
  const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
31910
32824
  const recommended2 = alwaysRecommend.map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
@@ -31997,8 +32911,8 @@ function extractEnvVars(text) {
31997
32911
  }
31998
32912
  function readIfExists(path) {
31999
32913
  try {
32000
- if (existsSync4(path)) {
32001
- return readFileSync4(path, "utf-8");
32914
+ if (existsSync7(path)) {
32915
+ return readFileSync7(path, "utf-8");
32002
32916
  }
32003
32917
  } catch {}
32004
32918
  return null;
@@ -32106,6 +33020,48 @@ var runOutputSchema = objectSchema({
32106
33020
  })
32107
33021
  }, [], "Skill run result.");
32108
33022
  var toolContracts = [
33023
+ {
33024
+ name: "scaffold_skill",
33025
+ title: "Scaffold Skill",
33026
+ description: "Create a portable skill folder under ~/.hasna/skills/<name> from the standard template.",
33027
+ params: ["name", "description?", "overwrite?"],
33028
+ category: "scaffolding",
33029
+ sideEffects: "filesystem",
33030
+ stable: true,
33031
+ inputSchema: objectSchema({
33032
+ name: skillNameInput,
33033
+ description: stringSchema("Short description for the new skill."),
33034
+ overwrite: { type: "boolean", default: false }
33035
+ }, ["name"]),
33036
+ outputSchema: objectSchema({
33037
+ name: stringSchema("Normalized skill name."),
33038
+ path: stringSchema("Created skill directory."),
33039
+ created: { type: "boolean" },
33040
+ manifest: objectSchema({}, [], "Portable skill manifest.", true)
33041
+ }, ["name", "path", "created", "manifest"])
33042
+ },
33043
+ {
33044
+ name: "port_skill",
33045
+ title: "Port Skill",
33046
+ description: "Import an existing skill folder into the portable ~/.hasna/skills/<name> standard.",
33047
+ params: ["path", "name?", "overwrite?"],
33048
+ category: "scaffolding",
33049
+ sideEffects: "filesystem",
33050
+ stable: true,
33051
+ inputSchema: objectSchema({
33052
+ path: stringSchema("Existing skill folder to import."),
33053
+ name: skillNameInput,
33054
+ overwrite: { type: "boolean", default: false }
33055
+ }, ["path"]),
33056
+ outputSchema: objectSchema({
33057
+ name: stringSchema("Normalized skill name."),
33058
+ path: stringSchema("Imported skill directory."),
33059
+ created: { type: "boolean" },
33060
+ valid: { type: "boolean" },
33061
+ issues: arraySchema(validationMessageSchema),
33062
+ warnings: arraySchema(validationMessageSchema)
33063
+ }, ["name", "path", "created", "valid"])
33064
+ },
32109
33065
  {
32110
33066
  name: "list_skills",
32111
33067
  title: "List Skills",
@@ -32929,25 +33885,25 @@ function registerDiscoveryTools(server) {
32929
33885
  }
32930
33886
 
32931
33887
  // src/mcp/operation-tools.ts
32932
- import { existsSync as existsSync8, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
32933
- import { join as join8 } from "path";
33888
+ import { existsSync as existsSync10, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
33889
+ import { join as join10 } from "path";
32934
33890
 
32935
33891
  // src/lib/run-state.ts
32936
33892
  import { createHash, randomBytes } from "crypto";
32937
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
32938
- import { extname, join as join5, relative } from "path";
33893
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, readdirSync as readdirSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
33894
+ import { extname, join as join8, relative as relative2 } from "path";
32939
33895
  function createSkillRun(params, targetDir = process.cwd()) {
32940
33896
  const now = new Date;
32941
33897
  const id = createRunId(now);
32942
33898
  const day = now.toISOString().slice(0, 10);
32943
33899
  const skillName = normalizeSkillName(params.skill);
32944
33900
  const root = getProjectStateDir(targetDir);
32945
- const runDir = join5(root, "runs", day, id);
32946
- const logsDir = join5(runDir, "logs");
32947
- const exportDir = join5(root, "exports", skillName, id);
32948
- mkdirSync2(logsDir, { recursive: true });
32949
- mkdirSync2(exportDir, { recursive: true });
32950
- mkdirSync2(join5(root, "tmp"), { recursive: true });
33901
+ const runDir = join8(root, "runs", day, id);
33902
+ const logsDir = join8(runDir, "logs");
33903
+ const exportDir = join8(root, "exports", skillName, id);
33904
+ mkdirSync4(logsDir, { recursive: true });
33905
+ mkdirSync4(exportDir, { recursive: true });
33906
+ mkdirSync4(join8(root, "tmp"), { recursive: true });
32951
33907
  const record3 = {
32952
33908
  id,
32953
33909
  skill: skillName,
@@ -32994,42 +33950,42 @@ function updateSkillRun(context, patch) {
32994
33950
  return context.record;
32995
33951
  }
32996
33952
  function writeRunLogs(context, stdout = "", stderr = "") {
32997
- writeFileSync2(join5(context.logsDir, "stdout.log"), stdout);
32998
- writeFileSync2(join5(context.logsDir, "stderr.log"), stderr);
33953
+ writeFileSync4(join8(context.logsDir, "stdout.log"), stdout);
33954
+ writeFileSync4(join8(context.logsDir, "stderr.log"), stderr);
32999
33955
  }
33000
33956
  function appendRunEvent(context, event, data = {}) {
33001
33957
  const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
33002
33958
  `;
33003
- const path = join5(context.runDir, "events.ndjson");
33004
- const previous = existsSync5(path) ? readFileSync5(path, "utf-8") : "";
33005
- writeFileSync2(path, previous + line);
33959
+ const path = join8(context.runDir, "events.ndjson");
33960
+ const previous = existsSync8(path) ? readFileSync8(path, "utf-8") : "";
33961
+ writeFileSync4(path, previous + line);
33006
33962
  }
33007
33963
  function findSkillRun(runId, targetDir = process.cwd()) {
33008
- const runsRoot = join5(getProjectStateDir(targetDir), "runs");
33009
- if (!existsSync5(runsRoot))
33964
+ const runsRoot = join8(getProjectStateDir(targetDir), "runs");
33965
+ if (!existsSync8(runsRoot))
33010
33966
  return null;
33011
- for (const day of readdirSync2(runsRoot)) {
33012
- const record3 = readRunRecord(join5(runsRoot, day, runId));
33967
+ for (const day of readdirSync4(runsRoot)) {
33968
+ const record3 = readRunRecord(join8(runsRoot, day, runId));
33013
33969
  if (record3)
33014
33970
  return record3;
33015
33971
  }
33016
33972
  return null;
33017
33973
  }
33018
33974
  function writeRunRecord(context) {
33019
- writeFileSync2(join5(context.runDir, "run.json"), JSON.stringify(context.record, null, 2) + `
33975
+ writeFileSync4(join8(context.runDir, "run.json"), JSON.stringify(context.record, null, 2) + `
33020
33976
  `);
33021
33977
  }
33022
33978
  function writeArtifactsManifest(context, artifacts) {
33023
- writeFileSync2(join5(context.runDir, "artifacts.json"), JSON.stringify({ runId: context.record.id, artifacts }, null, 2) + `
33979
+ writeFileSync4(join8(context.runDir, "artifacts.json"), JSON.stringify({ runId: context.record.id, artifacts }, null, 2) + `
33024
33980
  `);
33025
33981
  }
33026
33982
  function collectRunArtifacts(context) {
33027
- if (!existsSync5(context.exportDir))
33983
+ if (!existsSync8(context.exportDir))
33028
33984
  return [];
33029
33985
  const artifacts = [];
33030
33986
  for (const path of walkFiles(context.exportDir)) {
33031
- const stat = statSync(path);
33032
- const bytes = readFileSync5(path);
33987
+ const stat = statSync3(path);
33988
+ const bytes = readFileSync8(path);
33033
33989
  artifacts.push({
33034
33990
  path: toProjectRelative(context.targetDir, path),
33035
33991
  mime: mimeForPath(path),
@@ -33040,20 +33996,20 @@ function collectRunArtifacts(context) {
33040
33996
  return artifacts.sort((a, b) => a.path.localeCompare(b.path));
33041
33997
  }
33042
33998
  function readRunRecord(runDir) {
33043
- const path = join5(runDir, "run.json");
33044
- if (!existsSync5(path))
33999
+ const path = join8(runDir, "run.json");
34000
+ if (!existsSync8(path))
33045
34001
  return null;
33046
34002
  try {
33047
- return JSON.parse(readFileSync5(path, "utf-8"));
34003
+ return JSON.parse(readFileSync8(path, "utf-8"));
33048
34004
  } catch {
33049
34005
  return null;
33050
34006
  }
33051
34007
  }
33052
34008
  function walkFiles(dir) {
33053
34009
  const files = [];
33054
- for (const entry of readdirSync2(dir)) {
33055
- const full = join5(dir, entry);
33056
- if (statSync(full).isDirectory())
34010
+ for (const entry of readdirSync4(dir)) {
34011
+ const full = join8(dir, entry);
34012
+ if (statSync3(full).isDirectory())
33057
34013
  files.push(...walkFiles(full));
33058
34014
  else
33059
34015
  files.push(full);
@@ -33064,7 +34020,7 @@ function createRunId(now) {
33064
34020
  return `run_${now.getTime().toString(36)}_${randomBytes(4).toString("hex")}`;
33065
34021
  }
33066
34022
  function toProjectRelative(targetDir, path) {
33067
- const rel = relative(targetDir, path).split(/[\\/]/).join("/");
34023
+ const rel = relative2(targetDir, path).split(/[\\/]/).join("/");
33068
34024
  return rel.startsWith(".") ? rel : `./${rel}`;
33069
34025
  }
33070
34026
  function mimeForPath(path) {
@@ -33100,6 +34056,46 @@ function mimeForPath(path) {
33100
34056
 
33101
34057
  // src/mcp/operation-tools.ts
33102
34058
  function registerOperationTools(server) {
34059
+ server.registerTool("scaffold_skill", {
34060
+ title: "Scaffold Skill",
34061
+ description: "Create a portable skill folder under ~/.hasna/skills/<name> with SKILL.md, skill.json, AGENTS.md, package.json, and src/index.ts.",
34062
+ inputSchema: {
34063
+ name: exports_external.string(),
34064
+ description: exports_external.string().optional(),
34065
+ overwrite: exports_external.boolean().optional()
34066
+ }
34067
+ }, async ({ name, description, overwrite }) => {
34068
+ try {
34069
+ const result = scaffoldPortableSkill(name, { description, overwrite });
34070
+ clearRegistryCache();
34071
+ cacheClear();
34072
+ return mcpJson(result);
34073
+ } catch (err) {
34074
+ return mcpError("SCAFFOLD_FAILED", err.message);
34075
+ }
34076
+ });
34077
+ server.registerTool("port_skill", {
34078
+ title: "Port Skill",
34079
+ description: "Import an existing skill folder into the portable ~/.hasna/skills/<name> standard and add missing standard files.",
34080
+ inputSchema: {
34081
+ path: exports_external.string(),
34082
+ name: exports_external.string().optional(),
34083
+ overwrite: exports_external.boolean().optional()
34084
+ }
34085
+ }, async ({ path, name, overwrite }) => {
34086
+ try {
34087
+ const result = portPortableSkill(path, { name, overwrite });
34088
+ const validation = validatePortableSkillDirectory(result.name, result.path);
34089
+ clearRegistryCache();
34090
+ cacheClear();
34091
+ return {
34092
+ content: [{ type: "text", text: JSON.stringify({ ...result, valid: validation.valid, issues: validation.issues, warnings: validation.warnings }, null, 2) }],
34093
+ isError: !validation.valid
34094
+ };
34095
+ } catch (err) {
34096
+ return mcpError("PORT_FAILED", err.message);
34097
+ }
34098
+ });
33103
34099
  server.registerTool("pin_skill", {
33104
34100
  title: "Pin Skill",
33105
34101
  description: "Pin a skill to .skills/project.json. Agent skill-folder installs are disabled; use skills mcp --register.",
@@ -33492,13 +34488,13 @@ function registerOperationTools(server) {
33492
34488
  const agents = [];
33493
34489
  for (const agent of AGENT_TARGETS) {
33494
34490
  const agentSkillsPath = getAgentSkillsDir(agent, "global");
33495
- const exists = existsSync8(agentSkillsPath);
34491
+ const exists = existsSync10(agentSkillsPath);
33496
34492
  let skillCount = 0;
33497
34493
  if (exists) {
33498
34494
  try {
33499
- skillCount = readdirSync3(agentSkillsPath).filter((f) => {
33500
- const full = join8(agentSkillsPath, f);
33501
- return !f.startsWith(".") && statSync2(full).isDirectory();
34495
+ skillCount = readdirSync5(agentSkillsPath).filter((f) => {
34496
+ const full = join10(agentSkillsPath, f);
34497
+ return !f.startsWith(".") && statSync4(full).isDirectory();
33502
34498
  }).length;
33503
34499
  } catch {}
33504
34500
  }
@@ -33518,18 +34514,18 @@ function registerOperationTools(server) {
33518
34514
  }
33519
34515
 
33520
34516
  // src/lib/feedback.ts
33521
- import { existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
33522
- import { homedir as homedir5 } from "os";
33523
- import { dirname as dirname3, join as join9 } from "path";
34517
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
34518
+ import { homedir as homedir4 } from "os";
34519
+ import { dirname as dirname4, join as join11 } from "path";
33524
34520
  import { Database } from "bun:sqlite";
33525
34521
  function getFeedbackDbPath() {
33526
- return join9(homedir5(), ".hasna", "skills", "skills.db");
34522
+ return join11(homedir4(), ".hasna", "skills", "skills.db");
33527
34523
  }
33528
34524
  function getFeedbackDb() {
33529
34525
  const dbPath = getFeedbackDbPath();
33530
- const dir = dirname3(dbPath);
33531
- if (!existsSync9(dir))
33532
- mkdirSync5(dir, { recursive: true });
34526
+ const dir = dirname4(dbPath);
34527
+ if (!existsSync11(dir))
34528
+ mkdirSync6(dir, { recursive: true });
33533
34529
  const db = new Database(dbPath);
33534
34530
  db.exec("PRAGMA journal_mode = WAL");
33535
34531
  db.exec([
@@ -33620,544 +34616,238 @@ function registerResourceMetaTools(server) {
33620
34616
  title: "Describe Tools",
33621
34617
  description: "Get machine-readable contracts for specific tools by name.",
33622
34618
  inputSchema: { names: exports_external.array(exports_external.string()) }
33623
- }, async ({ names }) => {
33624
- return mcpJson({ schemaVersion: 1, tools: describeMcpToolContracts(names) });
33625
- });
33626
- server.registerTool("get_mcp_contracts", {
33627
- title: "Get MCP Contracts",
33628
- description: "Return the machine-readable MCP tool and resource contract manifest.",
33629
- inputSchema: {
33630
- names: exports_external.array(exports_external.string()).optional(),
33631
- includeResources: exports_external.boolean().optional()
33632
- }
33633
- }, async ({ names, includeResources }) => {
33634
- return mcpJson(createMcpContractManifest({
33635
- names,
33636
- includeResources: includeResources ?? false
33637
- }));
33638
- });
33639
- const _agentReg = new Map;
33640
- server.tool("register_agent", "Register this agent session. Returns agent_id for use in heartbeat/set_focus.", { name: exports_external.string(), session_id: exports_external.string().optional() }, async (a) => {
33641
- const existing = [..._agentReg.values()].find((x) => x.name === a.name);
33642
- if (existing) {
33643
- existing.last_seen_at = new Date().toISOString();
33644
- return mcpJson({ ...existing, registered: false });
33645
- }
33646
- const id = Math.random().toString(36).slice(2, 10);
33647
- const ag = { id, name: a.name, last_seen_at: new Date().toISOString() };
33648
- _agentReg.set(id, ag);
33649
- return mcpJson({ ...ag, registered: true });
33650
- });
33651
- server.tool("heartbeat", "Update last_seen_at to signal agent is active.", { agent_id: exports_external.string() }, async (a) => {
33652
- const ag = _agentReg.get(a.agent_id);
33653
- if (!ag)
33654
- return mcpError("AGENT_NOT_FOUND", `Agent not found: ${a.agent_id}`);
33655
- ag.last_seen_at = new Date().toISOString();
33656
- return mcpJson({ agent_id: a.agent_id, name: ag.name, active: true, last_seen_at: ag.last_seen_at });
33657
- });
33658
- server.tool("set_focus", "Set active project context for this agent session.", { agent_id: exports_external.string(), project_id: exports_external.string().optional() }, async (a) => {
33659
- const ag = _agentReg.get(a.agent_id);
33660
- if (!ag)
33661
- return mcpError("AGENT_NOT_FOUND", `Agent not found: ${a.agent_id}`);
33662
- ag.project_id = a.project_id;
33663
- return mcpJson({ agent_id: a.agent_id, project_id: a.project_id ?? null });
33664
- });
33665
- server.tool("list_agents", "List all registered agents.", {}, async () => {
33666
- const agents = [..._agentReg.values()];
33667
- return mcpJson({ agents, total: agents.length }, true);
33668
- });
33669
- server.tool("send_feedback", "Send feedback about this service", { message: exports_external.string(), email: exports_external.string().optional(), category: exports_external.enum(["bug", "feature", "general"]).optional() }, async (params) => {
33670
- try {
33671
- const result = saveFeedback({ ...params, version: package_default.version });
33672
- return mcpJson(result);
33673
- } catch (e) {
33674
- return mcpError("FEEDBACK_SAVE_FAILED", String(e));
33675
- }
33676
- });
33677
- }
33678
-
33679
- // src/lib/scheduler.ts
33680
- import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
33681
- import { join as join10 } from "path";
33682
- function getSchedulesPath(targetDir = process.cwd()) {
33683
- return join10(targetDir, ".skills", "schedules.json");
33684
- }
33685
- function loadSchedules(targetDir = process.cwd()) {
33686
- const path = getSchedulesPath(targetDir);
33687
- if (existsSync10(path)) {
33688
- try {
33689
- return JSON.parse(readFileSync8(path, "utf-8"));
33690
- } catch {}
33691
- }
33692
- return { version: 1, schedules: [] };
33693
- }
33694
- function saveSchedules(data, targetDir = process.cwd()) {
33695
- const path = getSchedulesPath(targetDir);
33696
- const dir = join10(targetDir, ".skills");
33697
- if (!existsSync10(dir))
33698
- mkdirSync6(dir, { recursive: true });
33699
- writeFileSync5(path, JSON.stringify(data, null, 2));
33700
- }
33701
- function validateCronField(expr, min, max, label) {
33702
- for (const part of expr.split(",")) {
33703
- if (part === "*")
33704
- continue;
33705
- let valuePart = part;
33706
- if (part.includes("/")) {
33707
- const slashIdx = part.indexOf("/");
33708
- valuePart = part.slice(0, slashIdx);
33709
- const stepStr = part.slice(slashIdx + 1);
33710
- const step = parseInt(stepStr);
33711
- if (isNaN(step) || step < 1)
33712
- return { valid: false, error: `Invalid step value in "${part}" in ${label}` };
33713
- }
33714
- if (valuePart === "*")
33715
- continue;
33716
- if (valuePart.includes("-")) {
33717
- const rangeParts = valuePart.split("-");
33718
- if (rangeParts.length !== 2)
33719
- return { valid: false, error: `Invalid range expression "${valuePart}" in ${label}` };
33720
- const lo = parseInt(rangeParts[0]);
33721
- const hi = parseInt(rangeParts[1]);
33722
- if (isNaN(lo) || isNaN(hi))
33723
- return { valid: false, error: `Invalid range "${valuePart}" in ${label}` };
33724
- if (lo < min || hi > max || lo > hi) {
33725
- return { valid: false, error: `Range ${lo}-${hi} outside valid ${min}-${max} in ${label}` };
33726
- }
33727
- continue;
33728
- }
33729
- const n = parseInt(valuePart);
33730
- if (isNaN(n))
33731
- return { valid: false, error: `Invalid value "${valuePart}" in ${label}` };
33732
- if (n < min || n > max) {
33733
- return { valid: false, error: `Value ${n} outside valid ${min}-${max} in ${label}` };
33734
- }
33735
- }
33736
- return { valid: true };
33737
- }
33738
- function validateCron(expr) {
33739
- const fields = expr.trim().split(/\s+/);
33740
- if (fields.length !== 5) {
33741
- return { valid: false, error: `Expected 5 fields, got ${fields.length}. Format: "minute hour day-of-month month day-of-week"` };
33742
- }
33743
- const [minuteF, hourF, domF, monthF, dowF] = fields;
33744
- const checks4 = [
33745
- { expr: minuteF, min: 0, max: 59, label: "minute" },
33746
- { expr: hourF, min: 0, max: 23, label: "hour" },
33747
- { expr: domF, min: 1, max: 31, label: "day-of-month" },
33748
- { expr: monthF, min: 1, max: 12, label: "month" },
33749
- { expr: dowF, min: 0, max: 6, label: "day-of-week" }
33750
- ];
33751
- for (const { expr: f, min, max, label } of checks4) {
33752
- const result = validateCronField(f, min, max, label);
33753
- if (!result.valid)
33754
- return result;
33755
- }
33756
- return { valid: true };
33757
- }
33758
- function getNextRun(cron, from = new Date) {
33759
- const { valid } = validateCron(cron);
33760
- if (!valid)
33761
- return null;
33762
- const [minuteF, hourF, domF, monthF, dowF] = cron.trim().split(/\s+/);
33763
- function parseField(f, min, max) {
33764
- if (f === "*")
33765
- return Array.from({ length: max - min + 1 }, (_, i) => i + min);
33766
- if (f.startsWith("*/")) {
33767
- const step = parseInt(f.slice(2));
33768
- if (isNaN(step))
33769
- return [];
33770
- const vals = [];
33771
- for (let i = min;i <= max; i += step)
33772
- vals.push(i);
33773
- return vals;
33774
- }
33775
- return f.split(",").flatMap((part) => {
33776
- if (part.includes("-")) {
33777
- const [lo, hi] = part.split("-").map(Number);
33778
- return Array.from({ length: hi - lo + 1 }, (_, i) => i + lo);
33779
- }
33780
- const n = parseInt(part);
33781
- return isNaN(n) ? [] : [n];
33782
- });
33783
- }
33784
- const minutes = parseField(minuteF, 0, 59);
33785
- const hours = parseField(hourF, 0, 23);
33786
- const doms = parseField(domF, 1, 31);
33787
- const months = parseField(monthF, 1, 12);
33788
- const dows = parseField(dowF, 0, 6);
33789
- const candidate = new Date(from);
33790
- candidate.setSeconds(0, 0);
33791
- candidate.setMinutes(candidate.getMinutes() + 1);
33792
- const limit = new Date(from);
33793
- limit.setFullYear(limit.getFullYear() + 1);
33794
- while (candidate < limit) {
33795
- const month = candidate.getMonth() + 1;
33796
- const dom = candidate.getDate();
33797
- const dow = candidate.getDay();
33798
- const hour = candidate.getHours();
33799
- const minute = candidate.getMinutes();
33800
- if (!months.includes(month)) {
33801
- candidate.setMonth(candidate.getMonth() + 1, 1);
33802
- candidate.setHours(0, 0, 0, 0);
33803
- continue;
33804
- }
33805
- if (!doms.includes(dom) || !dows.includes(dow)) {
33806
- candidate.setDate(candidate.getDate() + 1);
33807
- candidate.setHours(0, 0, 0, 0);
33808
- continue;
34619
+ }, async ({ names }) => {
34620
+ return mcpJson({ schemaVersion: 1, tools: describeMcpToolContracts(names) });
34621
+ });
34622
+ server.registerTool("get_mcp_contracts", {
34623
+ title: "Get MCP Contracts",
34624
+ description: "Return the machine-readable MCP tool and resource contract manifest.",
34625
+ inputSchema: {
34626
+ names: exports_external.array(exports_external.string()).optional(),
34627
+ includeResources: exports_external.boolean().optional()
33809
34628
  }
33810
- if (!hours.includes(hour)) {
33811
- candidate.setHours(candidate.getHours() + 1, 0, 0, 0);
33812
- continue;
34629
+ }, async ({ names, includeResources }) => {
34630
+ return mcpJson(createMcpContractManifest({
34631
+ names,
34632
+ includeResources: includeResources ?? false
34633
+ }));
34634
+ });
34635
+ const _agentReg = new Map;
34636
+ server.tool("register_agent", "Register this agent session. Returns agent_id for use in heartbeat/set_focus.", { name: exports_external.string(), session_id: exports_external.string().optional() }, async (a) => {
34637
+ const existing = [..._agentReg.values()].find((x) => x.name === a.name);
34638
+ if (existing) {
34639
+ existing.last_seen_at = new Date().toISOString();
34640
+ return mcpJson({ ...existing, registered: false });
33813
34641
  }
33814
- if (!minutes.includes(minute)) {
33815
- candidate.setMinutes(candidate.getMinutes() + 1, 0, 0);
33816
- continue;
34642
+ const id = Math.random().toString(36).slice(2, 10);
34643
+ const ag = { id, name: a.name, last_seen_at: new Date().toISOString() };
34644
+ _agentReg.set(id, ag);
34645
+ return mcpJson({ ...ag, registered: true });
34646
+ });
34647
+ server.tool("heartbeat", "Update last_seen_at to signal agent is active.", { agent_id: exports_external.string() }, async (a) => {
34648
+ const ag = _agentReg.get(a.agent_id);
34649
+ if (!ag)
34650
+ return mcpError("AGENT_NOT_FOUND", `Agent not found: ${a.agent_id}`);
34651
+ ag.last_seen_at = new Date().toISOString();
34652
+ return mcpJson({ agent_id: a.agent_id, name: ag.name, active: true, last_seen_at: ag.last_seen_at });
34653
+ });
34654
+ server.tool("set_focus", "Set active project context for this agent session.", { agent_id: exports_external.string(), project_id: exports_external.string().optional() }, async (a) => {
34655
+ const ag = _agentReg.get(a.agent_id);
34656
+ if (!ag)
34657
+ return mcpError("AGENT_NOT_FOUND", `Agent not found: ${a.agent_id}`);
34658
+ ag.project_id = a.project_id;
34659
+ return mcpJson({ agent_id: a.agent_id, project_id: a.project_id ?? null });
34660
+ });
34661
+ server.tool("list_agents", "List all registered agents.", {}, async () => {
34662
+ const agents = [..._agentReg.values()];
34663
+ return mcpJson({ agents, total: agents.length }, true);
34664
+ });
34665
+ server.tool("send_feedback", "Send feedback about this service", { message: exports_external.string(), email: exports_external.string().optional(), category: exports_external.enum(["bug", "feature", "general"]).optional() }, async (params) => {
34666
+ try {
34667
+ const result = saveFeedback({ ...params, version: package_default.version });
34668
+ return mcpJson(result);
34669
+ } catch (e) {
34670
+ return mcpError("FEEDBACK_SAVE_FAILED", String(e));
33817
34671
  }
33818
- return new Date(candidate);
33819
- }
33820
- return null;
33821
- }
33822
- function addSchedule(skill, cron, options = {}) {
33823
- const { valid, error: error48 } = validateCron(cron);
33824
- if (!valid)
33825
- return { schedule: null, error: error48 };
33826
- const data = loadSchedules(options.targetDir);
33827
- const id = `${skill}-${Date.now()}`;
33828
- const now = new Date;
33829
- const nextRun = getNextRun(cron, now);
33830
- const schedule = {
33831
- id,
33832
- name: options.name || `${skill} (${cron})`,
33833
- skill,
33834
- cron,
33835
- args: options.args,
33836
- enabled: true,
33837
- createdAt: now.toISOString(),
33838
- nextRun: nextRun?.toISOString()
33839
- };
33840
- data.schedules.push(schedule);
33841
- saveSchedules(data, options.targetDir);
33842
- return { schedule };
33843
- }
33844
- function listSchedules(targetDir) {
33845
- return loadSchedules(targetDir).schedules;
33846
- }
33847
- function removeSchedule(idOrName, targetDir) {
33848
- const data = loadSchedules(targetDir);
33849
- const before = data.schedules.length;
33850
- data.schedules = data.schedules.filter((s) => s.id !== idOrName && s.name !== idOrName);
33851
- if (data.schedules.length === before)
33852
- return false;
33853
- saveSchedules(data, targetDir);
33854
- return true;
34672
+ });
33855
34673
  }
33856
34674
 
33857
- // src/lib/skill-validation.ts
33858
- init_pricing();
33859
- import { existsSync as existsSync11, lstatSync, readFileSync as readFileSync9, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
33860
- import { isAbsolute, join as join11, normalize } from "path";
33861
- var DOC_FILES = ["SKILL.md", "README.md", "CLAUDE.md"];
33862
- var RESERVED_SKILL_ENTRIES = new Set([
33863
- ".env",
33864
- ".npmrc",
33865
- ".pypirc",
33866
- ".netrc",
33867
- "id_rsa",
33868
- "id_ed25519"
33869
- ]);
33870
- var KNOWN_TOP_LEVEL_ENTRIES = new Set([
33871
- ".claude",
33872
- ".env.example",
33873
- ".gitignore",
33874
- ".skills",
33875
- "CLAUDE.md",
33876
- "LICENSE",
33877
- "PROJECT_OVERVIEW.md",
33878
- "QUICKSTART.md",
33879
- "README.md",
33880
- "SKILL.md",
33881
- "api-docs-list.json",
33882
- "auth.ts",
33883
- "bun.lock",
33884
- "bunfig.toml",
33885
- "data",
33886
- "dist",
33887
- "examples",
33888
- "exports",
33889
- "http-client.ts",
33890
- "index.ts",
33891
- "install.sh",
33892
- "installer.ts",
33893
- "logs",
33894
- "node_modules",
33895
- "package.json",
33896
- "scripts",
33897
- "skill-install.ts",
33898
- "src",
33899
- "tests",
33900
- "tsconfig.json",
33901
- "vision.ts"
33902
- ]);
33903
- var VALID_PROVENANCE_SOURCES = new Set(["official", "custom", "remote", "private", "private-hosted", "upstream"]);
33904
- var VALID_BIN_COMMAND = /^[a-z0-9][a-z0-9._-]*$/;
33905
- function add(target, code, message) {
33906
- target.push({ code, message });
33907
- }
33908
- function readJsonFile(path) {
33909
- return JSON.parse(readFileSync9(path, "utf-8"));
33910
- }
33911
- function asRecord(value) {
33912
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
33913
- }
33914
- function sortMessages(messages) {
33915
- return [...messages].sort((a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
33916
- }
33917
- function isSafeRelativePath(value) {
33918
- if (!value.trim() || isAbsolute(value))
33919
- return false;
33920
- const normalized = normalize(value).replace(/\\/g, "/");
33921
- return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
34675
+ // src/lib/scheduler.ts
34676
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
34677
+ import { join as join12 } from "path";
34678
+ function getSchedulesPath(targetDir = process.cwd()) {
34679
+ return join12(targetDir, ".skills", "schedules.json");
33922
34680
  }
33923
- function isHostedPackageMetadata(pkg) {
33924
- const skills = asRecord(pkg.skills);
33925
- if (!skills)
33926
- return false;
33927
- const runtime = typeof skills.runtime === "string" ? skills.runtime.trim().toLowerCase() : "";
33928
- const source = typeof skills.source === "string" ? skills.source.trim().toLowerCase() : "";
33929
- return runtime === "hosted" || source === "remote" || source === "private-hosted";
34681
+ function loadSchedules(targetDir = process.cwd()) {
34682
+ const path = getSchedulesPath(targetDir);
34683
+ if (existsSync12(path)) {
34684
+ try {
34685
+ return JSON.parse(readFileSync10(path, "utf-8"));
34686
+ } catch {}
34687
+ }
34688
+ return { version: 1, schedules: [] };
33930
34689
  }
33931
- function isHostedMetadataSkill(skillName, frontmatter, registryMeta, packageDeclaresHosted) {
33932
- if (packageDeclaresHosted)
33933
- return true;
33934
- if (isPremiumSkill(skillName))
33935
- return true;
33936
- if (frontmatter?.source === "private-hosted")
33937
- return true;
33938
- if (frontmatter?.source === "remote" && !registryMeta?.tags.includes("local"))
33939
- return true;
33940
- return false;
34690
+ function saveSchedules(data, targetDir = process.cwd()) {
34691
+ const path = getSchedulesPath(targetDir);
34692
+ const dir = join12(targetDir, ".skills");
34693
+ if (!existsSync12(dir))
34694
+ mkdirSync7(dir, { recursive: true });
34695
+ writeFileSync6(path, JSON.stringify(data, null, 2));
33941
34696
  }
33942
- function parseSkillFrontmatter(content) {
33943
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
33944
- if (!match)
33945
- return null;
33946
- const result = {};
33947
- const lines = match[1].split(/\r?\n/);
33948
- for (let i = 0;i < lines.length; i++) {
33949
- const line = lines[i];
33950
- const colon = line.indexOf(":");
33951
- if (colon === -1)
33952
- continue;
33953
- const key = line.slice(0, colon).trim();
33954
- const rawValue = line.slice(colon + 1).trim();
33955
- if (!key)
33956
- continue;
33957
- if (key === "tags" && rawValue === "") {
33958
- const tags = [];
33959
- while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
33960
- i++;
33961
- tags.push(lines[i].replace(/^\s+-\s+/, "").trim());
33962
- }
33963
- result.tags = tags;
33964
- continue;
33965
- }
33966
- const value = rawValue.replace(/^["']|["']$/g, "");
33967
- if (!value)
34697
+ function validateCronField(expr, min, max, label) {
34698
+ for (const part of expr.split(",")) {
34699
+ if (part === "*")
33968
34700
  continue;
33969
- if (key === "name")
33970
- result.name = value;
33971
- else if (key === "description")
33972
- result.description = value;
33973
- else if (key === "displayName" || key === "display_name")
33974
- result.displayName = value;
33975
- else if (key === "category")
33976
- result.category = value;
33977
- else if (key === "version")
33978
- result.version = value;
33979
- else if (key === "source")
33980
- result.source = value;
33981
- else if (key === "tags") {
33982
- result.tags = value.replace(/[\[\]]/g, "").split(",").map((tag) => tag.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
33983
- }
33984
- }
33985
- return Object.keys(result).length > 0 ? result : null;
33986
- }
33987
- function validateSkillDirectory(name, skillPath, registryMeta) {
33988
- const bareName = name;
33989
- const issues = [];
33990
- const warnings = [];
33991
- let packageDeclaresHosted = false;
33992
- let packageSkillSource;
33993
- const metadata = {
33994
- binCommands: [],
33995
- docFiles: []
33996
- };
33997
- if (!existsSync11(skillPath)) {
33998
- add(issues, "skill.dir_missing", `Skill directory not found: ${skillPath}`);
33999
- return {
34000
- name: bareName,
34001
- path: skillPath,
34002
- valid: false,
34003
- issues: sortMessages(issues),
34004
- warnings: sortMessages(warnings),
34005
- metadata
34006
- };
34007
- }
34008
- if (!VALID_BIN_COMMAND.test(bareName)) {
34009
- add(issues, "skill.name_invalid", `Skill name '${bareName}' must use lowercase letters, numbers, dots, underscores, or hyphens`);
34010
- }
34011
- for (const entry of readdirSync4(skillPath).sort()) {
34012
- const entryPath = join11(skillPath, entry);
34013
- if (RESERVED_SKILL_ENTRIES.has(entry)) {
34014
- add(issues, "skill.reserved_file", `Reserved file '${entry}' is not allowed in skill packages`);
34701
+ let valuePart = part;
34702
+ if (part.includes("/")) {
34703
+ const slashIdx = part.indexOf("/");
34704
+ valuePart = part.slice(0, slashIdx);
34705
+ const stepStr = part.slice(slashIdx + 1);
34706
+ const step = parseInt(stepStr);
34707
+ if (isNaN(step) || step < 1)
34708
+ return { valid: false, error: `Invalid step value in "${part}" in ${label}` };
34015
34709
  }
34016
- if (lstatSync(entryPath).isSymbolicLink()) {
34017
- add(issues, "skill.symlink_forbidden", `Symlink '${entry}' is not allowed in skill packages`);
34710
+ if (valuePart === "*")
34711
+ continue;
34712
+ if (valuePart.includes("-")) {
34713
+ const rangeParts = valuePart.split("-");
34714
+ if (rangeParts.length !== 2)
34715
+ return { valid: false, error: `Invalid range expression "${valuePart}" in ${label}` };
34716
+ const lo = parseInt(rangeParts[0]);
34717
+ const hi = parseInt(rangeParts[1]);
34718
+ if (isNaN(lo) || isNaN(hi))
34719
+ return { valid: false, error: `Invalid range "${valuePart}" in ${label}` };
34720
+ if (lo < min || hi > max || lo > hi) {
34721
+ return { valid: false, error: `Range ${lo}-${hi} outside valid ${min}-${max} in ${label}` };
34722
+ }
34723
+ continue;
34018
34724
  }
34019
- if (!KNOWN_TOP_LEVEL_ENTRIES.has(entry)) {
34020
- add(warnings, "skill.file_unrecognized", `Unrecognized top-level skill entry '${entry}'`);
34725
+ const n = parseInt(valuePart);
34726
+ if (isNaN(n))
34727
+ return { valid: false, error: `Invalid value "${valuePart}" in ${label}` };
34728
+ if (n < min || n > max) {
34729
+ return { valid: false, error: `Value ${n} outside valid ${min}-${max} in ${label}` };
34021
34730
  }
34022
34731
  }
34023
- for (const docFile of DOC_FILES) {
34024
- if (existsSync11(join11(skillPath, docFile)))
34025
- metadata.docFiles.push(docFile);
34732
+ return { valid: true };
34733
+ }
34734
+ function validateCron(expr) {
34735
+ const fields = expr.trim().split(/\s+/);
34736
+ if (fields.length !== 5) {
34737
+ return { valid: false, error: `Expected 5 fields, got ${fields.length}. Format: "minute hour day-of-month month day-of-week"` };
34026
34738
  }
34027
- if (metadata.docFiles.length === 0) {
34028
- add(issues, "skill.docs_missing", "Missing documentation file: expected SKILL.md, README.md, or CLAUDE.md");
34739
+ const [minuteF, hourF, domF, monthF, dowF] = fields;
34740
+ const checks4 = [
34741
+ { expr: minuteF, min: 0, max: 59, label: "minute" },
34742
+ { expr: hourF, min: 0, max: 23, label: "hour" },
34743
+ { expr: domF, min: 1, max: 31, label: "day-of-month" },
34744
+ { expr: monthF, min: 1, max: 12, label: "month" },
34745
+ { expr: dowF, min: 0, max: 6, label: "day-of-week" }
34746
+ ];
34747
+ for (const { expr: f, min, max, label } of checks4) {
34748
+ const result = validateCronField(f, min, max, label);
34749
+ if (!result.valid)
34750
+ return result;
34029
34751
  }
34030
- const skillMdPath = join11(skillPath, "SKILL.md");
34031
- if (existsSync11(skillMdPath)) {
34032
- const frontmatter = parseSkillFrontmatter(readFileSync9(skillMdPath, "utf-8"));
34033
- if (!frontmatter) {
34034
- add(warnings, "skill.frontmatter_missing", "SKILL.md has no YAML frontmatter");
34035
- } else {
34036
- metadata.skillMdFrontmatter = frontmatter;
34037
- metadata.provenance = {
34038
- ...metadata.provenance ?? { directoryName: bareName },
34039
- ...frontmatter.source ? { frontmatterSource: frontmatter.source } : {},
34040
- ...registryMeta?.source ? { registrySource: registryMeta.source } : {}
34041
- };
34042
- if (!frontmatter.name)
34043
- add(issues, "skill.frontmatter_name_missing", "SKILL.md frontmatter missing name");
34044
- if (!frontmatter.description)
34045
- add(issues, "skill.frontmatter_description_missing", "SKILL.md frontmatter missing description");
34046
- if (frontmatter.name && frontmatter.name !== bareName) {
34047
- add(issues, "skill.frontmatter_name_mismatch", `SKILL.md name '${frontmatter.name}' does not match '${bareName}'`);
34048
- }
34049
- if (frontmatter.source && !VALID_PROVENANCE_SOURCES.has(frontmatter.source)) {
34050
- add(issues, "skill.frontmatter_source_invalid", `SKILL.md source '${frontmatter.source}' is not one of: ${[...VALID_PROVENANCE_SOURCES].join(", ")}`);
34051
- }
34052
- if (frontmatter.tags && frontmatter.tags.some((tag) => !tag.trim())) {
34053
- add(issues, "skill.frontmatter_tags_invalid", "SKILL.md tags must be non-empty strings");
34054
- }
34055
- if (registryMeta?.description && frontmatter.description && frontmatter.description.length < 8) {
34056
- add(warnings, "skill.frontmatter_description_short", "SKILL.md description is very short");
34057
- }
34058
- if (registryMeta?.category && frontmatter.category && frontmatter.category !== registryMeta.category) {
34059
- add(warnings, "skill.frontmatter_category_mismatch", `SKILL.md category '${frontmatter.category}' does not match registry category '${registryMeta.category}'`);
34060
- }
34752
+ return { valid: true };
34753
+ }
34754
+ function getNextRun(cron, from = new Date) {
34755
+ const { valid } = validateCron(cron);
34756
+ if (!valid)
34757
+ return null;
34758
+ const [minuteF, hourF, domF, monthF, dowF] = cron.trim().split(/\s+/);
34759
+ function parseField(f, min, max) {
34760
+ if (f === "*")
34761
+ return Array.from({ length: max - min + 1 }, (_, i) => i + min);
34762
+ if (f.startsWith("*/")) {
34763
+ const step = parseInt(f.slice(2));
34764
+ if (isNaN(step))
34765
+ return [];
34766
+ const vals = [];
34767
+ for (let i = min;i <= max; i += step)
34768
+ vals.push(i);
34769
+ return vals;
34061
34770
  }
34062
- } else {
34063
- add(warnings, "skill.skill_md_missing", "Missing SKILL.md; registry docs may need generated agent-facing instructions");
34064
- }
34065
- const pkgPath = join11(skillPath, "package.json");
34066
- if (!existsSync11(pkgPath)) {
34067
- add(issues, "package.missing", "Missing package.json");
34068
- } else {
34069
- try {
34070
- const pkg = readJsonFile(pkgPath);
34071
- const packageRecord = asRecord(pkg);
34072
- if (!packageRecord) {
34073
- add(issues, "package.invalid_shape", "package.json must be an object");
34074
- } else {
34075
- packageDeclaresHosted = isHostedPackageMetadata(pkg);
34076
- const skillsRecord = asRecord(pkg.skills);
34077
- if (skillsRecord && typeof skillsRecord.source === "string") {
34078
- packageSkillSource = skillsRecord.source;
34079
- }
34080
- const hostedMetadata2 = isHostedMetadataSkill(bareName, metadata.skillMdFrontmatter, registryMeta, packageDeclaresHosted);
34081
- metadata.runtime = hostedMetadata2 ? "hosted" : "local";
34082
- if (typeof pkg.name === "string") {
34083
- metadata.packageName = pkg.name;
34084
- if (pkg.name !== bareName) {
34085
- add(issues, "package.name_mismatch", `package.json name '${pkg.name}' does not match '${bareName}'`);
34086
- }
34087
- } else {
34088
- add(issues, "package.name_missing", "package.json missing string name");
34089
- }
34090
- if (typeof pkg.version === "string" && pkg.version.trim())
34091
- metadata.version = pkg.version;
34092
- else
34093
- add(warnings, "package.version_missing", "package.json missing string version");
34094
- metadata.provenance = {
34095
- ...metadata.provenance ?? { directoryName: bareName },
34096
- ...typeof pkg.name === "string" ? { packageName: pkg.name } : {},
34097
- ...typeof pkg.version === "string" && pkg.version.trim() ? { packageVersion: pkg.version } : {},
34098
- ...registryMeta?.source ? { registrySource: registryMeta.source } : {},
34099
- ...packageSkillSource ? { packageSkillSource } : {}
34100
- };
34101
- const binRecord = asRecord(pkg.bin);
34102
- if (!binRecord || Object.keys(binRecord).length === 0) {
34103
- if (!hostedMetadata2) {
34104
- add(issues, "package.bin_missing", "package.json missing non-empty bin object");
34105
- }
34106
- } else {
34107
- if (hostedMetadata2) {
34108
- add(issues, "package.hosted_bin_forbidden", "Hosted metadata packages must not expose a local bin entry");
34109
- }
34110
- for (const [command, target] of Object.entries(binRecord)) {
34111
- if (!VALID_BIN_COMMAND.test(command)) {
34112
- add(issues, "package.bin_command_invalid", `package.json bin command '${command}' must use lowercase letters, numbers, dots, underscores, or hyphens`);
34113
- }
34114
- if (typeof target !== "string" || !target.trim()) {
34115
- add(issues, "package.bin_invalid", `package.json bin '${command}' must point to a file`);
34116
- continue;
34117
- }
34118
- metadata.binCommands.push(command);
34119
- if (!isSafeRelativePath(target)) {
34120
- add(issues, "package.bin_target_unsafe", `package.json bin '${command}' target '${target}' must stay inside the skill directory`);
34121
- continue;
34122
- }
34123
- const targetPath = join11(skillPath, target);
34124
- if (!existsSync11(targetPath)) {
34125
- add(warnings, "package.bin_target_missing", `package.json bin '${command}' target '${target}' is not present before build`);
34126
- } else if (statSync3(targetPath).isDirectory()) {
34127
- add(issues, "package.bin_target_directory", `package.json bin '${command}' target '${target}' must point to a file, not a directory`);
34128
- }
34129
- }
34130
- }
34771
+ return f.split(",").flatMap((part) => {
34772
+ if (part.includes("-")) {
34773
+ const [lo, hi] = part.split("-").map(Number);
34774
+ return Array.from({ length: hi - lo + 1 }, (_, i) => i + lo);
34131
34775
  }
34132
- } catch (error48) {
34133
- add(issues, "package.invalid_json", `package.json is invalid JSON: ${error48.message}`);
34134
- }
34776
+ const n = parseInt(part);
34777
+ return isNaN(n) ? [] : [n];
34778
+ });
34135
34779
  }
34136
- const hostedMetadata = isHostedMetadataSkill(bareName, metadata.skillMdFrontmatter, registryMeta, packageDeclaresHosted);
34137
- metadata.runtime = hostedMetadata ? "hosted" : "local";
34138
- const srcDir = join11(skillPath, "src");
34139
- if (hostedMetadata) {
34140
- if (existsSync11(srcDir)) {
34141
- add(issues, "skill.hosted_source_forbidden", "Hosted metadata skills must not include local implementation source");
34780
+ const minutes = parseField(minuteF, 0, 59);
34781
+ const hours = parseField(hourF, 0, 23);
34782
+ const doms = parseField(domF, 1, 31);
34783
+ const months = parseField(monthF, 1, 12);
34784
+ const dows = parseField(dowF, 0, 6);
34785
+ const candidate = new Date(from);
34786
+ candidate.setSeconds(0, 0);
34787
+ candidate.setMinutes(candidate.getMinutes() + 1);
34788
+ const limit = new Date(from);
34789
+ limit.setFullYear(limit.getFullYear() + 1);
34790
+ while (candidate < limit) {
34791
+ const month = candidate.getMonth() + 1;
34792
+ const dom = candidate.getDate();
34793
+ const dow = candidate.getDay();
34794
+ const hour = candidate.getHours();
34795
+ const minute = candidate.getMinutes();
34796
+ if (!months.includes(month)) {
34797
+ candidate.setMonth(candidate.getMonth() + 1, 1);
34798
+ candidate.setHours(0, 0, 0, 0);
34799
+ continue;
34142
34800
  }
34143
- } else if (!existsSync11(srcDir)) {
34144
- add(issues, "skill.src_missing", "Missing src/ directory");
34145
- } else if (!existsSync11(join11(srcDir, "index.ts")) && !existsSync11(join11(srcDir, "index.js"))) {
34146
- add(issues, "skill.src_index_missing", "Missing src/index.ts or src/index.js");
34147
- } else {
34148
- const indexPath = existsSync11(join11(srcDir, "index.ts")) ? join11(srcDir, "index.ts") : join11(srcDir, "index.js");
34149
- const size = statSync3(indexPath).size;
34150
- if (size < 50)
34151
- add(warnings, "skill.src_index_minimal", `Source entry point is very small (${size}B)`);
34801
+ if (!doms.includes(dom) || !dows.includes(dow)) {
34802
+ candidate.setDate(candidate.getDate() + 1);
34803
+ candidate.setHours(0, 0, 0, 0);
34804
+ continue;
34805
+ }
34806
+ if (!hours.includes(hour)) {
34807
+ candidate.setHours(candidate.getHours() + 1, 0, 0, 0);
34808
+ continue;
34809
+ }
34810
+ if (!minutes.includes(minute)) {
34811
+ candidate.setMinutes(candidate.getMinutes() + 1, 0, 0);
34812
+ continue;
34813
+ }
34814
+ return new Date(candidate);
34152
34815
  }
34153
- return {
34154
- name: bareName,
34155
- path: skillPath,
34156
- valid: issues.length === 0,
34157
- issues: sortMessages(issues),
34158
- warnings: sortMessages(warnings),
34159
- metadata
34816
+ return null;
34817
+ }
34818
+ function addSchedule(skill, cron, options = {}) {
34819
+ const { valid, error: error48 } = validateCron(cron);
34820
+ if (!valid)
34821
+ return { schedule: null, error: error48 };
34822
+ const data = loadSchedules(options.targetDir);
34823
+ const id = `${skill}-${Date.now()}`;
34824
+ const now = new Date;
34825
+ const nextRun = getNextRun(cron, now);
34826
+ const schedule = {
34827
+ id,
34828
+ name: options.name || `${skill} (${cron})`,
34829
+ skill,
34830
+ cron,
34831
+ args: options.args,
34832
+ enabled: true,
34833
+ createdAt: now.toISOString(),
34834
+ nextRun: nextRun?.toISOString()
34160
34835
  };
34836
+ data.schedules.push(schedule);
34837
+ saveSchedules(data, options.targetDir);
34838
+ return { schedule };
34839
+ }
34840
+ function listSchedules(targetDir) {
34841
+ return loadSchedules(targetDir).schedules;
34842
+ }
34843
+ function removeSchedule(idOrName, targetDir) {
34844
+ const data = loadSchedules(targetDir);
34845
+ const before = data.schedules.length;
34846
+ data.schedules = data.schedules.filter((s) => s.id !== idOrName && s.name !== idOrName);
34847
+ if (data.schedules.length === before)
34848
+ return false;
34849
+ saveSchedules(data, targetDir);
34850
+ return true;
34161
34851
  }
34162
34852
 
34163
34853
  // src/mcp/schedule-tools.ts
@@ -34223,8 +34913,9 @@ function registerScheduleTools(server) {
34223
34913
  name: exports_external.string()
34224
34914
  }
34225
34915
  }, async ({ name }) => {
34226
- const skillPath = getSkillPath(name);
34227
- const result = validateSkillDirectory(name, skillPath, getSkill(name));
34916
+ const portable = findPortableSkill(name);
34917
+ const skillPath = portable?.path ?? getSkillPath(name);
34918
+ const result = portable ? validatePortableSkillDirectory(portable.name, portable.path) : validateSkillDirectory(name, skillPath, getSkill(name));
34228
34919
  return {
34229
34920
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
34230
34921
  isError: !result.valid