@dotsetlabs/dotclaw 1.9.0 → 2.1.0
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/.env.example +6 -0
- package/README.md +13 -8
- package/config-examples/groups/global/CLAUDE.md +6 -14
- package/config-examples/groups/main/CLAUDE.md +8 -39
- package/config-examples/runtime.json +16 -122
- package/config-examples/tool-policy.json +2 -15
- package/container/agent-runner/package-lock.json +258 -0
- package/container/agent-runner/package.json +2 -1
- package/container/agent-runner/src/agent-config.ts +62 -47
- package/container/agent-runner/src/browser.ts +180 -0
- package/container/agent-runner/src/container-protocol.ts +4 -9
- package/container/agent-runner/src/id.ts +3 -2
- package/container/agent-runner/src/index.ts +331 -846
- package/container/agent-runner/src/ipc.ts +3 -33
- package/container/agent-runner/src/mcp-client.ts +222 -0
- package/container/agent-runner/src/mcp-registry.ts +163 -0
- package/container/agent-runner/src/skill-loader.ts +375 -0
- package/container/agent-runner/src/tools.ts +154 -184
- package/container/agent-runner/src/tts.ts +61 -0
- package/dist/admin-commands.d.ts.map +1 -1
- package/dist/admin-commands.js +12 -0
- package/dist/admin-commands.js.map +1 -1
- package/dist/agent-execution.d.ts +5 -9
- package/dist/agent-execution.d.ts.map +1 -1
- package/dist/agent-execution.js +32 -20
- package/dist/agent-execution.js.map +1 -1
- package/dist/cli.js +61 -16
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -5
- package/dist/config.js.map +1 -1
- package/dist/container-protocol.d.ts +4 -9
- package/dist/container-protocol.d.ts.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +3 -8
- package/dist/container-runner.js.map +1 -1
- package/dist/dashboard.d.ts +5 -6
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +12 -60
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts +1 -59
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +41 -262
- package/dist/db.js.map +1 -1
- package/dist/error-messages.d.ts.map +1 -1
- package/dist/error-messages.js +5 -1
- package/dist/error-messages.js.map +1 -1
- package/dist/hooks.d.ts +7 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +93 -0
- package/dist/hooks.js.map +1 -0
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +2 -1
- package/dist/id.js.map +1 -1
- package/dist/index.js +673 -2790
- package/dist/index.js.map +1 -1
- package/dist/ipc-dispatcher.d.ts +26 -0
- package/dist/ipc-dispatcher.d.ts.map +1 -0
- package/dist/ipc-dispatcher.js +861 -0
- package/dist/ipc-dispatcher.js.map +1 -0
- package/dist/local-embeddings.d.ts +7 -0
- package/dist/local-embeddings.d.ts.map +1 -0
- package/dist/local-embeddings.js +60 -0
- package/dist/local-embeddings.js.map +1 -0
- package/dist/maintenance.d.ts.map +1 -1
- package/dist/maintenance.js +3 -7
- package/dist/maintenance.js.map +1 -1
- package/dist/memory-embeddings.d.ts +1 -1
- package/dist/memory-embeddings.d.ts.map +1 -1
- package/dist/memory-embeddings.js +59 -31
- package/dist/memory-embeddings.js.map +1 -1
- package/dist/memory-store.d.ts +0 -10
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +11 -27
- package/dist/memory-store.js.map +1 -1
- package/dist/message-pipeline.d.ts +47 -0
- package/dist/message-pipeline.d.ts.map +1 -0
- package/dist/message-pipeline.js +652 -0
- package/dist/message-pipeline.js.map +1 -0
- package/dist/metrics.d.ts +7 -10
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +2 -33
- package/dist/metrics.js.map +1 -1
- package/dist/model-registry.d.ts +0 -14
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +0 -36
- package/dist/model-registry.js.map +1 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +2 -0
- package/dist/paths.js.map +1 -1
- package/dist/providers/discord/discord-format.d.ts +16 -0
- package/dist/providers/discord/discord-format.d.ts.map +1 -0
- package/dist/providers/discord/discord-format.js +153 -0
- package/dist/providers/discord/discord-format.js.map +1 -0
- package/dist/providers/discord/discord-provider.d.ts +50 -0
- package/dist/providers/discord/discord-provider.d.ts.map +1 -0
- package/dist/providers/discord/discord-provider.js +607 -0
- package/dist/providers/discord/discord-provider.js.map +1 -0
- package/dist/providers/discord/index.d.ts +4 -0
- package/dist/providers/discord/index.d.ts.map +1 -0
- package/dist/providers/discord/index.js +3 -0
- package/dist/providers/discord/index.js.map +1 -0
- package/dist/providers/registry.d.ts +14 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +49 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/telegram/index.d.ts +4 -0
- package/dist/providers/telegram/index.d.ts.map +1 -0
- package/dist/providers/telegram/index.js +3 -0
- package/dist/providers/telegram/index.js.map +1 -0
- package/dist/providers/telegram/telegram-format.d.ts +3 -0
- package/dist/providers/telegram/telegram-format.d.ts.map +1 -0
- package/dist/providers/telegram/telegram-format.js +215 -0
- package/dist/providers/telegram/telegram-format.js.map +1 -0
- package/dist/providers/telegram/telegram-provider.d.ts +51 -0
- package/dist/providers/telegram/telegram-provider.d.ts.map +1 -0
- package/dist/providers/telegram/telegram-provider.js +824 -0
- package/dist/providers/telegram/telegram-provider.js.map +1 -0
- package/dist/providers/types.d.ts +107 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/request-router.d.ts +9 -31
- package/dist/request-router.d.ts.map +1 -1
- package/dist/request-router.js +12 -142
- package/dist/request-router.js.map +1 -1
- package/dist/runtime-config.d.ts +79 -101
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +140 -208
- package/dist/runtime-config.js.map +1 -1
- package/dist/skill-manager.d.ts +39 -0
- package/dist/skill-manager.d.ts.map +1 -0
- package/dist/skill-manager.js +286 -0
- package/dist/skill-manager.js.map +1 -0
- package/dist/streaming.d.ts +58 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +196 -0
- package/dist/streaming.js.map +1 -0
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +11 -45
- package/dist/task-scheduler.js.map +1 -1
- package/dist/tool-policy.d.ts.map +1 -1
- package/dist/tool-policy.js +13 -5
- package/dist/tool-policy.js.map +1 -1
- package/dist/transcription.d.ts +8 -0
- package/dist/transcription.d.ts.map +1 -0
- package/dist/transcription.js +174 -0
- package/dist/transcription.js.map +1 -0
- package/dist/types.d.ts +2 -50
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -4
- package/scripts/bootstrap.js +40 -4
- package/scripts/configure.js +129 -7
- package/scripts/doctor.js +30 -4
- package/scripts/init.js +13 -6
- package/scripts/install.sh +1 -1
- package/config-examples/plugin-http.json +0 -18
- package/container/skills/agent-browser.md +0 -159
- package/dist/background-job-classifier.d.ts +0 -20
- package/dist/background-job-classifier.d.ts.map +0 -1
- package/dist/background-job-classifier.js +0 -145
- package/dist/background-job-classifier.js.map +0 -1
- package/dist/background-jobs.d.ts +0 -56
- package/dist/background-jobs.d.ts.map +0 -1
- package/dist/background-jobs.js +0 -550
- package/dist/background-jobs.js.map +0 -1
- package/dist/planner-probe.d.ts +0 -14
- package/dist/planner-probe.d.ts.map +0 -1
- package/dist/planner-probe.js +0 -97
- package/dist/planner-probe.js.map +0 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Loader — parses SKILL.md frontmatter, builds a catalog of
|
|
3
|
+
* name+description summaries for the system prompt.
|
|
4
|
+
*
|
|
5
|
+
* Skills use YAML frontmatter in SKILL.md files:
|
|
6
|
+
* skills/<name>/SKILL.md (directory form)
|
|
7
|
+
* skills/<name>.md (single-file form, with frontmatter)
|
|
8
|
+
*
|
|
9
|
+
* Plain .md files without frontmatter are ignored.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
|
|
15
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export type SkillFrontmatter = {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
license?: string;
|
|
21
|
+
compatibility?: string;
|
|
22
|
+
metadata?: { author?: string; version?: string; tags?: string[] };
|
|
23
|
+
plugins?: string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type SkillEntry = {
|
|
27
|
+
scope: 'group' | 'global';
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
directory: string;
|
|
31
|
+
skillMdPath: string;
|
|
32
|
+
frontmatter: SkillFrontmatter;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type SkillCatalog = {
|
|
36
|
+
entries: SkillEntry[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Frontmatter parser ───────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Minimal YAML-subset parser for SKILL.md frontmatter.
|
|
45
|
+
* Handles simple key: value, multiline `>`, and `- item` arrays.
|
|
46
|
+
* Returns null if the content has no frontmatter block.
|
|
47
|
+
*/
|
|
48
|
+
export function parseFrontmatter(content: string): SkillFrontmatter | null {
|
|
49
|
+
const match = content.match(FRONTMATTER_RE);
|
|
50
|
+
if (!match) return null;
|
|
51
|
+
|
|
52
|
+
const yaml = match[1];
|
|
53
|
+
const result: Record<string, unknown> = {};
|
|
54
|
+
let currentKey = '';
|
|
55
|
+
let currentIndent = 0;
|
|
56
|
+
let collectingMultiline = false;
|
|
57
|
+
let multilineValue = '';
|
|
58
|
+
let collectingArray = false;
|
|
59
|
+
let arrayItems: string[] = [];
|
|
60
|
+
let nestedObject: Record<string, unknown> = {};
|
|
61
|
+
let collectingNested = false;
|
|
62
|
+
let nestedKey = '';
|
|
63
|
+
|
|
64
|
+
const flushMultiline = () => {
|
|
65
|
+
if (collectingMultiline && currentKey) {
|
|
66
|
+
result[currentKey] = multilineValue.trim();
|
|
67
|
+
collectingMultiline = false;
|
|
68
|
+
multilineValue = '';
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const flushArray = () => {
|
|
73
|
+
if (collectingArray && currentKey) {
|
|
74
|
+
result[currentKey] = arrayItems;
|
|
75
|
+
collectingArray = false;
|
|
76
|
+
arrayItems = [];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const flushNested = () => {
|
|
81
|
+
if (collectingNested && nestedKey) {
|
|
82
|
+
result[nestedKey] = { ...nestedObject };
|
|
83
|
+
collectingNested = false;
|
|
84
|
+
nestedObject = {};
|
|
85
|
+
nestedKey = '';
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
for (const rawLine of yaml.split('\n')) {
|
|
90
|
+
const line = rawLine.replace(/\r$/, '');
|
|
91
|
+
const trimmed = line.trimStart();
|
|
92
|
+
const indent = line.length - trimmed.length;
|
|
93
|
+
|
|
94
|
+
// Blank line in multiline block
|
|
95
|
+
if (!trimmed && collectingMultiline) {
|
|
96
|
+
multilineValue += '\n';
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Array item continuation
|
|
101
|
+
if (collectingArray && indent > currentIndent && trimmed.startsWith('- ')) {
|
|
102
|
+
// First array item disambiguates: this is an array, not a nested object
|
|
103
|
+
if (collectingNested) {
|
|
104
|
+
collectingNested = false;
|
|
105
|
+
nestedObject = {};
|
|
106
|
+
nestedKey = '';
|
|
107
|
+
}
|
|
108
|
+
const item = trimmed.slice(2).trim();
|
|
109
|
+
const unquoted = item.replace(/^["']|["']$/g, '');
|
|
110
|
+
arrayItems.push(unquoted);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Nested object key: value
|
|
115
|
+
if (collectingNested && indent > currentIndent && trimmed.includes(':') && !trimmed.startsWith('- ')) {
|
|
116
|
+
// First nested key disambiguates: this is a nested object, not an array
|
|
117
|
+
if (collectingArray) {
|
|
118
|
+
collectingArray = false;
|
|
119
|
+
arrayItems = [];
|
|
120
|
+
}
|
|
121
|
+
const colonIdx = trimmed.indexOf(':');
|
|
122
|
+
const nk = trimmed.slice(0, colonIdx).trim();
|
|
123
|
+
let nv: unknown = trimmed.slice(colonIdx + 1).trim();
|
|
124
|
+
// Handle inline arrays: ["tag1", "tag2"]
|
|
125
|
+
if (typeof nv === 'string' && (nv as string).startsWith('[')) {
|
|
126
|
+
try {
|
|
127
|
+
nv = JSON.parse(nv as string);
|
|
128
|
+
} catch {
|
|
129
|
+
// leave as string
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Unquote strings
|
|
133
|
+
if (typeof nv === 'string') {
|
|
134
|
+
nv = (nv as string).replace(/^["']|["']$/g, '');
|
|
135
|
+
}
|
|
136
|
+
nestedObject[nk] = nv;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Multiline continuation
|
|
141
|
+
if (collectingMultiline && indent > currentIndent) {
|
|
142
|
+
multilineValue += (multilineValue ? ' ' : '') + trimmed;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// End of any multiline/array/nested collection
|
|
147
|
+
flushMultiline();
|
|
148
|
+
flushArray();
|
|
149
|
+
flushNested();
|
|
150
|
+
|
|
151
|
+
// Skip empty or comment lines
|
|
152
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
153
|
+
|
|
154
|
+
// Parse "key: value" line
|
|
155
|
+
const colonIdx = trimmed.indexOf(':');
|
|
156
|
+
if (colonIdx === -1) continue;
|
|
157
|
+
|
|
158
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
159
|
+
const rawValue = trimmed.slice(colonIdx + 1).trim();
|
|
160
|
+
currentKey = key;
|
|
161
|
+
currentIndent = indent;
|
|
162
|
+
|
|
163
|
+
if (rawValue === '>' || rawValue === '|') {
|
|
164
|
+
collectingMultiline = true;
|
|
165
|
+
multilineValue = '';
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (rawValue === '') {
|
|
170
|
+
// Could be a nested object or array — peek at next line
|
|
171
|
+
// We set up for both; the first continuation line determines which
|
|
172
|
+
collectingNested = true;
|
|
173
|
+
nestedKey = key;
|
|
174
|
+
nestedObject = {};
|
|
175
|
+
collectingArray = true;
|
|
176
|
+
// Array items also collected, we'll pick whichever gets data
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Inline array: [a, b, c]
|
|
181
|
+
if (rawValue.startsWith('[')) {
|
|
182
|
+
try {
|
|
183
|
+
result[key] = JSON.parse(rawValue);
|
|
184
|
+
} catch {
|
|
185
|
+
result[key] = rawValue;
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Unquote
|
|
191
|
+
result[key] = rawValue.replace(/^["']|["']$/g, '');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
flushMultiline();
|
|
195
|
+
flushArray();
|
|
196
|
+
flushNested();
|
|
197
|
+
|
|
198
|
+
// Validate required fields
|
|
199
|
+
const name = typeof result.name === 'string' ? result.name.trim() : '';
|
|
200
|
+
const description = typeof result.description === 'string' ? result.description.trim() : '';
|
|
201
|
+
if (!name || !description) return null;
|
|
202
|
+
|
|
203
|
+
// Build typed result
|
|
204
|
+
const fm: SkillFrontmatter = { name, description };
|
|
205
|
+
if (typeof result.license === 'string') fm.license = result.license;
|
|
206
|
+
if (typeof result.compatibility === 'string') fm.compatibility = result.compatibility;
|
|
207
|
+
|
|
208
|
+
// Metadata
|
|
209
|
+
if (result.metadata && typeof result.metadata === 'object' && !Array.isArray(result.metadata)) {
|
|
210
|
+
const m = result.metadata as Record<string, unknown>;
|
|
211
|
+
fm.metadata = {};
|
|
212
|
+
if (typeof m.author === 'string') fm.metadata.author = m.author;
|
|
213
|
+
if (typeof m.version === 'string') fm.metadata.version = m.version;
|
|
214
|
+
if (Array.isArray(m.tags)) fm.metadata.tags = m.tags.filter((t): t is string => typeof t === 'string');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Plugins
|
|
218
|
+
if (Array.isArray(result.plugins)) {
|
|
219
|
+
fm.plugins = result.plugins.filter((p): p is string => typeof p === 'string');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return fm;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Filesystem helpers ───────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
function readdirSafe(dir: string): string[] {
|
|
228
|
+
try {
|
|
229
|
+
return fs.readdirSync(dir);
|
|
230
|
+
} catch {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isDirectorySafe(p: string): boolean {
|
|
236
|
+
try {
|
|
237
|
+
return fs.statSync(p).isDirectory();
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isFileSafe(p: string): boolean {
|
|
244
|
+
try {
|
|
245
|
+
const stat = fs.lstatSync(p);
|
|
246
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
247
|
+
} catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function readFileSafe(p: string, maxChars: number): string | null {
|
|
253
|
+
try {
|
|
254
|
+
const content = fs.readFileSync(p, 'utf-8');
|
|
255
|
+
return content.length > maxChars ? content.slice(0, maxChars) : content;
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Catalog builder ──────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
const DEFAULT_MAX_SKILLS = 32;
|
|
264
|
+
|
|
265
|
+
export function buildSkillCatalog(params: {
|
|
266
|
+
groupDir: string;
|
|
267
|
+
globalDir: string;
|
|
268
|
+
maxSkills?: number;
|
|
269
|
+
}): SkillCatalog {
|
|
270
|
+
const maxSkills = params.maxSkills ?? DEFAULT_MAX_SKILLS;
|
|
271
|
+
const entries: SkillEntry[] = [];
|
|
272
|
+
|
|
273
|
+
const scanScope = (scope: 'group' | 'global', rootDir: string) => {
|
|
274
|
+
const skillsDir = path.join(rootDir, 'skills');
|
|
275
|
+
if (!isDirectorySafe(skillsDir)) return;
|
|
276
|
+
|
|
277
|
+
const items = readdirSafe(skillsDir).sort();
|
|
278
|
+
for (const item of items) {
|
|
279
|
+
if (entries.length >= maxSkills) break;
|
|
280
|
+
|
|
281
|
+
const itemPath = path.join(skillsDir, item);
|
|
282
|
+
|
|
283
|
+
// Directory form: skills/<name>/SKILL.md
|
|
284
|
+
if (isDirectorySafe(itemPath)) {
|
|
285
|
+
const skillMdPath = path.join(itemPath, 'SKILL.md');
|
|
286
|
+
if (!isFileSafe(skillMdPath)) continue;
|
|
287
|
+
|
|
288
|
+
const content = readFileSafe(skillMdPath, 8000);
|
|
289
|
+
if (!content) continue;
|
|
290
|
+
|
|
291
|
+
const fm = parseFrontmatter(content);
|
|
292
|
+
if (fm) {
|
|
293
|
+
entries.push({
|
|
294
|
+
scope,
|
|
295
|
+
name: fm.name,
|
|
296
|
+
description: fm.description,
|
|
297
|
+
directory: itemPath,
|
|
298
|
+
skillMdPath,
|
|
299
|
+
frontmatter: fm,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Single-file form: skills/<name>.md (must have frontmatter)
|
|
306
|
+
if (item.endsWith('.md') && isFileSafe(itemPath)) {
|
|
307
|
+
const content = readFileSafe(itemPath, 8000);
|
|
308
|
+
if (!content) continue;
|
|
309
|
+
|
|
310
|
+
const fm = parseFrontmatter(content);
|
|
311
|
+
if (fm) {
|
|
312
|
+
entries.push({
|
|
313
|
+
scope,
|
|
314
|
+
name: fm.name,
|
|
315
|
+
description: fm.description,
|
|
316
|
+
directory: path.dirname(itemPath),
|
|
317
|
+
skillMdPath: itemPath,
|
|
318
|
+
frontmatter: fm,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
scanScope('group', params.groupDir);
|
|
326
|
+
scanScope('global', params.globalDir);
|
|
327
|
+
|
|
328
|
+
return { entries };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── Formatters ───────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
export function formatSkillCatalog(catalog: SkillCatalog): string {
|
|
334
|
+
if (catalog.entries.length === 0) return '';
|
|
335
|
+
|
|
336
|
+
const lines = [
|
|
337
|
+
'Skills available (use Read tool to load full instructions when needed):',
|
|
338
|
+
];
|
|
339
|
+
for (const entry of catalog.entries) {
|
|
340
|
+
const scopePrefix = entry.scope === 'global' ? '/workspace/global' : '/workspace/group';
|
|
341
|
+
const dir = path.basename(entry.directory);
|
|
342
|
+
const readPath = entry.skillMdPath.startsWith('/')
|
|
343
|
+
? entry.skillMdPath // already absolute container path
|
|
344
|
+
: `${scopePrefix}/skills/${dir}/SKILL.md`;
|
|
345
|
+
lines.push(`- ${entry.name}: ${entry.description} (${readPath})`);
|
|
346
|
+
}
|
|
347
|
+
lines.push('');
|
|
348
|
+
lines.push('When a task matches a skill, read its SKILL.md for full instructions.');
|
|
349
|
+
return lines.join('\n');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Skill plugin directory collector ─────────────────────────────────
|
|
353
|
+
|
|
354
|
+
export function collectSkillPluginDirs(params: {
|
|
355
|
+
groupDir: string;
|
|
356
|
+
globalDir: string;
|
|
357
|
+
}): string[] {
|
|
358
|
+
const dirs: string[] = [];
|
|
359
|
+
|
|
360
|
+
const scan = (rootDir: string) => {
|
|
361
|
+
const skillsDir = path.join(rootDir, 'skills');
|
|
362
|
+
if (!isDirectorySafe(skillsDir)) return;
|
|
363
|
+
|
|
364
|
+
for (const entry of readdirSafe(skillsDir)) {
|
|
365
|
+
const pluginDir = path.join(skillsDir, entry, 'plugins');
|
|
366
|
+
if (isDirectorySafe(pluginDir)) {
|
|
367
|
+
dirs.push(pluginDir);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
scan(params.groupDir);
|
|
373
|
+
scan(params.globalDir);
|
|
374
|
+
return dirs;
|
|
375
|
+
}
|