-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathskills.js
More file actions
138 lines (125 loc) · 3.35 KB
/
Copy pathskills.js
File metadata and controls
138 lines (125 loc) · 3.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/**
* Skill discovery for the MCP server.
*
* Skills are SKILL.md files (or SKILL.jsonld) at well-known paths:
*
* <pod>/SKILL.md pod-wide
* <pod>/public/apps/<name>/SKILL.md per-app
* <pod>/private/bots/<name>/SKILL.md per-bot
*
* The discovery channel (list_skills) is stable; the payload format
* declared via `skill:format` evolves (anthropic.skill.v1 today,
* future vocabularies plug in by name).
*/
import * as storage from '../storage/filesystem.js';
const POD_ROOT_SKILL = ['/SKILL.md', '/SKILL.jsonld'];
const APPS_BASE = '/public/apps/';
const BOTS_BASE = '/private/bots/';
function formatForPath(path) {
if (path.endsWith('.jsonld')) return 'jsonld';
return 'anthropic.skill.v1';
}
function scopeFromPath(path) {
if (path.startsWith(APPS_BASE)) return 'app';
if (path.startsWith(BOTS_BASE)) return 'bot';
return 'pod';
}
async function tryEntry(path) {
for (const variant of [path, path.replace(/\.md$/, '.jsonld')]) {
if (await storage.exists(variant)) {
const s = await storage.stat(variant).catch(() => null);
return {
'@id': variant,
'skill:scope': scopeFromPath(variant),
'skill:format': formatForPath(variant),
'skill:source': variant,
'schema:contentSize': s?.size ?? null
};
}
}
return null;
}
async function listContainerNames(containerPath) {
if (!(await storage.exists(containerPath))) return [];
try {
const entries = await storage.listContainer(containerPath);
return entries
.filter(e => e.isDirectory)
.map(e => e.name);
} catch {
return [];
}
}
/**
* Walk the conventional skill locations and return discovered skills.
*/
export async function discoverSkills() {
const items = [];
// Pod-wide
for (const p of POD_ROOT_SKILL) {
if (await storage.exists(p)) {
items.push({
'@id': p,
'skill:scope': 'pod',
'skill:format': formatForPath(p),
'skill:source': p,
'schema:name': 'pod'
});
break;
}
}
// Apps
for (const name of await listContainerNames(APPS_BASE)) {
const skill = await tryEntry(`${APPS_BASE}${name}/SKILL.md`);
if (skill) {
skill['schema:name'] = name;
items.push(skill);
}
}
// Bots
for (const name of await listContainerNames(BOTS_BASE)) {
const skill = await tryEntry(`${BOTS_BASE}${name}/SKILL.md`);
if (skill) {
skill['schema:name'] = name;
items.push(skill);
}
}
return {
'@context': {
skill: 'urn:skill:',
schema: 'https://schema.org/'
},
'@type': 'skill:SkillIndex',
'skill:items': items
};
}
/**
* Read a single skill file by path (relative to pod root).
*/
export async function readSkill(path) {
if (!path || typeof path !== 'string') {
throw new Error('skill path required');
}
if (!path.startsWith('/')) path = '/' + path;
if (!(await storage.exists(path))) {
throw new Error(`skill not found: ${path}`);
}
const content = await storage.read(path);
return {
path,
format: formatForPath(path),
scope: scopeFromPath(path),
body: content.toString('utf8')
};
}
/**
* Read the pod-wide SKILL.md (or .jsonld). Returns null if none exists.
*/
export async function readPodSkill() {
for (const p of POD_ROOT_SKILL) {
if (await storage.exists(p)) {
return readSkill(p);
}
}
return null;
}