skyloom 1.12.0 → 1.13.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.
Files changed (137) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +137 -46
  3. package/config/default.yaml +43 -47
  4. package/config/models.yaml +155 -155
  5. package/config/providers.yaml +39 -39
  6. package/config/skills/api_integrator/SKILL.md +15 -15
  7. package/config/skills/arch_designer/SKILL.md +13 -13
  8. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  9. package/config/skills/code_analysis/SKILL.md +13 -13
  10. package/config/skills/code_generator/SKILL.md +12 -12
  11. package/config/skills/code_reviewer/SKILL.md +13 -13
  12. package/config/skills/content_writer/SKILL.md +14 -14
  13. package/config/skills/data_transformer/SKILL.md +15 -15
  14. package/config/skills/document_analysis/SKILL.md +13 -13
  15. package/config/skills/emotional_companion/SKILL.md +15 -15
  16. package/config/skills/performance_checker/SKILL.md +14 -14
  17. package/config/skills/security_auditor/SKILL.md +14 -14
  18. package/config/skills/self_evolve/SKILL.md +13 -13
  19. package/config/skills/sys_operator/SKILL.md +15 -15
  20. package/config/skills/task_planner/SKILL.md +14 -14
  21. package/config/skills/web_research/SKILL.md +14 -14
  22. package/config/skills/workflow_designer/SKILL.md +13 -13
  23. package/dist/agents/dew.js +52 -52
  24. package/dist/agents/fair.js +84 -84
  25. package/dist/agents/fog.js +30 -30
  26. package/dist/agents/frost.js +32 -32
  27. package/dist/agents/rain.js +32 -32
  28. package/dist/agents/snow.js +68 -68
  29. package/dist/cli/main.js +127 -74
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli/tui.d.ts +52 -19
  32. package/dist/cli/tui.d.ts.map +1 -1
  33. package/dist/cli/tui.js +198 -265
  34. package/dist/cli/tui.js.map +1 -1
  35. package/dist/core/agent/task.d.ts +58 -0
  36. package/dist/core/agent/task.d.ts.map +1 -0
  37. package/dist/core/agent/task.js +83 -0
  38. package/dist/core/agent/task.js.map +1 -0
  39. package/dist/core/agent.d.ts +2 -45
  40. package/dist/core/agent.d.ts.map +1 -1
  41. package/dist/core/agent.js +61 -145
  42. package/dist/core/agent.js.map +1 -1
  43. package/dist/core/agent_helpers.d.ts +10 -0
  44. package/dist/core/agent_helpers.d.ts.map +1 -1
  45. package/dist/core/agent_helpers.js +39 -0
  46. package/dist/core/agent_helpers.js.map +1 -1
  47. package/dist/core/catalog.d.ts +71 -0
  48. package/dist/core/catalog.d.ts.map +1 -0
  49. package/dist/core/catalog.js +176 -0
  50. package/dist/core/catalog.js.map +1 -0
  51. package/dist/core/config.d.ts +8 -0
  52. package/dist/core/config.d.ts.map +1 -1
  53. package/dist/core/config.js +12 -4
  54. package/dist/core/config.js.map +1 -1
  55. package/dist/core/factory.js +16 -16
  56. package/dist/core/llm.d.ts +7 -0
  57. package/dist/core/llm.d.ts.map +1 -1
  58. package/dist/core/llm.js +139 -7
  59. package/dist/core/llm.js.map +1 -1
  60. package/dist/core/longdoc.js +5 -5
  61. package/dist/core/memory.d.ts.map +1 -1
  62. package/dist/core/memory.js +69 -62
  63. package/dist/core/memory.js.map +1 -1
  64. package/dist/core/theme.d.ts +46 -0
  65. package/dist/core/theme.d.ts.map +1 -0
  66. package/dist/core/theme.js +42 -0
  67. package/dist/core/theme.js.map +1 -0
  68. package/dist/web/server.js +542 -519
  69. package/dist/web/server.js.map +1 -1
  70. package/docs/AESTHETIC_DESIGN.md +144 -0
  71. package/docs/OPTIMIZATION_PLAN.md +178 -0
  72. package/package.json +60 -60
  73. package/scripts/install.js +48 -48
  74. package/scripts/link.js +10 -10
  75. package/setup.bat +79 -79
  76. package/skill-test-ty2fOA/test.md +10 -10
  77. package/src/agents/dew.ts +70 -70
  78. package/src/agents/fair.ts +102 -102
  79. package/src/agents/fog.ts +48 -48
  80. package/src/agents/frost.ts +50 -50
  81. package/src/agents/rain.ts +50 -50
  82. package/src/agents/snow.ts +239 -239
  83. package/src/cli/main.ts +417 -372
  84. package/src/cli/mode.ts +58 -58
  85. package/src/cli/tui.ts +174 -223
  86. package/src/core/agent/task.ts +100 -0
  87. package/src/core/agent.ts +1446 -1549
  88. package/src/core/agent_helpers.ts +496 -461
  89. package/src/core/arbitrate.ts +162 -162
  90. package/src/core/catalog.ts +178 -0
  91. package/src/core/checkpoint.ts +94 -94
  92. package/src/core/config.ts +20 -4
  93. package/src/core/estimate.ts +104 -104
  94. package/src/core/evolve.ts +191 -191
  95. package/src/core/factory.ts +627 -627
  96. package/src/core/filter.ts +103 -103
  97. package/src/core/graph.ts +156 -156
  98. package/src/core/icons.ts +53 -53
  99. package/src/core/index.ts +37 -37
  100. package/src/core/learn.ts +146 -146
  101. package/src/core/llm.ts +108 -5
  102. package/src/core/longdoc.ts +155 -155
  103. package/src/core/mcp_server.ts +176 -176
  104. package/src/core/memory.ts +1178 -1171
  105. package/src/core/profile.ts +255 -255
  106. package/src/core/router.ts +124 -124
  107. package/src/core/sandbox.ts +142 -142
  108. package/src/core/security.ts +243 -243
  109. package/src/core/skill.ts +342 -342
  110. package/src/core/theme.ts +65 -0
  111. package/src/core/tool_router.ts +193 -193
  112. package/src/core/vector.ts +152 -152
  113. package/src/core/workspace.ts +150 -150
  114. package/src/plugins/loader.ts +66 -66
  115. package/src/skills/loader.ts +46 -46
  116. package/src/sql.js.d.ts +29 -29
  117. package/src/tools/builtin.ts +380 -380
  118. package/src/tools/computer.ts +269 -269
  119. package/src/tools/delegate.ts +49 -49
  120. package/src/web/server.ts +660 -634
  121. package/src/web/tts.ts +93 -93
  122. package/tests/agent_helpers.test.ts +48 -0
  123. package/tests/bus.test.ts +121 -121
  124. package/tests/catalog.test.ts +86 -0
  125. package/tests/config.test.ts +41 -0
  126. package/tests/icons.test.ts +45 -45
  127. package/tests/memory.test.ts +147 -0
  128. package/tests/router.test.ts +86 -86
  129. package/tests/schemas.test.ts +51 -51
  130. package/tests/semantic.test.ts +83 -83
  131. package/tests/setup.ts +10 -10
  132. package/tests/skill.test.ts +172 -172
  133. package/tests/task.test.ts +60 -0
  134. package/tests/tool.test.ts +108 -108
  135. package/tests/tool_router.test.ts +71 -71
  136. package/tests/tui.test.ts +67 -0
  137. package/vitest.config.ts +17 -17
