foliko 2.0.4 → 2.0.6

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 (59) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/.cli_default_systemPrompt.md +291 -0
  3. package/CLAUDE.md +3 -0
  4. package/README.md +20 -3
  5. package/docs/architecture.md +34 -2
  6. package/docs/extensions.md +199 -0
  7. package/docs/migration.md +100 -0
  8. package/docs/public-api.md +1180 -6
  9. package/docs/usage.md +122 -30
  10. package/package.json +1 -1
  11. package/plugins/core/audit/index.js +1 -1
  12. package/plugins/core/default/bootstrap.js +65 -19
  13. package/plugins/core/python-loader/index.js +43 -25
  14. package/plugins/core/skill-manager/PROMPT.md +6 -0
  15. package/plugins/core/skill-manager/index.js +538 -93
  16. package/plugins/core/sub-agent/PROMPT.md +10 -0
  17. package/plugins/core/sub-agent/index.js +36 -3
  18. package/plugins/core/think/index.js +1 -0
  19. package/plugins/core/workflow/index.js +106 -22
  20. package/plugins/executors/data-splitter/PROMPT.md +13 -0
  21. package/plugins/executors/data-splitter/index.js +5 -4
  22. package/plugins/executors/extension/extension-registry.js +145 -0
  23. package/plugins/executors/extension/index.js +405 -437
  24. package/plugins/executors/extension/prompt-builder.js +359 -0
  25. package/plugins/executors/extension/skill-helper.js +143 -0
  26. package/plugins/messaging/feishu/index.js +5 -3
  27. package/plugins/messaging/qq/index.js +6 -4
  28. package/plugins/messaging/telegram/index.js +6 -3
  29. package/plugins/messaging/weixin/index.js +5 -3
  30. package/plugins/tools/PROMPT.md +26 -0
  31. package/plugins/tools/index.js +6 -5
  32. package/skills/foliko/AGENTS.md +196 -43
  33. package/skills/foliko/SKILL.md +157 -28
  34. package/skills/mcp/SKILL.md +77 -118
  35. package/skills/plugins/SKILL.md +89 -3
  36. package/skills/python/SKILL.md +57 -39
  37. package/skills/skill-guide/SKILL.md +42 -34
  38. package/skills/workflows/SKILL.md +224 -9
  39. package/skills/workflows/workflow-troubleshooting/SKILL.md +221 -281
  40. package/src/agent/chat.js +48 -27
  41. package/src/agent/main.js +34 -13
  42. package/src/agent/prompt-registry.js +133 -87
  43. package/src/agent/prompts/PROMPT.md +3 -0
  44. package/src/agent/sub.js +1 -1
  45. package/src/cli/ui/chat-ui-old.js +5 -2
  46. package/src/cli/ui/chat-ui.js +5 -2
  47. package/src/common/constants.js +12 -0
  48. package/src/common/error-capture.js +91 -0
  49. package/src/common/logger.js +2 -2
  50. package/src/context/compressor.js +6 -2
  51. package/src/executors/mcp-executor.js +105 -125
  52. package/src/framework/framework.js +632 -6
  53. package/src/index.js +4 -0
  54. package/src/plugin/base.js +913 -10
  55. package/src/plugin/manager.js +29 -8
  56. package/src/tool/schema.js +32 -9
  57. package/tests/core/plugin-prompts.test.js +13 -13
  58. package/tests/core/prompt-registry.test.js +59 -51
  59. package/website/index.html +821 -0
@@ -130,6 +130,205 @@ class SkillMetadata {
130
130
  }
131
131
  }
132
132
 
