@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/README.md +70 -6
- package/bin/index.js +52303 -10365
- package/bin/mcp.js +2279 -1281
- package/dist/cli/commands/portable-skills.d.ts +2 -0
- package/dist/cli/commands/storage.d.ts +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5268 -3853
- package/dist/lib/cli-mcp-parity.d.ts +14 -0
- package/dist/lib/mcp-contracts.d.ts +1 -1
- package/dist/lib/native-storage.d.ts +251 -0
- package/dist/lib/portable-skills.d.ts +79 -0
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/skill-validation.d.ts +2 -0
- package/dist/mcp/storage-tools.d.ts +2 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +854 -0
- package/docs/skill-standard.md +126 -0
- package/package.json +9 -3
- package/skills/apidocs/.claude/settings.json +5 -0
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 =
|
|
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
|
|
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
|
|
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
|
|
7175
|
-
import { join as
|
|
7176
|
-
import { homedir as
|
|
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 =
|
|
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
|
-
|
|
7196
|
-
|
|
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 =
|
|
7235
|
-
AUTH_FILE =
|
|
7236
|
-
LEGACY_AUTH_FILE =
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
29477
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
29478
|
-
import {
|
|
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/
|
|
29482
|
-
|
|
29483
|
-
|
|
29484
|
-
|
|
29485
|
-
|
|
29486
|
-
|
|
29487
|
-
|
|
29488
|
-
|
|
29489
|
-
|
|
29490
|
-
|
|
29491
|
-
|
|
29492
|
-
|
|
29493
|
-
|
|
29494
|
-
|
|
29495
|
-
|
|
29496
|
-
|
|
29497
|
-
|
|
29498
|
-
|
|
29499
|
-
|
|
29500
|
-
|
|
29501
|
-
|
|
29502
|
-
|
|
29503
|
-
|
|
29504
|
-
|
|
29505
|
-
|
|
29506
|
-
|
|
29507
|
-
|
|
29508
|
-
|
|
29509
|
-
|
|
29510
|
-
|
|
29511
|
-
|
|
29512
|
-
|
|
29513
|
-
|
|
29514
|
-
|
|
29515
|
-
|
|
29516
|
-
|
|
29517
|
-
|
|
29518
|
-
|
|
29519
|
-
|
|
29520
|
-
|
|
29521
|
-
|
|
29522
|
-
|
|
29523
|
-
|
|
29524
|
-
|
|
29525
|
-
|
|
29526
|
-
|
|
29527
|
-
|
|
29528
|
-
|
|
29529
|
-
|
|
29530
|
-
|
|
29531
|
-
|
|
29532
|
-
|
|
29533
|
-
|
|
29534
|
-
|
|
29535
|
-
|
|
29536
|
-
|
|
29537
|
-
|
|
29538
|
-
|
|
29539
|
-
|
|
29540
|
-
|
|
29541
|
-
|
|
29542
|
-
|
|
29543
|
-
|
|
29544
|
-
|
|
29545
|
-
|
|
29546
|
-
|
|
29547
|
-
|
|
29548
|
-
|
|
29549
|
-
|
|
29550
|
-
|
|
29551
|
-
|
|
29552
|
-
|
|
29553
|
-
|
|
29554
|
-
|
|
29555
|
-
|
|
29556
|
-
|
|
29557
|
-
|
|
29558
|
-
|
|
29559
|
-
|
|
29560
|
-
|
|
29561
|
-
|
|
29562
|
-
|
|
29563
|
-
|
|
29564
|
-
|
|
29565
|
-
|
|
29566
|
-
|
|
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 (!
|
|
32347
|
+
if (!existsSync4(dir))
|
|
31450
32348
|
return [];
|
|
31451
32349
|
const result = [];
|
|
31452
32350
|
try {
|
|
31453
|
-
const entries =
|
|
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 =
|
|
31458
|
-
if (!
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
31517
|
-
import { dirname, join as
|
|
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
|
|
31531
|
-
import { join as
|
|
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
|
|
32450
|
+
return join5(targetDir, SKILLS_PROJECT_DIR);
|
|
31537
32451
|
}
|
|
31538
32452
|
function getProjectConfigPath(targetDir = process.cwd()) {
|
|
31539
|
-
return
|
|
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 (!
|
|
32457
|
+
if (!existsSync5(path))
|
|
31544
32458
|
return null;
|
|
31545
32459
|
try {
|
|
31546
|
-
return normalizeProjectConfig(JSON.parse(
|
|
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
|
-
|
|
32481
|
+
mkdirSync3(dir, { recursive: true });
|
|
31568
32482
|
const normalized = normalizeProjectConfig({ ...config2, updatedAt: new Date().toISOString() });
|
|
31569
|
-
|
|
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 =
|
|
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 =
|
|
31646
|
-
if (
|
|
32559
|
+
const candidate = join6(dir, "skills");
|
|
32560
|
+
if (existsSync6(candidate) && !dir.includes(".skills"))
|
|
31647
32561
|
return candidate;
|
|
31648
|
-
dir =
|
|
32562
|
+
dir = dirname3(dir);
|
|
31649
32563
|
}
|
|
31650
|
-
return
|
|
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
|
-
|
|
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 (!
|
|
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" ?
|
|
32633
|
+
return scope === "project" ? join6(base, ".pi", "skills") : join6(homedir2(), ".pi", "agent", "skills");
|
|
31714
32634
|
case "opencode":
|
|
31715
|
-
return scope === "project" ?
|
|
32635
|
+
return scope === "project" ? join6(base, ".opencode", "skills") : join6(homedir2(), ".config", "opencode", "skills");
|
|
31716
32636
|
default:
|
|
31717
|
-
return scope === "project" ?
|
|
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 =
|
|
31733
|
-
if (!
|
|
32652
|
+
const pkgPath = join6(getSkillPath(name), "package.json");
|
|
32653
|
+
if (!existsSync6(pkgPath))
|
|
31734
32654
|
return "unknown";
|
|
31735
32655
|
try {
|
|
31736
|
-
const pkg = JSON.parse(
|
|
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
|
|
31745
|
-
import { join as
|
|
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 (!
|
|
32685
|
+
if (!existsSync7(skillPath))
|
|
31766
32686
|
return null;
|
|
31767
32687
|
return {
|
|
31768
|
-
skillMd: readIfExists(
|
|
31769
|
-
readme: readIfExists(
|
|
31770
|
-
claudeMd: readIfExists(
|
|
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 (!
|
|
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(
|
|
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 =
|
|
31825
|
-
if (
|
|
32744
|
+
const pkgPath = join7(skillPath, "package.json");
|
|
32745
|
+
if (existsSync7(pkgPath)) {
|
|
31826
32746
|
try {
|
|
31827
|
-
const pkg = JSON.parse(
|
|
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 (!
|
|
32764
|
+
if (!existsSync7(skillPath)) {
|
|
31845
32765
|
return { exitCode: 1, error: `Skill '${name}' not found` };
|
|
31846
32766
|
}
|
|
31847
|
-
const pkgPath =
|
|
31848
|
-
if (!
|
|
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(
|
|
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 =
|
|
31868
|
-
if (!
|
|
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 =
|
|
31872
|
-
if (!
|
|
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 =
|
|
31900
|
-
if (!
|
|
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(
|
|
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 (
|
|
32001
|
-
return
|
|
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
|
|
32933
|
-
import { join as
|
|
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
|
|
32938
|
-
import { extname, join as
|
|
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 =
|
|
32946
|
-
const logsDir =
|
|
32947
|
-
const exportDir =
|
|
32948
|
-
|
|
32949
|
-
|
|
32950
|
-
|
|
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
|
-
|
|
32998
|
-
|
|
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 =
|
|
33004
|
-
const previous =
|
|
33005
|
-
|
|
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 =
|
|
33009
|
-
if (!
|
|
34010
|
+
const runsRoot = join8(getProjectStateDir(targetDir), "runs");
|
|
34011
|
+
if (!existsSync8(runsRoot))
|
|
33010
34012
|
return null;
|
|
33011
|
-
for (const day of
|
|
33012
|
-
const record3 = readRunRecord(
|
|
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
|
-
|
|
34021
|
+
writeFileSync4(join8(context.runDir, "run.json"), JSON.stringify(context.record, null, 2) + `
|
|
33020
34022
|
`);
|
|
33021
34023
|
}
|
|
33022
34024
|
function writeArtifactsManifest(context, artifacts) {
|
|
33023
|
-
|
|
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 (!
|
|
34029
|
+
if (!existsSync8(context.exportDir))
|
|
33028
34030
|
return [];
|
|
33029
34031
|
const artifacts = [];
|
|
33030
34032
|
for (const path of walkFiles(context.exportDir)) {
|
|
33031
|
-
const stat =
|
|
33032
|
-
const bytes =
|
|
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 =
|
|
33044
|
-
if (!
|
|
34045
|
+
const path = join8(runDir, "run.json");
|
|
34046
|
+
if (!existsSync8(path))
|
|
33045
34047
|
return null;
|
|
33046
34048
|
try {
|
|
33047
|
-
return JSON.parse(
|
|
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
|
|
33055
|
-
const full =
|
|
33056
|
-
if (
|
|
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 =
|
|
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("
|
|
33104
|
-
title: "
|
|
33105
|
-
description: "
|
|
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
|
-
|
|
33249
|
-
|
|
33250
|
-
|
|
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("
|
|
33255
|
-
title: "
|
|
33256
|
-
description: "
|
|
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
|
-
|
|
33259
|
-
|
|
33260
|
-
|
|
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
|
-
|
|
33268
|
-
|
|
33269
|
-
|
|
33270
|
-
|
|
33271
|
-
|
|
33272
|
-
|
|
33273
|
-
|
|
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("
|
|
33282
|
-
title: "
|
|
33283
|
-
description: "
|
|
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
|
-
|
|
33287
|
-
|
|
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
|
-
|
|
34153
|
+
}, async ({ name, for: agentArg, scope }) => {
|
|
34154
|
+
if (agentArg) {
|
|
34155
|
+
let agents;
|
|
33340
34156
|
try {
|
|
33341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33385
|
-
|
|
33386
|
-
|
|
33387
|
-
|
|
33388
|
-
|
|
33389
|
-
|
|
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(
|
|
33392
|
-
isError:
|
|
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(
|
|
34177
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
34178
|
+
isError: !result.success
|
|
33397
34179
|
};
|
|
33398
34180
|
});
|
|
33399
|
-
server.registerTool("
|
|
33400
|
-
title: "
|
|
33401
|
-
description: "
|
|
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
|
-
|
|
33404
|
-
|
|
33405
|
-
|
|
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
|
-
|
|
33417
|
-
|
|
33418
|
-
|
|
33419
|
-
|
|
33420
|
-
|
|
33421
|
-
|
|
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
|
-
|
|
33434
|
-
|
|
33435
|
-
|
|
33436
|
-
|
|
33437
|
-
|
|
33438
|
-
|
|
33439
|
-
|
|
33440
|
-
|
|
33441
|
-
|
|
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("
|
|
33446
|
-
title: "
|
|
33447
|
-
description: "
|
|
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
|
-
|
|
34231
|
+
name: exports_external.string(),
|
|
33450
34232
|
for: exports_external.string().optional(),
|
|
33451
34233
|
scope: exports_external.string().optional()
|
|
33452
34234
|
}
|
|
33453
|
-
}, async ({
|
|
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
|
-
|
|
33466
|
-
|
|
33467
|
-
|
|
33468
|
-
|
|
33469
|
-
|
|
33470
|
-
|
|
33471
|
-
|
|
33472
|
-
|
|
33473
|
-
|
|
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
|
|
33479
|
-
|
|
34253
|
+
const removed = removeSkill(name);
|
|
34254
|
+
if (removed)
|
|
34255
|
+
cacheClear();
|
|
33480
34256
|
return {
|
|
33481
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
33482
|
-
isError: hasErrors
|
|
34257
|
+
content: [{ type: "text", text: JSON.stringify({ skill: name, removed }, null, 2) }]
|
|
33483
34258
|
};
|
|
33484
34259
|
});
|
|
33485
|
-
server.registerTool("
|
|
33486
|
-
title: "
|
|
33487
|
-
description: "
|
|
34260
|
+
server.registerTool("list_categories", {
|
|
34261
|
+
title: "List Categories",
|
|
34262
|
+
description: "List all 17 skill categories with skill counts."
|
|
33488
34263
|
}, async () => {
|
|
33489
|
-
const
|
|
33490
|
-
|
|
33491
|
-
|
|
33492
|
-
|
|
33493
|
-
|
|
33494
|
-
|
|
33495
|
-
|
|
33496
|
-
|
|
33497
|
-
|
|
33498
|
-
|
|
33499
|
-
|
|
33500
|
-
|
|
33501
|
-
|
|
33502
|
-
|
|
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
|
|
33508
|
-
|
|
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("
|
|
33627
|
-
title: "Get
|
|
33628
|
-
description: "
|
|
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
|
-
|
|
33631
|
-
includeResources: exports_external.boolean().optional()
|
|
34287
|
+
name: exports_external.string()
|
|
33632
34288
|
}
|
|
33633
|
-
}, async ({
|
|
33634
|
-
|
|
33635
|
-
|
|
33636
|
-
|
|
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
|
-
|
|
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.
|
|
33670
|
-
|
|
33671
|
-
|
|
33672
|
-
|
|
33673
|
-
|
|
33674
|
-
|
|
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
|
-
|
|
33680
|
-
|
|
33681
|
-
|
|
33682
|
-
|
|
33683
|
-
|
|
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
|
-
|
|
33715
|
-
|
|
33716
|
-
if (
|
|
33717
|
-
|
|
33718
|
-
|
|
33719
|
-
|
|
33720
|
-
|
|
33721
|
-
|
|
33722
|
-
|
|
33723
|
-
|
|
33724
|
-
|
|
33725
|
-
|
|
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
|
|
33730
|
-
|
|
33731
|
-
|
|
33732
|
-
|
|
33733
|
-
|
|
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
|
-
|
|
33737
|
-
}
|
|
33738
|
-
|
|
33739
|
-
|
|
33740
|
-
|
|
33741
|
-
|
|
33742
|
-
|
|
33743
|
-
|
|
33744
|
-
|
|
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
|
-
|
|
33776
|
-
|
|
33777
|
-
const
|
|
33778
|
-
|
|
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
|
-
|
|
33785
|
-
|
|
33786
|
-
|
|
33787
|
-
|
|
33788
|
-
|
|
33789
|
-
|
|
33790
|
-
|
|
33791
|
-
|
|
33792
|
-
|
|
33793
|
-
|
|
33794
|
-
|
|
33795
|
-
|
|
33796
|
-
|
|
33797
|
-
|
|
33798
|
-
|
|
33799
|
-
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
33803
|
-
|
|
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
|
-
|
|
33806
|
-
|
|
33807
|
-
|
|
33808
|
-
|
|
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
|
-
|
|
33811
|
-
|
|
33812
|
-
|
|
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
|
-
|
|
33815
|
-
|
|
33816
|
-
|
|
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
|
-
|
|
33819
|
-
|
|
33820
|
-
|
|
33821
|
-
}
|
|
33822
|
-
|
|
33823
|
-
|
|
33824
|
-
|
|
33825
|
-
|
|
33826
|
-
|
|
33827
|
-
|
|
33828
|
-
|
|
33829
|
-
|
|
33830
|
-
|
|
33831
|
-
|
|
33832
|
-
|
|
33833
|
-
|
|
33834
|
-
|
|
33835
|
-
|
|
33836
|
-
|
|
33837
|
-
|
|
33838
|
-
|
|
33839
|
-
|
|
33840
|
-
|
|
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
|
|
33967
|
-
|
|
33968
|
-
|
|
33969
|
-
|
|
33970
|
-
|
|
33971
|
-
|
|
33972
|
-
|
|
33973
|
-
|
|
33974
|
-
|
|
33975
|
-
|
|
33976
|
-
|
|
33977
|
-
|
|
33978
|
-
|
|
33979
|
-
|
|
33980
|
-
|
|
33981
|
-
|
|
33982
|
-
|
|
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
|
|
34605
|
+
return { saved: true, category, path: getFeedbackDbPath() };
|
|
33986
34606
|
}
|
|
33987
|
-
|
|
33988
|
-
|
|
33989
|
-
|
|
33990
|
-
|
|
33991
|
-
|
|
33992
|
-
|
|
33993
|
-
|
|
33994
|
-
|
|
33995
|
-
|
|
33996
|
-
|
|
33997
|
-
|
|
33998
|
-
|
|
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
|
-
|
|
34001
|
-
|
|
34002
|
-
|
|
34003
|
-
|
|
34004
|
-
|
|
34005
|
-
|
|
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
|
-
|
|
34009
|
-
|
|
34010
|
-
|
|
34011
|
-
|
|
34012
|
-
|
|
34013
|
-
|
|
34014
|
-
|
|
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
|
-
|
|
34017
|
-
|
|
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
|
-
|
|
34020
|
-
|
|
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
|
-
|
|
34024
|
-
|
|
34025
|
-
|
|
34026
|
-
|
|
34027
|
-
|
|
34028
|
-
|
|
34029
|
-
|
|
34030
|
-
|
|
34031
|
-
|
|
34032
|
-
|
|
34033
|
-
|
|
34034
|
-
|
|
34035
|
-
|
|
34036
|
-
|
|
34037
|
-
|
|
34038
|
-
|
|
34039
|
-
|
|
34040
|
-
|
|
34041
|
-
|
|
34042
|
-
if (
|
|
34043
|
-
|
|
34044
|
-
|
|
34045
|
-
|
|
34046
|
-
|
|
34047
|
-
|
|
34048
|
-
|
|
34049
|
-
if (
|
|
34050
|
-
|
|
34051
|
-
|
|
34052
|
-
|
|
34053
|
-
|
|
34054
|
-
|
|
34055
|
-
if (
|
|
34056
|
-
|
|
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
|
-
|
|
34066
|
-
|
|
34067
|
-
|
|
34068
|
-
|
|
34069
|
-
|
|
34070
|
-
|
|
34071
|
-
|
|
34072
|
-
|
|
34073
|
-
|
|
34074
|
-
|
|
34075
|
-
|
|
34076
|
-
|
|
34077
|
-
|
|
34078
|
-
|
|
34079
|
-
|
|
34080
|
-
|
|
34081
|
-
|
|
34082
|
-
|
|
34083
|
-
|
|
34084
|
-
|
|
34085
|
-
|
|
34086
|
-
|
|
34087
|
-
|
|
34088
|
-
|
|
34089
|
-
|
|
34090
|
-
|
|
34091
|
-
|
|
34092
|
-
|
|
34093
|
-
|
|
34094
|
-
|
|
34095
|
-
|
|
34096
|
-
|
|
34097
|
-
|
|
34098
|
-
|
|
34099
|
-
|
|
34100
|
-
|
|
34101
|
-
|
|
34102
|
-
|
|
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
|
|
34137
|
-
|
|
34138
|
-
const
|
|
34139
|
-
|
|
34140
|
-
|
|
34141
|
-
|
|
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
|
-
|
|
34144
|
-
|
|
34145
|
-
|
|
34146
|
-
|
|
34147
|
-
|
|
34148
|
-
|
|
34149
|
-
|
|
34150
|
-
|
|
34151
|
-
|
|
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
|
-
|
|
34155
|
-
|
|
34156
|
-
|
|
34157
|
-
|
|
34158
|
-
|
|
34159
|
-
|
|
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
|
|
34227
|
-
const
|
|
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
|
}
|