package/src/core/skill.ts CHANGED
@@ -1,342 +1,342 @@
1
- /**
2
- * Skill system — Anthropic-compatible composable capability modules.
3
- *
4
- * Skills use Markdown + YAML frontmatter format. When activated, a skill
5
- * injects its system prompt and can register custom handler tools.
6
- */
7
-
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import { parse as parseYaml } from 'yaml';
11
-
12
- // Tuning for lazy skill loading
13
- const SKILL_BODY_LITE_THRESHOLD = 2000;
14
- const SKILL_BODY_LITE_MAX_CHARS = 1500;
15
-
16
- // Claude Code -> sky tool-name aliases
17
- const CLAUDE_TOOL_ALIASES: Record<string, string> = {
18
- read: 'read_file',
19
- write: 'write_file',
20
- edit: 'edit_file',
21
- multiedit: 'edit_file',
22
- delete: 'delete_file',
23
- bash: 'run_bash',
24
- shell: 'run_bash',
25
- grep: 'grep',
26
- glob: 'file_search',
27
- search: 'code_search',
28
- websearch: 'web_search',
29
- webfetch: 'fetch_page',
30
- ls: 'list_directory',
31
- tree: 'tree',
32
- fetch: 'http_get',
33
- taskdone: 'task_done',
34
- };
35
-
36
- /**
37
- * A composable capability module for an agent.
38
- */
39
- export class Skill {
40
- name: string;
41
- description: string;
42
- systemPrompt: string = '';
43
- requiredTools: string[] = [];
44
- tools: any[] = [];
45
- handler: ((agent: any, toolRegistry: any) => any[]) | null = null;
46
- model: string | null = null;
47
- temperature: number | null = null;
48
- maxTokens: number | null = null;
49
- triggers: string[] = [];
50
- resourceDir: string | null = null;
51
- license: string | null = null;
52
- allowedTools: string[] | null = null;
53
- sourcePath: string | null = null;
54
- bodyTruncated: boolean = false;
55
- metadata: Record<string, any> = {};
56
-
57
- constructor(config: Partial<SkillConfig>) {
58
- this.name = config.name || '';
59
- this.description = config.description || '';
60
- this.systemPrompt = config.systemPrompt || '';
61
- this.requiredTools = config.requiredTools || [];
62
- this.tools = config.tools || [];
63
- this.handler = config.handler || null;
64
- this.model = config.model || null;
65
- this.temperature = config.temperature ?? null;
66
- this.maxTokens = config.maxTokens ?? null;
67
- this.triggers = config.triggers || [];
68
- this.resourceDir = config.resourceDir || null;
69
- this.license = config.license || null;
70
- this.allowedTools = config.allowedTools || null;
71
- this.sourcePath = config.sourcePath || null;
72
- this.bodyTruncated = config.bodyTruncated || false;
73
- this.metadata = config.metadata || {};
74
- }
75
-
76
- /**
77
- * Load a skill from a Markdown file with YAML frontmatter.
78
- */
79
- static fromMarkdown(filePath: string): Skill | null {
80
- const p = path.resolve(filePath);
81
- let text: string;
82
- try {
83
- text = fs.readFileSync(p, 'utf-8');
84
- } catch {
85
- return null;
86
- }
87
-
88
- const parsed = parseFrontmatter(text);
89
- if (!parsed) return null;
90
-
91
- const { fm, body } = parsed;
92
- const name = (fm.name as string) || path.basename(p, '.md');
93
- const description = (fm.description as string) || '';
94
-
95
- const toolsRaw = fm.tools;
96
- const requiredTools: string[] = Array.isArray(toolsRaw)
97
- ? toolsRaw.filter((t: any) => typeof t === 'string')
98
- : [];
99
-
100
- // Config overrides
101
- const model = fm.model as string | undefined;
102
- const temperature = fm.temperature as number | undefined;
103
- const maxTokens = (fm.maxTokens ?? fm.max_tokens) as number | undefined;
104
-
105
- // Triggers
106
- const triggersRaw = fm.triggers;
107
- const triggers: string[] = Array.isArray(triggersRaw)
108
- ? triggersRaw.filter((t: any) => typeof t === 'string')
109
- : [];
110
-
111
- // Auto-derive triggers from description if not specified
112
- const finalTriggers = triggers.length > 0 ? triggers
113
- : (description ? deriveTriggersFromDescription(description) : []);
114
-
115
- // License and allowed-tools
116
- const licenseRaw = fm.license as string | undefined;
117
- const license = licenseRaw?.trim() || null;
118
-
119
- const allowedRaw = fm['allowed-tools'] ?? fm.allowed_tools;
120
- let allowedTools: string[] | null = null;
121
- if (Array.isArray(allowedRaw)) {
122
- allowedTools = allowedRaw.filter((t: any) => typeof t === 'string');
123
- if (allowedTools.length === 0) allowedTools = null;
124
- } else if (typeof allowedRaw === 'string' && allowedRaw.trim()) {
125
- allowedTools = allowedRaw.split(',').map((t: string) => t.trim()).filter(Boolean);
126
- }
127
- if (allowedTools) {
128
- allowedTools = allowedTools.map(t => normalizeClaudeToolName(t));
129
- // Dedupe preserving order
130
- const seen = new Set<string>();
131
- const deduped: string[] = [];
132
- for (const t of allowedTools) {
133
- if (!seen.has(t)) { seen.add(t); deduped.push(t); }
134
- }
135
- allowedTools = deduped;
136
- }
137
-
138
- // Preserve metadata fields
139
- const knownKeys = new Set([
140
- 'name', 'description', 'tools', 'model', 'temperature', 'maxTokens', 'max_tokens',
141
- 'triggers', 'license', 'allowed-tools', 'allowed_tools',
142
- ]);
143
- const extraMetadata: Record<string, any> = {};
144
- for (const [k, v] of Object.entries(fm)) {
145
- if (!knownKeys.has(k) && !k.startsWith('_')) {
146
- extraMetadata[k] = v;
147
- }
148
- }
149
-
150
- // Lazy-load large skill bodies
151
- let bodyStripped = body.trim();
152
- let bodyTruncated = false;
153
- if (bodyStripped.length > SKILL_BODY_LITE_THRESHOLD) {
154
- bodyStripped = extractSkillHead(bodyStripped, SKILL_BODY_LITE_MAX_CHARS);
155
- bodyTruncated = true;
156
- }
157
-
158
- return new Skill({
159
- name,
160
- description,
161
- systemPrompt: bodyStripped,
162
- requiredTools,
163
- model: typeof model === 'string' ? model : null,
164
- temperature: typeof temperature === 'number' ? temperature : null,
165
- maxTokens: typeof maxTokens === 'number' ? maxTokens : null,
166
- triggers: finalTriggers,
167
- license,
168
- allowedTools,
169
- sourcePath: p,
170
- bodyTruncated,
171
- metadata: extraMetadata,
172
- });
173
- }
174
- }
175
-
176
- interface SkillConfig {
177
- name: string;
178
- description: string;
179
- systemPrompt?: string;
180
- requiredTools?: string[];
181
- tools?: any[];
182
- handler?: ((agent: any, toolRegistry: any) => any[]) | null;
183
- model?: string | null;
184
- temperature?: number | null;
185
- maxTokens?: number | null;
186
- triggers?: string[];
187
- resourceDir?: string | null;
188
- license?: string | null;
189
- allowedTools?: string[] | null;
190
- sourcePath?: string | null;
191
- bodyTruncated?: boolean;
192
- metadata?: Record<string, any>;
193
- }
194
-
195
- /**
196
- * Parse YAML frontmatter from markdown text.
197
- * Returns { fm, body } or null.
198
- */
199
- function parseFrontmatter(text: string): { fm: Record<string, any>; body: string } | null {
200
- const match = text.match(/^---\s*\n(.*?)\n---\s*\n?(.*)/s);
201
- if (!match) return null;
202
- try {
203
- const fm = parseYaml(match[1]) || {};
204
- return { fm, body: match[2] };
205
- } catch {
206
- return null;
207
- }
208
- }
209
-
210
- /**
211
- * Normalize a Claude Code tool name into sky's registry name.
212
- */
213
- function normalizeClaudeToolName(raw: string): string {
214
- let s = raw.trim();
215
- if (!s) return s;
216
- // Strip permission scoping: Bash(ls *) -> Bash
217
- const paren = s.indexOf('(');
218
- if (paren > 0) s = s.slice(0, paren).trim();
219
- // Check if it's already a valid sky name
220
- const aliasValues = new Set(Object.values(CLAUDE_TOOL_ALIASES));
221
- if (aliasValues.has(s)) return s;
222
- return CLAUDE_TOOL_ALIASES[s.toLowerCase()] ?? s;
223
- }
224
-
225
- /**
226
- * Extract the head of a SKILL.md body — title plus first major section.
227
- */
228
- function extractSkillHead(body: string, maxChars: number): string {
229
- const out: string[] = [];
230
- let charCount = 0;
231
- let h2Count = 0;
232
- for (const line of body.split('\n')) {
233
- const isH2 = line.startsWith('## ') && !line.startsWith('### ');
234
- if (isH2) {
235
- h2Count++;
236
- if (h2Count > 1) break;
237
- }
238
- if (out.length > 0 && charCount + line.length + 1 > maxChars) break;
239
- out.push(line);
240
- charCount += line.length + 1;
241
- }
242
- return out.join('\n').trimEnd();
243
- }
244
-
245
- // Patterns for auto-deriving triggers
246
- const TRIGGER_QUOTED = /["'"'""]([^"'""\n]{1,40})["'""']/g;
247
- const TRIGGER_EXT = /(?<![A-Za-z0-9])\.[A-Za-z0-9]{2,6}\b/g;
248
- const TRIGGER_STRIP = " \t,.;:!?,。、;:!?、。";
249
-
250
- /**
251
- * Pull candidate trigger phrases out of a skill description.
252
- */
253
- function deriveTriggersFromDescription(description: string): string[] {
254
- const raw: string[] = [];
255
- let m: RegExpExecArray | null;
256
- while ((m = TRIGGER_QUOTED.exec(description)) !== null) {
257
- raw.push(m[1]);
258
- }
259
- while ((m = TRIGGER_EXT.exec(description)) !== null) {
260
- raw.push(m[0]);
261
- }
262
-
263
- const seen = new Set<string>();
264
- const out: string[] = [];
265
- for (let token of raw) {
266
- token = token.replace(/[ \t,.;:!?,。、;:!?、。]+$/, '').replace(/^[ \t]+/, '');
267
- if (!token || token.length < 2) continue;
268
- const key = token.toLowerCase();
269
- if (seen.has(key)) continue;
270
- seen.add(key);
271
- out.push(token);
272
- if (out.length >= 12) break;
273
- }
274
- return out;
275
- }
276
-
277
- /**
278
- * Central registry for all available skills.
279
- */
280
- export class SkillRegistry {
281
- private _skills: Map<string, Skill> = new Map();
282
-
283
- register(skill: Skill): void {
284
- this._skills.set(skill.name, skill);
285
- }
286
-
287
- get(name: string): Skill | undefined {
288
- return this._skills.get(name);
289
- }
290
-
291
- getSkills(names?: string[]): Skill[] {
292
- if (!names) return Array.from(this._skills.values());
293
- return names.map(n => this._skills.get(n)).filter(Boolean) as Skill[];
294
- }
295
-
296
- listNames(): string[] {
297
- return Array.from(this._skills.keys());
298
- }
299
-
300
- merge(other: SkillRegistry): void {
301
- for (const [name, skill] of other._skills) {
302
- this._skills.set(name, skill);
303
- }
304
- }
305
-
306
- /**
307
- * Load all .md skill files from a directory (Anthropic format).
308
- */
309
- loadSkillsFromDirectory(directory: string): Skill[] {
310
- const loaded: Skill[] = [];
311
- let dirPath: string;
312
- try {
313
- dirPath = path.resolve(directory.replace(/^~/, process.env.HOME || process.env.USERPROFILE || ''));
314
- } catch {
315
- return loaded;
316
- }
317
-
318
- if (!fs.existsSync(dirPath)) return loaded;
319
-
320
- let entries: string[];
321
- try {
322
- entries = fs.readdirSync(dirPath);
323
- } catch {
324
- return loaded;
325
- }
326
-
327
- for (const entry of entries.sort()) {
328
- if (entry.startsWith('_') || entry.startsWith('.') || !entry.endsWith('.md')) continue;
329
- const fullPath = path.join(dirPath, entry);
330
- const skill = Skill.fromMarkdown(fullPath);
331
- if (skill) {
332
- skill.resourceDir = dirPath;
333
- this.register(skill);
334
- loaded.push(skill);
335
- }
336
- }
337
- return loaded;
338
- }
339
- }
340
-
341
- // Global skill registry singleton
342
- export const globalSkillRegistry = new SkillRegistry();
1
+ /**
2
+ * Skill system — Anthropic-compatible composable capability modules.
3
+ *
4
+ * Skills use Markdown + YAML frontmatter format. When activated, a skill
5
+ * injects its system prompt and can register custom handler tools.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { parse as parseYaml } from 'yaml';
11
+
12
+ // Tuning for lazy skill loading
13
+ const SKILL_BODY_LITE_THRESHOLD = 2000;
14
+ const SKILL_BODY_LITE_MAX_CHARS = 1500;
15
+
16
+ // Claude Code -> sky tool-name aliases
17
+ const CLAUDE_TOOL_ALIASES: Record<string, string> = {
18
+ read: 'read_file',
19
+ write: 'write_file',
20
+ edit: 'edit_file',
21
+ multiedit: 'edit_file',
22
+ delete: 'delete_file',
23
+ bash: 'run_bash',
24
+ shell: 'run_bash',
25
+ grep: 'grep',
26
+ glob: 'file_search',
27
+ search: 'code_search',
28
+ websearch: 'web_search',
29
+ webfetch: 'fetch_page',
30
+ ls: 'list_directory',
31
+ tree: 'tree',
32
+ fetch: 'http_get',
33
+ taskdone: 'task_done',
34
+ };
35
+
36
+ /**
37
+ * A composable capability module for an agent.
38
+ */
39
+ export class Skill {
40
+ name: string;
41
+ description: string;
42
+ systemPrompt: string = '';
43
+ requiredTools: string[] = [];
44
+ tools: any[] = [];
45
+ handler: ((agent: any, toolRegistry: any) => any[]) | null = null;
46
+ model: string | null = null;
47
+ temperature: number | null = null;
48
+ maxTokens: number | null = null;
49
+ triggers: string[] = [];
50
+ resourceDir: string | null = null;
51
+ license: string | null = null;
52
+ allowedTools: string[] | null = null;
53
+ sourcePath: string | null = null;
54
+ bodyTruncated: boolean = false;
55
+ metadata: Record<string, any> = {};
56
+
57
+ constructor(config: Partial<SkillConfig>) {
58
+ this.name = config.name || '';
59
+ this.description = config.description || '';
60
+ this.systemPrompt = config.systemPrompt || '';
61
+ this.requiredTools = config.requiredTools || [];
62
+ this.tools = config.tools || [];
63
+ this.handler = config.handler || null;
64
+ this.model = config.model || null;
65
+ this.temperature = config.temperature ?? null;
66
+ this.maxTokens = config.maxTokens ?? null;
67
+ this.triggers = config.triggers || [];
68
+ this.resourceDir = config.resourceDir || null;
69
+ this.license = config.license || null;
70
+ this.allowedTools = config.allowedTools || null;
71
+ this.sourcePath = config.sourcePath || null;
72
+ this.bodyTruncated = config.bodyTruncated || false;
73
+ this.metadata = config.metadata || {};
74
+ }
75
+
76
+ /**
77
+ * Load a skill from a Markdown file with YAML frontmatter.
78
+ */
79
+ static fromMarkdown(filePath: string): Skill | null {
80
+ const p = path.resolve(filePath);
81
+ let text: string;
82
+ try {
83
+ text = fs.readFileSync(p, 'utf-8');
84
+ } catch {
85
+ return null;
86
+ }
87
+
88
+ const parsed = parseFrontmatter(text);
89
+ if (!parsed) return null;
90
+
91
+ const { fm, body } = parsed;
92
+ const name = (fm.name as string) || path.basename(p, '.md');
93
+ const description = (fm.description as string) || '';
94
+
95
+ const toolsRaw = fm.tools;
96
+ const requiredTools: string[] = Array.isArray(toolsRaw)
97
+ ? toolsRaw.filter((t: any) => typeof t === 'string')
98
+ : [];
99
+
100
+ // Config overrides
101
+ const model = fm.model as string | undefined;
102
+ const temperature = fm.temperature as number | undefined;
103
+ const maxTokens = (fm.maxTokens ?? fm.max_tokens) as number | undefined;
104
+
105
+ // Triggers
106
+ const triggersRaw = fm.triggers;
107
+ const triggers: string[] = Array.isArray(triggersRaw)
108
+ ? triggersRaw.filter((t: any) => typeof t === 'string')
109
+ : [];
110
+
111
+ // Auto-derive triggers from description if not specified
112
+ const finalTriggers = triggers.length > 0 ? triggers
113
+ : (description ? deriveTriggersFromDescription(description) : []);
114
+
115
+ // License and allowed-tools
116
+ const licenseRaw = fm.license as string | undefined;
117
+ const license = licenseRaw?.trim() || null;
118
+
119
+ const allowedRaw = fm['allowed-tools'] ?? fm.allowed_tools;
120
+ let allowedTools: string[] | null = null;
121
+ if (Array.isArray(allowedRaw)) {
122
+ allowedTools = allowedRaw.filter((t: any) => typeof t === 'string');
123
+ if (allowedTools.length === 0) allowedTools = null;
124
+ } else if (typeof allowedRaw === 'string' && allowedRaw.trim()) {
125
+ allowedTools = allowedRaw.split(',').map((t: string) => t.trim()).filter(Boolean);
126
+ }
127
+ if (allowedTools) {
128
+ allowedTools = allowedTools.map(t => normalizeClaudeToolName(t));
129
+ // Dedupe preserving order
130
+ const seen = new Set<string>();
131
+ const deduped: string[] = [];
132
+ for (const t of allowedTools) {
133
+ if (!seen.has(t)) { seen.add(t); deduped.push(t); }
134
+ }
135
+ allowedTools = deduped;
136
+ }
137
+
138
+ // Preserve metadata fields
139
+ const knownKeys = new Set([
140
+ 'name', 'description', 'tools', 'model', 'temperature', 'maxTokens', 'max_tokens',
141
+ 'triggers', 'license', 'allowed-tools', 'allowed_tools',
142
+ ]);
143
+ const extraMetadata: Record<string, any> = {};
144
+ for (const [k, v] of Object.entries(fm)) {
145
+ if (!knownKeys.has(k) && !k.startsWith('_')) {
146
+ extraMetadata[k] = v;
147
+ }
148
+ }
149
+
150
+ // Lazy-load large skill bodies
151
+ let bodyStripped = body.trim();
152
+ let bodyTruncated = false;
153
+ if (bodyStripped.length > SKILL_BODY_LITE_THRESHOLD) {
154
+ bodyStripped = extractSkillHead(bodyStripped, SKILL_BODY_LITE_MAX_CHARS);
155
+ bodyTruncated = true;
156
+ }
157
+
158
+ return new Skill({
159
+ name,
160
+ description,
161
+ systemPrompt: bodyStripped,
162
+ requiredTools,
163
+ model: typeof model === 'string' ? model : null,
164
+ temperature: typeof temperature === 'number' ? temperature : null,
165
+ maxTokens: typeof maxTokens === 'number' ? maxTokens : null,
166
+ triggers: finalTriggers,
167
+ license,
168
+ allowedTools,
169
+ sourcePath: p,
170
+ bodyTruncated,
171
+ metadata: extraMetadata,
172
+ });
173
+ }
174
+ }
175
+
176
+ interface SkillConfig {
177
+ name: string;
178
+ description: string;
179
+ systemPrompt?: string;
180
+ requiredTools?: string[];
181
+ tools?: any[];
182
+ handler?: ((agent: any, toolRegistry: any) => any[]) | null;
183
+ model?: string | null;
184
+ temperature?: number | null;
185
+ maxTokens?: number | null;
186
+ triggers?: string[];
187
+ resourceDir?: string | null;
188
+ license?: string | null;
189
+ allowedTools?: string[] | null;
190
+ sourcePath?: string | null;
191
+ bodyTruncated?: boolean;
192
+ metadata?: Record<string, any>;
193
+ }
194
+
195
+ /**
196
+ * Parse YAML frontmatter from markdown text.
197
+ * Returns { fm, body } or null.
198
+ */
199
+ function parseFrontmatter(text: string): { fm: Record<string, any>; body: string } | null {
200
+ const match = text.match(/^---\s*\n(.*?)\n---\s*\n?(.*)/s);
201
+ if (!match) return null;
202
+ try {
203
+ const fm = parseYaml(match[1]) || {};
204
+ return { fm, body: match[2] };
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Normalize a Claude Code tool name into sky's registry name.
212
+ */
213
+ function normalizeClaudeToolName(raw: string): string {
214
+ let s = raw.trim();
215
+ if (!s) return s;
216
+ // Strip permission scoping: Bash(ls *) -> Bash
217
+ const paren = s.indexOf('(');
218
+ if (paren > 0) s = s.slice(0, paren).trim();
219
+ // Check if it's already a valid sky name
220
+ const aliasValues = new Set(Object.values(CLAUDE_TOOL_ALIASES));
221
+ if (aliasValues.has(s)) return s;
222
+ return CLAUDE_TOOL_ALIASES[s.toLowerCase()] ?? s;
223
+ }
224
+
225
+ /**
226
+ * Extract the head of a SKILL.md body — title plus first major section.
227
+ */
228
+ function extractSkillHead(body: string, maxChars: number): string {
229
+ const out: string[] = [];
230
+ let charCount = 0;
231
+ let h2Count = 0;
232
+ for (const line of body.split('\n')) {
233
+ const isH2 = line.startsWith('## ') && !line.startsWith('### ');
234
+ if (isH2) {
235
+ h2Count++;
236
+ if (h2Count > 1) break;
237
+ }
238
+ if (out.length > 0 && charCount + line.length + 1 > maxChars) break;
239
+ out.push(line);
240
+ charCount += line.length + 1;
241
+ }
242
+ return out.join('\n').trimEnd();
243
+ }
244
+
245
+ // Patterns for auto-deriving triggers
246
+ const TRIGGER_QUOTED = /["'"'""]([^"'""\n]{1,40})["'""']/g;
247
+ const TRIGGER_EXT = /(?<![A-Za-z0-9])\.[A-Za-z0-9]{2,6}\b/g;
248
+ const TRIGGER_STRIP = " \t,.;:!?,。、;:!?、。";
249
+
250
+ /**
251
+ * Pull candidate trigger phrases out of a skill description.
252
+ */
253
+ function deriveTriggersFromDescription(description: string): string[] {
254
+ const raw: string[] = [];
255
+ let m: RegExpExecArray | null;
256
+ while ((m = TRIGGER_QUOTED.exec(description)) !== null) {
257
+ raw.push(m[1]);
258
+ }
259
+ while ((m = TRIGGER_EXT.exec(description)) !== null) {
260
+ raw.push(m[0]);
261
+ }
262
+
263
+ const seen = new Set<string>();
264
+ const out: string[] = [];
265
+ for (let token of raw) {
266
+ token = token.replace(/[ \t,.;:!?,。、;:!?、。]+$/, '').replace(/^[ \t]+/, '');
267
+ if (!token || token.length < 2) continue;
268
+ const key = token.toLowerCase();
269
+ if (seen.has(key)) continue;
270
+ seen.add(key);
271
+ out.push(token);
272
+ if (out.length >= 12) break;
273
+ }
274
+ return out;
275
+ }
276
+
277
+ /**
278
+ * Central registry for all available skills.
279
+ */
280
+ export class SkillRegistry {
281
+ private _skills: Map<string, Skill> = new Map();
282
+
283
+ register(skill: Skill): void {
284
+ this._skills.set(skill.name, skill);
285
+ }
286
+
287
+ get(name: string): Skill | undefined {
288
+ return this._skills.get(name);
289
+ }
290
+
291
+ getSkills(names?: string[]): Skill[] {
292
+ if (!names) return Array.from(this._skills.values());
293
+ return names.map(n => this._skills.get(n)).filter(Boolean) as Skill[];
294
+ }
295
+
296
+ listNames(): string[] {
297
+ return Array.from(this._skills.keys());
298
+ }
299
+
300
+ merge(other: SkillRegistry): void {
301
+ for (const [name, skill] of other._skills) {
302
+ this._skills.set(name, skill);
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Load all .md skill files from a directory (Anthropic format).
308
+ */
309
+ loadSkillsFromDirectory(directory: string): Skill[] {
310
+ const loaded: Skill[] = [];
311
+ let dirPath: string;
312
+ try {
313
+ dirPath = path.resolve(directory.replace(/^~/, process.env.HOME || process.env.USERPROFILE || ''));
314
+ } catch {
315
+ return loaded;
316
+ }
317
+
318
+ if (!fs.existsSync(dirPath)) return loaded;
319
+
320
+ let entries: string[];
321
+ try {
322
+ entries = fs.readdirSync(dirPath);
323
+ } catch {
324
+ return loaded;
325
+ }
326
+
327
+ for (const entry of entries.sort()) {
328
+ if (entry.startsWith('_') || entry.startsWith('.') || !entry.endsWith('.md')) continue;
329
+ const fullPath = path.join(dirPath, entry);
330
+ const skill = Skill.fromMarkdown(fullPath);
331
+ if (skill) {
332
+ skill.resourceDir = dirPath;
333
+ this.register(skill);
334
+ loaded.push(skill);
335
+ }
336
+ }
337
+ return loaded;
338
+ }
339
+ }
340
+
341
+ // Global skill registry singleton
342
+ export const globalSkillRegistry = new SkillRegistry();