133
+ /**
134
+ * 从 commander.js 的 flags 字符串中提取字段名
135
+ * "--video-id <id>" → "videoId"
136
+ * "-v, --video-id <id>" → "videoId" (优先用长名)
137
+ * "--flag" → "flag" (布尔)
138
+ * "-v" → "v" (布尔)
139
+ */
140
+ function extractOptionName(flags) {
141
+ if (!flags) return null;
142
+ // 提取所有 long 选项(--xxx)和 short 选项(-x)
143
+ const tokens = flags.split(/[,\s]+/).filter(t => t && !t.includes('<'));
144
+ // 优先选 long 选项(--开头)
145
+ const longOpt = tokens.find(t => t.startsWith('--'));
146
+ const target = longOpt || tokens[0];
147
+ if (!target) return null;
148
+ // 去掉前导 -,转 camelCase
149
+ const stripped = target.replace(/^-+/, '');
150
+ return stripped.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
151
+ }
152
+
153
+ /**
154
+ * 判断 option 是否需要值(flags 包含 `<...>`)
155
+ */
156
+ function optionHasValue(flags) {
157
+ return /<[^>]+>/.test(flags || '');
158
+ }
159
+
160
+ /**
161
+ * 将 cmd.options 数组转换为 Zod 对象 schema
162
+ *
163
+ * 支持两种类型声明方式(优先级从高到低):
164
+ * 1. schema — 直接写 Zod 实例,如 { flags: '-n <value>', schema: z.number(), description: '...' }
165
+ * 2. type — 字符串类型,如 { flags: '-n <value>', type: 'number', description: '...' }
166
+ * 3. fallback — 无 schema/type,根据 hasValue 推断(string / boolean)
167
+ *
168
+ * type 可选值:'string' | 'number' | 'boolean' | 'array' | 'object'
169
+ */
170
+ function buildZodSchemaFromOptions(options, commandDescription) {
171
+ if (!Array.isArray(options) || options.length === 0) {
172
+ return z.object({}).passthrough();
173
+ }
174
+
175
+ const shape = {};
176
+ for (const opt of options) {
177
+ const name = extractOptionName(opt.flags);
178
+ if (!name) continue;
179
+
180
+ const hasValue = optionHasValue(opt.flags);
181
+ const isRequired = opt.required === true && hasValue;
182
+ let desc = opt.description || '';
183
+ if (opt.defaultValue !== undefined && opt.defaultValue !== '') {
184
+ desc += ` (默认: ${opt.defaultValue})`;
185
+ }
186
+
187
+ let fieldSchema;
188
+
189
+ // 优先用 schema(Zod 实例)
190
+ if (opt.schema && typeof opt.schema.parse === 'function') {
191
+ fieldSchema = opt.schema.describe(desc);
192
+ if (opt.defaultValue !== undefined && opt.defaultValue !== null) {
193
+ fieldSchema = fieldSchema.default(opt.defaultValue);
194
+ }
195
+ }
196
+ // 其次用 type(字符串类型)
197
+ else if (opt.type) {
198
+ fieldSchema = buildSchemaFromType(opt.type, desc, opt.defaultValue);
199
+ }
200
+ // 默认行为:带 <value> 为 string,否则 boolean
201
+ else if (hasValue) {
202
+ fieldSchema = z.string().describe(desc);
203
+ if (opt.defaultValue !== undefined && opt.defaultValue !== null && opt.defaultValue !== '') {
204
+ fieldSchema = fieldSchema.default(String(opt.defaultValue));
205
+ }
206
+ } else {
207
+ fieldSchema = z.boolean().describe(desc || '布尔标志');
208
+ }
209
+
210
+ if (!isRequired) {
211
+ fieldSchema = fieldSchema.optional();
212
+ }
213
+ shape[name] = fieldSchema;
214
+ }
215
+ return z.object(shape).passthrough();
216
+ }
217
+
218
+ /**
219
+ * 根据 type 字符串构建 Zod Schema
220
+ */
221
+ function buildSchemaFromType(type, desc, defaultValue) {
222
+ const LOWER = type.toLowerCase();
223
+ let schema;
224
+
225
+ switch (LOWER) {
226
+ case 'number':
227
+ case 'int':
228
+ case 'integer':
229
+ schema = z.number().describe(desc);
230
+ if (defaultValue !== undefined && defaultValue !== null) {
231
+ schema = schema.default(Number(defaultValue));
232
+ }
233
+ break;
234
+ case 'boolean':
235
+ case 'bool':
236
+ schema = z.boolean().describe(desc || '布尔标志');
237
+ if (defaultValue !== undefined && defaultValue !== null) {
238
+ schema = schema.default(Boolean(defaultValue));
239
+ }
240
+ break;
241
+ case 'array':
242
+ schema = z.array(z.string()).describe(desc);
243
+ if (defaultValue !== undefined && defaultValue !== null) {
244
+ const arr = Array.isArray(defaultValue) ? defaultValue : [defaultValue];
245
+ schema = schema.default(arr.map(String));
246
+ }
247
+ break;
248
+ case 'object':
249
+ schema = z.record(z.any()).describe(desc);
250
+ if (defaultValue !== undefined && defaultValue !== null && typeof defaultValue === 'object') {
251
+ schema = schema.default(defaultValue);
252
+ }
253
+ break;
254
+ case 'string':
255
+ default:
256
+ schema = z.string().describe(desc);
257
+ if (defaultValue !== undefined && defaultValue !== null && defaultValue !== '') {
258
+ schema = schema.default(String(defaultValue));
259
+ }
260
+ break;
261
+ }
262
+
263
+ return schema;
264
+ }
265
+
266
+ /**
267
+ * 生成等效的 JSON schema(用于 _rawSchema,让 ext_skill 渲染更清晰)
268
+ */
269
+ function buildRawJsonSchema(options) {
270
+ if (!Array.isArray(options) || options.length === 0) {
271
+ return { type: 'object', properties: {}, required: [] };
272
+ }
273
+ const properties = {};
274
+ const required = [];
275
+ for (const opt of options) {
276
+ const name = extractOptionName(opt.flags);
277
+ if (!name) continue;
278
+ const hasValue = optionHasValue(opt.flags);
279
+ const isRequired = opt.required === true && hasValue;
280
+ const desc = opt.description || '';
281
+
282
+ let jsonType = 'string';
283
+ if (opt.schema && typeof opt.schema.parse === 'function') {
284
+ // 从 Zod 实例猜测类型
285
+ const s = opt.schema;
286
+ if (s._def.typeName === 'ZodNumber' || s._def.typeName === 'ZodInt') jsonType = 'number';
287
+ else if (s._def.typeName === 'ZodBoolean') jsonType = 'boolean';
288
+ else if (s._def.typeName === 'ZodArray') jsonType = 'array';
289
+ else if (s._def.typeName === 'ZodObject' || s._def.typeName === 'ZodRecord') jsonType = 'object';
290
+ } else if (opt.type) {
291
+ const L = opt.type.toLowerCase();
292
+ if (L === 'number' || L === 'int' || L === 'integer') jsonType = 'number';
293
+ else if (L === 'boolean' || L === 'bool') jsonType = 'boolean';
294
+ else if (L === 'array') jsonType = 'array';
295
+ else if (L === 'object') jsonType = 'object';
296
+ } else {
297
+ jsonType = hasValue ? 'string' : 'boolean';
298
+ }
299
+
300
+ properties[name] = { type: jsonType, description: desc };
301
+ if (isRequired) required.push(name);
302
+ }
303
+ return { type: 'object', properties, required };
304
+ }
305
+
306
+ /**
307
+ * 从 options 构建示例命令行字符串
308
+ */
309
+ function buildExampleFromOptions(options) {
310
+ if (!Array.isArray(options) || options.length === 0) return '';
311
+ const parts = [];
312
+ for (const opt of options) {
313
+ const flags = opt.flags || '';
314
+ // 提取短选项和长选项
315
+ const tokens = flags.split(/[,\s]+/).filter(t => t && !t.includes('<'));
316
+ const shortOpt = tokens.find(t => /^-[a-zA-Z]$/.test(t));
317
+ const longOpt = tokens.find(t => t.startsWith('--'));
318
+ const flag = shortOpt || longOpt || tokens[0];
319
+ if (!flag) continue;
320
+
321
+ const hasValue = /<[^>]+>/.test(flags);
322
+ if (hasValue) {
323
+ const defaultVal = opt.defaultValue !== undefined && opt.defaultValue !== null ? String(opt.defaultValue) : '<value>';
324
+ parts.push(`${flag} ${defaultVal}`);
325
+ } else {
326
+ parts.push(flag);
327
+ }
328
+ }
329
+ return parts.join(' ');
330
+ }
331
+
133
332
  /**
134
333
  * Skill 类
135
334
  */
