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