@hasna/skills 0.1.43 → 0.1.45

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.45",
21801
21801
  description: "Skills library for AI coding agents",
21802
21802
  type: "module",
21803
21803
  bin: {
@@ -21808,6 +21808,10 @@ var package_default = {
21808
21808
  ".": {
21809
21809
  import: "./dist/index.js",
21810
21810
  types: "./dist/index.d.ts"
21811
+ },
21812
+ "./storage": {
21813
+ import: "./dist/storage.js",
21814
+ types: "./dist/storage.d.ts"
21811
21815
  }
21812
21816
  },
21813
21817
  files: [
@@ -21816,6 +21820,7 @@ var package_default = {
21816
21820
  "!dist/platform",
21817
21821
  "!dist/server",
21818
21822
  "bin/",
21823
+ "docs/skill-standard.md",
21819
21824
  "skills/",
21820
21825
  "!skills/**/node_modules",
21821
21826
  "!skills/scaffold-project/my-app",
@@ -21827,7 +21832,7 @@ var package_default = {
21827
21832
  types: "./dist/index.d.ts",
21828
21833
  scripts: {
21829
21834
  clean: "rm -rf bin/ dist/",
21830
- build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
21835
+ build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/index.ts ./src/storage.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
21831
21836
  test: "bun test",
21832
21837
  dev: "bun run ./src/cli/index.tsx",
21833
21838
  "dev:watch": "bun --watch run ./src/cli/index.tsx",
@@ -21857,10 +21862,11 @@ var package_default = {
21857
21862
  devDependencies: {
21858
21863
  "@types/bun": "latest",
21859
21864
  "@types/react": "^18.2.0",
21865
+ "react-devtools-core": "^7.0.1",
21860
21866
  typescript: "^5"
21861
21867
  },
21862
21868
  dependencies: {
21863
- "@hasna/events": "^0.1.7",
21869
+ "@hasna/events": "^0.1.3",
21864
21870
  "@modelcontextprotocol/sdk": "^1.26.0",
21865
21871
  chalk: "^5.3.0",
21866
21872
  commander: "^12.1.0",
@@ -29473,97 +29479,989 @@ var EMPTY_COMPLETION_RESULT = {
29473
29479
  };
29474
29480
 
29475
29481
  // 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";
29482
+ init_config();
29483
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
29484
+ import { join as join4 } from "path";
29480
29485
 
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"]
29486
+ // src/lib/portable-skills.ts
29487
+ init_config();
29488
+ import {
29489
+ cpSync,
29490
+ existsSync as existsSync3,
29491
+ lstatSync as lstatSync2,
29492
+ mkdirSync as mkdirSync2,
29493
+ readFileSync as readFileSync3,
29494
+ readdirSync as readdirSync2,
29495
+ rmSync,
29496
+ statSync as statSync2,
29497
+ writeFileSync as writeFileSync2
29498
+ } from "fs";
29499
+ import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join3, normalize as normalize2, relative } from "path";
29500
+
29501
+ // src/lib/skill-validation.ts
29502
+ init_pricing();
29503
+ import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
29504
+ import { isAbsolute, join as join2, normalize } from "path";
29505
+ var DOC_FILES = ["SKILL.md", "README.md", "CLAUDE.md"];
29506
+ var RESERVED_SKILL_ENTRIES = new Set([
29507
+ ".env",
29508
+ ".npmrc",
29509
+ ".pypirc",
29510
+ ".netrc",
29511
+ "id_rsa",
29512
+ "id_ed25519"
29513
+ ]);
29514
+ var KNOWN_TOP_LEVEL_ENTRIES = new Set([
29515
+ ".claude",
29516
+ ".env.example",
29517
+ ".gitignore",
29518
+ ".skills",
29519
+ "CLAUDE.md",
29520
+ "AGENTS.md",
29521
+ "LICENSE",
29522
+ "PROJECT_OVERVIEW.md",
29523
+ "QUICKSTART.md",
29524
+ "README.md",
29525
+ "SKILL.md",
29526
+ "api-docs-list.json",
29527
+ "auth.ts",
29528
+ "bun.lock",
29529
+ "bunfig.toml",
29530
+ "data",
29531
+ "dist",
29532
+ "examples",
29533
+ "exports",
29534
+ "http-client.ts",
29535
+ "index.ts",
29536
+ "install.sh",
29537
+ "installer.ts",
29538
+ "logs",
29539
+ "node_modules",
29540
+ "package.json",
29541
+ "scripts",
29542
+ "skill-install.ts",
29543
+ "skill.json",
29544
+ "src",
29545
+ "tests",
29546
+ "references",
29547
+ "assets",
29548
+ "tsconfig.json",
29549
+ "vision.ts"
29550
+ ]);
29551
+ var VALID_PROVENANCE_SOURCES = new Set(["official", "custom", "remote", "private", "private-hosted", "upstream"]);
29552
+ var VALID_BIN_COMMAND = /^[a-z0-9][a-z0-9._-]*$/;
29553
+ function add(target, code, message) {
29554
+ target.push({ code, message });
29555
+ }
29556
+ function readJsonFile(path) {
29557
+ return JSON.parse(readFileSync2(path, "utf-8"));
29558
+ }
29559
+ function asRecord(value) {
29560
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
29561
+ }
29562
+ function sortMessages(messages) {
29563
+ return [...messages].sort((a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
29564
+ }
29565
+ function isSafeRelativePath(value) {
29566
+ if (!value.trim() || isAbsolute(value))
29567
+ return false;
29568
+ const normalized = normalize(value).replace(/\\/g, "/");
29569
+ return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
29570
+ }
29571
+ function isHostedPackageMetadata(pkg) {
29572
+ const skills = asRecord(pkg.skills);
29573
+ if (!skills)
29574
+ return false;
29575
+ const runtime = typeof skills.runtime === "string" ? skills.runtime.trim().toLowerCase() : "";
29576
+ const source = typeof skills.source === "string" ? skills.source.trim().toLowerCase() : "";
29577
+ return runtime === "hosted" || source === "remote" || source === "private-hosted";
29578
+ }
29579
+ function isHostedMetadataSkill(skillName, frontmatter, registryMeta, packageDeclaresHosted) {
29580
+ if (packageDeclaresHosted)
29581
+ return true;
29582
+ if (isPremiumSkill(skillName))
29583
+ return true;
29584
+ if (frontmatter?.source === "private-hosted")
29585
+ return true;
29586
+ if (frontmatter?.source === "remote" && !registryMeta?.tags.includes("local"))
29587
+ return true;
29588
+ return false;
29589
+ }
29590
+ function parseSkillFrontmatter(content) {
29591
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
29592
+ if (!match)
29593
+ return null;
29594
+ const result = {};
29595
+ const lines = match[1].split(/\r?\n/);
29596
+ for (let i = 0;i < lines.length; i++) {
29597
+ const line = lines[i];
29598
+ const colon = line.indexOf(":");
29599
+ if (colon === -1)
29600
+ continue;
29601
+ const key = line.slice(0, colon).trim();
29602
+ const rawValue = line.slice(colon + 1).trim();
29603
+ if (!key)
29604
+ continue;
29605
+ if (key === "tags" && rawValue === "") {
29606
+ const tags = [];
29607
+ while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
29608
+ i++;
29609
+ tags.push(lines[i].replace(/^\s+-\s+/, "").trim());
29610
+ }
29611
+ result.tags = tags;
29612
+ continue;
29613
+ }
29614
+ const value = rawValue.replace(/^["']|["']$/g, "");
29615
+ if (!value)
29616
+ continue;
29617
+ if (key === "name")
29618
+ result.name = value;
29619
+ else if (key === "description")
29620
+ result.description = value;
29621
+ else if (key === "displayName" || key === "display_name")
29622
+ result.displayName = value;
29623
+ else if (key === "category")
29624
+ result.category = value;
29625
+ else if (key === "version")
29626
+ result.version = value;
29627
+ else if (key === "source")
29628
+ result.source = value;
29629
+ else if (key === "tags") {
29630
+ result.tags = value.replace(/[\[\]]/g, "").split(",").map((tag) => tag.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
29631
+ }
29632
+ }
29633
+ return Object.keys(result).length > 0 ? result : null;
29634
+ }
29635
+ function validateSkillDirectory(name, skillPath, registryMeta) {
29636
+ const bareName = name;
29637
+ const issues = [];
29638
+ const warnings = [];
29639
+ let packageDeclaresHosted = false;
29640
+ let packageSkillSource;
29641
+ const metadata = {
29642
+ binCommands: [],
29643
+ docFiles: []
29644
+ };
29645
+ if (!existsSync2(skillPath)) {
29646
+ add(issues, "skill.dir_missing", `Skill directory not found: ${skillPath}`);
29647
+ return {
29648
+ name: bareName,
29649
+ path: skillPath,
29650
+ valid: false,
29651
+ issues: sortMessages(issues),
29652
+ warnings: sortMessages(warnings),
29653
+ metadata
29654
+ };
29655
+ }
29656
+ if (!VALID_BIN_COMMAND.test(bareName)) {
29657
+ add(issues, "skill.name_invalid", `Skill name '${bareName}' must use lowercase letters, numbers, dots, underscores, or hyphens`);
29658
+ }
29659
+ for (const entry of readdirSync(skillPath).sort()) {
29660
+ const entryPath = join2(skillPath, entry);
29661
+ if (RESERVED_SKILL_ENTRIES.has(entry)) {
29662
+ add(issues, "skill.reserved_file", `Reserved file '${entry}' is not allowed in skill packages`);
29663
+ }
29664
+ if (lstatSync(entryPath).isSymbolicLink()) {
29665
+ add(issues, "skill.symlink_forbidden", `Symlink '${entry}' is not allowed in skill packages`);
29666
+ }
29667
+ if (!KNOWN_TOP_LEVEL_ENTRIES.has(entry)) {
29668
+ add(warnings, "skill.file_unrecognized", `Unrecognized top-level skill entry '${entry}'`);
29669
+ }
29670
+ }
29671
+ for (const docFile of DOC_FILES) {
29672
+ if (existsSync2(join2(skillPath, docFile)))
29673
+ metadata.docFiles.push(docFile);
29674
+ }
29675
+ if (metadata.docFiles.length === 0) {
29676
+ add(issues, "skill.docs_missing", "Missing documentation file: expected SKILL.md, README.md, or CLAUDE.md");
29677
+ }
29678
+ const skillMdPath = join2(skillPath, "SKILL.md");
29679
+ if (existsSync2(skillMdPath)) {
29680
+ const frontmatter = parseSkillFrontmatter(readFileSync2(skillMdPath, "utf-8"));
29681
+ if (!frontmatter) {
29682
+ add(warnings, "skill.frontmatter_missing", "SKILL.md has no YAML frontmatter");
29683
+ } else {
29684
+ metadata.skillMdFrontmatter = frontmatter;
29685
+ metadata.provenance = {
29686
+ ...metadata.provenance ?? { directoryName: bareName },
29687
+ ...frontmatter.source ? { frontmatterSource: frontmatter.source } : {},
29688
+ ...registryMeta?.source ? { registrySource: registryMeta.source } : {}
29689
+ };
29690
+ if (!frontmatter.name)
29691
+ add(issues, "skill.frontmatter_name_missing", "SKILL.md frontmatter missing name");
29692
+ if (!frontmatter.description)
29693
+ add(issues, "skill.frontmatter_description_missing", "SKILL.md frontmatter missing description");
29694
+ if (frontmatter.name && frontmatter.name !== bareName) {
29695
+ add(issues, "skill.frontmatter_name_mismatch", `SKILL.md name '${frontmatter.name}' does not match '${bareName}'`);
29696
+ }
29697
+ if (frontmatter.source && !VALID_PROVENANCE_SOURCES.has(frontmatter.source)) {
29698
+ add(issues, "skill.frontmatter_source_invalid", `SKILL.md source '${frontmatter.source}' is not one of: ${[...VALID_PROVENANCE_SOURCES].join(", ")}`);
29699
+ }
29700
+ if (frontmatter.tags && frontmatter.tags.some((tag) => !tag.trim())) {
29701
+ add(issues, "skill.frontmatter_tags_invalid", "SKILL.md tags must be non-empty strings");
29702
+ }
29703
+ if (registryMeta?.description && frontmatter.description && frontmatter.description.length < 8) {
29704
+ add(warnings, "skill.frontmatter_description_short", "SKILL.md description is very short");
29705
+ }
29706
+ if (registryMeta?.category && frontmatter.category && frontmatter.category !== registryMeta.category) {
29707
+ add(warnings, "skill.frontmatter_category_mismatch", `SKILL.md category '${frontmatter.category}' does not match registry category '${registryMeta.category}'`);
29708
+ }
29709
+ }
29710
+ } else {
29711
+ add(warnings, "skill.skill_md_missing", "Missing SKILL.md; registry docs may need generated agent-facing instructions");
29712
+ }
29713
+ const pkgPath = join2(skillPath, "package.json");
29714
+ if (!existsSync2(pkgPath)) {
29715
+ add(issues, "package.missing", "Missing package.json");
29716
+ } else {
29717
+ try {
29718
+ const pkg = readJsonFile(pkgPath);
29719
+ const packageRecord = asRecord(pkg);
29720
+ if (!packageRecord) {
29721
+ add(issues, "package.invalid_shape", "package.json must be an object");
29722
+ } else {
29723
+ packageDeclaresHosted = isHostedPackageMetadata(pkg);
29724
+ const skillsRecord = asRecord(pkg.skills);
29725
+ if (skillsRecord && typeof skillsRecord.source === "string") {
29726
+ packageSkillSource = skillsRecord.source;
29727
+ }
29728
+ const hostedMetadata2 = isHostedMetadataSkill(bareName, metadata.skillMdFrontmatter, registryMeta, packageDeclaresHosted);
29729
+ metadata.runtime = hostedMetadata2 ? "hosted" : "local";
29730
+ if (typeof pkg.name === "string") {
29731
+ metadata.packageName = pkg.name;
29732
+ if (pkg.name !== bareName) {
29733
+ add(issues, "package.name_mismatch", `package.json name '${pkg.name}' does not match '${bareName}'`);
29734
+ }
29735
+ } else {
29736
+ add(issues, "package.name_missing", "package.json missing string name");
29737
+ }
29738
+ if (typeof pkg.version === "string" && pkg.version.trim())
29739
+ metadata.version = pkg.version;
29740
+ else
29741
+ add(warnings, "package.version_missing", "package.json missing string version");
29742
+ metadata.provenance = {
29743
+ ...metadata.provenance ?? { directoryName: bareName },
29744
+ ...typeof pkg.name === "string" ? { packageName: pkg.name } : {},
29745
+ ...typeof pkg.version === "string" && pkg.version.trim() ? { packageVersion: pkg.version } : {},
29746
+ ...registryMeta?.source ? { registrySource: registryMeta.source } : {},
29747
+ ...packageSkillSource ? { packageSkillSource } : {}
29748
+ };
29749
+ const binRecord = asRecord(pkg.bin);
29750
+ if (!binRecord || Object.keys(binRecord).length === 0) {
29751
+ if (!hostedMetadata2) {
29752
+ add(issues, "package.bin_missing", "package.json missing non-empty bin object");
29753
+ }
29754
+ } else {
29755
+ if (hostedMetadata2) {
29756
+ add(issues, "package.hosted_bin_forbidden", "Hosted metadata packages must not expose a local bin entry");
29757
+ }
29758
+ for (const [command, target] of Object.entries(binRecord)) {
29759
+ if (!VALID_BIN_COMMAND.test(command)) {
29760
+ add(issues, "package.bin_command_invalid", `package.json bin command '${command}' must use lowercase letters, numbers, dots, underscores, or hyphens`);
29761
+ }
29762
+ if (typeof target !== "string" || !target.trim()) {
29763
+ add(issues, "package.bin_invalid", `package.json bin '${command}' must point to a file`);
29764
+ continue;
29765
+ }
29766
+ metadata.binCommands.push(command);
29767
+ if (!isSafeRelativePath(target)) {
29768
+ add(issues, "package.bin_target_unsafe", `package.json bin '${command}' target '${target}' must stay inside the skill directory`);
29769
+ continue;
29770
+ }
29771
+ const targetPath = join2(skillPath, target);
29772
+ if (!existsSync2(targetPath)) {
29773
+ add(warnings, "package.bin_target_missing", `package.json bin '${command}' target '${target}' is not present before build`);
29774
+ } else if (statSync(targetPath).isDirectory()) {
29775
+ add(issues, "package.bin_target_directory", `package.json bin '${command}' target '${target}' must point to a file, not a directory`);
29776
+ }
29777
+ }
29778
+ }
29779
+ }
29780
+ } catch (error48) {
29781
+ add(issues, "package.invalid_json", `package.json is invalid JSON: ${error48.message}`);
29782
+ }
29783
+ }
29784
+ const hostedMetadata = isHostedMetadataSkill(bareName, metadata.skillMdFrontmatter, registryMeta, packageDeclaresHosted);
29785
+ metadata.runtime = hostedMetadata ? "hosted" : "local";
29786
+ const srcDir = join2(skillPath, "src");
29787
+ if (hostedMetadata) {
29788
+ if (existsSync2(srcDir)) {
29789
+ add(issues, "skill.hosted_source_forbidden", "Hosted metadata skills must not include local implementation source");
29790
+ }
29791
+ } else if (!existsSync2(srcDir)) {
29792
+ add(issues, "skill.src_missing", "Missing src/ directory");
29793
+ } else if (!existsSync2(join2(srcDir, "index.ts")) && !existsSync2(join2(srcDir, "index.js"))) {
29794
+ add(issues, "skill.src_index_missing", "Missing src/index.ts or src/index.js");
29795
+ } else {
29796
+ const indexPath = existsSync2(join2(srcDir, "index.ts")) ? join2(srcDir, "index.ts") : join2(srcDir, "index.js");
29797
+ const size = statSync(indexPath).size;
29798
+ if (size < 50)
29799
+ add(warnings, "skill.src_index_minimal", `Source entry point is very small (${size}B)`);
29800
+ }
29801
+ return {
29802
+ name: bareName,
29803
+ path: skillPath,
29804
+ valid: issues.length === 0,
29805
+ issues: sortMessages(issues),
29806
+ warnings: sortMessages(warnings),
29807
+ metadata
29808
+ };
29809
+ }
29810
+
29811
+ // src/lib/portable-skills.ts
29812
+ var PORTABLE_SKILL_STANDARD = "hasna.skill.v1";
29813
+ var PORTABLE_SKILL_SCHEMA = "https://hasna.dev/schemas/skill.v1.json";
29814
+ var PORTABLE_SKILL_DEFAULT_VERSION = "0.1.0";
29815
+ var DATA_DIR_NON_SKILL_ENTRIES = new Set([
29816
+ "auth.json",
29817
+ "config.json",
29818
+ "custom",
29819
+ "skills.db"
29820
+ ]);
29821
+ var COPY_EXCLUDES = new Set([
29822
+ ".git",
29823
+ ".DS_Store",
29824
+ "node_modules",
29825
+ "dist",
29826
+ "build",
29827
+ ".turbo"
29828
+ ]);
29829
+ var DEFAULT_INPUTS = [
29830
+ {
29831
+ name: "args",
29832
+ type: "string[]",
29833
+ required: false,
29834
+ description: "Arguments passed after `skills run <name>`."
29835
+ }
29836
+ ];
29837
+ function normalizePortableSkillName(name) {
29838
+ const normalized = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
29839
+ if (!normalized || !/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) {
29840
+ throw new Error(`Invalid skill name '${name}'. Use letters, numbers, dots, underscores, or hyphens.`);
29841
+ }
29842
+ return normalized;
29843
+ }
29844
+ function getPortableSkillsRoot(options = {}) {
29845
+ if (options.rootDir)
29846
+ return options.rootDir;
29847
+ if (process.env["HASNA_SKILLS_DIR"])
29848
+ return process.env["HASNA_SKILLS_DIR"];
29849
+ if (options.homeDir)
29850
+ return join3(options.homeDir, ".hasna", "skills");
29851
+ return getDataDir();
29852
+ }
29853
+ function getPortableSkillPath(name, options = {}) {
29854
+ return join3(getPortableSkillsRoot(options), normalizePortableSkillName(name));
29855
+ }
29856
+ function findPortableSkill(name, options = {}) {
29857
+ let normalized;
29858
+ try {
29859
+ normalized = normalizePortableSkillName(name);
29860
+ } catch {
29861
+ return null;
29862
+ }
29863
+ const path = getPortableSkillPath(normalized, options);
29864
+ if (!existsSync3(path) || !statSync2(path).isDirectory())
29865
+ return null;
29866
+ try {
29867
+ return summarizePortableSkill(path, normalized);
29868
+ } catch {
29869
+ return null;
29870
+ }
29871
+ }
29872
+ function listPortableSkills(options = {}) {
29873
+ const root = getPortableSkillsRoot(options);
29874
+ if (!existsSync3(root))
29875
+ return [];
29876
+ const skills = [];
29877
+ for (const entry of readdirSync2(root).sort()) {
29878
+ if (entry.startsWith(".") || DATA_DIR_NON_SKILL_ENTRIES.has(entry))
29879
+ continue;
29880
+ const path = join3(root, entry);
29881
+ if (!safeIsDirectory(path))
29882
+ continue;
29883
+ try {
29884
+ skills.push(summarizePortableSkill(path, entry));
29885
+ } catch {
29886
+ continue;
29887
+ }
29888
+ }
29889
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
29890
+ }
29891
+ function listPortableSkillMetas(options = {}) {
29892
+ return listPortableSkills(options).map((skill) => ({
29893
+ name: skill.name,
29894
+ displayName: skill.displayName,
29895
+ description: skill.description,
29896
+ category: readPortableSkillManifest(skill.path).category || "Development Tools",
29897
+ tags: readPortableSkillManifest(skill.path).tags || ["custom"],
29898
+ version: skill.version,
29899
+ source: "custom",
29900
+ pricing: {
29901
+ tier: "free",
29902
+ billingUnit: "run",
29903
+ costCents: 0,
29904
+ formattedCost: "free",
29905
+ estimated: false,
29906
+ quoteDependsOnInput: false,
29907
+ quoteRequired: false,
29908
+ description: "Local portable skill"
29909
+ }
29910
+ }));
29911
+ }
29912
+ function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)) {
29913
+ const skillJsonPath = join3(skillPath, "skill.json");
29914
+ const skillMdPath = join3(skillPath, "SKILL.md");
29915
+ const pkgPath = join3(skillPath, "package.json");
29916
+ const jsonManifest = existsSync3(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
29917
+ const frontmatter = existsSync3(skillMdPath) ? parseSkillFrontmatter(readFileSync3(skillMdPath, "utf-8")) ?? undefined : undefined;
29918
+ const pkg = existsSync3(pkgPath) ? readJsonObject(pkgPath) : undefined;
29919
+ const name = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
29920
+ const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
29921
+ const version2 = stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version) ?? PORTABLE_SKILL_DEFAULT_VERSION;
29922
+ const commands = parseManifestCommands(jsonManifest) ?? inferPackageCommands(pkg, name) ?? [];
29923
+ return {
29924
+ $schema: stringField(jsonManifest, "$schema") ?? PORTABLE_SKILL_SCHEMA,
29925
+ standard: stringField(jsonManifest, "standard") ?? PORTABLE_SKILL_STANDARD,
29926
+ name,
29927
+ description,
29928
+ version: version2,
29929
+ displayName: stringField(jsonManifest, "displayName") ?? frontmatter?.displayName ?? displayName(name),
29930
+ category: stringField(jsonManifest, "category") ?? frontmatter?.category ?? "Development Tools",
29931
+ tags: stringArrayField(jsonManifest, "tags") ?? frontmatter?.tags ?? ["custom"],
29932
+ inputs: parseManifestInputs(jsonManifest) ?? DEFAULT_INPUTS,
29933
+ commands
29934
+ };
29935
+ }
29936
+ function scaffoldPortableSkill(name, options = {}) {
29937
+ const skillName = normalizePortableSkillName(name);
29938
+ const root = getPortableSkillsRoot(options);
29939
+ const skillPath = join3(root, skillName);
29940
+ if (existsSync3(skillPath)) {
29941
+ if (!options.overwrite)
29942
+ throw new Error(`Skill '${skillName}' already exists at ${skillPath}`);
29943
+ rmSync(skillPath, { recursive: true, force: true });
29944
+ }
29945
+ const manifest = createPortableManifest(skillName, {
29946
+ description: options.description ?? `${displayName(skillName)} skill`
29947
+ });
29948
+ writePortableSkillTemplate(skillPath, manifest);
29949
+ return { name: skillName, path: skillPath, manifest, created: true };
29950
+ }
29951
+ function portPortableSkill(sourcePath, options = {}) {
29952
+ const absoluteSource = normalize2(sourcePath);
29953
+ if (!existsSync3(absoluteSource) || !statSync2(absoluteSource).isDirectory()) {
29954
+ throw new Error(`Skill source directory not found: ${sourcePath}`);
29955
+ }
29956
+ const inferred = readPortableSkillManifest(absoluteSource, basename(absoluteSource));
29957
+ const skillName = normalizePortableSkillName(options.name ?? inferred.name);
29958
+ const root = getPortableSkillsRoot(options);
29959
+ const destination = join3(root, skillName);
29960
+ if (existsSync3(destination)) {
29961
+ if (!options.overwrite)
29962
+ throw new Error(`Skill '${skillName}' already exists at ${destination}`);
29963
+ rmSync(destination, { recursive: true, force: true });
29964
+ }
29965
+ mkdirSync2(dirname2(destination), { recursive: true });
29966
+ copySkillDirectory(absoluteSource, destination);
29967
+ const manifest = ensurePortableSkillFiles(destination, {
29968
+ ...inferred,
29969
+ name: skillName,
29970
+ displayName: inferred.displayName ?? displayName(skillName)
29971
+ });
29972
+ return { name: skillName, path: destination, manifest, created: true };
29973
+ }
29974
+ function validatePortableSkillDirectory(name, skillPath) {
29975
+ const normalizedName = normalizePortableSkillName(name);
29976
+ const base = validateSkillDirectory(normalizedName, skillPath);
29977
+ const issues = [...base.issues];
29978
+ const warnings = [...base.warnings];
29979
+ let manifest;
29980
+ if (existsSync3(skillPath)) {
29981
+ const skillJsonPath = join3(skillPath, "skill.json");
29982
+ const skillMdPath = join3(skillPath, "SKILL.md");
29983
+ if (!existsSync3(skillJsonPath) && !existsSync3(skillMdPath)) {
29984
+ add2(issues, "portable.manifest_missing", "Missing portable manifest: expected SKILL.md frontmatter and/or skill.json");
29985
+ }
29986
+ try {
29987
+ manifest = readPortableSkillManifest(skillPath, normalizedName);
29988
+ if (manifest.name !== normalizedName) {
29989
+ add2(issues, "portable.name_mismatch", `Portable manifest name '${manifest.name}' does not match '${normalizedName}'`);
29990
+ }
29991
+ if (manifest.standard !== PORTABLE_SKILL_STANDARD) {
29992
+ add2(issues, "portable.standard_invalid", `Portable manifest standard must be '${PORTABLE_SKILL_STANDARD}'`);
29993
+ }
29994
+ if (!manifest.description.trim()) {
29995
+ add2(issues, "portable.description_missing", "Portable manifest missing description");
29996
+ }
29997
+ if (!manifest.version.trim()) {
29998
+ add2(issues, "portable.version_missing", "Portable manifest missing version");
29999
+ }
30000
+ if (!Array.isArray(manifest.inputs) || manifest.inputs.length === 0) {
30001
+ add2(issues, "portable.inputs_missing", "Portable manifest must declare inputs");
30002
+ }
30003
+ if (!Array.isArray(manifest.commands) || manifest.commands.length === 0) {
30004
+ add2(issues, "portable.commands_missing", "Portable manifest must declare at least one command");
30005
+ } else {
30006
+ for (const command of manifest.commands) {
30007
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(command.name)) {
30008
+ add2(issues, "portable.command_name_invalid", `Command '${command.name}' must use lowercase letters, numbers, dots, underscores, or hyphens`);
30009
+ }
30010
+ if (!command.entry && !command.command) {
30011
+ add2(issues, "portable.command_target_missing", `Command '${command.name}' must declare entry or command`);
30012
+ continue;
30013
+ }
30014
+ if (command.entry) {
30015
+ if (!isSafeRelativePath2(command.entry)) {
30016
+ add2(issues, "portable.command_entry_unsafe", `Command '${command.name}' entry '${command.entry}' must stay inside the skill directory`);
30017
+ continue;
30018
+ }
30019
+ const entryPath = join3(skillPath, command.entry);
30020
+ if (!existsSync3(entryPath))
30021
+ add2(issues, "portable.command_entry_missing", `Command '${command.name}' entry '${command.entry}' is missing`);
30022
+ else if (statSync2(entryPath).isDirectory())
30023
+ add2(issues, "portable.command_entry_directory", `Command '${command.name}' entry '${command.entry}' must be a file`);
30024
+ }
30025
+ }
30026
+ }
30027
+ } catch (error48) {
30028
+ add2(issues, "portable.manifest_invalid", error48.message);
30029
+ }
30030
+ if (!existsSync3(join3(skillPath, "AGENTS.md"))) {
30031
+ add2(issues, "portable.agents_missing", "Missing AGENTS.md with build-out instructions for coding agents");
30032
+ }
30033
+ }
30034
+ const sortedIssues = sortMessages2(issues);
30035
+ const sortedWarnings = sortMessages2(warnings);
30036
+ return {
30037
+ ...base,
30038
+ valid: sortedIssues.length === 0,
30039
+ issues: sortedIssues,
30040
+ warnings: sortedWarnings,
30041
+ metadata: {
30042
+ ...base.metadata,
30043
+ portableManifest: manifest
30044
+ }
30045
+ };
30046
+ }
30047
+ function summarizePortableSkill(skillPath, fallbackName) {
30048
+ const manifest = readPortableSkillManifest(skillPath, fallbackName);
30049
+ return {
30050
+ name: manifest.name,
30051
+ displayName: manifest.displayName ?? displayName(manifest.name),
30052
+ description: manifest.description,
30053
+ version: manifest.version,
30054
+ path: skillPath,
30055
+ commands: manifest.commands,
30056
+ source: "custom",
30057
+ standard: manifest.standard
30058
+ };
30059
+ }
30060
+ function createPortableManifest(name, options) {
30061
+ return {
30062
+ $schema: PORTABLE_SKILL_SCHEMA,
30063
+ standard: PORTABLE_SKILL_STANDARD,
30064
+ name,
30065
+ description: options.description,
30066
+ version: PORTABLE_SKILL_DEFAULT_VERSION,
30067
+ displayName: displayName(name),
30068
+ category: "Development Tools",
30069
+ tags: ["custom", name],
30070
+ inputs: DEFAULT_INPUTS,
30071
+ commands: [{
30072
+ name,
30073
+ description: `Run ${displayName(name)}.`,
30074
+ entry: "src/index.ts",
30075
+ args: ["...args"]
30076
+ }]
30077
+ };
30078
+ }
30079
+ function writePortableSkillTemplate(skillPath, manifest) {
30080
+ mkdirSync2(join3(skillPath, "src"), { recursive: true });
30081
+ writeFileSync2(join3(skillPath, "SKILL.md"), renderSkillMd(manifest));
30082
+ writeFileSync2(join3(skillPath, "skill.json"), renderSkillJson(manifest));
30083
+ writeFileSync2(join3(skillPath, "AGENTS.md"), renderAgentsMd(manifest));
30084
+ writeFileSync2(join3(skillPath, "package.json"), renderPackageJson(manifest));
30085
+ writeFileSync2(join3(skillPath, "tsconfig.json"), renderTsconfig());
30086
+ writeFileSync2(join3(skillPath, "src", "index.ts"), renderEntrypoint(manifest));
30087
+ }
30088
+ function ensurePortableSkillFiles(skillPath, manifest) {
30089
+ let next = manifest;
30090
+ if (!next.commands.length) {
30091
+ next = {
30092
+ ...next,
30093
+ commands: [{
30094
+ name: next.name,
30095
+ description: `Run ${displayName(next.name)}.`,
30096
+ entry: "src/index.ts",
30097
+ args: ["...args"]
30098
+ }]
30099
+ };
30100
+ }
30101
+ if (!next.inputs.length)
30102
+ next = { ...next, inputs: DEFAULT_INPUTS };
30103
+ next = {
30104
+ ...next,
30105
+ standard: PORTABLE_SKILL_STANDARD,
30106
+ $schema: next.$schema ?? PORTABLE_SKILL_SCHEMA,
30107
+ displayName: next.displayName ?? displayName(next.name),
30108
+ category: next.category ?? "Development Tools",
30109
+ tags: next.tags?.length ? next.tags : ["custom", next.name]
30110
+ };
30111
+ const entry = next.commands[0]?.entry ?? "src/index.ts";
30112
+ if (entry && !existsSync3(join3(skillPath, entry))) {
30113
+ mkdirSync2(dirname2(join3(skillPath, entry)), { recursive: true });
30114
+ writeFileSync2(join3(skillPath, entry), renderEntrypoint(next));
30115
+ }
30116
+ if (!existsSync3(join3(skillPath, "SKILL.md")))
30117
+ writeFileSync2(join3(skillPath, "SKILL.md"), renderSkillMd(next));
30118
+ else
30119
+ writeFileSync2(join3(skillPath, "SKILL.md"), ensureSkillMdFrontmatter(readFileSync3(join3(skillPath, "SKILL.md"), "utf-8"), next));
30120
+ if (!existsSync3(join3(skillPath, "skill.json")))
30121
+ writeFileSync2(join3(skillPath, "skill.json"), renderSkillJson(next));
30122
+ if (!existsSync3(join3(skillPath, "AGENTS.md")))
30123
+ writeFileSync2(join3(skillPath, "AGENTS.md"), renderAgentsMd(next));
30124
+ if (!existsSync3(join3(skillPath, "package.json")))
30125
+ writeFileSync2(join3(skillPath, "package.json"), renderPackageJson(next));
30126
+ if (!existsSync3(join3(skillPath, "tsconfig.json")))
30127
+ writeFileSync2(join3(skillPath, "tsconfig.json"), renderTsconfig());
30128
+ return readPortableSkillManifest(skillPath, next.name);
30129
+ }
30130
+ function copySkillDirectory(source, destination) {
30131
+ mkdirSync2(destination, { recursive: true });
30132
+ cpSync(source, destination, {
30133
+ recursive: true,
30134
+ filter: (src) => {
30135
+ const rel = relative(source, src);
30136
+ if (!rel)
30137
+ return true;
30138
+ const first = rel.split(/[\\/]/)[0];
30139
+ if (COPY_EXCLUDES.has(first))
30140
+ return false;
30141
+ if (lstatSync2(src).isSymbolicLink())
30142
+ return false;
30143
+ return true;
30144
+ }
30145
+ });
30146
+ }
30147
+ function renderSkillMd(manifest) {
30148
+ const tags = manifest.tags?.length ? `tags:
30149
+ ${manifest.tags.map((tag) => ` - ${tag}`).join(`
30150
+ `)}
30151
+ ` : "";
30152
+ return `---
30153
+ name: ${manifest.name}
30154
+ description: ${manifest.description}
30155
+ version: ${manifest.version}
30156
+ source: custom
30157
+ category: ${manifest.category ?? "Development Tools"}
30158
+ ${tags}---
30159
+
30160
+ # ${manifest.displayName ?? displayName(manifest.name)}
30161
+
30162
+ ${manifest.description}
30163
+
30164
+ ## Usage
30165
+
30166
+ \`\`\`bash
30167
+ skills run ${manifest.name} --help
30168
+ \`\`\`
30169
+ `;
30170
+ }
30171
+ function renderSkillJson(manifest) {
30172
+ return `${JSON.stringify({
30173
+ $schema: manifest.$schema ?? PORTABLE_SKILL_SCHEMA,
30174
+ standard: PORTABLE_SKILL_STANDARD,
30175
+ name: manifest.name,
30176
+ description: manifest.description,
30177
+ version: manifest.version,
30178
+ displayName: manifest.displayName ?? displayName(manifest.name),
30179
+ category: manifest.category ?? "Development Tools",
30180
+ tags: manifest.tags ?? ["custom", manifest.name],
30181
+ inputs: manifest.inputs,
30182
+ commands: manifest.commands
30183
+ }, null, 2)}
30184
+ `;
30185
+ }
30186
+ function renderPackageJson(manifest) {
30187
+ const first = manifest.commands[0] ?? { name: manifest.name, entry: "src/index.ts" };
30188
+ return `${JSON.stringify({
30189
+ name: manifest.name,
30190
+ version: manifest.version,
30191
+ description: manifest.description,
30192
+ type: "module",
30193
+ bin: { [first.name]: first.entry ?? "src/index.ts" },
30194
+ scripts: { dev: `bun run ${first.entry ?? "src/index.ts"}` },
30195
+ dependencies: {}
30196
+ }, null, 2)}
30197
+ `;
30198
+ }
30199
+ function renderTsconfig() {
30200
+ return `${JSON.stringify({
30201
+ compilerOptions: {
30202
+ target: "ES2022",
30203
+ module: "ESNext",
30204
+ moduleResolution: "bundler",
30205
+ strict: true,
30206
+ outDir: "dist"
30207
+ },
30208
+ include: ["src/**/*.ts"]
30209
+ }, null, 2)}
30210
+ `;
30211
+ }
30212
+ function renderEntrypoint(manifest) {
30213
+ return `#!/usr/bin/env bun
30214
+
30215
+ const args = process.argv.slice(2);
30216
+
30217
+ if (args.includes("--help") || args.includes("-h")) {
30218
+ console.log("${manifest.name}");
30219
+ console.log("");
30220
+ console.log("${escapeJsString(manifest.description)}");
30221
+ console.log("");
30222
+ console.log("Usage: skills run ${manifest.name} [args...]");
30223
+ process.exit(0);
30224
+ }
30225
+
30226
+ console.log(JSON.stringify({
30227
+ skill: "${manifest.name}",
30228
+ args,
30229
+ }, null, 2));
30230
+ `;
30231
+ }
30232
+ function renderAgentsMd(manifest) {
30233
+ const command = manifest.commands[0];
30234
+ const entry = command?.entry ?? "src/index.ts";
30235
+ return `# Agent Build Instructions: ${manifest.name}
30236
+
30237
+ This folder is a portable @hasna/skills skill. Build it in place and keep it valid against the portable skill standard.
30238
+
30239
+ ## Contract
30240
+
30241
+ - Skill name: \`${manifest.name}\`
30242
+ - Description: ${manifest.description}
30243
+ - Manifest files: \`SKILL.md\` frontmatter and \`skill.json\`
30244
+ - Runtime entrypoint: \`${entry}\`
30245
+ - User command: \`skills run ${manifest.name} [args]\`
30246
+
30247
+ ## Build Rules
30248
+
30249
+ 1. Put executable logic in \`${entry}\` or files imported by it.
30250
+ 2. Keep \`skill.json\` updated when inputs, commands, or version change.
30251
+ 3. Keep \`SKILL.md\` concise and compatible with Codewith-style skill frontmatter: \`name\`, \`description\`, \`version\`, optional \`category\`, and optional \`tags\`.
30252
+ 4. Add tests under \`tests/\` when behavior is non-trivial, then run \`bun test\` from this folder if tests exist.
30253
+ 5. Verify with \`skills validate ${manifest.name}\` and smoke-test with \`skills run ${manifest.name} --help\`.
30254
+ 6. Do not commit secrets, generated credentials, \`.env\`, \`node_modules\`, or build output.
30255
+ `;
30256
+ }
30257
+ function ensureSkillMdFrontmatter(content, manifest) {
30258
+ const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trimStart();
30259
+ const generated = renderSkillMd(manifest);
30260
+ const frontmatter = generated.match(/^---\r?\n[\s\S]*?\r?\n---/)?.[0] ?? "";
30261
+ return `${frontmatter}
30262
+
30263
+ ${body || `# ${manifest.displayName ?? displayName(manifest.name)}
30264
+
30265
+ ${manifest.description}
30266
+ `}`;
30267
+ }
30268
+ function parseManifestCommands(value) {
30269
+ const raw = value?.commands;
30270
+ if (!Array.isArray(raw))
30271
+ return;
30272
+ const commands = raw.map((item) => {
30273
+ if (!isRecord(item))
30274
+ return null;
30275
+ const name = stringValue(item.name);
30276
+ if (!name)
30277
+ return null;
30278
+ return {
30279
+ name: normalizePortableSkillName(name),
30280
+ ...stringValue(item.description) ? { description: stringValue(item.description) } : {},
30281
+ ...stringValue(item.entry) ? { entry: stringValue(item.entry) } : {},
30282
+ ...stringValue(item.command) ? { command: stringValue(item.command) } : {},
30283
+ ...Array.isArray(item.args) ? { args: item.args.filter((arg) => typeof arg === "string") } : {}
30284
+ };
30285
+ }).filter((item) => item !== null);
30286
+ return commands.length ? commands : undefined;
30287
+ }
30288
+ function parseManifestInputs(value) {
30289
+ const raw = value?.inputs;
30290
+ if (!Array.isArray(raw))
30291
+ return;
30292
+ const inputs = raw.map((item) => {
30293
+ if (!isRecord(item))
30294
+ return null;
30295
+ const name = stringValue(item.name);
30296
+ const type = stringValue(item.type);
30297
+ if (!name || !type)
30298
+ return null;
30299
+ return {
30300
+ name,
30301
+ type,
30302
+ ...typeof item.required === "boolean" ? { required: item.required } : {},
30303
+ ...stringValue(item.description) ? { description: stringValue(item.description) } : {}
30304
+ };
30305
+ }).filter((item) => item !== null);
30306
+ return inputs.length ? inputs : undefined;
30307
+ }
30308
+ function inferPackageCommands(pkg, fallbackName) {
30309
+ if (!pkg)
30310
+ return;
30311
+ if (isRecord(pkg.bin)) {
30312
+ const commands = Object.entries(pkg.bin).filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0).map(([name, entry]) => ({
30313
+ name: normalizePortableSkillName(name),
30314
+ entry: entry.replace(/^\.\//, ""),
30315
+ description: `Run ${displayName(fallbackName)}.`
30316
+ }));
30317
+ if (commands.length)
30318
+ return commands;
30319
+ }
30320
+ const scripts = isRecord(pkg.scripts) ? pkg.scripts : undefined;
30321
+ const dev = stringValue(scripts?.dev);
30322
+ const match = dev?.match(/(?:bun\s+run\s+|bun\s+)([^ ]+)/);
30323
+ if (match?.[1]) {
30324
+ return [{ name: fallbackName, entry: match[1].replace(/^\.\//, ""), description: `Run ${displayName(fallbackName)}.` }];
30325
+ }
30326
+ return;
30327
+ }
30328
+ function readJsonObject(path) {
30329
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
30330
+ if (!isRecord(parsed))
30331
+ throw new Error(`${basename(path)} must contain a JSON object`);
30332
+ return parsed;
30333
+ }
30334
+ function isRecord(value) {
30335
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30336
+ }
30337
+ function stringField(value, key) {
30338
+ return stringValue(value?.[key]);
30339
+ }
30340
+ function stringArrayField(value, key) {
30341
+ const raw = value?.[key];
30342
+ if (!Array.isArray(raw))
30343
+ return;
30344
+ const strings = raw.filter((item) => typeof item === "string" && item.trim().length > 0);
30345
+ return strings.length ? strings : undefined;
30346
+ }
30347
+ function stringValue(value) {
30348
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
30349
+ }
30350
+ function displayName(name) {
30351
+ return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
30352
+ }
30353
+ function safeIsDirectory(path) {
30354
+ try {
30355
+ return statSync2(path).isDirectory();
30356
+ } catch {
30357
+ return false;
30358
+ }
30359
+ }
30360
+ function isSafeRelativePath2(value) {
30361
+ if (!value.trim() || isAbsolute2(value))
30362
+ return false;
30363
+ const normalized = normalize2(value).replace(/\\/g, "/");
30364
+ return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
30365
+ }
30366
+ function add2(target, code, message) {
30367
+ target.push({ code, message });
30368
+ }
30369
+ function sortMessages2(messages) {
30370
+ return [...messages].sort((a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
30371
+ }
30372
+ function escapeJsString(value) {
30373
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\r?\n/g, " ");
30374
+ }
30375
+
30376
+ // src/lib/registry.ts
30377
+ init_skill_aliases();
30378
+
30379
+ // src/lib/registry-data/development-tools.ts
30380
+ var DEVELOPMENT_TOOLS_SKILLS = [
30381
+ {
30382
+ name: "api-test-suite",
30383
+ displayName: "API Test Suite",
30384
+ description: "Generate and run API test suites with comprehensive endpoint coverage",
30385
+ category: "Development Tools",
30386
+ tags: ["api", "testing", "automation", "qa"]
30387
+ },
30388
+ {
30389
+ name: "apidocs",
30390
+ displayName: "API Docs",
30391
+ description: "Agentic web crawler for API documentation indexing and semantic search",
30392
+ category: "Development Tools",
30393
+ tags: ["api", "documentation", "search", "indexing"]
30394
+ },
30395
+ {
30396
+ name: "api-docs-portal",
30397
+ displayName: "API Docs Portal",
30398
+ description: "Generate premium static API documentation portals from OpenAPI specs, route lists, and endpoint examples",
30399
+ category: "Development Tools",
30400
+ tags: ["api", "documentation", "openapi", "portal", "premium", "remote"]
30401
+ },
30402
+ {
30403
+ name: "sdk-generator",
30404
+ displayName: "SDK Generator",
30405
+ description: "Generate hosted TypeScript SDK scaffolds with client code, types, package files, tests, README, examples, and API summaries",
30406
+ category: "Development Tools",
30407
+ tags: ["sdk", "api", "typescript", "developer-tools", "premium", "remote"]
30408
+ },
30409
+ {
30410
+ name: "repo-onboarding-report",
30411
+ displayName: "Repo Onboarding Report",
30412
+ description: "Generate hosted repository onboarding packages with architecture maps, setup guides, risk registers, and first-week plans",
30413
+ category: "Development Tools",
30414
+ tags: ["repository", "onboarding", "architecture", "developer-tools", "premium", "remote"]
30415
+ },
30416
+ {
30417
+ name: "codefix",
30418
+ displayName: "Code Fix",
30419
+ description: "Code quality CLI for auto-linting, formatting, fixing, and style enforcement",
30420
+ category: "Development Tools",
30421
+ tags: ["code", "linting", "formatting", "quality"]
30422
+ },
30423
+ {
30424
+ name: "commitpush",
30425
+ displayName: "Commit Push",
30426
+ description: "Create logical commits from repo changes and push directly to the main branch",
30427
+ category: "Development Tools",
30428
+ tags: ["git", "commit", "push", "automation"]
30429
+ },
30430
+ {
30431
+ name: "commitpushpr",
30432
+ displayName: "Commit Push PR",
30433
+ description: "Create logical commits, push a feature branch, and open a GitHub pull request",
30434
+ category: "Development Tools",
30435
+ tags: ["git", "commit", "pull-request", "github", "automation"],
30436
+ dependencies: ["commitpush"]
30437
+ },
30438
+ {
30439
+ name: "consolelog",
30440
+ displayName: "Console Log",
30441
+ description: "Monitor console logs from web applications using Playwright headless browser",
30442
+ category: "Development Tools",
30443
+ tags: ["console", "monitoring", "debugging", "logs"]
30444
+ },
30445
+ {
30446
+ name: "database-explorer",
30447
+ displayName: "Database Explorer",
30448
+ description: "Explore and query databases with an interactive interface",
30449
+ category: "Development Tools",
30450
+ tags: ["database", "explorer", "sql", "query"]
30451
+ },
30452
+ {
30453
+ name: "deploy",
30454
+ displayName: "Deploy",
30455
+ description: "Deployment CLI for managing EC2 deployments with automated health checks",
30456
+ category: "Development Tools",
30457
+ tags: ["deployment", "ec2", "aws", "ci-cd"]
30458
+ },
30459
+ {
30460
+ name: "diff-viewer",
30461
+ displayName: "Diff Viewer",
30462
+ description: "View and analyze file differences with visual diff representation",
30463
+ category: "Development Tools",
30464
+ tags: ["diff", "comparison", "files", "code-review"]
29567
30465
  },
29568
30466
  {
29569
30467
  name: "generate-api-client",
@@ -31446,20 +32344,20 @@ function parseSkillMdFrontmatter(content) {
31446
32344
  return Object.keys(result).length > 0 ? result : null;
31447
32345
  }
31448
32346
  function discoverSkillsInDir(dir) {
31449
- if (!existsSync(dir))
32347
+ if (!existsSync4(dir))
31450
32348
  return [];
31451
32349
  const result = [];
31452
32350
  try {
31453
- const entries = readdirSync(dir, { withFileTypes: true });
32351
+ const entries = readdirSync3(dir, { withFileTypes: true });
31454
32352
  for (const entry of entries) {
31455
32353
  if (!entry.isDirectory())
31456
32354
  continue;
31457
- const skillMdPath = join(dir, entry.name, "SKILL.md");
31458
- if (!existsSync(skillMdPath))
32355
+ const skillMdPath = join4(dir, entry.name, "SKILL.md");
32356
+ if (!existsSync4(skillMdPath))
31459
32357
  continue;
31460
32358
  let content;
31461
32359
  try {
31462
- content = readFileSync(skillMdPath, "utf-8");
32360
+ content = readFileSync4(skillMdPath, "utf-8");
31463
32361
  } catch {
31464
32362
  continue;
31465
32363
  }
@@ -31488,7 +32386,10 @@ function loadRegistry(cwd) {
31488
32386
  return registryCache;
31489
32387
  }
31490
32388
  const official = SKILLS.map((s) => ({ ...s, source: "official" }));
31491
- const globalCustom = discoverSkillsInDir(join(homedir(), ".hasna", "skills", "custom"));
32389
+ const dataDir = getDataDir();
32390
+ const portableCustom = listPortableSkillMetas({ rootDir: dataDir });
32391
+ const legacyCustom = discoverSkillsInDir(join4(dataDir, "custom"));
32392
+ const globalCustom = mergeCustomSkills([...legacyCustom, ...portableCustom]);
31492
32393
  const customNames = new Set(globalCustom.map((s) => s.name));
31493
32394
  const filtered = official.filter((s) => !customNames.has(s.name));
31494
32395
  registryCache = [...filtered, ...globalCustom];
@@ -31498,11 +32399,17 @@ function loadRegistry(cwd) {
31498
32399
  function loadBasicRegistry(cwd) {
31499
32400
  const registry2 = loadRegistry(cwd);
31500
32401
  const byName = new Map(registry2.map((skill) => [skill.name, skill]));
31501
- return BASIC_SKILL_NAMES.map((name) => byName.get(name)).filter((skill) => skill !== undefined);
32402
+ const basic = BASIC_SKILL_NAMES.map((name) => byName.get(name)).filter((skill) => skill !== undefined);
32403
+ const custom2 = registry2.filter((skill) => skill.source === "custom" && !BASIC_SKILL_NAMES.includes(skill.name));
32404
+ return [...basic, ...custom2];
31502
32405
  }
31503
32406
  function loadRegistryProfile(profile = "basic", cwd) {
31504
32407
  return profile === "all" ? loadRegistry(cwd) : loadBasicRegistry(cwd);
31505
32408
  }
32409
+ function clearRegistryCache() {
32410
+ registryCache = null;
32411
+ registryCacheTime = 0;
32412
+ }
31506
32413
  function getSkillsByCategory(category) {
31507
32414
  return loadRegistry().filter((s) => s.category === category);
31508
32415
  }
@@ -31511,10 +32418,16 @@ function getSkill(name) {
31511
32418
  const slug = normalizeSkillSlug(name);
31512
32419
  return registry2.find((s) => s.name === slug) ?? registry2.find((s) => s.name === resolveSkillAlias(slug));
31513
32420
  }
32421
+ function mergeCustomSkills(skills) {
32422
+ const byName = new Map;
32423
+ for (const skill of skills)
32424
+ byName.set(skill.name, skill);
32425
+ return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
32426
+ }
31514
32427
 
31515
32428
  // src/lib/installer.ts
31516
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
31517
- import { dirname, join as join3 } from "path";
32429
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
32430
+ import { dirname as dirname3, join as join6 } from "path";
31518
32431
  import { homedir as homedir2 } from "os";
31519
32432
  import { fileURLToPath } from "url";
31520
32433
 
@@ -31524,26 +32437,27 @@ function normalizeSkillName(name) {
31524
32437
  }
31525
32438
 
31526
32439
  // src/lib/installer.ts
32440
+ init_config();
31527
32441
  init_skill_aliases();
31528
32442
 
31529
32443
  // src/lib/project-state.ts
31530
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
31531
- import { join as join2 } from "path";
32444
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
32445
+ import { join as join5 } from "path";
31532
32446
  var SKILLS_PROJECT_DIR = ".skills";
31533
32447
  var PROJECT_CONFIG_FILE = "project.json";
31534
32448
  var DEFAULT_EXPORT_DIR = ".skills/exports";
31535
32449
  function getProjectStateDir(targetDir = process.cwd()) {
31536
- return join2(targetDir, SKILLS_PROJECT_DIR);
32450
+ return join5(targetDir, SKILLS_PROJECT_DIR);
31537
32451
  }
31538
32452
  function getProjectConfigPath(targetDir = process.cwd()) {
31539
- return join2(getProjectStateDir(targetDir), PROJECT_CONFIG_FILE);
32453
+ return join5(getProjectStateDir(targetDir), PROJECT_CONFIG_FILE);
31540
32454
  }
31541
32455
  function loadProjectConfig(targetDir = process.cwd()) {
31542
32456
  const path = getProjectConfigPath(targetDir);
31543
- if (!existsSync2(path))
32457
+ if (!existsSync5(path))
31544
32458
  return null;
31545
32459
  try {
31546
- return normalizeProjectConfig(JSON.parse(readFileSync2(path, "utf-8")));
32460
+ return normalizeProjectConfig(JSON.parse(readFileSync5(path, "utf-8")));
31547
32461
  } catch {
31548
32462
  return null;
31549
32463
  }
@@ -31564,9 +32478,9 @@ function ensureProjectConfig(targetDir = process.cwd()) {
31564
32478
  }
31565
32479
  function saveProjectConfig(config2, targetDir = process.cwd()) {
31566
32480
  const dir = getProjectStateDir(targetDir);
31567
- mkdirSync(dir, { recursive: true });
32481
+ mkdirSync3(dir, { recursive: true });
31568
32482
  const normalized = normalizeProjectConfig({ ...config2, updatedAt: new Date().toISOString() });
31569
- writeFileSync(getProjectConfigPath(targetDir), JSON.stringify(normalized, null, 2) + `
32483
+ writeFileSync3(getProjectConfigPath(targetDir), JSON.stringify(normalized, null, 2) + `
31570
32484
  `);
31571
32485
  }
31572
32486
  function pinProjectSkill(name, details = {}, targetDir = process.cwd()) {
@@ -31638,21 +32552,27 @@ function isPinSource(value) {
31638
32552
  }
31639
32553
 
31640
32554
  // src/lib/installer.ts
31641
- var __dirname2 = dirname(fileURLToPath(import.meta.url));
32555
+ var __dirname2 = dirname3(fileURLToPath(import.meta.url));
31642
32556
  function findSkillsDir() {
31643
32557
  let dir = __dirname2;
31644
32558
  for (let i = 0;i < 5; i++) {
31645
- const candidate = join3(dir, "skills");
31646
- if (existsSync3(candidate) && !dir.includes(".skills"))
32559
+ const candidate = join6(dir, "skills");
32560
+ if (existsSync6(candidate) && !dir.includes(".skills"))
31647
32561
  return candidate;
31648
- dir = dirname(dir);
32562
+ dir = dirname3(dir);
31649
32563
  }
31650
- return join3(__dirname2, "..", "skills");
32564
+ return join6(__dirname2, "..", "skills");
31651
32565
  }
31652
32566
  var SKILLS_DIR = findSkillsDir();
31653
32567
  function getSkillPath(name) {
31654
32568
  const skillName = normalizeSkillName(getCanonicalSkillName(name));
31655
- return join3(SKILLS_DIR, skillName);
32569
+ const portable = findPortableSkill(skillName);
32570
+ if (portable)
32571
+ return portable.path;
32572
+ const legacyCustomPath = join6(getDataDir(), "custom", skillName);
32573
+ if (existsSync6(legacyCustomPath))
32574
+ return legacyCustomPath;
32575
+ return join6(SKILLS_DIR, skillName);
31656
32576
  }
31657
32577
  function getCanonicalSkillName(name) {
31658
32578
  return getSkill(name)?.name ?? resolveSkillAlias(normalizeSkillSlug(name));
@@ -31661,7 +32581,7 @@ function installSkill(name, options = {}) {
31661
32581
  const { targetDir = process.cwd(), overwrite = false } = options;
31662
32582
  const canonicalName = getCanonicalSkillName(name);
31663
32583
  const skillName = normalizeSkillName(canonicalName);
31664
- if (!existsSync3(getSkillPath(name))) {
32584
+ if (!existsSync6(getSkillPath(name))) {
31665
32585
  return { skill: canonicalName, success: false, error: `Skill '${name}' not found`, mode: "pin" };
31666
32586
  }
31667
32587
  const existing = new Set(listPinnedSkills(targetDir));
@@ -31710,11 +32630,11 @@ function getAgentSkillsDir(agent, scope = "global", projectDir) {
31710
32630
  const base = projectDir || process.cwd();
31711
32631
  switch (agent) {
31712
32632
  case "pi":
31713
- return scope === "project" ? join3(base, ".pi", "skills") : join3(homedir2(), ".pi", "agent", "skills");
32633
+ return scope === "project" ? join6(base, ".pi", "skills") : join6(homedir2(), ".pi", "agent", "skills");
31714
32634
  case "opencode":
31715
- return scope === "project" ? join3(base, ".opencode", "skills") : join3(homedir2(), ".config", "opencode", "skills");
32635
+ return scope === "project" ? join6(base, ".opencode", "skills") : join6(homedir2(), ".config", "opencode", "skills");
31716
32636
  default:
31717
- return scope === "project" ? join3(base, `.${agent}`, "skills") : join3(homedir2(), `.${agent}`, "skills");
32637
+ return scope === "project" ? join6(base, `.${agent}`, "skills") : join6(homedir2(), `.${agent}`, "skills");
31718
32638
  }
31719
32639
  }
31720
32640
  function warnMissingDependencies(name, targetDir) {
@@ -31729,11 +32649,11 @@ function warnMissingDependencies(name, targetDir) {
31729
32649
  }
31730
32650
  }
31731
32651
  function readBundledSkillVersion(name) {
31732
- const pkgPath = join3(getSkillPath(name), "package.json");
31733
- if (!existsSync3(pkgPath))
32652
+ const pkgPath = join6(getSkillPath(name), "package.json");
32653
+ if (!existsSync6(pkgPath))
31734
32654
  return "unknown";
31735
32655
  try {
31736
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
32656
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
31737
32657
  return pkg.version || "unknown";
31738
32658
  } catch {
31739
32659
  return "unknown";
@@ -31741,8 +32661,8 @@ function readBundledSkillVersion(name) {
31741
32661
  }
31742
32662
 
31743
32663
  // src/lib/skillinfo.ts
31744
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
31745
- import { join as join4 } from "path";
32664
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
32665
+ import { join as join7 } from "path";
31746
32666
  init_pricing();
31747
32667
  var HOSTED_PROVIDER_ENV_PREFIXES = [
31748
32668
  "OPENAI_",
@@ -31762,12 +32682,12 @@ var HOSTED_PROVIDER_ENV_PREFIXES = [
31762
32682
  ];
31763
32683
  function getSkillDocs(name) {
31764
32684
  const skillPath = getSkillPath(name);
31765
- if (!existsSync4(skillPath))
32685
+ if (!existsSync7(skillPath))
31766
32686
  return null;
31767
32687
  return {
31768
- skillMd: readIfExists(join4(skillPath, "SKILL.md")),
31769
- readme: readIfExists(join4(skillPath, "README.md")),
31770
- claudeMd: readIfExists(join4(skillPath, "CLAUDE.md"))
32688
+ skillMd: readIfExists(join7(skillPath, "SKILL.md")),
32689
+ readme: readIfExists(join7(skillPath, "README.md")),
32690
+ claudeMd: readIfExists(join7(skillPath, "CLAUDE.md"))
31771
32691
  };
31772
32692
  }
31773
32693
  function getSkillBestDoc(name) {
@@ -31778,11 +32698,11 @@ function getSkillBestDoc(name) {
31778
32698
  }
31779
32699
  function getSkillRequirements(name) {
31780
32700
  const skillPath = getSkillPath(name);
31781
- if (!existsSync4(skillPath))
32701
+ if (!existsSync7(skillPath))
31782
32702
  return null;
31783
32703
  const texts = [];
31784
32704
  for (const file2 of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example", ".env.local.example"]) {
31785
- const content = readIfExists(join4(skillPath, file2));
32705
+ const content = readIfExists(join7(skillPath, file2));
31786
32706
  if (content)
31787
32707
  texts.push(content);
31788
32708
  }
@@ -31821,10 +32741,10 @@ function getSkillRequirements(name) {
31821
32741
  const skillName = normalizeSkillName(name);
31822
32742
  let cliCommand = `skills run ${skillName}`;
31823
32743
  let dependencies = {};
31824
- const pkgPath = join4(skillPath, "package.json");
31825
- if (existsSync4(pkgPath)) {
32744
+ const pkgPath = join7(skillPath, "package.json");
32745
+ if (existsSync7(pkgPath)) {
31826
32746
  try {
31827
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
32747
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
31828
32748
  dependencies = pkg.dependencies || {};
31829
32749
  } catch {}
31830
32750
  }
@@ -31841,16 +32761,16 @@ function isHostedPremiumSkill(skillName, meta3) {
31841
32761
  async function runSkill(name, args, options = {}) {
31842
32762
  const canonicalName = getSkill(name)?.name ?? name;
31843
32763
  const skillPath = getSkillPath(canonicalName);
31844
- if (!existsSync4(skillPath)) {
32764
+ if (!existsSync7(skillPath)) {
31845
32765
  return { exitCode: 1, error: `Skill '${name}' not found` };
31846
32766
  }
31847
- const pkgPath = join4(skillPath, "package.json");
31848
- if (!existsSync4(pkgPath)) {
32767
+ const pkgPath = join7(skillPath, "package.json");
32768
+ if (!existsSync7(pkgPath)) {
31849
32769
  return { exitCode: 1, error: `No package.json in skill '${name}'` };
31850
32770
  }
31851
32771
  let entryPoint;
31852
32772
  try {
31853
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
32773
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
31854
32774
  if (pkg.bin) {
31855
32775
  const binValues = Object.values(pkg.bin);
31856
32776
  entryPoint = binValues[0];
@@ -31864,12 +32784,12 @@ async function runSkill(name, args, options = {}) {
31864
32784
  } catch {
31865
32785
  return { exitCode: 1, error: `Failed to parse package.json for skill '${name}'` };
31866
32786
  }
31867
- const entryPath = join4(skillPath, entryPoint);
31868
- if (!existsSync4(entryPath)) {
32787
+ const entryPath = join7(skillPath, entryPoint);
32788
+ if (!existsSync7(entryPath)) {
31869
32789
  return { exitCode: 1, error: `Entry point '${entryPoint}' not found in skill '${name}'` };
31870
32790
  }
31871
- const nodeModules = join4(skillPath, "node_modules");
31872
- if (!existsSync4(nodeModules)) {
32791
+ const nodeModules = join7(skillPath, "node_modules");
32792
+ if (!existsSync7(nodeModules)) {
31873
32793
  const install = Bun.spawn(["bun", "install", "--no-save"], {
31874
32794
  cwd: skillPath,
31875
32795
  stdout: "pipe",
@@ -31896,15 +32816,15 @@ async function runSkill(name, args, options = {}) {
31896
32816
  return { exitCode };
31897
32817
  }
31898
32818
  function detectProjectSkills(cwd = process.cwd()) {
31899
- const pkgPath = join4(cwd, "package.json");
31900
- if (!existsSync4(pkgPath)) {
32819
+ const pkgPath = join7(cwd, "package.json");
32820
+ if (!existsSync7(pkgPath)) {
31901
32821
  const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
31902
32822
  const recommended2 = alwaysRecommend.map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
31903
32823
  return { detected: [], recommended: recommended2 };
31904
32824
  }
31905
32825
  let pkg;
31906
32826
  try {
31907
- pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
32827
+ pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
31908
32828
  } catch {
31909
32829
  const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
31910
32830
  const recommended2 = alwaysRecommend.map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
@@ -31997,8 +32917,8 @@ function extractEnvVars(text) {
31997
32917
  }
31998
32918
  function readIfExists(path) {
31999
32919
  try {
32000
- if (existsSync4(path)) {
32001
- return readFileSync4(path, "utf-8");
32920
+ if (existsSync7(path)) {
32921
+ return readFileSync7(path, "utf-8");
32002
32922
  }
32003
32923
  } catch {}
32004
32924
  return null;
@@ -32106,6 +33026,48 @@ var runOutputSchema = objectSchema({
32106
33026
  })
32107
33027
  }, [], "Skill run result.");
32108
33028
  var toolContracts = [
33029
+ {
33030
+ name: "scaffold_skill",
33031
+ title: "Scaffold Skill",
33032
+ description: "Create a portable skill folder under ~/.hasna/skills/<name> from the standard template.",
33033
+ params: ["name", "description?", "overwrite?"],
33034
+ category: "scaffolding",
33035
+ sideEffects: "filesystem",
33036
+ stable: true,
33037
+ inputSchema: objectSchema({
33038
+ name: skillNameInput,
33039
+ description: stringSchema("Short description for the new skill."),
33040
+ overwrite: { type: "boolean", default: false }
33041
+ }, ["name"]),
33042
+ outputSchema: objectSchema({
33043
+ name: stringSchema("Normalized skill name."),
33044
+ path: stringSchema("Created skill directory."),
33045
+ created: { type: "boolean" },
33046
+ manifest: objectSchema({}, [], "Portable skill manifest.", true)
33047
+ }, ["name", "path", "created", "manifest"])
33048
+ },
33049
+ {
33050
+ name: "port_skill",
33051
+ title: "Port Skill",
33052
+ description: "Import an existing skill folder into the portable ~/.hasna/skills/<name> standard.",
33053
+ params: ["path", "name?", "overwrite?"],
33054
+ category: "scaffolding",
33055
+ sideEffects: "filesystem",
33056
+ stable: true,
33057
+ inputSchema: objectSchema({
33058
+ path: stringSchema("Existing skill folder to import."),
33059
+ name: skillNameInput,
33060
+ overwrite: { type: "boolean", default: false }
33061
+ }, ["path"]),
33062
+ outputSchema: objectSchema({
33063
+ name: stringSchema("Normalized skill name."),
33064
+ path: stringSchema("Imported skill directory."),
33065
+ created: { type: "boolean" },
33066
+ valid: { type: "boolean" },
33067
+ issues: arraySchema(validationMessageSchema),
33068
+ warnings: arraySchema(validationMessageSchema)
33069
+ }, ["name", "path", "created", "valid"])
33070
+ },
32109
33071
  {
32110
33072
  name: "list_skills",
32111
33073
  title: "List Skills",
@@ -32341,6 +33303,46 @@ var toolContracts = [
32341
33303
  inputSchema: objectSchema(),
32342
33304
  outputSchema: objectSchema({}, [], "Setup summary.", true)
32343
33305
  },
33306
+ {
33307
+ name: "storage_status",
33308
+ title: "Storage Status",
33309
+ description: "Show local-first storage paths and optional repo-owned Postgres/S3 readiness.",
33310
+ params: ["directory?"],
33311
+ category: "storage",
33312
+ sideEffects: "none",
33313
+ stable: true,
33314
+ inputSchema: objectSchema({ directory: stringSchema("Project directory.") }),
33315
+ outputSchema: objectSchema({
33316
+ package: stringSchema("Package name."),
33317
+ mode: { type: "string", enum: ["local", "remote", "hybrid"] },
33318
+ local: objectSchema({}, [], "Local storage paths.", true),
33319
+ remote: objectSchema({}, [], "Remote storage readiness.", true)
33320
+ }, ["package", "mode", "local", "remote"])
33321
+ },
33322
+ {
33323
+ name: "storage_sync_plan",
33324
+ title: "Storage Sync Plan",
33325
+ description: "Plan .skills snapshot sync for optional Postgres/S3 storage without network access.",
33326
+ params: ["directory?", "includeSchemaSql?"],
33327
+ category: "storage",
33328
+ sideEffects: "none",
33329
+ stable: true,
33330
+ inputSchema: objectSchema({
33331
+ directory: stringSchema("Project directory."),
33332
+ includeSchemaSql: { type: "boolean", default: false }
33333
+ }),
33334
+ outputSchema: objectSchema({
33335
+ package: stringSchema("Package name."),
33336
+ noNetwork: { type: "boolean", const: true },
33337
+ mode: { type: "string", enum: ["local", "remote", "hybrid"] },
33338
+ databaseConfigured: { type: "boolean" },
33339
+ s3Configured: { type: "boolean" },
33340
+ snapshotFileCount: { type: "number" },
33341
+ s3ObjectCount: { type: "number" },
33342
+ env: objectSchema({}, [], "Storage env var names.", true),
33343
+ schemaSql: stringSchema("Optional Postgres schema SQL.")
33344
+ }, ["package", "noNetwork", "mode", "databaseConfigured", "s3Configured"])
33345
+ },
32344
33346
  {
32345
33347
  name: "schedule_skill",
32346
33348
  title: "Schedule Skill",
@@ -32929,25 +33931,25 @@ function registerDiscoveryTools(server) {
32929
33931
  }
32930
33932
 
32931
33933
  // 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";
33934
+ import { existsSync as existsSync10, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
33935
+ import { join as join10 } from "path";
32934
33936
 
32935
33937
  // src/lib/run-state.ts
32936
33938
  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";
33939
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, readdirSync as readdirSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
33940
+ import { extname, join as join8, relative as relative2 } from "path";
32939
33941
  function createSkillRun(params, targetDir = process.cwd()) {
32940
33942
  const now = new Date;
32941
33943
  const id = createRunId(now);
32942
33944
  const day = now.toISOString().slice(0, 10);
32943
33945
  const skillName = normalizeSkillName(params.skill);
32944
33946
  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 });
33947
+ const runDir = join8(root, "runs", day, id);
33948
+ const logsDir = join8(runDir, "logs");
33949
+ const exportDir = join8(root, "exports", skillName, id);
33950
+ mkdirSync4(logsDir, { recursive: true });
33951
+ mkdirSync4(exportDir, { recursive: true });
33952
+ mkdirSync4(join8(root, "tmp"), { recursive: true });
32951
33953
  const record3 = {
32952
33954
  id,
32953
33955
  skill: skillName,
@@ -32994,42 +33996,42 @@ function updateSkillRun(context, patch) {
32994
33996
  return context.record;
32995
33997
  }
32996
33998
  function writeRunLogs(context, stdout = "", stderr = "") {
32997
- writeFileSync2(join5(context.logsDir, "stdout.log"), stdout);
32998
- writeFileSync2(join5(context.logsDir, "stderr.log"), stderr);
33999
+ writeFileSync4(join8(context.logsDir, "stdout.log"), stdout);
34000
+ writeFileSync4(join8(context.logsDir, "stderr.log"), stderr);
32999
34001
  }
33000
34002
  function appendRunEvent(context, event, data = {}) {
33001
34003
  const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
33002
34004
  `;
33003
- const path = join5(context.runDir, "events.ndjson");
33004
- const previous = existsSync5(path) ? readFileSync5(path, "utf-8") : "";
33005
- writeFileSync2(path, previous + line);
34005
+ const path = join8(context.runDir, "events.ndjson");
34006
+ const previous = existsSync8(path) ? readFileSync8(path, "utf-8") : "";
34007
+ writeFileSync4(path, previous + line);
33006
34008
  }
33007
34009
  function findSkillRun(runId, targetDir = process.cwd()) {
33008
- const runsRoot = join5(getProjectStateDir(targetDir), "runs");
33009
- if (!existsSync5(runsRoot))
34010
+ const runsRoot = join8(getProjectStateDir(targetDir), "runs");
34011
+ if (!existsSync8(runsRoot))
33010
34012
  return null;
33011
- for (const day of readdirSync2(runsRoot)) {
33012
- const record3 = readRunRecord(join5(runsRoot, day, runId));
34013
+ for (const day of readdirSync4(runsRoot)) {
34014
+ const record3 = readRunRecord(join8(runsRoot, day, runId));
33013
34015
  if (record3)
33014
34016
  return record3;
33015
34017
  }
33016
34018
  return null;
33017
34019
  }
33018
34020
  function writeRunRecord(context) {
33019
- writeFileSync2(join5(context.runDir, "run.json"), JSON.stringify(context.record, null, 2) + `
34021
+ writeFileSync4(join8(context.runDir, "run.json"), JSON.stringify(context.record, null, 2) + `
33020
34022
  `);
33021
34023
  }
33022
34024
  function writeArtifactsManifest(context, artifacts) {
33023
- writeFileSync2(join5(context.runDir, "artifacts.json"), JSON.stringify({ runId: context.record.id, artifacts }, null, 2) + `
34025
+ writeFileSync4(join8(context.runDir, "artifacts.json"), JSON.stringify({ runId: context.record.id, artifacts }, null, 2) + `
33024
34026
  `);
33025
34027
  }
33026
34028
  function collectRunArtifacts(context) {
33027
- if (!existsSync5(context.exportDir))
34029
+ if (!existsSync8(context.exportDir))
33028
34030
  return [];
33029
34031
  const artifacts = [];
33030
34032
  for (const path of walkFiles(context.exportDir)) {
33031
- const stat = statSync(path);
33032
- const bytes = readFileSync5(path);
34033
+ const stat = statSync3(path);
34034
+ const bytes = readFileSync8(path);
33033
34035
  artifacts.push({
33034
34036
  path: toProjectRelative(context.targetDir, path),
33035
34037
  mime: mimeForPath(path),
@@ -33040,20 +34042,20 @@ function collectRunArtifacts(context) {
33040
34042
  return artifacts.sort((a, b) => a.path.localeCompare(b.path));
33041
34043
  }
33042
34044
  function readRunRecord(runDir) {
33043
- const path = join5(runDir, "run.json");
33044
- if (!existsSync5(path))
34045
+ const path = join8(runDir, "run.json");
34046
+ if (!existsSync8(path))
33045
34047
  return null;
33046
34048
  try {
33047
- return JSON.parse(readFileSync5(path, "utf-8"));
34049
+ return JSON.parse(readFileSync8(path, "utf-8"));
33048
34050
  } catch {
33049
34051
  return null;
33050
34052
  }
33051
34053
  }
33052
34054
  function walkFiles(dir) {
33053
34055
  const files = [];
33054
- for (const entry of readdirSync2(dir)) {
33055
- const full = join5(dir, entry);
33056
- if (statSync(full).isDirectory())
34056
+ for (const entry of readdirSync4(dir)) {
34057
+ const full = join8(dir, entry);
34058
+ if (statSync3(full).isDirectory())
33057
34059
  files.push(...walkFiles(full));
33058
34060
  else
33059
34061
  files.push(full);
@@ -33064,7 +34066,7 @@ function createRunId(now) {
33064
34066
  return `run_${now.getTime().toString(36)}_${randomBytes(4).toString("hex")}`;
33065
34067
  }
33066
34068
  function toProjectRelative(targetDir, path) {
33067
- const rel = relative(targetDir, path).split(/[\\/]/).join("/");
34069
+ const rel = relative2(targetDir, path).split(/[\\/]/).join("/");
33068
34070
  return rel.startsWith(".") ? rel : `./${rel}`;
33069
34071
  }
33070
34072
  function mimeForPath(path) {
@@ -33083,378 +34085,154 @@ function mimeForPath(path) {
33083
34085
  case ".pdf":
33084
34086
  return "application/pdf";
33085
34087
  case ".md":
33086
- return "text/markdown";
33087
- case ".txt":
33088
- case ".log":
33089
- return "text/plain";
33090
- case ".json":
33091
- return "application/json";
33092
- case ".mp4":
33093
- return "video/mp4";
33094
- case ".mov":
33095
- return "video/quicktime";
33096
- default:
33097
- return "application/octet-stream";
33098
- }
33099
- }
33100
-
33101
- // src/mcp/operation-tools.ts
33102
- function registerOperationTools(server) {
33103
- server.registerTool("pin_skill", {
33104
- title: "Pin Skill",
33105
- description: "Pin a skill to .skills/project.json. Agent skill-folder installs are disabled; use skills mcp --register.",
33106
- inputSchema: {
33107
- name: exports_external.string(),
33108
- for: exports_external.string().optional(),
33109
- scope: exports_external.string().optional()
33110
- }
33111
- }, async ({ name, for: agentArg, scope }) => {
33112
- if (agentArg) {
33113
- let agents;
33114
- try {
33115
- agents = resolveAgents(agentArg);
33116
- } catch (err) {
33117
- return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
33118
- }
33119
- const results = agents.map((a) => ({
33120
- skill: name,
33121
- success: false,
33122
- agent: a,
33123
- scope: scope || "global",
33124
- error: `Direct agent skill-folder installs are disabled. Register Skills MCP instead: skills mcp --register ${a}`
33125
- }));
33126
- return {
33127
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
33128
- isError: results.some((r) => !r.success)
33129
- };
33130
- }
33131
- const result = installSkill(name);
33132
- if (result.success)
33133
- cacheClear();
33134
- return {
33135
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
33136
- isError: !result.success
33137
- };
33138
- });
33139
- server.registerTool("pin_category", {
33140
- title: "Pin Category",
33141
- description: "Pin all skills in a category. Agent skill-folder installs are disabled.",
33142
- inputSchema: {
33143
- category: exports_external.string(),
33144
- for: exports_external.string().optional(),
33145
- scope: exports_external.string().optional()
33146
- }
33147
- }, async ({ category, for: agentArg, scope }) => {
33148
- const matchedCategory = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
33149
- if (!matchedCategory) {
33150
- return {
33151
- ...mcpError("UNKNOWN_CATEGORY", `Unknown category: ${category}`, CATEGORIES.slice())
33152
- };
33153
- }
33154
- const categorySkills = getSkillsByCategory(matchedCategory);
33155
- const names = categorySkills.map((s) => s.name);
33156
- if (agentArg) {
33157
- let agents;
33158
- try {
33159
- agents = resolveAgents(agentArg);
33160
- } catch (err) {
33161
- return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
33162
- }
33163
- const results2 = [];
33164
- for (const name of names) {
33165
- for (const a of agents) {
33166
- const r = {
33167
- skill: name,
33168
- success: false,
33169
- error: `Direct agent skill-folder installs are disabled. Register Skills MCP instead: skills mcp --register ${a}`
33170
- };
33171
- results2.push({ ...r, agent: a, scope: scope || "global" });
33172
- }
33173
- }
33174
- return {
33175
- content: [{ type: "text", text: JSON.stringify({ category: matchedCategory, count: names.length, results: results2 }, null, 2) }],
33176
- isError: results2.some((r) => !r.success)
33177
- };
33178
- }
33179
- const results = names.map((name) => installSkill(name));
33180
- return {
33181
- content: [{ type: "text", text: JSON.stringify({ category: matchedCategory, count: names.length, results }, null, 2) }],
33182
- isError: results.some((r) => !r.success)
33183
- };
33184
- });
33185
- server.registerTool("unpin_skill", {
33186
- title: "Unpin Skill",
33187
- description: "Unpin a skill from .skills/project.json. Agent skill folders are unmanaged.",
33188
- inputSchema: {
33189
- name: exports_external.string(),
33190
- for: exports_external.string().optional(),
33191
- scope: exports_external.string().optional()
33192
- }
33193
- }, async ({ name, for: agentArg, scope }) => {
33194
- if (agentArg) {
33195
- let agents;
33196
- try {
33197
- agents = resolveAgents(agentArg);
33198
- } catch (err) {
33199
- return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
33200
- }
33201
- const results = agents.map((a) => ({
33202
- skill: name,
33203
- agent: a,
33204
- removed: false,
33205
- error: `Agent skill folders are unmanaged. Register Skills MCP instead: skills mcp --register ${a}`
33206
- }));
33207
- return {
33208
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
33209
- };
33210
- }
33211
- const removed = removeSkill(name);
33212
- if (removed)
33213
- cacheClear();
33214
- return {
33215
- content: [{ type: "text", text: JSON.stringify({ skill: name, removed }, null, 2) }]
33216
- };
33217
- });
33218
- server.registerTool("list_categories", {
33219
- title: "List Categories",
33220
- description: "List all 17 skill categories with skill counts."
33221
- }, async () => {
33222
- const cats = CATEGORIES.map((category) => ({
33223
- name: category,
33224
- count: getSkillsByCategory(category).length
33225
- }));
33226
- return { content: [{ type: "text", text: JSON.stringify(cats, null, 2) }] };
33227
- });
33228
- server.registerTool("list_tags", {
33229
- title: "List Tags",
33230
- description: "List all unique skill tags with occurrence counts."
33231
- }, async () => {
33232
- const tagCounts = new Map;
33233
- for (const skill of loadRegistry()) {
33234
- for (const tag of skill.tags) {
33235
- tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
33236
- }
33237
- }
33238
- const sorted = Array.from(tagCounts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({ name, count }));
33239
- return { content: [{ type: "text", text: JSON.stringify(sorted, null, 2) }] };
33240
- });
33241
- server.registerTool("get_requirements", {
33242
- title: "Get Requirements",
33243
- description: "Get env vars, system deps, and npm dependencies for a skill.",
34088
+ return "text/markdown";
34089
+ case ".txt":
34090
+ case ".log":
34091
+ return "text/plain";
34092
+ case ".json":
34093
+ return "application/json";
34094
+ case ".mp4":
34095
+ return "video/mp4";
34096
+ case ".mov":
34097
+ return "video/quicktime";
34098
+ default:
34099
+ return "application/octet-stream";
34100
+ }
34101
+ }
34102
+
34103
+ // src/mcp/operation-tools.ts
34104
+ function registerOperationTools(server) {
34105
+ server.registerTool("scaffold_skill", {
34106
+ title: "Scaffold Skill",
34107
+ description: "Create a portable skill folder under ~/.hasna/skills/<name> with SKILL.md, skill.json, AGENTS.md, package.json, and src/index.ts.",
33244
34108
  inputSchema: {
33245
- name: exports_external.string()
34109
+ name: exports_external.string(),
34110
+ description: exports_external.string().optional(),
34111
+ overwrite: exports_external.boolean().optional()
33246
34112
  }
33247
- }, async ({ name }) => {
33248
- const reqs = getSkillRequirements(name);
33249
- if (!reqs) {
33250
- return mcpError("SKILL_NOT_FOUND", `Skill '${name}' not found`, findSimilarSkills(name));
34113
+ }, async ({ name, description, overwrite }) => {
34114
+ try {
34115
+ const result = scaffoldPortableSkill(name, { description, overwrite });
34116
+ clearRegistryCache();
34117
+ cacheClear();
34118
+ return mcpJson(result);
34119
+ } catch (err) {
34120
+ return mcpError("SCAFFOLD_FAILED", err.message);
33251
34121
  }
33252
- return { content: [{ type: "text", text: JSON.stringify(reqs, null, 2) }] };
33253
34122
  });
33254
- server.registerTool("quote_skill", {
33255
- title: "Quote Skill",
33256
- description: "Quote a skill run before spending account balance.",
34123
+ server.registerTool("port_skill", {
34124
+ title: "Port Skill",
34125
+ description: "Import an existing skill folder into the portable ~/.hasna/skills/<name> standard and add missing standard files.",
33257
34126
  inputSchema: {
33258
- name: exports_external.string(),
33259
- input: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
33260
- args: exports_external.array(exports_external.string()).optional()
33261
- }
33262
- }, async ({ name, input, args }) => {
33263
- const skill = getSkill(name);
33264
- if (!skill) {
33265
- return mcpError("SKILL_NOT_FOUND", `Skill '${name}' not found`, findSimilarSkills(name));
34127
+ path: exports_external.string(),
34128
+ name: exports_external.string().optional(),
34129
+ overwrite: exports_external.boolean().optional()
33266
34130
  }
33267
- const { ARTICLE_GENERATION_SLUG: ARTICLE_GENERATION_SLUG2, getPublicSkillPricing: getPublicSkillPricing2, validateBlogArticleRunOptions: validateBlogArticleRunOptions2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
33268
- const runInput = input || {};
33269
- const runArgs = args || [];
33270
- if (skill.name === ARTICLE_GENERATION_SLUG2) {
33271
- const validation = validateBlogArticleRunOptions2(runInput, runArgs);
33272
- if (!validation.ok) {
33273
- return mcpError("INVALID_BLOG_ARTICLE_OPTIONS", validation.errors.join(" "));
33274
- }
34131
+ }, async ({ path, name, overwrite }) => {
34132
+ try {
34133
+ const result = portPortableSkill(path, { name, overwrite });
34134
+ const validation = validatePortableSkillDirectory(result.name, result.path);
34135
+ clearRegistryCache();
34136
+ cacheClear();
34137
+ return {
34138
+ content: [{ type: "text", text: JSON.stringify({ ...result, valid: validation.valid, issues: validation.issues, warnings: validation.warnings }, null, 2) }],
34139
+ isError: !validation.valid
34140
+ };
34141
+ } catch (err) {
34142
+ return mcpError("PORT_FAILED", err.message);
33275
34143
  }
33276
- return mcpJson({
33277
- skill: skill.name,
33278
- pricing: getPublicSkillPricing2(skill.name, runInput, runArgs)
33279
- });
33280
34144
  });
33281
- server.registerTool("run_skill", {
33282
- title: "Run Skill",
33283
- description: "Run a skill by name with optional arguments.",
34145
+ server.registerTool("pin_skill", {
34146
+ title: "Pin Skill",
34147
+ description: "Pin a skill to .skills/project.json. Agent skill-folder installs are disabled; use skills mcp --register.",
33284
34148
  inputSchema: {
33285
34149
  name: exports_external.string(),
33286
- input: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
33287
- args: exports_external.array(exports_external.string()).optional(),
33288
- approved: exports_external.boolean().optional()
33289
- }
33290
- }, async ({ name, input, args, approved }) => {
33291
- const skill = getSkill(name);
33292
- if (!skill) {
33293
- return mcpError("SKILL_NOT_FOUND", `Skill '${name}' not found`, findSimilarSkills(name));
33294
- }
33295
- const { getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_auth_store(), exports_auth_store));
33296
- const {
33297
- ARTICLE_GENERATION_SLUG: ARTICLE_GENERATION_SLUG2,
33298
- isPremiumSkill: isPremiumSkill2,
33299
- getSkillRunCostCents: getSkillRunCostCents2,
33300
- formatCost: formatCost2,
33301
- validateBlogArticleRunOptions: validateBlogArticleRunOptions2
33302
- } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
33303
- const skillName = skill.name;
33304
- const runInput = input || {};
33305
- const runArgs = args || [];
33306
- if (skillName === ARTICLE_GENERATION_SLUG2) {
33307
- const validation = validateBlogArticleRunOptions2(runInput, runArgs, { requireTopic: true });
33308
- if (!validation.ok) {
33309
- return mcpError("INVALID_BLOG_ARTICLE_OPTIONS", validation.errors.join(" "));
33310
- }
33311
- }
33312
- const apiKey = getApiKey2();
33313
- const costCents = isPremiumSkill2(skillName) ? getSkillRunCostCents2(skillName, runInput, runArgs) : undefined;
33314
- const runContext = createSkillRun({
33315
- skill: skillName,
33316
- args: runArgs,
33317
- remote: isPremiumSkill2(skillName),
33318
- costCents
33319
- });
33320
- if (isPremiumSkill2(skillName) && !apiKey) {
33321
- const cost = formatCost2(costCents ?? 0);
33322
- const error48 = `${skillName} is a hosted skill (${cost}). Run: skills setup --mode hosted && skills auth login`;
33323
- writeRunLogs(runContext, "", error48 + `
33324
- `);
33325
- const run = completeSkillRun(runContext, { status: "failed", error: error48, costCents });
33326
- return mcpError("AUTH_REQUIRED", `${error48}. Local run metadata: ${run.paths.runDir}/run.json`, ["skills auth login"]);
33327
- }
33328
- if (isPremiumSkill2(skillName) && apiKey && approved !== true) {
33329
- const cost = formatCost2(costCents ?? 0);
33330
- const error48 = `${skillName} is a paid hosted skill (${cost}). Call quote_skill first, then call run_skill with approved: true after user approval.`;
33331
- writeRunLogs(runContext, "", error48 + `
33332
- `);
33333
- const run = completeSkillRun(runContext, { status: "failed", error: error48, costCents });
33334
- return mcpError("APPROVAL_REQUIRED", `${error48}. Local run metadata: ${run.paths.runDir}/run.json`, [
33335
- "quote_skill",
33336
- "run_skill approved=true"
33337
- ]);
34150
+ for: exports_external.string().optional(),
34151
+ scope: exports_external.string().optional()
33338
34152
  }
33339
- if (isPremiumSkill2(skillName) && apiKey) {
34153
+ }, async ({ name, for: agentArg, scope }) => {
34154
+ if (agentArg) {
34155
+ let agents;
33340
34156
  try {
33341
- const { RemoteSkillsClient: RemoteSkillsClient2 } = await Promise.resolve().then(() => (init_remote_client(), exports_remote_client));
33342
- const client = new RemoteSkillsClient2(apiKey);
33343
- const run = await client.submitRun(skillName, runInput, runArgs);
33344
- if (run.error) {
33345
- writeRunLogs(runContext, "", String(run.error) + `
33346
- `);
33347
- const localRun3 = completeSkillRun(runContext, { status: "failed", error: String(run.error) });
33348
- return mcpError("RUN_FAILED", `${run.error}. Local run metadata: ${localRun3.paths.runDir}/run.json`);
33349
- }
33350
- const localRun2 = updateSkillRun(runContext, {
33351
- status: run.status === "running" || run.status === "completed" || run.status === "failed" ? run.status : "queued",
33352
- remoteRunId: typeof run.id === "string" ? run.id : undefined
33353
- });
33354
- writeRunLogs(runContext, "", "");
33355
- const remoteRunId = typeof run.id === "string" ? run.id : undefined;
33356
- return mcpJson({
33357
- contractVersion: REMOTE_SKILL_RUN_CONTRACT_VERSION,
33358
- id: run.id,
33359
- localRunId: localRun2.id,
33360
- skill: skillName,
33361
- status: run.status,
33362
- correlationId: run.correlationId,
33363
- remote: true,
33364
- remoteRun: run,
33365
- run: localRun2,
33366
- nextActions: remoteRunNextActions(remoteRunId)
33367
- });
34157
+ agents = resolveAgents(agentArg);
33368
34158
  } catch (err) {
33369
- const error48 = `Hosted skill ${skillName} requires hosted access: ${err.message}`;
33370
- writeRunLogs(runContext, "", error48 + `
33371
- `);
33372
- const localRun2 = completeSkillRun(runContext, { status: "failed", error: error48 });
33373
- return mcpError("PLATFORM_ERROR", `${error48}. Local run metadata: ${localRun2.paths.runDir}/run.json`);
33374
- }
33375
- }
33376
- const result = await runSkill(skillName, runArgs, {
33377
- stdio: "pipe",
33378
- env: {
33379
- SKILLS_RUN_ID: runContext.record.id,
33380
- SKILLS_RUN_DIR: runContext.runDir,
33381
- SKILLS_EXPORT_DIR: runContext.exportDir
34159
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
33382
34160
  }
33383
- });
33384
- writeRunLogs(runContext, result.stdout ?? "", result.stderr ?? result.error ?? "");
33385
- const localRun = completeSkillRun(runContext, {
33386
- status: result.exitCode === 0 ? "completed" : "failed",
33387
- error: result.error
33388
- });
33389
- if (result.error) {
34161
+ const results = agents.map((a) => ({
34162
+ skill: name,
34163
+ success: false,
34164
+ agent: a,
34165
+ scope: scope || "global",
34166
+ error: `Direct agent skill-folder installs are disabled. Register Skills MCP instead: skills mcp --register ${a}`
34167
+ }));
33390
34168
  return {
33391
- content: [{ type: "text", text: JSON.stringify({ exitCode: result.exitCode, error: result.error, run: localRun }, null, 2) }],
33392
- isError: true
34169
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
34170
+ isError: results.some((r) => !r.success)
33393
34171
  };
33394
34172
  }
34173
+ const result = installSkill(name);
34174
+ if (result.success)
34175
+ cacheClear();
33395
34176
  return {
33396
- content: [{ type: "text", text: JSON.stringify({ exitCode: result.exitCode, skill: skillName, stdout: result.stdout, stderr: result.stderr, run: localRun }, null, 2) }]
34177
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
34178
+ isError: !result.success
33397
34179
  };
33398
34180
  });
33399
- server.registerTool("get_run_status", {
33400
- title: "Get Run Status",
33401
- description: "Fetch remote run status. Accepts a remote run id or a local run id linked to a remote run.",
34181
+ server.registerTool("pin_category", {
34182
+ title: "Pin Category",
34183
+ description: "Pin all skills in a category. Agent skill-folder installs are disabled.",
33402
34184
  inputSchema: {
33403
- run_id: exports_external.string()
33404
- }
33405
- }, async ({ run_id }) => {
33406
- const { getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_auth_store(), exports_auth_store));
33407
- const apiKey = getApiKey2();
33408
- if (!apiKey) {
33409
- return mcpError("AUTH_REQUIRED", "Remote run status requires hosted access. Run: skills auth login", ["skills auth login"]);
33410
- }
33411
- const localRun = findSkillRun(run_id);
33412
- const remoteRunId = localRun?.remoteRunId || run_id;
33413
- if (localRun && !localRun.remoteRunId) {
33414
- return mcpError("LOCAL_RUN", `Run '${run_id}' is local and has no remote run id`);
34185
+ category: exports_external.string(),
34186
+ for: exports_external.string().optional(),
34187
+ scope: exports_external.string().optional()
33415
34188
  }
33416
- try {
33417
- const { RemoteSkillsClient: RemoteSkillsClient2 } = await Promise.resolve().then(() => (init_remote_client(), exports_remote_client));
33418
- const client = new RemoteSkillsClient2(apiKey);
33419
- const run = await client.getRun(remoteRunId);
33420
- if (!run)
33421
- return mcpError("RUN_NOT_FOUND", `Remote run '${remoteRunId}' not found`);
33422
- return mcpJson({
33423
- contractVersion: REMOTE_SKILL_RUN_CONTRACT_VERSION,
33424
- runId: remoteRunId,
33425
- ...localRun ? { localRunId: localRun.id } : {},
33426
- run,
33427
- nextActions: remoteRunNextActions(remoteRunId)
33428
- });
33429
- } catch (err) {
33430
- return mcpError("SKILLS_MD_ERROR", err.message);
34189
+ }, async ({ category, for: agentArg, scope }) => {
34190
+ const matchedCategory = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
34191
+ if (!matchedCategory) {
34192
+ return {
34193
+ ...mcpError("UNKNOWN_CATEGORY", `Unknown category: ${category}`, CATEGORIES.slice())
34194
+ };
33431
34195
  }
33432
- });
33433
- server.registerTool("export_skills", {
33434
- title: "Export Pinned Skills",
33435
- description: "Export pinned skills as a JSON payload for import elsewhere."
33436
- }, async () => {
33437
- const skills = getInstalledSkills();
33438
- const payload = {
33439
- version: 1,
33440
- skills,
33441
- timestamp: new Date().toISOString()
34196
+ const categorySkills = getSkillsByCategory(matchedCategory);
34197
+ const names = categorySkills.map((s) => s.name);
34198
+ if (agentArg) {
34199
+ let agents;
34200
+ try {
34201
+ agents = resolveAgents(agentArg);
34202
+ } catch (err) {
34203
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
34204
+ }
34205
+ const results2 = [];
34206
+ for (const name of names) {
34207
+ for (const a of agents) {
34208
+ const r = {
34209
+ skill: name,
34210
+ success: false,
34211
+ error: `Direct agent skill-folder installs are disabled. Register Skills MCP instead: skills mcp --register ${a}`
34212
+ };
34213
+ results2.push({ ...r, agent: a, scope: scope || "global" });
34214
+ }
34215
+ }
34216
+ return {
34217
+ content: [{ type: "text", text: JSON.stringify({ category: matchedCategory, count: names.length, results: results2 }, null, 2) }],
34218
+ isError: results2.some((r) => !r.success)
34219
+ };
34220
+ }
34221
+ const results = names.map((name) => installSkill(name));
34222
+ return {
34223
+ content: [{ type: "text", text: JSON.stringify({ category: matchedCategory, count: names.length, results }, null, 2) }],
34224
+ isError: results.some((r) => !r.success)
33442
34225
  };
33443
- return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
33444
34226
  });
33445
- server.registerTool("import_skills", {
33446
- title: "Import Pinned Skills",
33447
- description: "Pin skills from an export payload. Supports MCP setup guidance via 'for'.",
34227
+ server.registerTool("unpin_skill", {
34228
+ title: "Unpin Skill",
34229
+ description: "Unpin a skill from .skills/project.json. Agent skill folders are unmanaged.",
33448
34230
  inputSchema: {
33449
- skills: exports_external.array(exports_external.string()),
34231
+ name: exports_external.string(),
33450
34232
  for: exports_external.string().optional(),
33451
34233
  scope: exports_external.string().optional()
33452
34234
  }
33453
- }, async ({ skills: skillList, for: agentArg, scope }) => {
33454
- if (!skillList || skillList.length === 0) {
33455
- return { content: [{ type: "text", text: JSON.stringify({ imported: 0, results: [] }, null, 2) }] };
33456
- }
33457
- const results = [];
34235
+ }, async ({ name, for: agentArg, scope }) => {
33458
34236
  if (agentArg) {
33459
34237
  let agents;
33460
34238
  try {
@@ -33462,702 +34240,660 @@ function registerOperationTools(server) {
33462
34240
  } catch (err) {
33463
34241
  return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
33464
34242
  }
33465
- for (const name of skillList) {
33466
- results.push({
33467
- skill: name,
33468
- success: false,
33469
- error: `Direct agent skill-folder installs are disabled. Register Skills MCP instead: skills mcp --register ${agents.join(",")}`
33470
- });
33471
- }
33472
- } else {
33473
- for (const name of skillList) {
33474
- const result = installSkill(name);
33475
- results.push({ skill: result.skill, success: result.success, ...result.error ? { error: result.error } : {} });
33476
- }
34243
+ const results = agents.map((a) => ({
34244
+ skill: name,
34245
+ agent: a,
34246
+ removed: false,
34247
+ error: `Agent skill folders are unmanaged. Register Skills MCP instead: skills mcp --register ${a}`
34248
+ }));
34249
+ return {
34250
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
34251
+ };
33477
34252
  }
33478
- const imported = results.filter((r) => r.success).length;
33479
- const hasErrors = results.some((r) => !r.success);
34253
+ const removed = removeSkill(name);
34254
+ if (removed)
34255
+ cacheClear();
33480
34256
  return {
33481
- content: [{ type: "text", text: JSON.stringify({ imported, total: skillList.length, results }, null, 2) }],
33482
- isError: hasErrors
34257
+ content: [{ type: "text", text: JSON.stringify({ skill: name, removed }, null, 2) }]
33483
34258
  };
33484
34259
  });
33485
- server.registerTool("whoami", {
33486
- title: "Skills Whoami",
33487
- description: "Show setup summary: version, pinned skills, agent configs, cwd."
34260
+ server.registerTool("list_categories", {
34261
+ title: "List Categories",
34262
+ description: "List all 17 skill categories with skill counts."
33488
34263
  }, async () => {
33489
- const version2 = package_default.version;
33490
- const cwd = process.cwd();
33491
- const installed = getInstalledSkills();
33492
- const agents = [];
33493
- for (const agent of AGENT_TARGETS) {
33494
- const agentSkillsPath = getAgentSkillsDir(agent, "global");
33495
- const exists = existsSync8(agentSkillsPath);
33496
- let skillCount = 0;
33497
- if (exists) {
33498
- try {
33499
- skillCount = readdirSync3(agentSkillsPath).filter((f) => {
33500
- const full = join8(agentSkillsPath, f);
33501
- return !f.startsWith(".") && statSync2(full).isDirectory();
33502
- }).length;
33503
- } catch {}
34264
+ const cats = CATEGORIES.map((category) => ({
34265
+ name: category,
34266
+ count: getSkillsByCategory(category).length
34267
+ }));
34268
+ return { content: [{ type: "text", text: JSON.stringify(cats, null, 2) }] };
34269
+ });
34270
+ server.registerTool("list_tags", {
34271
+ title: "List Tags",
34272
+ description: "List all unique skill tags with occurrence counts."
34273
+ }, async () => {
34274
+ const tagCounts = new Map;
34275
+ for (const skill of loadRegistry()) {
34276
+ for (const tag of skill.tags) {
34277
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
33504
34278
  }
33505
- agents.push({ agent, label: AGENT_LABELS[agent], path: agentSkillsPath, exists, skillCount });
33506
34279
  }
33507
- const skillsDir = getSkillPath("image").replace(/[/\\][^/\\]*$/, "");
33508
- const result = {
33509
- version: version2,
33510
- installedCount: installed.length,
33511
- installed,
33512
- agents,
33513
- skillsDir,
33514
- cwd
33515
- };
33516
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
33517
- });
33518
- }
33519
-
33520
- // 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";
33524
- import { Database } from "bun:sqlite";
33525
- function getFeedbackDbPath() {
33526
- return join9(homedir5(), ".hasna", "skills", "skills.db");
33527
- }
33528
- function getFeedbackDb() {
33529
- const dbPath = getFeedbackDbPath();
33530
- const dir = dirname3(dbPath);
33531
- if (!existsSync9(dir))
33532
- mkdirSync5(dir, { recursive: true });
33533
- const db = new Database(dbPath);
33534
- db.exec("PRAGMA journal_mode = WAL");
33535
- db.exec([
33536
- "CREATE TABLE IF NOT EXISTS feedback (",
33537
- "id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),",
33538
- "message TEXT NOT NULL,",
33539
- "email TEXT,",
33540
- "category TEXT DEFAULT 'general',",
33541
- "agent TEXT,",
33542
- "version TEXT,",
33543
- "machine_id TEXT,",
33544
- "created_at TEXT NOT NULL DEFAULT (datetime('now'))",
33545
- ")"
33546
- ].join(" "));
33547
- try {
33548
- db.exec("ALTER TABLE feedback ADD COLUMN agent TEXT");
33549
- } catch {}
33550
- return db;
33551
- }
33552
- function saveFeedback(input) {
33553
- const message = input.message.trim();
33554
- if (!message)
33555
- throw new Error("Feedback message is required");
33556
- const category = input.category ?? "general";
33557
- const db = getFeedbackDb();
33558
- try {
33559
- db.run("INSERT INTO feedback (message, email, category, agent, version) VALUES (?, ?, ?, ?, ?)", [message, input.email || null, category, input.agent || null, input.version || null]);
33560
- } finally {
33561
- db.close();
33562
- }
33563
- return { saved: true, category, path: getFeedbackDbPath() };
33564
- }
33565
-
33566
- // src/mcp/resource-meta-tools.ts
33567
- function registerResourceMetaTools(server) {
33568
- server.registerResource("MCP Contracts", "skills://mcp/contracts", {
33569
- description: "Machine-readable MCP tool and resource contract manifest."
33570
- }, async () => ({
33571
- contents: [{
33572
- uri: "skills://mcp/contracts",
33573
- text: JSON.stringify(createMcpContractManifest(), null, 2),
33574
- mimeType: "application/json"
33575
- }]
33576
- }));
33577
- server.registerResource("Skills Registry", "skills://registry", {
33578
- description: "Compact default basic skill list [{name,category,pricing}]. Use list_skills with profile:'all' for the full registry, and skills://{name} for detail."
33579
- }, async () => ({
33580
- contents: [{
33581
- uri: "skills://registry",
33582
- text: JSON.stringify(loadRegistryProfile("basic").map(getCompactSkillDiscovery)),
33583
- mimeType: "application/json"
33584
- }]
33585
- }));
33586
- server.registerResource("Skill Info", new ResourceTemplate("skills://{name}", { list: undefined }), {
33587
- description: "Individual skill metadata and documentation"
33588
- }, async (uri, { name }) => {
33589
- const skill = getSkill(name);
33590
- const doc2 = getSkillBestDoc(name);
33591
- const reqs = getSkillRequirements(name);
33592
- const publicReqs = skill && reqs ? {
33593
- ...reqs,
33594
- envVars: publicDiscoveryEnvVars(skill.name, reqs.envVars),
33595
- dependencies: publicDiscoveryDependencies(skill.name, reqs.dependencies)
33596
- } : reqs;
33597
- return {
33598
- contents: [{
33599
- uri: uri.href,
33600
- text: JSON.stringify({
33601
- ...skill ? getPublicSkillDiscovery(skill) : {},
33602
- documentation: skill ? publicDiscoveryDocumentation(skill, doc2) : doc2,
33603
- requirements: publicReqs,
33604
- ...skill ? { mcp: createSkillMcpMetadata(getPublicSkillDiscovery(skill)) } : {}
33605
- }, null, 2),
33606
- mimeType: "application/json"
33607
- }]
33608
- };
33609
- });
33610
- server.registerTool("search_tools", {
33611
- title: "Search Tools",
33612
- description: "List tool names or summaries, optionally filtered by keyword.",
33613
- inputSchema: { query: exports_external.string().optional(), detail: exports_external.boolean().optional() }
33614
- }, async ({ query, detail }) => {
33615
- const contracts2 = listMcpToolContracts(query);
33616
- const tools = detail ? contracts2.map(summarizeMcpToolContract) : contracts2.map((contract) => contract.name);
33617
- return mcpJson({ schemaVersion: 1, tools, total: tools.length });
33618
- });
33619
- server.registerTool("describe_tools", {
33620
- title: "Describe Tools",
33621
- description: "Get machine-readable contracts for specific tools by name.",
33622
- inputSchema: { names: exports_external.array(exports_external.string()) }
33623
- }, async ({ names }) => {
33624
- return mcpJson({ schemaVersion: 1, tools: describeMcpToolContracts(names) });
34280
+ const sorted = Array.from(tagCounts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({ name, count }));
34281
+ return { content: [{ type: "text", text: JSON.stringify(sorted, null, 2) }] };
33625
34282
  });
33626
- server.registerTool("get_mcp_contracts", {
33627
- title: "Get MCP Contracts",
33628
- description: "Return the machine-readable MCP tool and resource contract manifest.",
34283
+ server.registerTool("get_requirements", {
34284
+ title: "Get Requirements",
34285
+ description: "Get env vars, system deps, and npm dependencies for a skill.",
33629
34286
  inputSchema: {
33630
- names: exports_external.array(exports_external.string()).optional(),
33631
- includeResources: exports_external.boolean().optional()
34287
+ name: exports_external.string()
33632
34288
  }
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 });
34289
+ }, async ({ name }) => {
34290
+ const reqs = getSkillRequirements(name);
34291
+ if (!reqs) {
34292
+ return mcpError("SKILL_NOT_FOUND", `Skill '${name}' not found`, findSimilarSkills(name));
33645
34293
  }
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);
34294
+ return { content: [{ type: "text", text: JSON.stringify(reqs, null, 2) }] };
33668
34295
  });
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));
34296
+ server.registerTool("quote_skill", {
34297
+ title: "Quote Skill",
34298
+ description: "Quote a skill run before spending account balance.",
34299
+ inputSchema: {
34300
+ name: exports_external.string(),
34301
+ input: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
34302
+ args: exports_external.array(exports_external.string()).optional()
34303
+ }
34304
+ }, async ({ name, input, args }) => {
34305
+ const skill = getSkill(name);
34306
+ if (!skill) {
34307
+ return mcpError("SKILL_NOT_FOUND", `Skill '${name}' not found`, findSimilarSkills(name));
34308
+ }
34309
+ const { ARTICLE_GENERATION_SLUG: ARTICLE_GENERATION_SLUG2, getPublicSkillPricing: getPublicSkillPricing2, validateBlogArticleRunOptions: validateBlogArticleRunOptions2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
34310
+ const runInput = input || {};
34311
+ const runArgs = args || [];
34312
+ if (skill.name === ARTICLE_GENERATION_SLUG2) {
34313
+ const validation = validateBlogArticleRunOptions2(runInput, runArgs);
34314
+ if (!validation.ok) {
34315
+ return mcpError("INVALID_BLOG_ARTICLE_OPTIONS", validation.errors.join(" "));
34316
+ }
33675
34317
  }
34318
+ return mcpJson({
34319
+ skill: skill.name,
34320
+ pricing: getPublicSkillPricing2(skill.name, runInput, runArgs)
34321
+ });
33676
34322
  });
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}` };
34323
+ server.registerTool("run_skill", {
34324
+ title: "Run Skill",
34325
+ description: "Run a skill by name with optional arguments.",
34326
+ inputSchema: {
34327
+ name: exports_external.string(),
34328
+ input: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
34329
+ args: exports_external.array(exports_external.string()).optional(),
34330
+ approved: exports_external.boolean().optional()
33713
34331
  }
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}` };
34332
+ }, async ({ name, input, args, approved }) => {
34333
+ const skill = getSkill(name);
34334
+ if (!skill) {
34335
+ return mcpError("SKILL_NOT_FOUND", `Skill '${name}' not found`, findSimilarSkills(name));
34336
+ }
34337
+ const { getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_auth_store(), exports_auth_store));
34338
+ const {
34339
+ ARTICLE_GENERATION_SLUG: ARTICLE_GENERATION_SLUG2,
34340
+ isPremiumSkill: isPremiumSkill2,
34341
+ getSkillRunCostCents: getSkillRunCostCents2,
34342
+ formatCost: formatCost2,
34343
+ validateBlogArticleRunOptions: validateBlogArticleRunOptions2
34344
+ } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
34345
+ const skillName = skill.name;
34346
+ const runInput = input || {};
34347
+ const runArgs = args || [];
34348
+ if (skillName === ARTICLE_GENERATION_SLUG2) {
34349
+ const validation = validateBlogArticleRunOptions2(runInput, runArgs, { requireTopic: true });
34350
+ if (!validation.ok) {
34351
+ return mcpError("INVALID_BLOG_ARTICLE_OPTIONS", validation.errors.join(" "));
33726
34352
  }
33727
- continue;
33728
34353
  }
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}` };
34354
+ const apiKey = getApiKey2();
34355
+ const costCents = isPremiumSkill2(skillName) ? getSkillRunCostCents2(skillName, runInput, runArgs) : undefined;
34356
+ const runContext = createSkillRun({
34357
+ skill: skillName,
34358
+ args: runArgs,
34359
+ remote: isPremiumSkill2(skillName),
34360
+ costCents
34361
+ });
34362
+ if (isPremiumSkill2(skillName) && !apiKey) {
34363
+ const cost = formatCost2(costCents ?? 0);
34364
+ const error48 = `${skillName} is a hosted skill (${cost}). Run: skills setup --mode hosted && skills auth login`;
34365
+ writeRunLogs(runContext, "", error48 + `
34366
+ `);
34367
+ const run = completeSkillRun(runContext, { status: "failed", error: error48, costCents });
34368
+ return mcpError("AUTH_REQUIRED", `${error48}. Local run metadata: ${run.paths.runDir}/run.json`, ["skills auth login"]);
33734
34369
  }
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;
34370
+ if (isPremiumSkill2(skillName) && apiKey && approved !== true) {
34371
+ const cost = formatCost2(costCents ?? 0);
34372
+ const error48 = `${skillName} is a paid hosted skill (${cost}). Call quote_skill first, then call run_skill with approved: true after user approval.`;
34373
+ writeRunLogs(runContext, "", error48 + `
34374
+ `);
34375
+ const run = completeSkillRun(runContext, { status: "failed", error: error48, costCents });
34376
+ return mcpError("APPROVAL_REQUIRED", `${error48}. Local run metadata: ${run.paths.runDir}/run.json`, [
34377
+ "quote_skill",
34378
+ "run_skill approved=true"
34379
+ ]);
33774
34380
  }
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);
34381
+ if (isPremiumSkill2(skillName) && apiKey) {
34382
+ try {
34383
+ const { RemoteSkillsClient: RemoteSkillsClient2 } = await Promise.resolve().then(() => (init_remote_client(), exports_remote_client));
34384
+ const client = new RemoteSkillsClient2(apiKey);
34385
+ const run = await client.submitRun(skillName, runInput, runArgs);
34386
+ if (run.error) {
34387
+ writeRunLogs(runContext, "", String(run.error) + `
34388
+ `);
34389
+ const localRun3 = completeSkillRun(runContext, { status: "failed", error: String(run.error) });
34390
+ return mcpError("RUN_FAILED", `${run.error}. Local run metadata: ${localRun3.paths.runDir}/run.json`);
34391
+ }
34392
+ const localRun2 = updateSkillRun(runContext, {
34393
+ status: run.status === "running" || run.status === "completed" || run.status === "failed" ? run.status : "queued",
34394
+ remoteRunId: typeof run.id === "string" ? run.id : undefined
34395
+ });
34396
+ writeRunLogs(runContext, "", "");
34397
+ const remoteRunId = typeof run.id === "string" ? run.id : undefined;
34398
+ return mcpJson({
34399
+ contractVersion: REMOTE_SKILL_RUN_CONTRACT_VERSION,
34400
+ id: run.id,
34401
+ localRunId: localRun2.id,
34402
+ skill: skillName,
34403
+ status: run.status,
34404
+ correlationId: run.correlationId,
34405
+ remote: true,
34406
+ remoteRun: run,
34407
+ run: localRun2,
34408
+ nextActions: remoteRunNextActions(remoteRunId)
34409
+ });
34410
+ } catch (err) {
34411
+ const error48 = `Hosted skill ${skillName} requires hosted access: ${err.message}`;
34412
+ writeRunLogs(runContext, "", error48 + `
34413
+ `);
34414
+ const localRun2 = completeSkillRun(runContext, { status: "failed", error: error48 });
34415
+ return mcpError("PLATFORM_ERROR", `${error48}. Local run metadata: ${localRun2.paths.runDir}/run.json`);
34416
+ }
34417
+ }
34418
+ const result = await runSkill(skillName, runArgs, {
34419
+ stdio: "pipe",
34420
+ env: {
34421
+ SKILLS_RUN_ID: runContext.record.id,
34422
+ SKILLS_RUN_DIR: runContext.runDir,
34423
+ SKILLS_EXPORT_DIR: runContext.exportDir
33779
34424
  }
33780
- const n = parseInt(part);
33781
- return isNaN(n) ? [] : [n];
33782
34425
  });
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;
34426
+ writeRunLogs(runContext, result.stdout ?? "", result.stderr ?? result.error ?? "");
34427
+ const localRun = completeSkillRun(runContext, {
34428
+ status: result.exitCode === 0 ? "completed" : "failed",
34429
+ error: result.error
34430
+ });
34431
+ if (result.error) {
34432
+ return {
34433
+ content: [{ type: "text", text: JSON.stringify({ exitCode: result.exitCode, error: result.error, run: localRun }, null, 2) }],
34434
+ isError: true
34435
+ };
34436
+ }
34437
+ return {
34438
+ content: [{ type: "text", text: JSON.stringify({ exitCode: result.exitCode, skill: skillName, stdout: result.stdout, stderr: result.stderr, run: localRun }, null, 2) }]
34439
+ };
34440
+ });
34441
+ server.registerTool("get_run_status", {
34442
+ title: "Get Run Status",
34443
+ description: "Fetch remote run status. Accepts a remote run id or a local run id linked to a remote run.",
34444
+ inputSchema: {
34445
+ run_id: exports_external.string()
34446
+ }
34447
+ }, async ({ run_id }) => {
34448
+ const { getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_auth_store(), exports_auth_store));
34449
+ const apiKey = getApiKey2();
34450
+ if (!apiKey) {
34451
+ return mcpError("AUTH_REQUIRED", "Remote run status requires hosted access. Run: skills auth login", ["skills auth login"]);
33804
34452
  }
33805
- if (!doms.includes(dom) || !dows.includes(dow)) {
33806
- candidate.setDate(candidate.getDate() + 1);
33807
- candidate.setHours(0, 0, 0, 0);
33808
- continue;
34453
+ const localRun = findSkillRun(run_id);
34454
+ const remoteRunId = localRun?.remoteRunId || run_id;
34455
+ if (localRun && !localRun.remoteRunId) {
34456
+ return mcpError("LOCAL_RUN", `Run '${run_id}' is local and has no remote run id`);
33809
34457
  }
33810
- if (!hours.includes(hour)) {
33811
- candidate.setHours(candidate.getHours() + 1, 0, 0, 0);
33812
- continue;
34458
+ try {
34459
+ const { RemoteSkillsClient: RemoteSkillsClient2 } = await Promise.resolve().then(() => (init_remote_client(), exports_remote_client));
34460
+ const client = new RemoteSkillsClient2(apiKey);
34461
+ const run = await client.getRun(remoteRunId);
34462
+ if (!run)
34463
+ return mcpError("RUN_NOT_FOUND", `Remote run '${remoteRunId}' not found`);
34464
+ return mcpJson({
34465
+ contractVersion: REMOTE_SKILL_RUN_CONTRACT_VERSION,
34466
+ runId: remoteRunId,
34467
+ ...localRun ? { localRunId: localRun.id } : {},
34468
+ run,
34469
+ nextActions: remoteRunNextActions(remoteRunId)
34470
+ });
34471
+ } catch (err) {
34472
+ return mcpError("SKILLS_MD_ERROR", err.message);
33813
34473
  }
33814
- if (!minutes.includes(minute)) {
33815
- candidate.setMinutes(candidate.getMinutes() + 1, 0, 0);
33816
- continue;
34474
+ });
34475
+ server.registerTool("export_skills", {
34476
+ title: "Export Pinned Skills",
34477
+ description: "Export pinned skills as a JSON payload for import elsewhere."
34478
+ }, async () => {
34479
+ const skills = getInstalledSkills();
34480
+ const payload = {
34481
+ version: 1,
34482
+ skills,
34483
+ timestamp: new Date().toISOString()
34484
+ };
34485
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
34486
+ });
34487
+ server.registerTool("import_skills", {
34488
+ title: "Import Pinned Skills",
34489
+ description: "Pin skills from an export payload. Supports MCP setup guidance via 'for'.",
34490
+ inputSchema: {
34491
+ skills: exports_external.array(exports_external.string()),
34492
+ for: exports_external.string().optional(),
34493
+ scope: exports_external.string().optional()
33817
34494
  }
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;
33855
- }
33856
-
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("/../");
33922
- }
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";
33930
- }
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;
33941
- }
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());
34495
+ }, async ({ skills: skillList, for: agentArg, scope }) => {
34496
+ if (!skillList || skillList.length === 0) {
34497
+ return { content: [{ type: "text", text: JSON.stringify({ imported: 0, results: [] }, null, 2) }] };
34498
+ }
34499
+ const results = [];
34500
+ if (agentArg) {
34501
+ let agents;
34502
+ try {
34503
+ agents = resolveAgents(agentArg);
34504
+ } catch (err) {
34505
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
34506
+ }
34507
+ for (const name of skillList) {
34508
+ results.push({
34509
+ skill: name,
34510
+ success: false,
34511
+ error: `Direct agent skill-folder installs are disabled. Register Skills MCP instead: skills mcp --register ${agents.join(",")}`
34512
+ });
34513
+ }
34514
+ } else {
34515
+ for (const name of skillList) {
34516
+ const result = installSkill(name);
34517
+ results.push({ skill: result.skill, success: result.success, ...result.error ? { error: result.error } : {} });
33962
34518
  }
33963
- result.tags = tags;
33964
- continue;
33965
34519
  }
33966
- const value = rawValue.replace(/^["']|["']$/g, "");
33967
- if (!value)
33968
- 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);
34520
+ const imported = results.filter((r) => r.success).length;
34521
+ const hasErrors = results.some((r) => !r.success);
34522
+ return {
34523
+ content: [{ type: "text", text: JSON.stringify({ imported, total: skillList.length, results }, null, 2) }],
34524
+ isError: hasErrors
34525
+ };
34526
+ });
34527
+ server.registerTool("whoami", {
34528
+ title: "Skills Whoami",
34529
+ description: "Show setup summary: version, pinned skills, agent configs, cwd."
34530
+ }, async () => {
34531
+ const version2 = package_default.version;
34532
+ const cwd = process.cwd();
34533
+ const installed = getInstalledSkills();
34534
+ const agents = [];
34535
+ for (const agent of AGENT_TARGETS) {
34536
+ const agentSkillsPath = getAgentSkillsDir(agent, "global");
34537
+ const exists = existsSync10(agentSkillsPath);
34538
+ let skillCount = 0;
34539
+ if (exists) {
34540
+ try {
34541
+ skillCount = readdirSync5(agentSkillsPath).filter((f) => {
34542
+ const full = join10(agentSkillsPath, f);
34543
+ return !f.startsWith(".") && statSync4(full).isDirectory();
34544
+ }).length;
34545
+ } catch {}
34546
+ }
34547
+ agents.push({ agent, label: AGENT_LABELS[agent], path: agentSkillsPath, exists, skillCount });
33983
34548
  }
34549
+ const skillsDir = getSkillPath("image").replace(/[/\\][^/\\]*$/, "");
34550
+ const result = {
34551
+ version: version2,
34552
+ installedCount: installed.length,
34553
+ installed,
34554
+ agents,
34555
+ skillsDir,
34556
+ cwd
34557
+ };
34558
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
34559
+ });
34560
+ }
34561
+
34562
+ // src/lib/feedback.ts
34563
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
34564
+ import { homedir as homedir4 } from "os";
34565
+ import { dirname as dirname4, join as join11 } from "path";
34566
+ import { Database } from "bun:sqlite";
34567
+ function getFeedbackDbPath() {
34568
+ return join11(homedir4(), ".hasna", "skills", "skills.db");
34569
+ }
34570
+ function getFeedbackDb() {
34571
+ const dbPath = getFeedbackDbPath();
34572
+ const dir = dirname4(dbPath);
34573
+ if (!existsSync11(dir))
34574
+ mkdirSync6(dir, { recursive: true });
34575
+ const db = new Database(dbPath);
34576
+ db.exec("PRAGMA journal_mode = WAL");
34577
+ db.exec([
34578
+ "CREATE TABLE IF NOT EXISTS feedback (",
34579
+ "id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),",
34580
+ "message TEXT NOT NULL,",
34581
+ "email TEXT,",
34582
+ "category TEXT DEFAULT 'general',",
34583
+ "agent TEXT,",
34584
+ "version TEXT,",
34585
+ "machine_id TEXT,",
34586
+ "created_at TEXT NOT NULL DEFAULT (datetime('now'))",
34587
+ ")"
34588
+ ].join(" "));
34589
+ try {
34590
+ db.exec("ALTER TABLE feedback ADD COLUMN agent TEXT");
34591
+ } catch {}
34592
+ return db;
34593
+ }
34594
+ function saveFeedback(input) {
34595
+ const message = input.message.trim();
34596
+ if (!message)
34597
+ throw new Error("Feedback message is required");
34598
+ const category = input.category ?? "general";
34599
+ const db = getFeedbackDb();
34600
+ try {
34601
+ db.run("INSERT INTO feedback (message, email, category, agent, version) VALUES (?, ?, ?, ?, ?)", [message, input.email || null, category, input.agent || null, input.version || null]);
34602
+ } finally {
34603
+ db.close();
33984
34604
  }
33985
- return Object.keys(result).length > 0 ? result : null;
34605
+ return { saved: true, category, path: getFeedbackDbPath() };
33986
34606
  }
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}`);
34607
+
34608
+ // src/mcp/resource-meta-tools.ts
34609
+ function registerResourceMetaTools(server) {
34610
+ server.registerResource("MCP Contracts", "skills://mcp/contracts", {
34611
+ description: "Machine-readable MCP tool and resource contract manifest."
34612
+ }, async () => ({
34613
+ contents: [{
34614
+ uri: "skills://mcp/contracts",
34615
+ text: JSON.stringify(createMcpContractManifest(), null, 2),
34616
+ mimeType: "application/json"
34617
+ }]
34618
+ }));
34619
+ server.registerResource("Skills Registry", "skills://registry", {
34620
+ description: "Compact default basic skill list [{name,category,pricing}]. Use list_skills with profile:'all' for the full registry, and skills://{name} for detail."
34621
+ }, async () => ({
34622
+ contents: [{
34623
+ uri: "skills://registry",
34624
+ text: JSON.stringify(loadRegistryProfile("basic").map(getCompactSkillDiscovery)),
34625
+ mimeType: "application/json"
34626
+ }]
34627
+ }));
34628
+ server.registerResource("Skill Info", new ResourceTemplate("skills://{name}", { list: undefined }), {
34629
+ description: "Individual skill metadata and documentation"
34630
+ }, async (uri, { name }) => {
34631
+ const skill = getSkill(name);
34632
+ const doc2 = getSkillBestDoc(name);
34633
+ const reqs = getSkillRequirements(name);
34634
+ const publicReqs = skill && reqs ? {
34635
+ ...reqs,
34636
+ envVars: publicDiscoveryEnvVars(skill.name, reqs.envVars),
34637
+ dependencies: publicDiscoveryDependencies(skill.name, reqs.dependencies)
34638
+ } : reqs;
33999
34639
  return {
34000
- name: bareName,
34001
- path: skillPath,
34002
- valid: false,
34003
- issues: sortMessages(issues),
34004
- warnings: sortMessages(warnings),
34005
- metadata
34640
+ contents: [{
34641
+ uri: uri.href,
34642
+ text: JSON.stringify({
34643
+ ...skill ? getPublicSkillDiscovery(skill) : {},
34644
+ documentation: skill ? publicDiscoveryDocumentation(skill, doc2) : doc2,
34645
+ requirements: publicReqs,
34646
+ ...skill ? { mcp: createSkillMcpMetadata(getPublicSkillDiscovery(skill)) } : {}
34647
+ }, null, 2),
34648
+ mimeType: "application/json"
34649
+ }]
34006
34650
  };
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`);
34651
+ });
34652
+ server.registerTool("search_tools", {
34653
+ title: "Search Tools",
34654
+ description: "List tool names or summaries, optionally filtered by keyword.",
34655
+ inputSchema: { query: exports_external.string().optional(), detail: exports_external.boolean().optional() }
34656
+ }, async ({ query, detail }) => {
34657
+ const contracts2 = listMcpToolContracts(query);
34658
+ const tools = detail ? contracts2.map(summarizeMcpToolContract) : contracts2.map((contract) => contract.name);
34659
+ return mcpJson({ schemaVersion: 1, tools, total: tools.length });
34660
+ });
34661
+ server.registerTool("describe_tools", {
34662
+ title: "Describe Tools",
34663
+ description: "Get machine-readable contracts for specific tools by name.",
34664
+ inputSchema: { names: exports_external.array(exports_external.string()) }
34665
+ }, async ({ names }) => {
34666
+ return mcpJson({ schemaVersion: 1, tools: describeMcpToolContracts(names) });
34667
+ });
34668
+ server.registerTool("get_mcp_contracts", {
34669
+ title: "Get MCP Contracts",
34670
+ description: "Return the machine-readable MCP tool and resource contract manifest.",
34671
+ inputSchema: {
34672
+ names: exports_external.array(exports_external.string()).optional(),
34673
+ includeResources: exports_external.boolean().optional()
34015
34674
  }
34016
- if (lstatSync(entryPath).isSymbolicLink()) {
34017
- add(issues, "skill.symlink_forbidden", `Symlink '${entry}' is not allowed in skill packages`);
34675
+ }, async ({ names, includeResources }) => {
34676
+ return mcpJson(createMcpContractManifest({
34677
+ names,
34678
+ includeResources: includeResources ?? false
34679
+ }));
34680
+ });
34681
+ const _agentReg = new Map;
34682
+ 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) => {
34683
+ const existing = [..._agentReg.values()].find((x) => x.name === a.name);
34684
+ if (existing) {
34685
+ existing.last_seen_at = new Date().toISOString();
34686
+ return mcpJson({ ...existing, registered: false });
34018
34687
  }
34019
- if (!KNOWN_TOP_LEVEL_ENTRIES.has(entry)) {
34020
- add(warnings, "skill.file_unrecognized", `Unrecognized top-level skill entry '${entry}'`);
34688
+ const id = Math.random().toString(36).slice(2, 10);
34689
+ const ag = { id, name: a.name, last_seen_at: new Date().toISOString() };
34690
+ _agentReg.set(id, ag);
34691
+ return mcpJson({ ...ag, registered: true });
34692
+ });
34693
+ server.tool("heartbeat", "Update last_seen_at to signal agent is active.", { agent_id: exports_external.string() }, async (a) => {
34694
+ const ag = _agentReg.get(a.agent_id);
34695
+ if (!ag)
34696
+ return mcpError("AGENT_NOT_FOUND", `Agent not found: ${a.agent_id}`);
34697
+ ag.last_seen_at = new Date().toISOString();
34698
+ return mcpJson({ agent_id: a.agent_id, name: ag.name, active: true, last_seen_at: ag.last_seen_at });
34699
+ });
34700
+ 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) => {
34701
+ const ag = _agentReg.get(a.agent_id);
34702
+ if (!ag)
34703
+ return mcpError("AGENT_NOT_FOUND", `Agent not found: ${a.agent_id}`);
34704
+ ag.project_id = a.project_id;
34705
+ return mcpJson({ agent_id: a.agent_id, project_id: a.project_id ?? null });
34706
+ });
34707
+ server.tool("list_agents", "List all registered agents.", {}, async () => {
34708
+ const agents = [..._agentReg.values()];
34709
+ return mcpJson({ agents, total: agents.length }, true);
34710
+ });
34711
+ 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) => {
34712
+ try {
34713
+ const result = saveFeedback({ ...params, version: package_default.version });
34714
+ return mcpJson(result);
34715
+ } catch (e) {
34716
+ return mcpError("FEEDBACK_SAVE_FAILED", String(e));
34021
34717
  }
34718
+ });
34719
+ }
34720
+
34721
+ // src/lib/scheduler.ts
34722
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
34723
+ import { join as join12 } from "path";
34724
+ function getSchedulesPath(targetDir = process.cwd()) {
34725
+ return join12(targetDir, ".skills", "schedules.json");
34726
+ }
34727
+ function loadSchedules(targetDir = process.cwd()) {
34728
+ const path = getSchedulesPath(targetDir);
34729
+ if (existsSync12(path)) {
34730
+ try {
34731
+ return JSON.parse(readFileSync10(path, "utf-8"));
34732
+ } catch {}
34022
34733
  }
34023
- for (const docFile of DOC_FILES) {
34024
- if (existsSync11(join11(skillPath, docFile)))
34025
- metadata.docFiles.push(docFile);
34026
- }
34027
- if (metadata.docFiles.length === 0) {
34028
- add(issues, "skill.docs_missing", "Missing documentation file: expected SKILL.md, README.md, or CLAUDE.md");
34029
- }
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}'`);
34734
+ return { version: 1, schedules: [] };
34735
+ }
34736
+ function saveSchedules(data, targetDir = process.cwd()) {
34737
+ const path = getSchedulesPath(targetDir);
34738
+ const dir = join12(targetDir, ".skills");
34739
+ if (!existsSync12(dir))
34740
+ mkdirSync7(dir, { recursive: true });
34741
+ writeFileSync6(path, JSON.stringify(data, null, 2));
34742
+ }
34743
+ function validateCronField(expr, min, max, label) {
34744
+ for (const part of expr.split(",")) {
34745
+ if (part === "*")
34746
+ continue;
34747
+ let valuePart = part;
34748
+ if (part.includes("/")) {
34749
+ const slashIdx = part.indexOf("/");
34750
+ valuePart = part.slice(0, slashIdx);
34751
+ const stepStr = part.slice(slashIdx + 1);
34752
+ const step = parseInt(stepStr);
34753
+ if (isNaN(step) || step < 1)
34754
+ return { valid: false, error: `Invalid step value in "${part}" in ${label}` };
34755
+ }
34756
+ if (valuePart === "*")
34757
+ continue;
34758
+ if (valuePart.includes("-")) {
34759
+ const rangeParts = valuePart.split("-");
34760
+ if (rangeParts.length !== 2)
34761
+ return { valid: false, error: `Invalid range expression "${valuePart}" in ${label}` };
34762
+ const lo = parseInt(rangeParts[0]);
34763
+ const hi = parseInt(rangeParts[1]);
34764
+ if (isNaN(lo) || isNaN(hi))
34765
+ return { valid: false, error: `Invalid range "${valuePart}" in ${label}` };
34766
+ if (lo < min || hi > max || lo > hi) {
34767
+ return { valid: false, error: `Range ${lo}-${hi} outside valid ${min}-${max} in ${label}` };
34060
34768
  }
34769
+ continue;
34770
+ }
34771
+ const n = parseInt(valuePart);
34772
+ if (isNaN(n))
34773
+ return { valid: false, error: `Invalid value "${valuePart}" in ${label}` };
34774
+ if (n < min || n > max) {
34775
+ return { valid: false, error: `Value ${n} outside valid ${min}-${max} in ${label}` };
34061
34776
  }
34062
- } else {
34063
- add(warnings, "skill.skill_md_missing", "Missing SKILL.md; registry docs may need generated agent-facing instructions");
34064
34777
  }
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
- }
34131
- }
34132
- } catch (error48) {
34133
- add(issues, "package.invalid_json", `package.json is invalid JSON: ${error48.message}`);
34778
+ return { valid: true };
34779
+ }
34780
+ function validateCron(expr) {
34781
+ const fields = expr.trim().split(/\s+/);
34782
+ if (fields.length !== 5) {
34783
+ return { valid: false, error: `Expected 5 fields, got ${fields.length}. Format: "minute hour day-of-month month day-of-week"` };
34784
+ }
34785
+ const [minuteF, hourF, domF, monthF, dowF] = fields;
34786
+ const checks4 = [
34787
+ { expr: minuteF, min: 0, max: 59, label: "minute" },
34788
+ { expr: hourF, min: 0, max: 23, label: "hour" },
34789
+ { expr: domF, min: 1, max: 31, label: "day-of-month" },
34790
+ { expr: monthF, min: 1, max: 12, label: "month" },
34791
+ { expr: dowF, min: 0, max: 6, label: "day-of-week" }
34792
+ ];
34793
+ for (const { expr: f, min, max, label } of checks4) {
34794
+ const result = validateCronField(f, min, max, label);
34795
+ if (!result.valid)
34796
+ return result;
34797
+ }
34798
+ return { valid: true };
34799
+ }
34800
+ function getNextRun(cron, from = new Date) {
34801
+ const { valid } = validateCron(cron);
34802
+ if (!valid)
34803
+ return null;
34804
+ const [minuteF, hourF, domF, monthF, dowF] = cron.trim().split(/\s+/);
34805
+ function parseField(f, min, max) {
34806
+ if (f === "*")
34807
+ return Array.from({ length: max - min + 1 }, (_, i) => i + min);
34808
+ if (f.startsWith("*/")) {
34809
+ const step = parseInt(f.slice(2));
34810
+ if (isNaN(step))
34811
+ return [];
34812
+ const vals = [];
34813
+ for (let i = min;i <= max; i += step)
34814
+ vals.push(i);
34815
+ return vals;
34134
34816
  }
34817
+ return f.split(",").flatMap((part) => {
34818
+ if (part.includes("-")) {
34819
+ const [lo, hi] = part.split("-").map(Number);
34820
+ return Array.from({ length: hi - lo + 1 }, (_, i) => i + lo);
34821
+ }
34822
+ const n = parseInt(part);
34823
+ return isNaN(n) ? [] : [n];
34824
+ });
34135
34825
  }
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");
34826
+ const minutes = parseField(minuteF, 0, 59);
34827
+ const hours = parseField(hourF, 0, 23);
34828
+ const doms = parseField(domF, 1, 31);
34829
+ const months = parseField(monthF, 1, 12);
34830
+ const dows = parseField(dowF, 0, 6);
34831
+ const candidate = new Date(from);
34832
+ candidate.setSeconds(0, 0);
34833
+ candidate.setMinutes(candidate.getMinutes() + 1);
34834
+ const limit = new Date(from);
34835
+ limit.setFullYear(limit.getFullYear() + 1);
34836
+ while (candidate < limit) {
34837
+ const month = candidate.getMonth() + 1;
34838
+ const dom = candidate.getDate();
34839
+ const dow = candidate.getDay();
34840
+ const hour = candidate.getHours();
34841
+ const minute = candidate.getMinutes();
34842
+ if (!months.includes(month)) {
34843
+ candidate.setMonth(candidate.getMonth() + 1, 1);
34844
+ candidate.setHours(0, 0, 0, 0);
34845
+ continue;
34142
34846
  }
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)`);
34847
+ if (!doms.includes(dom) || !dows.includes(dow)) {
34848
+ candidate.setDate(candidate.getDate() + 1);
34849
+ candidate.setHours(0, 0, 0, 0);
34850
+ continue;
34851
+ }
34852
+ if (!hours.includes(hour)) {
34853
+ candidate.setHours(candidate.getHours() + 1, 0, 0, 0);
34854
+ continue;
34855
+ }
34856
+ if (!minutes.includes(minute)) {
34857
+ candidate.setMinutes(candidate.getMinutes() + 1, 0, 0);
34858
+ continue;
34859
+ }
34860
+ return new Date(candidate);
34152
34861
  }
34153
- return {
34154
- name: bareName,
34155
- path: skillPath,
34156
- valid: issues.length === 0,
34157
- issues: sortMessages(issues),
34158
- warnings: sortMessages(warnings),
34159
- metadata
34862
+ return null;
34863
+ }
34864
+ function addSchedule(skill, cron, options = {}) {
34865
+ const { valid, error: error48 } = validateCron(cron);
34866
+ if (!valid)
34867
+ return { schedule: null, error: error48 };
34868
+ const data = loadSchedules(options.targetDir);
34869
+ const id = `${skill}-${Date.now()}`;
34870
+ const now = new Date;
34871
+ const nextRun = getNextRun(cron, now);
34872
+ const schedule = {
34873
+ id,
34874
+ name: options.name || `${skill} (${cron})`,
34875
+ skill,
34876
+ cron,
34877
+ args: options.args,
34878
+ enabled: true,
34879
+ createdAt: now.toISOString(),
34880
+ nextRun: nextRun?.toISOString()
34160
34881
  };
34882
+ data.schedules.push(schedule);
34883
+ saveSchedules(data, options.targetDir);
34884
+ return { schedule };
34885
+ }
34886
+ function listSchedules(targetDir) {
34887
+ return loadSchedules(targetDir).schedules;
34888
+ }
34889
+ function removeSchedule(idOrName, targetDir) {
34890
+ const data = loadSchedules(targetDir);
34891
+ const before = data.schedules.length;
34892
+ data.schedules = data.schedules.filter((s) => s.id !== idOrName && s.name !== idOrName);
34893
+ if (data.schedules.length === before)
34894
+ return false;
34895
+ saveSchedules(data, targetDir);
34896
+ return true;
34161
34897
  }
34162
34898
 
34163
34899
  // src/mcp/schedule-tools.ts
@@ -34223,8 +34959,9 @@ function registerScheduleTools(server) {
34223
34959
  name: exports_external.string()
34224
34960
  }
34225
34961
  }, async ({ name }) => {
34226
- const skillPath = getSkillPath(name);
34227
- const result = validateSkillDirectory(name, skillPath, getSkill(name));
34962
+ const portable = findPortableSkill(name);
34963
+ const skillPath = portable?.path ?? getSkillPath(name);
34964
+ const result = portable ? validatePortableSkillDirectory(portable.name, portable.path) : validateSkillDirectory(name, skillPath, getSkill(name));
34228
34965
  return {
34229
34966
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
34230
34967
  isError: !result.valid
@@ -34232,6 +34969,266 @@ function registerScheduleTools(server) {
34232
34969
  });
34233
34970
  }
34234
34971
 
34972
+ // src/lib/native-storage.ts
34973
+ init_config();
34974
+ import { createHash as createHash2, createHmac } from "crypto";
34975
+ import {
34976
+ existsSync as existsSync13,
34977
+ mkdirSync as mkdirSync8,
34978
+ readFileSync as readFileSync11,
34979
+ readdirSync as readdirSync6,
34980
+ statSync as statSync5,
34981
+ writeFileSync as writeFileSync7
34982
+ } from "fs";
34983
+ import { dirname as dirname5, join as join13, normalize as normalize3, relative as relative3, sep } from "path";
34984
+ var SKILLS_STORAGE_TABLES = [
34985
+ "skills_sync_records",
34986
+ "skills_sync_cursors"
34987
+ ];
34988
+ var SKILLS_NATIVE_STORAGE_ENV = {
34989
+ mode: "HASNA_SKILLS_STORAGE_MODE",
34990
+ databaseUrl: "HASNA_SKILLS_DATABASE_URL",
34991
+ databaseSsl: "HASNA_SKILLS_DATABASE_SSL",
34992
+ databaseSchema: "HASNA_SKILLS_DATABASE_SCHEMA",
34993
+ s3Bucket: "HASNA_SKILLS_S3_BUCKET",
34994
+ s3Prefix: "HASNA_SKILLS_S3_PREFIX",
34995
+ awsRegion: "HASNA_SKILLS_AWS_REGION",
34996
+ s3Endpoint: "HASNA_SKILLS_S3_ENDPOINT",
34997
+ s3ForcePathStyle: "HASNA_SKILLS_S3_FORCE_PATH_STYLE",
34998
+ s3AccessKeyId: "HASNA_SKILLS_S3_ACCESS_KEY_ID",
34999
+ s3SecretAccessKey: "HASNA_SKILLS_S3_SECRET_ACCESS_KEY",
35000
+ s3SessionToken: "HASNA_SKILLS_S3_SESSION_TOKEN",
35001
+ syncBatchSize: "HASNA_SKILLS_SYNC_BATCH_SIZE",
35002
+ dryRun: "HASNA_SKILLS_SYNC_DRY_RUN"
35003
+ };
35004
+ var SKILLS_NATIVE_STORAGE_FALLBACK_ENV = {
35005
+ mode: "SKILLS_STORAGE_MODE",
35006
+ databaseUrl: "SKILLS_DATABASE_URL",
35007
+ databaseSsl: "SKILLS_DATABASE_SSL",
35008
+ databaseSchema: "SKILLS_DATABASE_SCHEMA",
35009
+ s3Bucket: "SKILLS_S3_BUCKET",
35010
+ s3Prefix: "SKILLS_S3_PREFIX",
35011
+ awsRegion: "SKILLS_AWS_REGION",
35012
+ s3Endpoint: "SKILLS_S3_ENDPOINT",
35013
+ s3ForcePathStyle: "SKILLS_S3_FORCE_PATH_STYLE",
35014
+ s3AccessKeyId: "SKILLS_S3_ACCESS_KEY_ID",
35015
+ s3SecretAccessKey: "SKILLS_S3_SECRET_ACCESS_KEY",
35016
+ s3SessionToken: "SKILLS_S3_SESSION_TOKEN",
35017
+ syncBatchSize: "SKILLS_SYNC_BATCH_SIZE",
35018
+ dryRun: "SKILLS_SYNC_DRY_RUN"
35019
+ };
35020
+ function resolveSkillsNativeStorageConfig(env = process.env) {
35021
+ const mode = getSkillsStorageMode(env);
35022
+ return {
35023
+ mode,
35024
+ databaseUrl: getSkillsStorageDatabaseUrl(env),
35025
+ databaseSsl: parseBoolean(readStorageEnv(env, "databaseSsl").value),
35026
+ databaseSchema: readStorageEnv(env, "databaseSchema").value,
35027
+ s3Bucket: readStorageEnv(env, "s3Bucket").value,
35028
+ s3Prefix: readStorageEnv(env, "s3Prefix").value,
35029
+ awsRegion: readStorageEnv(env, "awsRegion").value ?? "us-east-1",
35030
+ s3Endpoint: readStorageEnv(env, "s3Endpoint").value,
35031
+ s3ForcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value) ?? false,
35032
+ syncBatchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value) ?? 500,
35033
+ dryRun: parseBoolean(readStorageEnv(env, "dryRun").value) ?? true
35034
+ };
35035
+ }
35036
+ function resolveStorageConfig(env = process.env) {
35037
+ return resolveSkillsNativeStorageConfig(env);
35038
+ }
35039
+ function getSkillsStorageMode(env = process.env) {
35040
+ return parseMode(readStorageEnv(env, "mode").value);
35041
+ }
35042
+ function getSkillsStorageDatabaseUrl(env = process.env) {
35043
+ return readStorageEnv(env, "databaseUrl").value;
35044
+ }
35045
+ function getSkillsNativeStorageStatus(options = {}) {
35046
+ const env = options.env ?? process.env;
35047
+ const config2 = resolveSkillsNativeStorageConfig(env);
35048
+ const modeEnv = readStorageEnv(env, "mode");
35049
+ const databaseEnv = readStorageEnv(env, "databaseUrl");
35050
+ const s3BucketEnv = readStorageEnv(env, "s3Bucket");
35051
+ const targetDir = options.targetDir ?? process.cwd();
35052
+ return {
35053
+ package: "open-skills",
35054
+ mode: config2.mode,
35055
+ tables: [...SKILLS_STORAGE_TABLES],
35056
+ env: {
35057
+ mode: SKILLS_NATIVE_STORAGE_ENV.mode,
35058
+ databaseUrl: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
35059
+ s3Bucket: SKILLS_NATIVE_STORAGE_ENV.s3Bucket
35060
+ },
35061
+ local: {
35062
+ dataDir: getDataDir(),
35063
+ projectStateDir: getProjectStateDir(targetDir),
35064
+ feedbackDbPath: join13(getDataDir(), "skills.db")
35065
+ },
35066
+ remote: {
35067
+ databaseConfigured: Boolean(config2.databaseUrl),
35068
+ s3Configured: Boolean(config2.s3Bucket),
35069
+ databaseEnv: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
35070
+ s3BucketEnv: SKILLS_NATIVE_STORAGE_ENV.s3Bucket,
35071
+ activeModeEnv: modeEnv.name,
35072
+ activeDatabaseEnv: databaseEnv.name,
35073
+ activeS3BucketEnv: s3BucketEnv.name,
35074
+ region: config2.awsRegion ?? "us-east-1",
35075
+ dryRun: config2.dryRun
35076
+ }
35077
+ };
35078
+ }
35079
+ function getStorageStatus(options = {}) {
35080
+ return getSkillsNativeStorageStatus(options);
35081
+ }
35082
+ function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
35083
+ const projectStateDir = getProjectStateDir(targetDir);
35084
+ const files = [];
35085
+ if (existsSync13(projectStateDir)) {
35086
+ for (const filePath of walkFiles2(projectStateDir)) {
35087
+ const bytes = readFileSync11(filePath);
35088
+ const relativePath = toPosix(relative3(targetDir, filePath));
35089
+ files.push({
35090
+ path: relativePath,
35091
+ sizeBytes: bytes.byteLength,
35092
+ sha256: createHash2("sha256").update(bytes).digest("hex"),
35093
+ ...options.includeFileContents ? { contentBase64: Buffer.from(bytes).toString("base64") } : {}
35094
+ });
35095
+ }
35096
+ }
35097
+ return {
35098
+ schemaVersion: 1,
35099
+ exportedAt: new Date().toISOString(),
35100
+ files: files.sort((a, b) => a.path.localeCompare(b.path))
35101
+ };
35102
+ }
35103
+ var skillsPostgresSyncSchemaSql = `
35104
+ CREATE TABLE IF NOT EXISTS skills_sync_records (
35105
+ scope TEXT NOT NULL,
35106
+ kind TEXT NOT NULL,
35107
+ id TEXT NOT NULL,
35108
+ updated_at TIMESTAMPTZ NOT NULL,
35109
+ deleted_at TIMESTAMPTZ,
35110
+ source TEXT,
35111
+ payload JSONB NOT NULL,
35112
+ PRIMARY KEY (scope, kind, id)
35113
+ );
35114
+
35115
+ CREATE INDEX IF NOT EXISTS skills_sync_records_updated_at_idx
35116
+ ON skills_sync_records (updated_at);
35117
+
35118
+ CREATE TABLE IF NOT EXISTS skills_sync_cursors (
35119
+ scope TEXT NOT NULL,
35120
+ cursor_name TEXT NOT NULL,
35121
+ value TEXT NOT NULL,
35122
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
35123
+ PRIMARY KEY (scope, cursor_name)
35124
+ );
35125
+ `.trim();
35126
+ function planSkillsS3SnapshotUpload(snapshot, options = {}) {
35127
+ const prefix = normalizeS3Prefix(options.prefix);
35128
+ return snapshot.files.map((file2) => ({
35129
+ path: file2.path,
35130
+ key: [prefix, file2.path.replace(/^\.?\//, "")].filter(Boolean).join("/"),
35131
+ sizeBytes: file2.sizeBytes,
35132
+ sha256: file2.sha256
35133
+ }));
35134
+ }
35135
+ function parseMode(value) {
35136
+ const normalized = value?.trim().toLowerCase();
35137
+ if (normalized === "remote" || normalized === "hybrid")
35138
+ return normalized;
35139
+ return "local";
35140
+ }
35141
+ function readStorageEnv(env, key) {
35142
+ const primaryName = SKILLS_NATIVE_STORAGE_ENV[key];
35143
+ const primaryValue = cleanOptional(env[primaryName]);
35144
+ if (primaryValue !== undefined)
35145
+ return { name: primaryName, value: primaryValue };
35146
+ const fallbackName = SKILLS_NATIVE_STORAGE_FALLBACK_ENV[key];
35147
+ const fallbackValue = cleanOptional(env[fallbackName]);
35148
+ if (fallbackValue !== undefined)
35149
+ return { name: fallbackName, value: fallbackValue };
35150
+ return { name: primaryName };
35151
+ }
35152
+ function cleanOptional(value) {
35153
+ const cleaned = value?.trim();
35154
+ return cleaned ? cleaned : undefined;
35155
+ }
35156
+ function parseBoolean(value) {
35157
+ if (value === undefined)
35158
+ return;
35159
+ const normalized = value.trim().toLowerCase();
35160
+ if (["1", "true", "yes", "on"].includes(normalized))
35161
+ return true;
35162
+ if (["0", "false", "no", "off"].includes(normalized))
35163
+ return false;
35164
+ return;
35165
+ }
35166
+ function parsePositiveInteger(value) {
35167
+ if (!value)
35168
+ return;
35169
+ const parsed = Number.parseInt(value, 10);
35170
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
35171
+ }
35172
+ function walkFiles2(dir) {
35173
+ const files = [];
35174
+ for (const entry of readdirSync6(dir)) {
35175
+ const full = join13(dir, entry);
35176
+ const stats = statSync5(full);
35177
+ if (stats.isDirectory())
35178
+ files.push(...walkFiles2(full));
35179
+ else
35180
+ files.push(full);
35181
+ }
35182
+ return files;
35183
+ }
35184
+ function normalizeS3Prefix(prefix) {
35185
+ return (prefix ?? "").trim().replace(/^\/+|\/+$/g, "");
35186
+ }
35187
+ function toPosix(path) {
35188
+ return path.split(/[\\/]+/).join("/");
35189
+ }
35190
+
35191
+ // src/mcp/storage-tools.ts
35192
+ function registerStorageTools(server) {
35193
+ server.registerTool("storage_status", {
35194
+ title: "Storage Status",
35195
+ description: "Show local-first storage paths and optional repo-owned Postgres/S3 readiness.",
35196
+ inputSchema: {
35197
+ directory: exports_external.string().optional()
35198
+ }
35199
+ }, async ({ directory }) => {
35200
+ return mcpJson(getStorageStatus({ targetDir: directory || process.cwd() }), true);
35201
+ });
35202
+ server.registerTool("storage_sync_plan", {
35203
+ title: "Storage Sync Plan",
35204
+ description: "Plan .skills snapshot sync for optional Postgres/S3 storage without network access.",
35205
+ inputSchema: {
35206
+ directory: exports_external.string().optional(),
35207
+ includeSchemaSql: exports_external.boolean().optional()
35208
+ }
35209
+ }, async ({ directory, includeSchemaSql }) => {
35210
+ const targetDir = directory || process.cwd();
35211
+ const config2 = resolveStorageConfig();
35212
+ const snapshot = exportSkillsLocalSnapshot(targetDir, { includeFileContents: false });
35213
+ const s3Plan = config2.s3Bucket ? planSkillsS3SnapshotUpload(snapshot, { prefix: config2.s3Prefix }) : [];
35214
+ return mcpJson({
35215
+ package: "open-skills",
35216
+ noNetwork: true,
35217
+ mode: config2.mode,
35218
+ databaseConfigured: Boolean(config2.databaseUrl),
35219
+ s3Configured: Boolean(config2.s3Bucket),
35220
+ snapshotFileCount: snapshot.files.length,
35221
+ s3ObjectCount: s3Plan.length,
35222
+ env: {
35223
+ mode: SKILLS_NATIVE_STORAGE_ENV.mode,
35224
+ databaseUrl: SKILLS_NATIVE_STORAGE_ENV.databaseUrl,
35225
+ s3Bucket: SKILLS_NATIVE_STORAGE_ENV.s3Bucket
35226
+ },
35227
+ ...includeSchemaSql ? { schemaSql: skillsPostgresSyncSchemaSql } : {}
35228
+ }, true);
35229
+ });
35230
+ }
35231
+
34235
35232
  // src/mcp/server.ts
34236
35233
  function buildServer() {
34237
35234
  const server = new McpServer({
@@ -34241,6 +35238,7 @@ function buildServer() {
34241
35238
  registerDiscoveryTools(server);
34242
35239
  registerOperationTools(server);
34243
35240
  registerScheduleTools(server);
35241
+ registerStorageTools(server);
34244
35242
  registerResourceMetaTools(server);
34245
35243
  return server;
34246
35244
  }