@@ -183,7 +382,7 @@ class Skill {
183
382
 
184
383
  const skillInfo = {
185
384
  name: skillName,
186
- description: this.metadata?.description || `Skill: ${skillName}`,
385
+ description: this.metadata?.description || '',
187
386
  version: this.metadata?.compatibility || '1.0.0',
188
387
  };
189
388
 
@@ -232,40 +431,71 @@ class Skill {
232
431
  return { ...program.opts(), args: program.args };
233
432
  } catch (err) {
234
433
  log.warn(`[SkillManager] Command parse error: ${err.message}`);
235
- return { args: rawArgs };
434
+ // Commander 解析失败时,用简单方式从 rawArgs 提取选项值
435
+ const result = {};
436
+ const tokens = parseShellArgs(rawArgs);
437
+ let i = 0;
438
+ while (i < tokens.length) {
439
+ const token = tokens[i];
440
+ // 匹配选项(-x 或 --xxx)
441
+ const optMatch = token.match(/^(-[a-zA-Z]|-{1,2}[a-zA-Z][a-zA-Z0-9-]*)$/);
442
+ if (optMatch) {
443
+ const optKey = optMatch[1];
444
+ // 找对应的 option
445
+ const opt = cmd.options.find(o => {
446
+ const flags = o.flags || '';
447
+ return flags.includes(optKey) || flags.includes(optKey.replace(/^--?/, '--'));
448
+ });
449
+ if (opt) {
450
+ const name = extractOptionName(opt.flags);
451
+ // 判断是否有值(下一个 token 不是选项)
452
+ const hasValue = /<[^>]+>/.test(opt.flags);
453
+ if (hasValue && i + 1 < tokens.length) {
454
+ const next = tokens[i + 1];
455
+ if (!next.match(/^-/)) {
456
+ result[name] = next;
457
+ i += 2;
458
+ continue;
459
+ } else {
460
+ // 下个 token 是另一个选项,但当前选项需要值,用默认值
461
+ result[name] = opt.defaultValue ?? undefined;
462
+ i += 1;
463
+ continue;
464
+ }
465
+ } else if (!hasValue) {
466
+ result[name] = true;
467
+ i += 1;
468
+ continue;
469
+ } else if (hasValue && i + 1 >= tokens.length) {
470
+ // 当前选项需要值但没有更多 token,用默认值
471
+ result[name] = opt.defaultValue ?? undefined;
472
+ i += 1;
473
+ continue;
474
+ }
475
+ }
476
+ }
477
+ i += 1;
478
+ }
479
+ // 剩余的 token 当 args
480
+ result.args = tokens.slice(i);
481
+ return result;
236
482
  }
237
483
  };
