foliko 1.0.75 → 1.0.76

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 (88) hide show
  1. package/.claude/settings.local.json +159 -157
  2. package/cli/bin/foliko.js +12 -12
  3. package/cli/src/commands/chat.js +143 -143
  4. package/cli/src/commands/list.js +93 -93
  5. package/cli/src/index.js +75 -75
  6. package/cli/src/ui/chat-ui.js +201 -201
  7. package/cli/src/utils/ansi.js +40 -40
  8. package/cli/src/utils/markdown.js +292 -292
  9. package/examples/ambient-example.js +194 -194
  10. package/examples/basic.js +115 -115
  11. package/examples/bootstrap.js +121 -121
  12. package/examples/mcp-example.js +56 -56
  13. package/examples/skill-example.js +49 -49
  14. package/examples/test-chat.js +137 -137
  15. package/examples/test-mcp.js +85 -85
  16. package/examples/test-reload.js +59 -59
  17. package/examples/test-telegram.js +50 -50
  18. package/examples/test-tg-bot.js +45 -45
  19. package/examples/test-tg-simple.js +47 -47
  20. package/examples/test-tg.js +62 -62
  21. package/examples/test-think.js +43 -43
  22. package/examples/test-web-plugin.js +103 -103
  23. package/examples/test-weixin-feishu.js +103 -103
  24. package/examples/workflow.js +158 -158
  25. package/package.json +1 -1
  26. package/plugins/ai-plugin.js +102 -102
  27. package/plugins/ambient-agent/EventWatcher.js +113 -113
  28. package/plugins/ambient-agent/ExplorerLoop.js +640 -640
  29. package/plugins/ambient-agent/GoalManager.js +197 -197
  30. package/plugins/ambient-agent/Reflector.js +95 -95
  31. package/plugins/ambient-agent/StateStore.js +90 -90
  32. package/plugins/ambient-agent/constants.js +101 -101
  33. package/plugins/ambient-agent/index.js +579 -579
  34. package/plugins/audit-plugin.js +187 -187
  35. package/plugins/default-plugins.js +662 -662
  36. package/plugins/email/constants.js +64 -64
  37. package/plugins/email/handlers.js +461 -461
  38. package/plugins/email/index.js +278 -278
  39. package/plugins/email/monitor.js +269 -269
  40. package/plugins/email/parser.js +138 -138
  41. package/plugins/email/reply.js +151 -151
  42. package/plugins/email/utils.js +124 -124
  43. package/plugins/feishu-plugin.js +481 -481
  44. package/plugins/file-system-plugin.js +826 -826
  45. package/plugins/install-plugin.js +199 -199
  46. package/plugins/python-executor-plugin.js +367 -367
  47. package/plugins/python-plugin-loader.js +481 -481
  48. package/plugins/rules-plugin.js +294 -294
  49. package/plugins/scheduler-plugin.js +691 -691
  50. package/plugins/session-plugin.js +369 -369
  51. package/plugins/shell-executor-plugin.js +197 -197
  52. package/plugins/storage-plugin.js +240 -240
  53. package/plugins/subagent-plugin.js +845 -845
  54. package/plugins/telegram-plugin.js +482 -482
  55. package/plugins/think-plugin.js +345 -345
  56. package/plugins/tools-plugin.js +196 -196
  57. package/plugins/web-plugin.js +606 -606
  58. package/plugins/weixin-plugin.js +545 -545
  59. package/src/capabilities/index.js +11 -11
  60. package/src/capabilities/skill-manager.js +609 -609
  61. package/src/capabilities/workflow-engine.js +1109 -1109
  62. package/src/core/agent-chat.js +882 -882
  63. package/src/core/agent.js +892 -892
  64. package/src/core/framework.js +465 -465
  65. package/src/core/index.js +19 -19
  66. package/src/core/plugin-base.js +219 -219
  67. package/src/core/plugin-manager.js +863 -863
  68. package/src/core/provider.js +114 -114
  69. package/src/core/sub-agent-config.js +264 -264
  70. package/src/core/system-prompt-builder.js +120 -120
  71. package/src/core/tool-registry.js +517 -517
  72. package/src/core/tool-router.js +297 -297
  73. package/src/executors/executor-base.js +58 -58
  74. package/src/executors/mcp-executor.js +741 -741
  75. package/src/index.js +25 -25
  76. package/src/utils/circuit-breaker.js +301 -301
  77. package/src/utils/error-boundary.js +363 -363
  78. package/src/utils/error.js +374 -374
  79. package/src/utils/event-emitter.js +97 -97
  80. package/src/utils/id.js +133 -133
  81. package/src/utils/index.js +217 -217
  82. package/src/utils/logger.js +181 -181
  83. package/src/utils/plugin-helpers.js +90 -90
  84. package/src/utils/retry.js +122 -122
  85. package/src/utils/sandbox.js +292 -292
  86. package/test/tool-registry-validation.test.js +218 -218
  87. package/website/script.js +136 -136
  88. package/foliko-1.0.75.tgz +0 -0
