@skilljack/mcp 0.6.0 → 0.7.1
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 +7 -396
- package/dist/github-config.d.ts +83 -0
- package/dist/github-config.js +191 -0
- package/dist/github-polling.d.ts +35 -0
- package/dist/github-polling.js +108 -0
- package/dist/github-sync.d.ts +49 -0
- package/dist/github-sync.js +259 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +309 -53
- package/dist/skill-config-tool.d.ts +22 -0
- package/dist/skill-config-tool.js +495 -0
- package/dist/skill-config.d.ts +163 -0
- package/dist/skill-config.js +450 -0
- package/dist/skill-discovery.d.ts +52 -1
- package/dist/skill-discovery.js +70 -1
- package/dist/skill-display-tool.d.ts +22 -0
- package/dist/skill-display-tool.js +302 -0
- package/dist/skill-prompts.d.ts +4 -0
- package/dist/skill-prompts.js +62 -10
- package/dist/skill-resources.d.ts +6 -3
- package/dist/skill-resources.js +10 -94
- package/dist/skill-tool.js +4 -3
- package/dist/subscriptions.d.ts +1 -1
- package/dist/subscriptions.js +1 -1
- package/dist/ui/mcp-app.d.ts +1 -0
- package/dist/ui/mcp-app.html +278 -0
- package/dist/ui/mcp-app.js +484 -0
- package/dist/ui/skill-display.d.ts +1 -0
- package/dist/ui/skill-display.html +188 -0
- package/dist/ui/skill-display.js +269 -0
- package/package.json +2 -1
- package/skills/skilljack-docs/SKILL.md +431 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for skill directories.
|
|
3
|
+
*
|
|
4
|
+
* Handles loading/saving skill directory configuration from:
|
|
5
|
+
* 1. CLI args (highest priority)
|
|
6
|
+
* 2. SKILLS_DIR environment variable
|
|
7
|
+
* 3. Config file (~/.skilljack/config.json)
|
|
8
|
+
*
|
|
9
|
+
* Supports both local directories and GitHub repository URLs.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import { isGitHubUrl, parseGitHubUrl, isRepoAllowed, getGitHubConfig } from "./github-config.js";
|
|
15
|
+
/**
|
|
16
|
+
* Check if a path is valid (exists for local, always valid for GitHub).
|
|
17
|
+
*/
|
|
18
|
+
function isValidPath(p) {
|
|
19
|
+
if (isGitHubUrl(p)) {
|
|
20
|
+
return true; // GitHub URLs are validated during sync
|
|
21
|
+
}
|
|
22
|
+
return fs.existsSync(p);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the source type for a path.
|
|
26
|
+
*/
|
|
27
|
+
function getSourceType(p) {
|
|
28
|
+
return isGitHubUrl(p) ? "github" : "local";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check if a path is allowed (local paths are always allowed,
|
|
32
|
+
* GitHub URLs must have their org/user in the allowlist).
|
|
33
|
+
*/
|
|
34
|
+
function isPathAllowed(p) {
|
|
35
|
+
if (!isGitHubUrl(p)) {
|
|
36
|
+
return true; // Local paths are always allowed
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const spec = parseGitHubUrl(p);
|
|
40
|
+
const config = getGitHubConfig();
|
|
41
|
+
return isRepoAllowed(spec, config);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false; // Invalid GitHub URL
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Separator for multiple paths in SKILLS_DIR environment variable.
|
|
49
|
+
*/
|
|
50
|
+
const PATH_LIST_SEPARATOR = ",";
|
|
51
|
+
/**
|
|
52
|
+
* Get the platform-appropriate config directory path.
|
|
53
|
+
* Returns ~/.skilljack on Unix, %USERPROFILE%\.skilljack on Windows.
|
|
54
|
+
*/
|
|
55
|
+
export function getConfigDir() {
|
|
56
|
+
return path.join(os.homedir(), ".skilljack");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get the full path to the config file.
|
|
60
|
+
*/
|
|
61
|
+
export function getConfigPath() {
|
|
62
|
+
return path.join(getConfigDir(), "config.json");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Ensure the config directory exists.
|
|
66
|
+
*/
|
|
67
|
+
function ensureConfigDir() {
|
|
68
|
+
const configDir = getConfigDir();
|
|
69
|
+
if (!fs.existsSync(configDir)) {
|
|
70
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Load config from the config file.
|
|
75
|
+
* Returns empty config if file doesn't exist.
|
|
76
|
+
*/
|
|
77
|
+
export function loadConfigFile() {
|
|
78
|
+
const configPath = getConfigPath();
|
|
79
|
+
if (!fs.existsSync(configPath)) {
|
|
80
|
+
return { skillDirectories: [], skillInvocationOverrides: {} };
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
84
|
+
const parsed = JSON.parse(content);
|
|
85
|
+
// Validate and normalize directories (handle both local paths and GitHub URLs)
|
|
86
|
+
const skillDirectories = Array.isArray(parsed.skillDirectories)
|
|
87
|
+
? parsed.skillDirectories
|
|
88
|
+
.filter((p) => typeof p === "string")
|
|
89
|
+
.map((p) => isGitHubUrl(p) ? p : path.resolve(p))
|
|
90
|
+
: [];
|
|
91
|
+
// Validate and normalize invocation overrides
|
|
92
|
+
const skillInvocationOverrides = {};
|
|
93
|
+
if (parsed.skillInvocationOverrides && typeof parsed.skillInvocationOverrides === "object") {
|
|
94
|
+
for (const [name, override] of Object.entries(parsed.skillInvocationOverrides)) {
|
|
95
|
+
if (typeof override === "object" && override !== null) {
|
|
96
|
+
const validOverride = {};
|
|
97
|
+
const o = override;
|
|
98
|
+
if (typeof o.assistant === "boolean")
|
|
99
|
+
validOverride.assistant = o.assistant;
|
|
100
|
+
if (typeof o.user === "boolean")
|
|
101
|
+
validOverride.user = o.user;
|
|
102
|
+
if (Object.keys(validOverride).length > 0) {
|
|
103
|
+
skillInvocationOverrides[name] = validOverride;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
skillDirectories,
|
|
110
|
+
skillInvocationOverrides,
|
|
111
|
+
staticMode: parsed.staticMode === true,
|
|
112
|
+
githubAllowedOrgs: Array.isArray(parsed.githubAllowedOrgs) ? parsed.githubAllowedOrgs : [],
|
|
113
|
+
githubAllowedUsers: Array.isArray(parsed.githubAllowedUsers) ? parsed.githubAllowedUsers : [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error(`Warning: Failed to parse config file: ${error}`);
|
|
118
|
+
return { skillDirectories: [], skillInvocationOverrides: {} };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Save config to the config file.
|
|
123
|
+
*/
|
|
124
|
+
export function saveConfigFile(config) {
|
|
125
|
+
ensureConfigDir();
|
|
126
|
+
const configPath = getConfigPath();
|
|
127
|
+
try {
|
|
128
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw new Error(`Failed to save config file: ${error}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Parse CLI arguments for skill directories.
|
|
136
|
+
* Returns resolved absolute paths for local dirs, unchanged for GitHub URLs.
|
|
137
|
+
*/
|
|
138
|
+
export function parseCLIArgs() {
|
|
139
|
+
const dirs = [];
|
|
140
|
+
const args = process.argv.slice(2);
|
|
141
|
+
for (const arg of args) {
|
|
142
|
+
if (!arg.startsWith("-")) {
|
|
143
|
+
const paths = arg
|
|
144
|
+
.split(PATH_LIST_SEPARATOR)
|
|
145
|
+
.map((p) => p.trim())
|
|
146
|
+
.filter((p) => p.length > 0)
|
|
147
|
+
.map((p) => isGitHubUrl(p) ? p : path.resolve(p));
|
|
148
|
+
dirs.push(...paths);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return [...new Set(dirs)]; // Deduplicate
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Parse SKILLS_DIR environment variable.
|
|
155
|
+
* Returns resolved absolute paths for local dirs, unchanged for GitHub URLs.
|
|
156
|
+
*/
|
|
157
|
+
export function parseEnvVar() {
|
|
158
|
+
const envDir = process.env.SKILLS_DIR;
|
|
159
|
+
if (!envDir) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const dirs = envDir
|
|
163
|
+
.split(PATH_LIST_SEPARATOR)
|
|
164
|
+
.map((p) => p.trim())
|
|
165
|
+
.filter((p) => p.length > 0)
|
|
166
|
+
.map((p) => isGitHubUrl(p) ? p : path.resolve(p));
|
|
167
|
+
return [...new Set(dirs)]; // Deduplicate
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get all skill directories with their source information.
|
|
171
|
+
* Priority: CLI args > env var > config file
|
|
172
|
+
*/
|
|
173
|
+
export function getConfigState() {
|
|
174
|
+
// Check CLI args first
|
|
175
|
+
const cliDirs = parseCLIArgs();
|
|
176
|
+
if (cliDirs.length > 0) {
|
|
177
|
+
return {
|
|
178
|
+
directories: cliDirs.map((p) => ({
|
|
179
|
+
path: p,
|
|
180
|
+
source: "cli",
|
|
181
|
+
type: getSourceType(p),
|
|
182
|
+
skillCount: 0, // Will be filled in by caller
|
|
183
|
+
valid: isValidPath(p),
|
|
184
|
+
allowed: isPathAllowed(p),
|
|
185
|
+
})),
|
|
186
|
+
activeSource: "cli",
|
|
187
|
+
isOverridden: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Check env var next
|
|
191
|
+
const envDirs = parseEnvVar();
|
|
192
|
+
if (envDirs.length > 0) {
|
|
193
|
+
return {
|
|
194
|
+
directories: envDirs.map((p) => ({
|
|
195
|
+
path: p,
|
|
196
|
+
source: "env",
|
|
197
|
+
type: getSourceType(p),
|
|
198
|
+
skillCount: 0,
|
|
199
|
+
valid: isValidPath(p),
|
|
200
|
+
allowed: isPathAllowed(p),
|
|
201
|
+
})),
|
|
202
|
+
activeSource: "env",
|
|
203
|
+
isOverridden: true,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Fall back to config file
|
|
207
|
+
const config = loadConfigFile();
|
|
208
|
+
return {
|
|
209
|
+
directories: config.skillDirectories.map((p) => ({
|
|
210
|
+
path: p,
|
|
211
|
+
source: "config",
|
|
212
|
+
type: getSourceType(p),
|
|
213
|
+
skillCount: 0,
|
|
214
|
+
valid: isValidPath(p),
|
|
215
|
+
allowed: isPathAllowed(p),
|
|
216
|
+
})),
|
|
217
|
+
activeSource: "config",
|
|
218
|
+
isOverridden: false,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get skill directories from all sources combined.
|
|
223
|
+
* Used for the UI to show all configured directories.
|
|
224
|
+
*/
|
|
225
|
+
export function getAllDirectoriesWithSources() {
|
|
226
|
+
const all = [];
|
|
227
|
+
const seen = new Set();
|
|
228
|
+
// CLI dirs
|
|
229
|
+
for (const p of parseCLIArgs()) {
|
|
230
|
+
if (!seen.has(p)) {
|
|
231
|
+
seen.add(p);
|
|
232
|
+
all.push({
|
|
233
|
+
path: p,
|
|
234
|
+
source: "cli",
|
|
235
|
+
type: getSourceType(p),
|
|
236
|
+
skillCount: 0,
|
|
237
|
+
valid: isValidPath(p),
|
|
238
|
+
allowed: isPathAllowed(p),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Env dirs
|
|
243
|
+
for (const p of parseEnvVar()) {
|
|
244
|
+
if (!seen.has(p)) {
|
|
245
|
+
seen.add(p);
|
|
246
|
+
all.push({
|
|
247
|
+
path: p,
|
|
248
|
+
source: "env",
|
|
249
|
+
type: getSourceType(p),
|
|
250
|
+
skillCount: 0,
|
|
251
|
+
valid: isValidPath(p),
|
|
252
|
+
allowed: isPathAllowed(p),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Config file dirs
|
|
257
|
+
const config = loadConfigFile();
|
|
258
|
+
for (const p of config.skillDirectories) {
|
|
259
|
+
if (!seen.has(p)) {
|
|
260
|
+
seen.add(p);
|
|
261
|
+
all.push({
|
|
262
|
+
path: p,
|
|
263
|
+
source: "config",
|
|
264
|
+
type: getSourceType(p),
|
|
265
|
+
skillCount: 0,
|
|
266
|
+
valid: isValidPath(p),
|
|
267
|
+
allowed: isPathAllowed(p),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return all;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Add a directory or GitHub URL to the config file.
|
|
275
|
+
* Does not affect CLI or env var configurations.
|
|
276
|
+
*/
|
|
277
|
+
export function addDirectoryToConfig(directory) {
|
|
278
|
+
// For GitHub URLs, store as-is; for local paths, resolve to absolute
|
|
279
|
+
const normalized = isGitHubUrl(directory) ? directory : path.resolve(directory);
|
|
280
|
+
// Validate local directories exist
|
|
281
|
+
if (!isGitHubUrl(directory)) {
|
|
282
|
+
if (!fs.existsSync(normalized)) {
|
|
283
|
+
throw new Error(`Directory does not exist: ${normalized}`);
|
|
284
|
+
}
|
|
285
|
+
if (!fs.statSync(normalized).isDirectory()) {
|
|
286
|
+
throw new Error(`Path is not a directory: ${normalized}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const config = loadConfigFile();
|
|
290
|
+
// Check for duplicate
|
|
291
|
+
if (config.skillDirectories.includes(normalized)) {
|
|
292
|
+
throw new Error(`Already configured: ${normalized}`);
|
|
293
|
+
}
|
|
294
|
+
config.skillDirectories.push(normalized);
|
|
295
|
+
saveConfigFile(config);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Remove a directory or GitHub URL from the config file.
|
|
299
|
+
* Only removes from config file, not CLI or env var.
|
|
300
|
+
*/
|
|
301
|
+
export function removeDirectoryFromConfig(directory) {
|
|
302
|
+
// For GitHub URLs, use as-is; for local paths, resolve to absolute
|
|
303
|
+
const normalized = isGitHubUrl(directory) ? directory : path.resolve(directory);
|
|
304
|
+
const config = loadConfigFile();
|
|
305
|
+
const index = config.skillDirectories.indexOf(normalized);
|
|
306
|
+
if (index === -1) {
|
|
307
|
+
throw new Error(`Not found in config: ${normalized}`);
|
|
308
|
+
}
|
|
309
|
+
config.skillDirectories.splice(index, 1);
|
|
310
|
+
saveConfigFile(config);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get only the active skill directories (respecting priority).
|
|
314
|
+
* This is what the server should use for skill discovery.
|
|
315
|
+
*/
|
|
316
|
+
export function getActiveDirectories() {
|
|
317
|
+
const state = getConfigState();
|
|
318
|
+
return state.directories.map((d) => d.path);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get static mode setting from config file.
|
|
322
|
+
*/
|
|
323
|
+
export function getStaticModeFromConfig() {
|
|
324
|
+
const config = loadConfigFile();
|
|
325
|
+
return config.staticMode === true;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Set static mode setting in config file.
|
|
329
|
+
*/
|
|
330
|
+
export function setStaticModeInConfig(enabled) {
|
|
331
|
+
const config = loadConfigFile();
|
|
332
|
+
config.staticMode = enabled;
|
|
333
|
+
saveConfigFile(config);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get all skill invocation overrides from the config file.
|
|
337
|
+
*/
|
|
338
|
+
export function getSkillInvocationOverrides() {
|
|
339
|
+
const config = loadConfigFile();
|
|
340
|
+
return config.skillInvocationOverrides || {};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Set an invocation override for a skill.
|
|
344
|
+
* @param skillName - The name of the skill
|
|
345
|
+
* @param setting - Which setting to override ("assistant" or "user")
|
|
346
|
+
* @param value - The new value for the setting
|
|
347
|
+
*/
|
|
348
|
+
export function setSkillInvocationOverride(skillName, setting, value) {
|
|
349
|
+
const config = loadConfigFile();
|
|
350
|
+
if (!config.skillInvocationOverrides) {
|
|
351
|
+
config.skillInvocationOverrides = {};
|
|
352
|
+
}
|
|
353
|
+
if (!config.skillInvocationOverrides[skillName]) {
|
|
354
|
+
config.skillInvocationOverrides[skillName] = {};
|
|
355
|
+
}
|
|
356
|
+
config.skillInvocationOverrides[skillName][setting] = value;
|
|
357
|
+
saveConfigFile(config);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Clear an invocation override for a skill (revert to frontmatter default).
|
|
361
|
+
* @param skillName - The name of the skill
|
|
362
|
+
* @param setting - Which setting to clear (omit to clear both)
|
|
363
|
+
*/
|
|
364
|
+
export function clearSkillInvocationOverride(skillName, setting) {
|
|
365
|
+
const config = loadConfigFile();
|
|
366
|
+
if (!config.skillInvocationOverrides || !config.skillInvocationOverrides[skillName]) {
|
|
367
|
+
return; // Nothing to clear
|
|
368
|
+
}
|
|
369
|
+
if (setting) {
|
|
370
|
+
delete config.skillInvocationOverrides[skillName][setting];
|
|
371
|
+
// Clean up empty override objects
|
|
372
|
+
if (Object.keys(config.skillInvocationOverrides[skillName]).length === 0) {
|
|
373
|
+
delete config.skillInvocationOverrides[skillName];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
delete config.skillInvocationOverrides[skillName];
|
|
378
|
+
}
|
|
379
|
+
saveConfigFile(config);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Get the GitHub allowed orgs from config file.
|
|
383
|
+
*/
|
|
384
|
+
export function getGitHubAllowedOrgs() {
|
|
385
|
+
const config = loadConfigFile();
|
|
386
|
+
return config.githubAllowedOrgs || [];
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get the GitHub allowed users from config file.
|
|
390
|
+
*/
|
|
391
|
+
export function getGitHubAllowedUsers() {
|
|
392
|
+
const config = loadConfigFile();
|
|
393
|
+
return config.githubAllowedUsers || [];
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Add a GitHub org to the allowed list.
|
|
397
|
+
* @param org - The org name to allow
|
|
398
|
+
*/
|
|
399
|
+
export function addGitHubAllowedOrg(org) {
|
|
400
|
+
const config = loadConfigFile();
|
|
401
|
+
if (!config.githubAllowedOrgs) {
|
|
402
|
+
config.githubAllowedOrgs = [];
|
|
403
|
+
}
|
|
404
|
+
const normalized = org.toLowerCase().trim();
|
|
405
|
+
if (!config.githubAllowedOrgs.some((o) => o.toLowerCase() === normalized)) {
|
|
406
|
+
config.githubAllowedOrgs.push(org.trim());
|
|
407
|
+
saveConfigFile(config);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Remove a GitHub org from the allowed list.
|
|
412
|
+
* @param org - The org name to remove
|
|
413
|
+
*/
|
|
414
|
+
export function removeGitHubAllowedOrg(org) {
|
|
415
|
+
const config = loadConfigFile();
|
|
416
|
+
if (!config.githubAllowedOrgs) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const normalized = org.toLowerCase().trim();
|
|
420
|
+
config.githubAllowedOrgs = config.githubAllowedOrgs.filter((o) => o.toLowerCase() !== normalized);
|
|
421
|
+
saveConfigFile(config);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Add a GitHub user to the allowed list.
|
|
425
|
+
* @param user - The user name to allow
|
|
426
|
+
*/
|
|
427
|
+
export function addGitHubAllowedUser(user) {
|
|
428
|
+
const config = loadConfigFile();
|
|
429
|
+
if (!config.githubAllowedUsers) {
|
|
430
|
+
config.githubAllowedUsers = [];
|
|
431
|
+
}
|
|
432
|
+
const normalized = user.toLowerCase().trim();
|
|
433
|
+
if (!config.githubAllowedUsers.some((u) => u.toLowerCase() === normalized)) {
|
|
434
|
+
config.githubAllowedUsers.push(user.trim());
|
|
435
|
+
saveConfigFile(config);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Remove a GitHub user from the allowed list.
|
|
440
|
+
* @param user - The user name to remove
|
|
441
|
+
*/
|
|
442
|
+
export function removeGitHubAllowedUser(user) {
|
|
443
|
+
const config = loadConfigFile();
|
|
444
|
+
if (!config.githubAllowedUsers) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const normalized = user.toLowerCase().trim();
|
|
448
|
+
config.githubAllowedUsers = config.githubAllowedUsers.filter((u) => u.toLowerCase() !== normalized);
|
|
449
|
+
saveConfigFile(config);
|
|
450
|
+
}
|
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
* Discovers Agent Skills from a directory, parses YAML frontmatter,
|
|
5
5
|
* and generates server instructions XML.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Source information for a skill.
|
|
9
|
+
* Indicates whether the skill comes from a local directory or GitHub repository.
|
|
10
|
+
*/
|
|
11
|
+
export interface SkillSource {
|
|
12
|
+
type: "local" | "github" | "bundled";
|
|
13
|
+
displayName: string;
|
|
14
|
+
owner?: string;
|
|
15
|
+
repo?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Default source for skills discovered without explicit source info.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DEFAULT_SKILL_SOURCE: SkillSource;
|
|
21
|
+
/**
|
|
22
|
+
* Source for skills bundled with the server package.
|
|
23
|
+
*/
|
|
24
|
+
export declare const BUNDLED_SKILL_SOURCE: SkillSource;
|
|
7
25
|
/**
|
|
8
26
|
* Metadata extracted from a skill's SKILL.md frontmatter.
|
|
9
27
|
*/
|
|
@@ -11,12 +29,22 @@ export interface SkillMetadata {
|
|
|
11
29
|
name: string;
|
|
12
30
|
description: string;
|
|
13
31
|
path: string;
|
|
32
|
+
disableModelInvocation?: boolean;
|
|
33
|
+
userInvocable?: boolean;
|
|
34
|
+
effectiveAssistantInvocable: boolean;
|
|
35
|
+
effectiveUserInvocable: boolean;
|
|
36
|
+
isAssistantOverridden: boolean;
|
|
37
|
+
isUserOverridden: boolean;
|
|
38
|
+
source: SkillSource;
|
|
14
39
|
}
|
|
15
40
|
/**
|
|
16
41
|
* Discover all skills in a directory.
|
|
17
42
|
* Scans for subdirectories containing SKILL.md files.
|
|
43
|
+
*
|
|
44
|
+
* @param skillsDir - The directory to scan for skills
|
|
45
|
+
* @param source - Optional source info to attach to discovered skills
|
|
18
46
|
*/
|
|
19
|
-
export declare function discoverSkills(skillsDir: string): SkillMetadata[];
|
|
47
|
+
export declare function discoverSkills(skillsDir: string, source?: SkillSource): SkillMetadata[];
|
|
20
48
|
/**
|
|
21
49
|
* Generate the server instructions with available skills.
|
|
22
50
|
* Includes a brief preamble about skill usage following the Agent Skills spec.
|
|
@@ -31,3 +59,26 @@ export declare function loadSkillContent(skillPath: string): string;
|
|
|
31
59
|
* Uses first-wins behavior: if duplicate names exist, the first occurrence is kept.
|
|
32
60
|
*/
|
|
33
61
|
export declare function createSkillMap(skills: SkillMetadata[]): Map<string, SkillMetadata>;
|
|
62
|
+
/**
|
|
63
|
+
* Invocation override settings per skill (imported type reference).
|
|
64
|
+
*/
|
|
65
|
+
interface SkillInvocationOverrides {
|
|
66
|
+
assistant?: boolean;
|
|
67
|
+
user?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Apply invocation overrides from config to compute effective values.
|
|
71
|
+
* Returns a new array with updated effective* fields.
|
|
72
|
+
*/
|
|
73
|
+
export declare function applyInvocationOverrides(skills: SkillMetadata[], overrides: Record<string, SkillInvocationOverrides>): SkillMetadata[];
|
|
74
|
+
/**
|
|
75
|
+
* Filter skills that can be invoked by the model (appear in tool description).
|
|
76
|
+
* Uses effective value which considers config overrides.
|
|
77
|
+
*/
|
|
78
|
+
export declare function getModelInvocableSkills(skills: SkillMetadata[]): SkillMetadata[];
|
|
79
|
+
/**
|
|
80
|
+
* Filter skills that can be invoked by the user (appear in prompts menu).
|
|
81
|
+
* Uses effective value which considers config overrides.
|
|
82
|
+
*/
|
|
83
|
+
export declare function getUserInvocableSkills(skills: SkillMetadata[]): SkillMetadata[];
|
|
84
|
+
export {};
|
package/dist/skill-discovery.js
CHANGED
|
@@ -7,6 +7,20 @@
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { parse as parseYaml } from "yaml";
|
|
10
|
+
/**
|
|
11
|
+
* Default source for skills discovered without explicit source info.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_SKILL_SOURCE = {
|
|
14
|
+
type: "local",
|
|
15
|
+
displayName: "Local",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Source for skills bundled with the server package.
|
|
19
|
+
*/
|
|
20
|
+
export const BUNDLED_SKILL_SOURCE = {
|
|
21
|
+
type: "bundled",
|
|
22
|
+
displayName: "Bundled",
|
|
23
|
+
};
|
|
10
24
|
/**
|
|
11
25
|
* Find the SKILL.md file in a skill directory.
|
|
12
26
|
* Prefers SKILL.md (uppercase) but accepts skill.md (lowercase).
|
|
@@ -43,8 +57,11 @@ function parseFrontmatter(content) {
|
|
|
43
57
|
/**
|
|
44
58
|
* Discover all skills in a directory.
|
|
45
59
|
* Scans for subdirectories containing SKILL.md files.
|
|
60
|
+
*
|
|
61
|
+
* @param skillsDir - The directory to scan for skills
|
|
62
|
+
* @param source - Optional source info to attach to discovered skills
|
|
46
63
|
*/
|
|
47
|
-
export function discoverSkills(skillsDir) {
|
|
64
|
+
export function discoverSkills(skillsDir, source) {
|
|
48
65
|
const skills = [];
|
|
49
66
|
if (!fs.existsSync(skillsDir)) {
|
|
50
67
|
console.error(`Skills directory not found: ${skillsDir}`);
|
|
@@ -63,6 +80,8 @@ export function discoverSkills(skillsDir) {
|
|
|
63
80
|
const { metadata } = parseFrontmatter(content);
|
|
64
81
|
const name = metadata.name;
|
|
65
82
|
const description = metadata.description;
|
|
83
|
+
const disableModelInvocation = metadata["disable-model-invocation"];
|
|
84
|
+
const userInvocable = metadata["user-invocable"];
|
|
66
85
|
if (typeof name !== "string" || !name.trim()) {
|
|
67
86
|
console.error(`Skill at ${skillDir}: missing or invalid 'name' field`);
|
|
68
87
|
continue;
|
|
@@ -71,10 +90,21 @@ export function discoverSkills(skillsDir) {
|
|
|
71
90
|
console.error(`Skill at ${skillDir}: missing or invalid 'description' field`);
|
|
72
91
|
continue;
|
|
73
92
|
}
|
|
93
|
+
const effectiveAssistant = disableModelInvocation !== true;
|
|
94
|
+
const effectiveUser = userInvocable !== false;
|
|
74
95
|
skills.push({
|
|
75
96
|
name: name.trim(),
|
|
76
97
|
description: description.trim(),
|
|
77
98
|
path: skillMdPath,
|
|
99
|
+
disableModelInvocation: disableModelInvocation === true,
|
|
100
|
+
userInvocable: userInvocable !== false, // Default to true
|
|
101
|
+
// Initialize effective values from frontmatter (overrides applied later)
|
|
102
|
+
effectiveAssistantInvocable: effectiveAssistant,
|
|
103
|
+
effectiveUserInvocable: effectiveUser,
|
|
104
|
+
isAssistantOverridden: false,
|
|
105
|
+
isUserOverridden: false,
|
|
106
|
+
// Source info (local or GitHub)
|
|
107
|
+
source: source || DEFAULT_SKILL_SOURCE,
|
|
78
108
|
});
|
|
79
109
|
}
|
|
80
110
|
catch (error) {
|
|
@@ -142,3 +172,42 @@ export function createSkillMap(skills) {
|
|
|
142
172
|
}
|
|
143
173
|
return map;
|
|
144
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Apply invocation overrides from config to compute effective values.
|
|
177
|
+
* Returns a new array with updated effective* fields.
|
|
178
|
+
*/
|
|
179
|
+
export function applyInvocationOverrides(skills, overrides) {
|
|
180
|
+
return skills.map((skill) => {
|
|
181
|
+
const override = overrides[skill.name];
|
|
182
|
+
if (!override) {
|
|
183
|
+
return skill; // No override, keep frontmatter defaults
|
|
184
|
+
}
|
|
185
|
+
const hasAssistantOverride = override.assistant !== undefined;
|
|
186
|
+
const hasUserOverride = override.user !== undefined;
|
|
187
|
+
return {
|
|
188
|
+
...skill,
|
|
189
|
+
effectiveAssistantInvocable: hasAssistantOverride
|
|
190
|
+
? override.assistant
|
|
191
|
+
: !skill.disableModelInvocation,
|
|
192
|
+
effectiveUserInvocable: hasUserOverride
|
|
193
|
+
? override.user
|
|
194
|
+
: skill.userInvocable !== false,
|
|
195
|
+
isAssistantOverridden: hasAssistantOverride,
|
|
196
|
+
isUserOverridden: hasUserOverride,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Filter skills that can be invoked by the model (appear in tool description).
|
|
202
|
+
* Uses effective value which considers config overrides.
|
|
203
|
+
*/
|
|
204
|
+
export function getModelInvocableSkills(skills) {
|
|
205
|
+
return skills.filter((skill) => skill.effectiveAssistantInvocable);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Filter skills that can be invoked by the user (appear in prompts menu).
|
|
209
|
+
* Uses effective value which considers config overrides.
|
|
210
|
+
*/
|
|
211
|
+
export function getUserInvocableSkills(skills) {
|
|
212
|
+
return skills.filter((skill) => skill.effectiveUserInvocable);
|
|
213
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP App tool registration for skill display UI.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - skill-display: Opens the skill display UI
|
|
6
|
+
* - skill-display-update-invocation: Updates invocation settings (UI-only)
|
|
7
|
+
* - skill-display-reset-override: Resets a skill to frontmatter defaults (UI-only)
|
|
8
|
+
*/
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { SkillState } from "./skill-tool.js";
|
|
11
|
+
/**
|
|
12
|
+
* Callback type for when invocation settings change.
|
|
13
|
+
*/
|
|
14
|
+
export type OnInvocationChangedCallback = () => void;
|
|
15
|
+
/**
|
|
16
|
+
* Register skill-display MCP App tools and resource.
|
|
17
|
+
*
|
|
18
|
+
* @param server - The MCP server instance
|
|
19
|
+
* @param skillState - Shared skill state for getting skill info
|
|
20
|
+
* @param onInvocationChanged - Callback when invocation settings are changed
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerSkillDisplayTool(server: McpServer, skillState: SkillState, onInvocationChanged: OnInvocationChangedCallback): void;
|