238
484
  }
239
485
 
240
- // 构建详细的命令描述,帮助 LLM 正确调用
241
- let commandHint = '';
242
- if (cmd.options && cmd.options.length > 0) {
243
- const optDescs = cmd.options.map(formatOptionForHelp);
244
- commandHint = `\n 可用选项:\n ${optDescs.join('\n ')}`;
245
- }
246
-
247
- // 生成示例命令
248
- const exampleCmd = cmd.options && cmd.options.length > 0
249
- ? cmd.options.map(o => {
250
- const flag = (o.flags || '').split(/[,\s]+/).filter(p => p && !p.includes('<') && !p.includes(','))[0]?.replace(/^-*/, '') || '';
251
- const hasValue = (o.flags || '').includes('<');
252
- const def = o.defaultValue !== undefined && o.defaultValue !== '' ? String(o.defaultValue) : '';
253
- const val = def || '<值>';
254
- return hasValue ? `-${flag} ${val}` : `-${flag}`;
255
- }).join(' ')
486
+ // 直接使用 command 字符串的 schema,describe 说明命令行格式
487
+ const exampleArgs = cmd.options && cmd.options.length > 0
488
+ ? buildExampleFromOptions(cmd.options)
256
489
  : '';
257
-
258
490
  const toolDef = {
259
- name: fullName,
260
- description: `${cmd.description || 'Skill command'}\n\n**重要**: args.command 是**字符串**,不是对象!\n例: args: { command: "${exampleCmd}" }\n\n${commandHint}`,
491
+ name: cmd.name,
492
+ description: cmd.description || 'Skill command',
261
493
  inputSchema: z.object({
262
- command: z.string().optional().describe(
263
- cmd.options && cmd.options.length > 0
264
- ? `**字符串**,空格分隔,如: ${cmd.options.map(formatOptionForExample).join(' ')}`
265
- : `**字符串**,命令行参数`
266
- ),
494
+ command: z.string().optional().describe(exampleArgs || '命令行字符串,如:-n value1 -v'),
267
495
  }),
268
496
  _options: cmd.options || null,
497
+ // 把原始 options 数组也保存下来,便于 ext_skill 显示
498
+ _rawSchema: buildRawJsonSchema(cmd.options),
269
499
  execute: async (toolArgs, framework) => {
270
500
  const ctx = {
271
501
  sessionId: framework?._currentSessionId || 'unknown',
@@ -273,16 +503,27 @@ class Skill {
273
503
  framework: framework,
274
504
  cwd: framework?.getCwd?.() ?? process.cwd(),
275
505
  };
276
- let parsedArgs = toolArgs.command || '';
506
+ // 直接使用 command 字符串
507
+ const commandString = toolArgs?.command || '';
508
+ let parsedArgs = commandString;
277
509
  if (typeof argumentParser === 'function') {
278
- parsedArgs = argumentParser(toolArgs.command || '');
510
+ parsedArgs = argumentParser(commandString);
279
511
  }
280
512
  return await handler(parsedArgs, ctx);
281
513
  },
282
514
  };
283
515
 
284
- // 注册到 extension 系统
285
- extExecutor.registerTool('skill', skillInfo, toolDef);
516
+ // 注册到 extension 系统:每个 skill 注册为独立扩展 (skill:<skillname>)
517
+ const skillExtName = `skill:${skillName}`;
518
+ // 优先用 SKILL.md frontmatter 的 description,回退到空(提示词会隐藏)
519
+ const extDescription = skillInfo.description
520
+ ? skillInfo.description
521
+ : `Skill ${skillName}`;
522
+ extExecutor.registerTool(skillExtName, {
523
+ name: skillExtName,
524
+ description: extDescription,
525
+ version: skillInfo.version || '1.0.0',
526
+ }, toolDef);
286
527
 
287
528
  // 同时添加到 this.tools,供 extension-executor 的 _scanPluginTools 扫描
288
529
  this.tools[fullName] = toolDef;
@@ -298,15 +539,20 @@ class Skill {
298
539
  /**
299
540
  * SkillManager 插件
300
541
  */
542
+ const PROMPT_DIR = __dirname;
543
+
301
544
  class SkillManagerPlugin extends Plugin {
302
545
  // 声明式 prompt parts
303
546
  prompts = [
304
547
  {
305
548
  name: 'skills',
306
- slot: 'CAPABILITY',
549
+ file: path.join(PROMPT_DIR, 'PROMPT.md'),
550
+ scope: 'global',
551
+ priority: 60,
307
552
  description: '可用技能列表',
308
- provider: function () {
309
- return this._buildSkillsDescription();
553
+ interpolate: (content) => {
554
+ const list = this._buildSkillsList();
555
+ return content.replace('{{skills}}', list || '(暂无技能)');
310
556
  },
311
557
  },
312
558
  ];
@@ -325,6 +571,9 @@ class SkillManagerPlugin extends Plugin {
325
571
  : [config.skillsDir || '.foliko/skills', 'skills'];
326
572
  this._skills = new Map();
327
573
  this._loaded = false;
574
+ // _loadReady 暴露给外部等待首加载完成(bootstrap 在创建 MainAgent 前 await 它,
575
+ // 确保系统提示词第一次构建就能看到所有 skill 命令)
576
+ this._loadReady = Promise.resolve();
328
577
  this.tools = {}; // 用于 extension-executor 扫描(由 Skill.registerCommands 填充)
329
578
  }
330
579
 
@@ -335,7 +584,9 @@ class SkillManagerPlugin extends Plugin {
335
584
 
336
585
  start(framework) {
337
586
  super.start(framework);
338
- this._loadAllSkills().catch(err => log.error('_loadAllSkills failed:', err.message));
587
+ this._loadReady = this._loadAllSkills().catch(err => {
588
+ log.error('_loadAllSkills failed:', err.message);
589
+ });
339
590
 
340
591
  // 注册 skill_load 工具
341
592
  framework.registerTool({
@@ -372,7 +623,7 @@ class SkillManagerPlugin extends Plugin {
372
623
  }
373
624
  cmdLines.push('');
374
625
  }
375
- cmdLines.push('调用:`ext_call({ plugin: "skill", tool: "' + skillName + ':<命令>", args: { command: "参数" } })`');
626
+ cmdLines.push('调用:`ext_call({ plugin: "skill:' + skillName + '", tool: "<command>", args: { /* Zod 结构化参数 */ } })`');
376
627
  commandDocs = cmdLines.join('\n');
377
628
  }
378
629
  return {
@@ -490,70 +741,23 @@ class SkillManagerPlugin extends Plugin {
490
741
  }
491
742
 
492
743
  /**
493
- * 构建技能描述
744
+ * 构建技能列表(简版,仅显示技能名 + 描述)
745
+ * 命令详情在【Extensions】扩展插件段落有完整展示(含 Zod 参数)
746
+ * 详细使用指南用 skill_load("技能名") 加载
494
747
  */
495
- _buildSkillsDescription() {
748
+ _buildSkillsList() {
496
749
  const skills = this.getAllSkills();
497
750
  if (!skills || skills.length === 0) {
498
751
  return '';
499
752
  }
500
753
 
501
- const lines = [
502
- '## 可用技能',
503
- '命令调用(仅对上方标注了「命令:」的技能有效):`ext_call({ plugin: "skill", tool: "<技能名>:<命令>", args: { command: "参数" } })`',
504
- '',
505
- ];
506
-
507
- const extExecutor = this._framework?.pluginManager?.get('extension-executor');
508
- const skillCommands = extExecutor?.getSkillCommands?.() || [];
509
-
510
- // 按技能名分组命令(保留完整信息以渲染选项)
511
- const toolsBySkill = {};
512
- for (const tool of skillCommands) {
513
- if (!tool || !tool.name) continue;
514
- const parts = tool.name.split(':');
515
- if (parts.length < 2) continue;
516
- const skillName = parts[0];
517
- const cmdName = parts.slice(1).join(':');
518
- if (!toolsBySkill[skillName]) toolsBySkill[skillName] = [];
519
- toolsBySkill[skillName].push({ name: cmdName, description: tool.description || '', options: tool.options || null });
520
- }
521
-
522
- // 一列一技能,命令含选项时显示参数
754
+ const lines = [];
523
755
  for (const skill of skills) {
524
756
  const name = skill.metadata?.name || skill.name || 'unknown';
525
757
  const descText = skill.metadata?.description || '';
526
- if (!descText && !toolsBySkill[name]) continue;
527
-
528
- const parts = [];
529
- if (descText) parts.push(descText);
530
-
531
- if (toolsBySkill[name]) {
532
- const cmdStrs = toolsBySkill[name].map(cmd => {
533
- if (cmd.options && cmd.options.length > 0) {
534
- const optStrs = cmd.options.map(o => {
535
- // 提取长选项名(--xxx)作为参数名,形如 "--tokens <值>"
536
- const flag = o.flags || '';
537
- const longOpt = flag.split(/\s+/).filter(f => f.startsWith('--'))[0];
538
- const valPart = flag.includes('<') ? flag.match(/<[^>]+>/)?.[0] || '' : '';
539
- if (longOpt && valPart) return `${longOpt} ${valPart}`;
540
- if (longOpt) return longOpt;
541
- return flag.split(/\s+/).filter(f => f && !f.includes('<')).map(f => f.replace(/,$/, ''))[0] || flag;
542
- });
543
- return `${cmd.name}(${optStrs.join(', ')})`;
544
- }
545
- return cmd.name;
546
- });
547
- parts.push(`命令: ${cmdStrs.join(', ')}`);
548
- }
549
- lines.push(`- **${name}**: ${parts.join(' | ')}`);
758
+ if (!descText) continue;
759
+ lines.push(`- **${name}**: ${descText}`);
550
760
  }
551
-
552
- lines.push(
553
- '',
554
- '> **提示**:使用 `skill_load("${skill名}")` 加载技能获取详细使用指南。'
555
- );
556
-
557
761
  return lines.join('\n');
558
762
  }
559
763
 
@@ -631,6 +835,19 @@ class SkillManagerPlugin extends Plugin {
631
835
 
632
836
  // log.info(` Loaded ${this._skills.size} skills`);
633
837
  this._loaded = true;
838
+
839
+ // 通知 extension-executor 失效 prompt 缓存,让 AI 看到新注册的 skill 命令
840
+ // 原因: _loadAllSkills 是异步的(_loadSkill 中 await skill.registerCommands() 会动态 import ESM 包),
841
+ // 但 framework:ready 在所有 plugin loadPlugin() 之后立即触发,那时 skill 命令可能还没注册。
842
+ // 不刷缓存的话,首启动后模型看不到这些命令,必须 reload 一次才出现。
843
+ if (this._framework) {
844
+ try {
845
+ const extExec = this._framework.pluginManager?.get('extension-executor');
846
+ if (extExec && typeof extExec._invalidatePromptCaches === 'function') {
847
+ extExec._invalidatePromptCaches();
848
+ }
849
+ } catch (_) { /* 失效失败不影响 skill 加载 */ }
850
+ }
634
851
  }
635
852
 
636
853
  /**
@@ -1006,6 +1223,233 @@ class SkillManagerPlugin extends Plugin {
1006
1223
  return this._skills.has(name);
1007
1224
  }
1008
1225
 
1226
+ /**
1227
+ * 获取 skill 详情(结构化信息)
1228
+ */
1229
+ getSkillDetails(name) {
1230
+ const skill = this._skills.get(name);
1231
+ if (!skill) return null;
1232
+ return {
1233
+ name: skill.name,
1234
+ description: skill.metadata?.description || '',
1235
+ content: skill.content || '',
1236
+ path: skill.path || null,
1237
+ metadata: skill.metadata ? {
1238
+ name: skill.metadata.name,
1239
+ description: skill.metadata.description,
1240
+ license: skill.metadata.license,
1241
+ compatibility: skill.metadata.compatibility,
1242
+ allowedTools: skill.metadata.allowedTools,
1243
+ } : null,
1244
+ commands: skill.instance?._commands || [],
1245
+ references: skill.references ? Array.from(skill.references.keys()) : [],
1246
+ scripts: skill.scripts ? Array.from(skill.scripts.keys()) : [],
1247
+ fromPlugin: skill.fromPlugin || false,
1248
+ registered: skill.registered || false,
1249
+ };
1250
+ }
1251
+
1252
+ /**
1253
+ * 注册一个新 skill(程序化注册,不依赖文件)
1254
+ * @param {string} name - 技能名称
1255
+ * @param {string} content - 技能内容(markdown)
1256
+ * @param {Object} [metadata] - 技能元数据
1257
+ * @param {Object} [options] - 选项
1258
+ * @param {Function[]} [options.commands] - 命令定义数组 [{ name, description, options, execute }]
1259
+ * @param {string} [options.path] - 技能路径(用于 references/scripts 解析)
1260
+ * @returns {Object} 注册的 skill 对象
1261
+ */
1262
+ register(name, content, metadata = {}, options = {}) {
1263
+ const skillMetadata = new SkillMetadata({
1264
+ name: name,
1265
+ description: metadata.description || '',
1266
+ license: metadata.license || null,
1267
+ compatibility: metadata.compatibility || null,
1268
+ metadata: metadata.metadata || {},
1269
+ allowedTools: metadata['allowed-tools'] || metadata.allowedTools || [],
1270
+ });
1271
+
1272
+ const skill = new Skill(skillMetadata, content);
1273
+ skill._skillPath = options.path || null;
1274
+ skill.install(this._framework);
1275
+
1276
+ // 注册命令
1277
+ if (Array.isArray(options.commands) && options.commands.length > 0) {
1278
+ for (const cmd of options.commands) {
1279
+ if (!cmd || !cmd.name) continue;
1280
+
1281
+ const handler = typeof cmd.execute === 'function' ? cmd.execute : null;
1282
+ if (!handler) continue;
1283
+
1284
+ const fullName = `${name}:${cmd.name}`;
1285
+
1286
+ // 获取参数解析器
1287
+ let argumentParser = cmd.argumentParser;
1288
+ if (!argumentParser && Array.isArray(cmd.options) && cmd.options.length > 0) {
1289
+ argumentParser = (rawArgs) => {
1290
+ try {
1291
+ const program = new Command();
1292
+ program.exitOverride();
1293
+ program.configureOutput({ writeErr: () => {} });
1294
+ for (const opt of cmd.options) {
1295
+ program.option(opt.flags, opt.description, opt.defaultValue);
1296
+ }
1297
+ program.arguments('[args...]');
1298
+ program.parse(['node', 'cmd', ...parseShellArgs(rawArgs)]);
1299
+ return { ...program.opts(), args: program.args };
1300
+ } catch (err) {
1301
+ log.warn(`[SkillManager] Command parse error: ${err.message}`);
1302
+ // Commander 解析失败时,用简单方式从 rawArgs 提取选项值
1303
+ const result = {};
1304
+ const tokens = parseShellArgs(rawArgs);
1305
+ let i = 0;
1306
+ while (i < tokens.length) {
1307
+ const token = tokens[i];
1308
+ const optMatch = token.match(/^(-[a-zA-Z]|-{1,2}[a-zA-Z][a-zA-Z0-9-]*)$/);
1309
+ if (optMatch) {
1310
+ const optKey = optMatch[1];
1311
+ const opt = cmd.options.find(o => {
1312
+ const flags = o.flags || '';
1313
+ return flags.includes(optKey) || flags.includes(optKey.replace(/^--?/, '--'));
1314
+ });
1315
+ if (opt) {
1316
+ const name = extractOptionName(opt.flags);
1317
+ const hasValue = /<[^>]+>/.test(opt.flags);
1318
+ if (hasValue && i + 1 < tokens.length) {
1319
+ const next = tokens[i + 1];
1320
+ if (!next.match(/^-/)) {
1321
+ result[name] = next;
1322
+ i += 2;
1323
+ continue;
1324
+ }
1325
+ } else if (!hasValue) {
1326
+ result[name] = true;
1327
+ i += 1;
1328
+ continue;
1329
+ }
1330
+ }
1331
+ }
1332
+ i += 1;
1333
+ }
1334
+ result.args = tokens.slice(i);
1335
+ return result;
1336
+ }
1337
+ };
1338
+ }
1339
+
1340
+ // 转为 Zod schema(统一风格)
1341
+ const exampleArgs2 = cmd.options && cmd.options.length > 0
1342
+ ? buildExampleFromOptions(cmd.options)
1343
+ : '';
1344
+ const toolDef = {
1345
+ name: cmd.name, // 只用命令名
1346
+ description: cmd.description || 'Skill command',
1347
+ inputSchema: z.object({
1348
+ command: z.string().optional().describe(exampleArgs2 || '命令行字符串'),
1349
+ }),
1350
+ _options: cmd.options || null,
1351
+ _rawSchema: buildRawJsonSchema(cmd.options),
1352
+ execute: async (toolArgs, framework) => {
1353
+ const ctx = {
1354
+ sessionId: framework?._currentSessionId || 'unknown',
1355
+ agent: null,
1356
+ framework: framework,
1357
+ cwd: framework?.getCwd?.() ?? process.cwd(),
1358
+ };
1359
+ const commandString = toolArgs?.command || '';
1360
+ let parsedArgs = commandString;
1361
+ if (typeof argumentParser === 'function') {
1362
+ parsedArgs = argumentParser(commandString);
1363
+ }
1364
+ return await handler(parsedArgs, ctx);
1365
+ },
1366
+ };
1367
+
1368
+ const extExecutor = this._framework?.pluginManager?.get('extension-executor');
1369
+ if (extExecutor) {
1370
+ // 每个 skill 注册为独立扩展 (skill:<name>)
1371
+ // 优先用 skillMetadata.description,回退到占位文本
1372
+ const extDescription = skillMetadata.description
1373
+ ? skillMetadata.description
1374
+ : `Skill ${name}`;
1375
+ extExecutor.registerTool(`skill:${name}`, {
1376
+ name: `skill:${name}`,
1377
+ description: extDescription,
1378
+ version: skillMetadata.compatibility || '1.0.0',
1379
+ }, toolDef);
1380
+ }
1381
+
1382
+ skill.tools[fullName] = toolDef;
1383
+ skill._commands.push(fullName);
1384
+ }
1385
+ }
1386
+
1387
+ // 扫描 references 和 scripts
1388
+ const references = skill._skillPath ? this._scanReferences(skill._skillPath) : new Map();
1389
+ const scripts = skill._skillPath ? this._scanScripts(skill._skillPath) : new Map();
1390
+
1391
+ const skillData = {
1392
+ name: name,
1393
+ metadata: skillMetadata,
1394
+ content: content,
1395
+ instance: skill,
1396
+ path: skill._skillPath,
1397
+ references,
1398
+ scripts,
1399
+ fromPlugin: false,
1400
+ registered: true, // 标记为程序化注册
1401
+ };
1402
+
1403
+ this._skills.set(name, skillData);
1404
+
1405
+ // 更新 SkillManagerPlugin.tools
1406
+ Object.assign(this.tools, skill.tools);
1407
+
1408
+ return skillData;
1409
+ }
1410
+
1411
+ /**
1412
+ * 移除 skill
1413
+ * @param {string} name 技能名称
1414
+ * @returns {boolean} 是否成功移除
1415
+ */
1416
+ remove(name) {
1417
+ if (!this._skills.has(name)) {
1418
+ return false;
1419
+ }
1420
+
1421
+ const skill = this._skills.get(name);
1422
+
1423
+ // 清理命令(从 extension-executor 移除)
1424
+ // 新结构:每个 skill 注册为 `skill:<name>` 扩展,命令是该扩展下的 tool
1425
+ if (skill.instance?._commands) {
1426
+ const extExecutor = this._framework?.pluginManager?.get('extension-executor');
1427
+ if (extExecutor) {
1428
+ const extName = `skill:${name}`;
1429
+ for (const cmdFullName of skill.instance._commands) {
1430
+ // cmdFullName 格式: `<skill>:<command>`,如 'demo-skill:hello'
1431
+ const cmdName = cmdFullName.includes(':') ? cmdFullName.split(':').slice(1).join(':') : cmdFullName;
1432
+ extExecutor._unregisterTool?.(extName, cmdName);
1433
+ }
1434
+ // 如果该扩展下没有工具了,移除整个扩展
1435
+ const ext = extExecutor._registry?.getPlugin?.(extName);
1436
+ if (ext && (!ext.tools || ext.tools.length === 0)) {
1437
+ extExecutor._registry.clearPlugin?.(extName);
1438
+ }
1439
+ }
1440
+ }
1441
+
1442
+ // 从 SkillManagerPlugin.tools 中移除
1443
+ if (skill.instance?.tools) {
1444
+ for (const toolName of Object.keys(skill.instance.tools)) {
1445
+ delete this.tools[toolName];
1446
+ }
1447
+ }
1448
+
1449
+ this._skills.delete(name);
1450
+ return true;
1451
+ }
1452
+
1009
1453
  /**
1010
1454
  * 获取 skill 数量
1011
1455
  */
@@ -1020,9 +1464,10 @@ class SkillManagerPlugin extends Plugin {
1020
1464
  this._framework = framework;
1021
1465
 
1022
1466
  // 清理 extension-executor 中的旧 skill 命令,避免重载后残留
1467
+ // 注意: extExecutor 的 Map 在 _registry._extensions 上,_extensions 自身是 undefined
1023
1468
  const extExecutor = framework.pluginManager?.get('extension-executor');
1024
- if (extExecutor) {
1025
- extExecutor._extensions.delete('skill');
1469
+ if (extExecutor?._registry?.clearPlugin) {
1470
+ extExecutor._registry.clearPlugin('skill');
1026
1471
  }
1027
1472
 
1028
1473
  await this._loadAllSkills();
@@ -1034,8 +1479,8 @@ class SkillManagerPlugin extends Plugin {
1034
1479
  async onCwdChanged(oldCwd, newCwd, framework) {
1035
1480
  // 切换工作目录前清理旧 skill 命令
1036
1481
  const extExecutor = framework.pluginManager?.get('extension-executor');
1037
- if (extExecutor) {
1038
- extExecutor._extensions.delete('skill');
1482
+ if (extExecutor?._registry?.clearPlugin) {
1483
+ extExecutor._registry.clearPlugin('skill');
1039
1484
  }
1040
1485
  }
1041
1486