@@ -1,609 +1,609 @@
1
- /**
2
- * SkillManager 技能管理器
3
- * 加载和管理 Skill
4
- */
5
-
6
- const fs = require('fs');
7
- const path = require('path');
8
- const { Plugin } = require('../core/plugin-base');
9
- const { logger } = require('../utils/logger');
10
- const log = logger.child('SkillManager');
11
- const { z } = require('zod');
12
-
13
- /**
14
- * 验证 skill 名称
15
- * 1-64字符,字母数字、下划线和连字符,不能以连字符或下划线开头或结尾
16
- */
17
- function isValidSkillName(name) {
18
- if (!name || name.length < 1 || name.length > 64) return false;
19
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) return false;
20
- if (name.startsWith('-') || name.startsWith('_') || name.endsWith('-') || name.endsWith('_'))
21
- return false;
22
- if (/--/.test(name) || /__/.test(name)) return false;
23
- return true;
24
- }
25
-
26
- /**
27
- * 解析 YAML frontmatter
28
- */
29
- function parseFrontmatter(content) {
30
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
31
- if (!match) return null;
32
-
33
- const frontmatter = {};
34
- const lines = match[1].split('\n');
35
- let currentKey = null;
36
-
37
- for (const line of lines) {
38
- // 处理 metadata 嵌套
39
- if (currentKey === 'metadata') {
40
- if (line.match(/^ {2}[a-zA-Z]/)) {
41
- const colonIndex = line.indexOf(':');
42
- if (colonIndex > 0) {
43
- const key = line.substring(2, colonIndex).trim();
44
- const value = line
45
- .substring(colonIndex + 1)
46
- .trim()
47
- .replace(/^["']|["']$/g, '');
48
- frontmatter[currentKey][key] = value;
49
- continue;
50
- }
51
- } else if (line.match(/^[a-zA-Z]/)) {
52
- currentKey = null;
53
- continue;
54
- }
55
- }
56
-
57
- const colonIndex = line.indexOf(':');
58
- if (colonIndex === -1) continue;
59
-
60
- const key = line.substring(0, colonIndex).trim();
61
- let value = line.substring(colonIndex + 1).trim();
62
-
63
- // 移除引号
64
- if (
65
- (value.startsWith('"') && value.endsWith('"')) ||
66
- (value.startsWith("'") && value.endsWith("'"))
67
- ) {
68
- value = value.slice(1, -1);
69
- }
70
-
71
- if (key === 'metadata') {
72
- currentKey = 'metadata';
73
- frontmatter.metadata = {};
74
- } else if (key === 'allowed-tools') {
75
- frontmatter[key] = value
76
- .split(',')
77
- .map((t) => t.trim().replace(/^["']|["']$/g, ''))
78
- .filter((t) => t);
79
- } else {
80
- frontmatter[key] = value;
81
- }
82
- }
83
-
84
- return frontmatter;
85
- }
86
-
87
- /**
88
- * 移除 frontmatter,获取正文
89
- */
90
- function stripFrontmatter(content) {
91
- const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
92
- return match ? content.slice(match[0].length).trim() : content.trim();
93
- }
94
-
95
- /**
96
- * Skill 元数据
97
- */
98
- class SkillMetadata {
99
- constructor(data) {
100
- this.name = data.name || '';
101
- this.description = data.description || '';
102
- this.license = data.license || null;
103
- this.compatibility = data.compatibility || null;
104
- this.metadata = data.metadata || {};
105
- this.allowedTools = data.allowedTools || [];
106
- }
107
- }
108
-
109
- /**
110
- * Skill 类
111
- */
112
- class Skill {
113
- constructor(metadata, content) {
114
- this.metadata = metadata;
115
- this.content = content;
116
- this._framework = null;
117
- }
118
-
119
- /**
120
- * 安装 skill
121
- */
122
- install(framework) {
123
- this._framework = framework;
124
- return this;
125
- }
126
-
127
- /**
128
- * 获取工具定义
129
- */
130
- getTools() {
131
- return [];
132
- }
133
- }
134
-
135
- /**
136
- * SkillManager 插件
137
- */
138
- class SkillManagerPlugin extends Plugin {
139
- constructor(config = {}) {
140
- super();
141
- this.name = 'skill-manager';
142
- this.version = '1.0.0';
143
- this.description = '技能管理器,加载和管理 Skill';
144
- this.priority = 5;
145
- this.system = true;
146
-
147
- this._framework = null;
148
- this._skillsDirs = Array.isArray(config.skillsDirs)
149
- ? config.skillsDirs
150
- : [config.skillsDir || '.agent/skills', 'skills'];
151
- this._skills = new Map();
152
- this._loaded = false;
153
- }
154
-
155
- install(framework) {
156
- this._framework = framework;
157
- return this;
158
- }
159
-
160
- start(framework) {
161
- this._loadAllSkills();
162
-
163
- // 注册 loadSkill 工具
164
- framework.registerTool({
165
- name: 'loadSkill',
166
- description: '加载指定技能,获取技能的使用指南和内容',
167
- inputSchema: z.object({
168
- skill: z.string().describe('技能名称'),
169
- }),
170
- execute: async (args) => {
171
- const skillName = args.skill;
172
- const skill = this.getSkill(skillName);
173
- if (!skill) {
174
- return { success: false, error: `Skill '${skillName}' not found` };
175
- }
176
- return {
177
- success: true,
178
- name: skill.name,
179
- description: skill.metadata?.description || '',
180
- content: skill.content,
181
- };
182
- },
183
- });
184
-
185
- // 注册 reloadSkills 工具
186
- framework.registerTool({
187
- name: 'reloadSkills',
188
- description: '重载所有技能,当用户添加新技能或修改技能后调用此工具',
189
- inputSchema: z.object({}),
190
- execute: async () => {
191
- this.reload(this._framework);
192
- return {
193
- success: true,
194
- message: `Skills reloaded. Total: ${this._skills.size}`,
195
- skills: Array.from(this._skills.keys()),
196
- };
197
- },
198
- });
199
-
200
- // 注册 loadReference 工具(按需加载 skill 的附加文档)
201
- framework.registerTool({
202
- name: 'loadReference',
203
- description: '加载指定技能的附加参考文档(references 目录下的文件)',
204
- inputSchema: z.object({
205
- skill: z.string().describe('技能名称'),
206
- reference: z.string().describe('参考文档名称(不含 .md 后缀)'),
207
- list: z.boolean().optional().describe('如果为 true,列出该技能的所有可用的 reference 文件'),
208
- }),
209
- execute: async (args) => {
210
- if (args.list) {
211
- // 列出该技能的所有 reference
212
- const refs = this.listReferences(args.skill);
213
- return {
214
- success: true,
215
- skill: args.skill,
216
- references: refs,
217
- };
218
- }
219
-
220
- const content = this.loadReference(args.skill, args.reference);
221
- if (content === null) {
222
- return {
223
- success: false,
224
- error: `Reference '${args.reference}' not found in skill '${args.skill}'`,
225
- };
226
- }
227
- return {
228
- success: true,
229
- skill: args.skill,
230
- reference: args.reference,
231
- content,
232
- };
233
- },
234
- });
235
-
236
- // 注册 listScripts 工具(列出 skill 的脚本)
237
- framework.registerTool({
238
- name: 'listScripts',
239
- description: '列出指定技能下的所有可执行脚本',
240
- inputSchema: z.object({
241
- skill: z.string().describe('技能名称'),
242
- }),
243
- execute: async (args) => {
244
- const scripts = this.listScripts(args.skill);
245
- return {
246
- success: true,
247
- skill: args.skill,
248
- scripts,
249
- };
250
- },
251
- });
252
-
253
- // 注册 loadScript 工具(读取脚本内容)
254
- framework.registerTool({
255
- name: 'loadScript',
256
- description: '读取指定技能下脚本文件的内容',
257
- inputSchema: z.object({
258
- skill: z.string().describe('技能名称'),
259
- script: z.string().describe('脚本名称(包含扩展名)'),
260
- }),
261
- execute: async (args) => {
262
- const content = this.loadScript(args.skill, args.script);
263
- if (content === null) {
264
- return {
265
- success: false,
266
- error: `Script '${args.script}' not found in skill '${args.skill}'`,
267
- };
268
- }
269
- return {
270
- success: true,
271
- skill: args.skill,
272
- script: args.script,
273
- content,
274
- };
275
- },
276
- });
277
-
278
- return this;
279
- }
280
-
281
- /**
282
- * 加载所有 skills
283
- */
284
- _loadAllSkills() {
285
- if (this._loaded) return;
286
-
287
- let totalLoaded = 0;
288
-
289
- for (const skillsDir of this._skillsDirs) {
290
- const resolvedDir = path.resolve(process.cwd(), skillsDir);
291
-
292
- if (!fs.existsSync(resolvedDir)) {
293
- continue;
294
- }
295
-
296
- try {
297
- const entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
298
-
299
- for (const entry of entries) {
300
- if (!entry.isDirectory()) continue;
301
-
302
- const skillPath = path.join(resolvedDir, entry.name);
303
-
304
- if (!isValidSkillName(entry.name)) {
305
- continue;
306
- }
307
-
308
- // 跳过已加载的 skill
309
- if (this._skills.has(entry.name)) {
310
- continue;
311
- }
312
-
313
- try {
314
- this._loadSkill(entry.name, skillPath);
315
- totalLoaded++;
316
- } catch (err) {
317
- log.error(` Failed to load skill '${entry.name}':`, err.message);
318
- }
319
- }
320
- } catch (err) {
321
- log.error('Failed to load skills from:', skillsDir, err.message);
322
- }
323
- }
324
-
325
- log.info(` Loaded ${this._skills.size} skills`);
326
- this._loaded = true;
327
- }
328
-
329
- /**
330
- * 递归查找 SKILL.md 或 AGENTS.md(支持多层嵌套目录)
331
- * @param {string} dir - 要搜索的目录
332
- * @returns {string|null} 找到的 markdown 文件路径
333
- */
334
- _findSkillMarkdown(dir) {
335
- if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
336
- return null;
337
- }
338
-
339
- const entries = fs.readdirSync(dir, { withFileTypes: true });
340
-
341
- // 第一遍:优先查找 SKILL.MD
342
- for (const entry of entries) {
343
- if (entry.isFile() && entry.name.toUpperCase() === 'SKILL.MD') {
344
- return path.join(dir, entry.name);
345
- }
346
- }
347
-
348
- // 第二遍:查找 AGENTS.MD
349
- for (const entry of entries) {
350
- if (entry.isFile() && entry.name.toUpperCase() === 'AGENTS.MD') {
351
- return path.join(dir, entry.name);
352
- }
353
- }
354
-
355
- // 如果没找到,递归搜索子目录(排除 node_modules)
356
- for (const entry of entries) {
357
- if (entry.isDirectory() && entry.name !== 'node_modules') {
358
- const subPath = path.join(dir, entry.name);
359
- const found = this._findSkillMarkdown(subPath);
360
- if (found) return found;
361
- }
362
- }
363
-
364
- return null;
365
- }
366
-
367
- /**
368
- * 加载单个 skill
369
- */
370
- _loadSkill(name, skillPath) {
371
- // 查找 markdown 文件,优先加载 SKILL.md 或 AGENTS.md(支持多层嵌套)
372
- const skillMdPath = this._findSkillMarkdown(skillPath);
373
- if (!skillMdPath) {
374
- throw new Error('No markdown file found');
375
- }
376
-
377
- const mainFile = skillMdPath;
378
- const content = fs.readFileSync(mainFile, 'utf-8');
379
- const frontmatter = parseFrontmatter(content);
380
-
381
- if (!frontmatter) {
382
- throw new Error('No valid frontmatter found');
383
- }
384
-
385
- const metadata = new SkillMetadata({
386
- name: frontmatter.name || name,
387
- description: frontmatter.description || '',
388
- license: frontmatter.license,
389
- compatibility: frontmatter.compatibility,
390
- metadata: frontmatter.metadata || {},
391
- allowedTools: frontmatter['allowed-tools'] || [],
392
- });
393
-
394
- const skill = new Skill(metadata, stripFrontmatter(content));
395
- skill.install(this._framework);
396
-
397
- // 扫描 references 子目录(按需加载的附加文档)
398
- const references = this._scanReferences(skillPath);
399
-
400
- // 扫描 scripts 子目录(可执行脚本)
401
- const scripts = this._scanScripts(skillPath);
402
-
403
- this._skills.set(name, {
404
- name,
405
- metadata,
406
- content: skill.content,
407
- instance: skill,
408
- path: skillPath,
409
- references,
410
- scripts,
411
- });
412
-
413
- const refsInfo = references.size > 0 ? `, ${references.size} refs` : '';
414
- const scriptsInfo = scripts.size > 0 ? `, ${scripts.size} scripts` : '';
415
- //log.info(` Loaded skill: ${name}${refsInfo}${scriptsInfo}`)
416
- }
417
-
418
- /**
419
- * 扫描 references 子目录,按需加载
420
- * @param {string} skillPath - skill 目录路径
421
- * @returns {Map} references 文件映射 { filename: { path, content } }
422
- */
423
- _scanReferences(skillPath) {
424
- const references = new Map();
425
- const refsDir = path.join(skillPath, 'references');
426
-
427
- if (!fs.existsSync(refsDir) || !fs.statSync(refsDir).isDirectory()) {
428
- return references;
429
- }
430
-
431
- try {
432
- const entries = fs.readdirSync(refsDir, { withFileTypes: true });
433
- for (const entry of entries) {
434
- if (entry.isFile() && entry.name.endsWith('.md')) {
435
- const refPath = path.join(refsDir, entry.name);
436
- const refName = entry.name.replace('.md', '');
437
- references.set(refName, {
438
- path: refPath,
439
- content: null, // 延迟加载
440
- });
441
- }
442
- }
443
- } catch (err) {
444
- log.warn(` Failed to scan references for ${skillPath}:`, err.message);
445
- }
446
-
447
- return references;
448
- }
449
-
450
- /**
451
- * 扫描 scripts 子目录,获取脚本列表
452
- * @param {string} skillPath - skill 目录路径
453
- * @returns {Map} scripts 映射 { filename: { path, isExecutable } }
454
- */
455
- _scanScripts(skillPath) {
456
- const scripts = new Map();
457
- const scriptsDir = path.join(skillPath, 'scripts');
458
-
459
- if (!fs.existsSync(scriptsDir) || !fs.statSync(scriptsDir).isDirectory()) {
460
- return scripts;
461
- }
462
-
463
- try {
464
- const entries = fs.readdirSync(scriptsDir, { withFileTypes: true });
465
- for (const entry of entries) {
466
- if (entry.isFile()) {
467
- const scriptPath = path.join(scriptsDir, entry.name);
468
- // 检查文件是否有执行权限(或检查扩展名)
469
- const isExecutable =
470
- entry.name.endsWith('.sh') ||
471
- entry.name.endsWith('.js') ||
472
- entry.name.endsWith('.py') ||
473
- entry.name.endsWith('.ts');
474
- scripts.set(entry.name, {
475
- path: scriptPath,
476
- isExecutable,
477
- });
478
- }
479
- }
480
- } catch (err) {
481
- log.warn(` Failed to scan scripts for ${skillPath}:`, err.message);
482
- }
483
-
484
- return scripts;
485
- }
486
-
487
- /**
488
- * 按需加载 reference 文件
489
- * @param {string} skillName - skill 名称
490
- * @param {string} refName - reference 文件名(不含 .md)
491
- * @returns {string|null} 文件内容
492
- */
493
- loadReference(skillName, refName) {
494
- const skill = this._skills.get(skillName);
495
- if (!skill || !skill.references.has(refName)) {
496
- return null;
497
- }
498
-
499
- const ref = skill.references.get(refName);
500
- if (!ref.content) {
501
- try {
502
- ref.content = fs.readFileSync(ref.path, 'utf-8');
503
- } catch (err) {
504
- log.error(` Failed to load reference ${skillName}/${refName}:`, err.message);
505
- return null;
506
- }
507
- }
508
-
509
- return ref.content;
510
- }
511
-
512
- /**
513
- * 获取 skill 的 reference 列表
514
- * @param {string} skillName
515
- * @returns {string[]} reference 文件名列表
516
- */
517
- listReferences(skillName) {
518
- const skill = this._skills.get(skillName);
519
- if (!skill) return [];
520
- return Array.from(skill.references.keys());
521
- }
522
-
523
- /**
524
- * 获取 skill 的 scripts 列表
525
- * @param {string} skillName
526
- * @returns {Object[]} script 信息列表 [{ name, path, isExecutable }]
527
- */
528
- listScripts(skillName) {
529
- const skill = this._skills.get(skillName);
530
- if (!skill) return [];
531
- return Array.from(skill.scripts.entries()).map(([name, info]) => ({
532
- name,
533
- path: info.path,
534
- isExecutable: info.isExecutable,
535
- }));
536
- }
537
-
538
- /**
539
- * 读取脚本内容
540
- * @param {string} skillName
541
- * @param {string} scriptName
542
- * @returns {string|null}
543
- */
544
- loadScript(skillName, scriptName) {
545
- const skill = this._skills.get(skillName);
546
- if (!skill || !skill.scripts.has(scriptName)) {
547
- return null;
548
- }
549
-
550
- const script = skill.scripts.get(scriptName);
551
- try {
552
- return fs.readFileSync(script.path, 'utf-8');
553
- } catch (err) {
554
- log.error(` Failed to load script ${skillName}/${scriptName}:`, err.message);
555
- return null;
556
- }
557
- }
558
-
559
- /**
560
- * 获取所有 skills
561
- */
562
- getAllSkills() {
563
- return Array.from(this._skills.values());
564
- }
565
-
566
- /**
567
- * 获取 skill
568
- */
569
- getSkill(name) {
570
- return this._skills.get(name);
571
- }
572
-
573
- /**
574
- * 检查 skill 是否存在
575
- */
576
- hasSkill(name) {
577
- return this._skills.has(name);
578
- }
579
-
580
- /**
581
- * 获取 skill 数量
582
- */
583
- size() {
584
- return this._skills.size;
585
- }
586
-
587
- reload(framework) {
588
- log.info(' Reloading...');
589
- this._skills.clear();
590
- this._loaded = false;
591
- this._framework = framework;
592
- this._loadAllSkills();
593
- }
594
-
595
- uninstall(framework) {
596
- this._skills.clear();
597
- this._framework = null;
598
- this._loaded = false;
599
- }
600
- }
601
-
602
- module.exports = {
603
- SkillManagerPlugin,
604
- Skill,
605
- SkillMetadata,
606
- isValidSkillName,
607
- parseFrontmatter,
608
- stripFrontmatter,
609
- };
1
+ /**
2
+ * SkillManager 技能管理器
3
+ * 加载和管理 Skill
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { Plugin } = require('../core/plugin-base');
9
+ const { logger } = require('../utils/logger');
10
+ const log = logger.child('SkillManager');
11
+ const { z } = require('zod');
12
+
13
+ /**
14
+ * 验证 skill 名称
15
+ * 1-64字符,字母数字、下划线和连字符,不能以连字符或下划线开头或结尾
16
+ */
17
+ function isValidSkillName(name) {
18
+ if (!name || name.length < 1 || name.length > 64) return false;
19
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) return false;
20
+ if (name.startsWith('-') || name.startsWith('_') || name.endsWith('-') || name.endsWith('_'))
21
+ return false;
22
+ if (/--/.test(name) || /__/.test(name)) return false;
23
+ return true;
24
+ }
25
+
26
+ /**
27
+ * 解析 YAML frontmatter
28
+ */
29
+ function parseFrontmatter(content) {
30
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
31
+ if (!match) return null;
32
+
33
+ const frontmatter = {};
34
+ const lines = match[1].split('\n');
35
+ let currentKey = null;
36
+
37
+ for (const line of lines) {
38
+ // 处理 metadata 嵌套
39
+ if (currentKey === 'metadata') {
40
+ if (line.match(/^ {2}[a-zA-Z]/)) {
41
+ const colonIndex = line.indexOf(':');
42
+ if (colonIndex > 0) {
43
+ const key = line.substring(2, colonIndex).trim();
44
+ const value = line
45
+ .substring(colonIndex + 1)
46
+ .trim()
47
+ .replace(/^["']|["']$/g, '');
48
+ frontmatter[currentKey][key] = value;
49
+ continue;
50
+ }
51
+ } else if (line.match(/^[a-zA-Z]/)) {
52
+ currentKey = null;
53
+ continue;
54
+ }
55
+ }
56
+
57
+ const colonIndex = line.indexOf(':');
58
+ if (colonIndex === -1) continue;
59
+
60
+ const key = line.substring(0, colonIndex).trim();
61
+ let value = line.substring(colonIndex + 1).trim();
62
+
63
+ // 移除引号
64
+ if (
65
+ (value.startsWith('"') && value.endsWith('"')) ||
66
+ (value.startsWith("'") && value.endsWith("'"))
67
+ ) {
68
+ value = value.slice(1, -1);
69
+ }
70
+
71
+ if (key === 'metadata') {
72
+ currentKey = 'metadata';
73
+ frontmatter.metadata = {};
74
+ } else if (key === 'allowed-tools') {
75
+ frontmatter[key] = value
76
+ .split(',')
77
+ .map((t) => t.trim().replace(/^["']|["']$/g, ''))
78
+ .filter((t) => t);
79
+ } else {
80
+ frontmatter[key] = value;
81
+ }
82
+ }
83
+
84
+ return frontmatter;
85
+ }
86
+
87
+ /**
88
+ * 移除 frontmatter,获取正文
89
+ */
90
+ function stripFrontmatter(content) {
91
+ const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
92
+ return match ? content.slice(match[0].length).trim() : content.trim();
93
+ }
94
+
95
+ /**
96
+ * Skill 元数据
97
+ */
98
+ class SkillMetadata {
99
+ constructor(data) {
100
+ this.name = data.name || '';
101
+ this.description = data.description || '';
102
+ this.license = data.license || null;
103
+ this.compatibility = data.compatibility || null;
104
+ this.metadata = data.metadata || {};
105
+ this.allowedTools = data.allowedTools || [];
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Skill 类
111
+ */
112
+ class Skill {
113
+ constructor(metadata, content) {
114
+ this.metadata = metadata;
115
+ this.content = content;
116
+ this._framework = null;
117
+ }
118
+
119
+ /**
120
+ * 安装 skill
121
+ */
122
+ install(framework) {
123
+ this._framework = framework;
124
+ return this;
125
+ }
126
+
127
+ /**
128
+ * 获取工具定义
129
+ */
130
+ getTools() {
131
+ return [];
132
+ }
133
+ }
134
+
135
+ /**
136
+ * SkillManager 插件
137
+ */
138
+ class SkillManagerPlugin extends Plugin {
139
+ constructor(config = {}) {
140
+ super();
141
+ this.name = 'skill-manager';
142
+ this.version = '1.0.0';
143
+ this.description = '技能管理器,加载和管理 Skill';
144
+ this.priority = 5;
145
+ this.system = true;
146
+
147
+ this._framework = null;
148
+ this._skillsDirs = Array.isArray(config.skillsDirs)
149
+ ? config.skillsDirs
150
+ : [config.skillsDir || '.agent/skills', 'skills'];
151
+ this._skills = new Map();
152
+ this._loaded = false;
153
+ }
154
+
155
+ install(framework) {
156
+ this._framework = framework;
157
+ return this;
158
+ }
159
+
160
+ start(framework) {
161
+ this._loadAllSkills();
162
+
163
+ // 注册 loadSkill 工具
164
+ framework.registerTool({
165
+ name: 'loadSkill',
166
+ description: '加载指定技能,获取技能的使用指南和内容',
167
+ inputSchema: z.object({
168
+ skill: z.string().describe('技能名称'),
169
+ }),
170
+ execute: async (args) => {
171
+ const skillName = args.skill;
172
+ const skill = this.getSkill(skillName);
173
+ if (!skill) {
174
+ return { success: false, error: `Skill '${skillName}' not found` };
175
+ }
176
+ return {
177
+ success: true,
178
+ name: skill.name,
179
+ description: skill.metadata?.description || '',
180
+ content: skill.content,
181
+ };
182
+ },
183
+ });
184
+
185
+ // 注册 reloadSkills 工具
186
+ framework.registerTool({
187
+ name: 'reloadSkills',
188
+ description: '重载所有技能,当用户添加新技能或修改技能后调用此工具',
189
+ inputSchema: z.object({}),
190
+ execute: async () => {
191
+ this.reload(this._framework);
192
+ return {
193
+ success: true,
194
+ message: `Skills reloaded. Total: ${this._skills.size}`,
195
+ skills: Array.from(this._skills.keys()),
196
+ };
197
+ },
198
+ });
199
+
200
+ // 注册 loadReference 工具(按需加载 skill 的附加文档)
201
+ framework.registerTool({
202
+ name: 'loadReference',
203
+ description: '加载指定技能的附加参考文档(references 目录下的文件)',
204
+ inputSchema: z.object({
205
+ skill: z.string().describe('技能名称'),
206
+ reference: z.string().describe('参考文档名称(不含 .md 后缀)'),
207
+ list: z.boolean().optional().describe('如果为 true,列出该技能的所有可用的 reference 文件'),
208
+ }),
209
+ execute: async (args) => {
210
+ if (args.list) {
211
+ // 列出该技能的所有 reference
212
+ const refs = this.listReferences(args.skill);
213
+ return {
214
+ success: true,
215
+ skill: args.skill,
216
+ references: refs,
217
+ };
218
+ }
219
+
220
+ const content = this.loadReference(args.skill, args.reference);
221
+ if (content === null) {
222
+ return {
223
+ success: false,
224
+ error: `Reference '${args.reference}' not found in skill '${args.skill}'`,
225
+ };
226
+ }
227
+ return {
228
+ success: true,
229
+ skill: args.skill,
230
+ reference: args.reference,
231
+ content,
232
+ };
233
+ },
234
+ });
235
+
236
+ // 注册 listScripts 工具(列出 skill 的脚本)
237
+ framework.registerTool({
238
+ name: 'listScripts',
239
+ description: '列出指定技能下的所有可执行脚本',
240
+ inputSchema: z.object({
241
+ skill: z.string().describe('技能名称'),
242
+ }),
243
+ execute: async (args) => {
244
+ const scripts = this.listScripts(args.skill);
245
+ return {
246
+ success: true,
247
+ skill: args.skill,
248
+ scripts,
249
+ };
250
+ },
251
+ });
252
+
253
+ // 注册 loadScript 工具(读取脚本内容)
254
+ framework.registerTool({
255
+ name: 'loadScript',
256
+ description: '读取指定技能下脚本文件的内容',
257
+ inputSchema: z.object({
258
+ skill: z.string().describe('技能名称'),
259
+ script: z.string().describe('脚本名称(包含扩展名)'),
260
+ }),
261
+ execute: async (args) => {
262
+ const content = this.loadScript(args.skill, args.script);
263
+ if (content === null) {
264
+ return {
265
+ success: false,
266
+ error: `Script '${args.script}' not found in skill '${args.skill}'`,
267
+ };
268
+ }
269
+ return {
270
+ success: true,
271
+ skill: args.skill,
272
+ script: args.script,
273
+ content,
274
+ };
275
+ },
276
+ });
277
+
278
+ return this;
279
+ }
280
+
281
+ /**
282
+ * 加载所有 skills
283
+ */
284
+ _loadAllSkills() {
285
+ if (this._loaded) return;
286
+
287
+ let totalLoaded = 0;
288
+
289
+ for (const skillsDir of this._skillsDirs) {
290
+ const resolvedDir = path.resolve(process.cwd(), skillsDir);
291
+
292
+ if (!fs.existsSync(resolvedDir)) {
293
+ continue;
294
+ }
295
+
296
+ try {
297
+ const entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
298
+
299
+ for (const entry of entries) {
300
+ if (!entry.isDirectory()) continue;
301
+
302
+ const skillPath = path.join(resolvedDir, entry.name);
303
+
304
+ if (!isValidSkillName(entry.name)) {
305
+ continue;
306
+ }
307
+
308
+ // 跳过已加载的 skill
309
+ if (this._skills.has(entry.name)) {
310
+ continue;
311
+ }
312
+
313
+ try {
314
+ this._loadSkill(entry.name, skillPath);
315
+ totalLoaded++;
316
+ } catch (err) {
317
+ log.error(` Failed to load skill '${entry.name}':`, err.message);
318
+ }
319
+ }
320
+ } catch (err) {
321
+ log.error('Failed to load skills from:', skillsDir, err.message);
322
+ }
323
+ }
324
+
325
+ log.info(` Loaded ${this._skills.size} skills`);
326
+ this._loaded = true;
327
+ }
328
+
329
+ /**
330
+ * 递归查找 SKILL.md 或 AGENTS.md(支持多层嵌套目录)
331
+ * @param {string} dir - 要搜索的目录
332
+ * @returns {string|null} 找到的 markdown 文件路径
333
+ */
334
+ _findSkillMarkdown(dir) {
335
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
336
+ return null;
337
+ }
338
+
339
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
340
+
341
+ // 第一遍:优先查找 SKILL.MD
342
+ for (const entry of entries) {
343
+ if (entry.isFile() && entry.name.toUpperCase() === 'SKILL.MD') {
344
+ return path.join(dir, entry.name);
345
+ }
346
+ }
347
+
348
+ // 第二遍:查找 AGENTS.MD
349
+ for (const entry of entries) {
350
+ if (entry.isFile() && entry.name.toUpperCase() === 'AGENTS.MD') {
351
+ return path.join(dir, entry.name);
352
+ }
353
+ }
354
+
355
+ // 如果没找到,递归搜索子目录(排除 node_modules)
356
+ for (const entry of entries) {
357
+ if (entry.isDirectory() && entry.name !== 'node_modules') {
358
+ const subPath = path.join(dir, entry.name);
359
+ const found = this._findSkillMarkdown(subPath);
360
+ if (found) return found;
361
+ }
362
+ }
363
+
364
+ return null;
365
+ }
366
+
367
+ /**
368
+ * 加载单个 skill
369
+ */
370
+ _loadSkill(name, skillPath) {
371
+ // 查找 markdown 文件,优先加载 SKILL.md 或 AGENTS.md(支持多层嵌套)
372
+ const skillMdPath = this._findSkillMarkdown(skillPath);
373
+ if (!skillMdPath) {
374
+ throw new Error('No markdown file found');
375
+ }
376
+
377
+ const mainFile = skillMdPath;
378
+ const content = fs.readFileSync(mainFile, 'utf-8');
379
+ const frontmatter = parseFrontmatter(content);
380
+
381
+ if (!frontmatter) {
382
+ throw new Error('No valid frontmatter found');
383
+ }
384
+
385
+ const metadata = new SkillMetadata({
386
+ name: frontmatter.name || name,
387
+ description: frontmatter.description || '',
388
+ license: frontmatter.license,
389
+ compatibility: frontmatter.compatibility,
390
+ metadata: frontmatter.metadata || {},
391
+ allowedTools: frontmatter['allowed-tools'] || [],
392
+ });
393
+
394
+ const skill = new Skill(metadata, stripFrontmatter(content));
395
+ skill.install(this._framework);
396
+
397
+ // 扫描 references 子目录(按需加载的附加文档)
398
+ const references = this._scanReferences(skillPath);
399
+
400
+ // 扫描 scripts 子目录(可执行脚本)
401
+ const scripts = this._scanScripts(skillPath);
402
+
403
+ this._skills.set(name, {
404
+ name,
405
+ metadata,
406
+ content: skill.content,
407
+ instance: skill,
408
+ path: skillPath,
409
+ references,
410
+ scripts,
411
+ });
412
+
413
+ const refsInfo = references.size > 0 ? `, ${references.size} refs` : '';
414
+ const scriptsInfo = scripts.size > 0 ? `, ${scripts.size} scripts` : '';
415
+ //log.info(` Loaded skill: ${name}${refsInfo}${scriptsInfo}`)
416
+ }
417
+
418
+ /**
419
+ * 扫描 references 子目录,按需加载
420
+ * @param {string} skillPath - skill 目录路径
421
+ * @returns {Map} references 文件映射 { filename: { path, content } }
422
+ */
423
+ _scanReferences(skillPath) {
424
+ const references = new Map();
425
+ const refsDir = path.join(skillPath, 'references');
426
+
427
+ if (!fs.existsSync(refsDir) || !fs.statSync(refsDir).isDirectory()) {
428
+ return references;
429
+ }
430
+
431
+ try {
432
+ const entries = fs.readdirSync(refsDir, { withFileTypes: true });
433
+ for (const entry of entries) {
434
+ if (entry.isFile() && entry.name.endsWith('.md')) {
435
+ const refPath = path.join(refsDir, entry.name);
436
+ const refName = entry.name.replace('.md', '');
437
+ references.set(refName, {
438
+ path: refPath,
439
+ content: null, // 延迟加载
440
+ });
441
+ }
442
+ }
443
+ } catch (err) {
444
+ log.warn(` Failed to scan references for ${skillPath}:`, err.message);
445
+ }
446
+
447
+ return references;
448
+ }
449
+
450
+ /**
451
+ * 扫描 scripts 子目录,获取脚本列表
452
+ * @param {string} skillPath - skill 目录路径
453
+ * @returns {Map} scripts 映射 { filename: { path, isExecutable } }
454
+ */
455
+ _scanScripts(skillPath) {
456
+ const scripts = new Map();
457
+ const scriptsDir = path.join(skillPath, 'scripts');
458
+
459
+ if (!fs.existsSync(scriptsDir) || !fs.statSync(scriptsDir).isDirectory()) {
460
+ return scripts;
461
+ }
462
+
463
+ try {
464
+ const entries = fs.readdirSync(scriptsDir, { withFileTypes: true });
465
+ for (const entry of entries) {
466
+ if (entry.isFile()) {
467
+ const scriptPath = path.join(scriptsDir, entry.name);
468
+ // 检查文件是否有执行权限(或检查扩展名)
469
+ const isExecutable =
470
+ entry.name.endsWith('.sh') ||
471
+ entry.name.endsWith('.js') ||
472
+ entry.name.endsWith('.py') ||
473
+ entry.name.endsWith('.ts');
474
+ scripts.set(entry.name, {
475
+ path: scriptPath,
476
+ isExecutable,
477
+ });
478
+ }
479
+ }
480
+ } catch (err) {
481
+ log.warn(` Failed to scan scripts for ${skillPath}:`, err.message);
482
+ }
483
+
484
+ return scripts;
485
+ }
486
+
487
+ /**
488
+ * 按需加载 reference 文件
489
+ * @param {string} skillName - skill 名称
490
+ * @param {string} refName - reference 文件名(不含 .md)
491
+ * @returns {string|null} 文件内容
492
+ */
493
+ loadReference(skillName, refName) {
494
+ const skill = this._skills.get(skillName);
495
+ if (!skill || !skill.references.has(refName)) {
496
+ return null;
497
+ }
498
+
499
+ const ref = skill.references.get(refName);
500
+ if (!ref.content) {
501
+ try {
502
+ ref.content = fs.readFileSync(ref.path, 'utf-8');
503
+ } catch (err) {
504
+ log.error(` Failed to load reference ${skillName}/${refName}:`, err.message);
505
+ return null;
506
+ }
507
+ }
508
+
509
+ return ref.content;
510
+ }
511
+
512
+ /**
513
+ * 获取 skill 的 reference 列表
514
+ * @param {string} skillName
515
+ * @returns {string[]} reference 文件名列表
516
+ */
517
+ listReferences(skillName) {
518
+ const skill = this._skills.get(skillName);
519
+ if (!skill) return [];
520
+ return Array.from(skill.references.keys());
521
+ }
522
+
523
+ /**
524
+ * 获取 skill 的 scripts 列表
525
+ * @param {string} skillName
526
+ * @returns {Object[]} script 信息列表 [{ name, path, isExecutable }]
527
+ */
528
+ listScripts(skillName) {
529
+ const skill = this._skills.get(skillName);
530
+ if (!skill) return [];
531
+ return Array.from(skill.scripts.entries()).map(([name, info]) => ({
532
+ name,
533
+ path: info.path,
534
+ isExecutable: info.isExecutable,
535
+ }));
536
+ }
537
+
538
+ /**
539
+ * 读取脚本内容
540
+ * @param {string} skillName
541
+ * @param {string} scriptName
542
+ * @returns {string|null}
543
+ */
544
+ loadScript(skillName, scriptName) {
545
+ const skill = this._skills.get(skillName);
546
+ if (!skill || !skill.scripts.has(scriptName)) {
547
+ return null;
548
+ }
549
+
550
+ const script = skill.scripts.get(scriptName);
551
+ try {
552
+ return fs.readFileSync(script.path, 'utf-8');
553
+ } catch (err) {
554
+ log.error(` Failed to load script ${skillName}/${scriptName}:`, err.message);
555
+ return null;
556
+ }
557
+ }
558
+
559
+ /**
560
+ * 获取所有 skills
561
+ */
562
+ getAllSkills() {
563
+ return Array.from(this._skills.values());
564
+ }
565
+
566
+ /**
567
+ * 获取 skill
568
+ */
569
+ getSkill(name) {
570
+ return this._skills.get(name);
571
+ }
572
+
573
+ /**
574
+ * 检查 skill 是否存在
575
+ */
576
+ hasSkill(name) {
577
+ return this._skills.has(name);
578
+ }
579
+
580
+ /**
581
+ * 获取 skill 数量
582
+ */
583
+ size() {
584
+ return this._skills.size;
585
+ }
586
+
587
+ reload(framework) {
588
+ log.info(' Reloading...');
589
+ this._skills.clear();
590
+ this._loaded = false;
591
+ this._framework = framework;
592
+ this._loadAllSkills();
593
+ }
594
+
595
+ uninstall(framework) {
596
+ this._skills.clear();
597
+ this._framework = null;
598
+ this._loaded = false;
599
+ }
600
+ }
601
+
602
+ module.exports = {
603
+ SkillManagerPlugin,
604
+ Skill,
605
+ SkillMetadata,
606
+ isValidSkillName,
607
+ parseFrontmatter,
608
+ stripFrontmatter,
609
+ };