foliko 2.0.5 → 2.0.7

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 (93) 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 +280 -24
  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 +44 -19
  13. package/plugins/core/python-loader/index.js +43 -25
  14. package/plugins/core/scheduler/index.js +1 -0
  15. package/plugins/core/skill-manager/PROMPT.md +6 -0
  16. package/plugins/core/skill-manager/index.js +402 -115
  17. package/plugins/core/sub-agent/PROMPT.md +10 -0
  18. package/plugins/core/sub-agent/index.js +36 -3
  19. package/plugins/core/think/index.js +1 -0
  20. package/plugins/core/workflow/context.js +941 -0
  21. package/plugins/core/workflow/engine.js +66 -0
  22. package/plugins/core/workflow/examples/01-basic.js +42 -0
  23. package/plugins/core/workflow/examples/01-basic.json +30 -0
  24. package/plugins/core/workflow/examples/02-choice.js +75 -0
  25. package/plugins/core/workflow/examples/02-choice.json +59 -0
  26. package/plugins/core/workflow/examples/03-chain-style.js +114 -0
  27. package/plugins/core/workflow/examples/03-each.json +41 -0
  28. package/plugins/core/workflow/examples/04-parallel.js +52 -0
  29. package/plugins/core/workflow/examples/04-parallel.json +51 -0
  30. package/plugins/core/workflow/examples/05-chain-style.json +68 -0
  31. package/plugins/core/workflow/examples/05-each.js +65 -0
  32. package/plugins/core/workflow/examples/06-script.js +82 -0
  33. package/plugins/core/workflow/examples/07-module-export.js +43 -0
  34. package/plugins/core/workflow/examples/08-default-export.js +29 -0
  35. package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
  36. package/plugins/core/workflow/examples/10-logger.js +34 -0
  37. package/plugins/core/workflow/examples/11-storage.js +68 -0
  38. package/plugins/core/workflow/examples/simple.js +77 -0
  39. package/plugins/core/workflow/examples/simple.json +75 -0
  40. package/plugins/core/workflow/index.js +204 -78
  41. package/plugins/core/workflow/js-runner.js +318 -0
  42. package/plugins/core/workflow/json-runner.js +323 -0
  43. package/plugins/core/workflow/stages/action.js +211 -0
  44. package/plugins/core/workflow/stages/choice.js +74 -0
  45. package/plugins/core/workflow/stages/delay.js +73 -0
  46. package/plugins/core/workflow/stages/each.js +123 -0
  47. package/plugins/core/workflow/stages/parallel.js +69 -0
  48. package/plugins/core/workflow/stages/try.js +142 -0
  49. package/plugins/executors/data-splitter/PROMPT.md +13 -0
  50. package/plugins/executors/data-splitter/index.js +8 -6
  51. package/plugins/executors/extension/extension-registry.js +145 -0
  52. package/plugins/executors/extension/index.js +405 -437
  53. package/plugins/executors/extension/prompt-builder.js +359 -0
  54. package/plugins/executors/extension/skill-helper.js +143 -0
  55. package/plugins/messaging/feishu/index.js +5 -3
  56. package/plugins/messaging/qq/index.js +6 -4
  57. package/plugins/messaging/telegram/index.js +6 -3
  58. package/plugins/messaging/weixin/index.js +5 -3
  59. package/plugins/tools/PROMPT.md +26 -0
  60. package/plugins/tools/index.js +6 -5
  61. package/sandbox/check-context.js +5 -0
  62. package/sandbox/test-context.js +27 -0
  63. package/sandbox/test-fixes.js +40 -0
  64. package/sandbox/test-hello-js.js +46 -0
  65. package/skills/foliko/AGENTS.md +196 -43
  66. package/skills/foliko/SKILL.md +157 -28
  67. package/skills/mcp/SKILL.md +77 -118
  68. package/skills/plugins/SKILL.md +89 -3
  69. package/skills/python/SKILL.md +57 -39
  70. package/skills/skill-guide/SKILL.md +42 -34
  71. package/skills/workflows/SKILL.md +753 -436
  72. package/src/agent/chat.js +56 -28
  73. package/src/agent/main.js +39 -14
  74. package/src/agent/prompt-registry.js +56 -16
  75. package/src/agent/prompts/PROMPT.md +3 -0
  76. package/src/agent/sub.js +1 -1
  77. package/src/cli/ui/chat-ui-old.js +5 -2
  78. package/src/cli/ui/chat-ui.js +9 -5
  79. package/src/common/constants.js +12 -0
  80. package/src/common/error-capture.js +91 -0
  81. package/src/common/json-safe.js +20 -0
  82. package/src/common/logger.js +2 -2
  83. package/src/context/compressor.js +6 -2
  84. package/src/executors/mcp-executor.js +105 -125
  85. package/src/framework/framework.js +78 -12
  86. package/src/index.js +4 -0
  87. package/src/plugin/base.js +908 -5
  88. package/src/plugin/manager.js +124 -9
  89. package/src/tool/schema.js +32 -9
  90. package/src/utils/sandbox.js +1 -1
  91. package/website/index.html +821 -0
  92. package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
  93. package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -391
@@ -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,32 @@ 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
+
1009
1252
  /**
1010
1253
  * 注册一个新 skill(程序化注册,不依赖文件)
1011
1254
  * @param {string} name - 技能名称
@@ -1055,29 +1298,57 @@ class SkillManagerPlugin extends Plugin {
1055
1298
  program.parse(['node', 'cmd', ...parseShellArgs(rawArgs)]);
1056
1299
  return { ...program.opts(), args: program.args };
1057
1300
  } catch (err) {
1058
- return { args: rawArgs };
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;
1059
1336
  }
1060
1337
  };
1061
1338
  }
1062
1339
 
1063
- // 构建命令描述
1064
- let commandHint = '';
1065
- if (cmd.options && cmd.options.length > 0) {
1066
- const optDescs = cmd.options.map(formatOptionForHelp);
1067
- commandHint = `\n 可用选项:\n ${optDescs.join('\n ')}`;
1068
- }
1069
-
1340
+ // 转为 Zod schema(统一风格)
1341
+ const exampleArgs2 = cmd.options && cmd.options.length > 0
1342
+ ? buildExampleFromOptions(cmd.options)
1343
+ : '';
1070
1344
  const toolDef = {
1071
- name: fullName,
1072
- description: `${cmd.description || 'Skill command'}\n\n**重要**: args.command 是**字符串**,不是对象!\n${commandHint}`,
1345
+ name: cmd.name, // 只用命令名
1346
+ description: cmd.description || 'Skill command',
1073
1347
  inputSchema: z.object({
1074
- command: z.string().optional().describe(
1075
- cmd.options && cmd.options.length > 0
1076
- ? `**字符串**,空格分隔,如: ${cmd.options.map(formatOptionForExample).join(' ')}`
1077
- : `**字符串**,命令行参数`
1078
- ),
1348
+ command: z.string().optional().describe(exampleArgs2 || '命令行字符串'),
1079
1349
  }),
1080
1350
  _options: cmd.options || null,
1351
+ _rawSchema: buildRawJsonSchema(cmd.options),
1081
1352
  execute: async (toolArgs, framework) => {
1082
1353
  const ctx = {
1083
1354
  sessionId: framework?._currentSessionId || 'unknown',
@@ -1085,9 +1356,10 @@ class SkillManagerPlugin extends Plugin {
1085
1356
  framework: framework,
1086
1357
  cwd: framework?.getCwd?.() ?? process.cwd(),
1087
1358
  };
1088
- let parsedArgs = toolArgs.command || '';
1359
+ const commandString = toolArgs?.command || '';
1360
+ let parsedArgs = commandString;
1089
1361
  if (typeof argumentParser === 'function') {
1090
- parsedArgs = argumentParser(toolArgs.command || '');
1362
+ parsedArgs = argumentParser(commandString);
1091
1363
  }
1092
1364
  return await handler(parsedArgs, ctx);
1093
1365
  },
@@ -1095,9 +1367,14 @@ class SkillManagerPlugin extends Plugin {
1095
1367
 
1096
1368
  const extExecutor = this._framework?.pluginManager?.get('extension-executor');
1097
1369
  if (extExecutor) {
1098
- extExecutor.registerTool('skill', {
1099
- name: name,
1100
- description: skillMetadata.description || `Skill: ${name}`,
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,
1101
1378
  version: skillMetadata.compatibility || '1.0.0',
1102
1379
  }, toolDef);
1103
1380
  }
@@ -1144,11 +1421,20 @@ class SkillManagerPlugin extends Plugin {
1144
1421
  const skill = this._skills.get(name);
1145
1422
 
1146
1423
  // 清理命令(从 extension-executor 移除)
1424
+ // 新结构:每个 skill 注册为 `skill:<name>` 扩展,命令是该扩展下的 tool
1147
1425
  if (skill.instance?._commands) {
1148
1426
  const extExecutor = this._framework?.pluginManager?.get('extension-executor');
1149
1427
  if (extExecutor) {
1150
- for (const cmd of skill.instance._commands) {
1151
- extExecutor._extensions.delete(`${name}:${cmd}`);
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);
1152
1438
  }
1153
1439
  }
1154
1440
  }
@@ -1178,9 +1464,10 @@ class SkillManagerPlugin extends Plugin {
1178
1464
  this._framework = framework;
1179
1465
 
1180
1466
  // 清理 extension-executor 中的旧 skill 命令,避免重载后残留
1467
+ // 注意: extExecutor 的 Map 在 _registry._extensions 上,_extensions 自身是 undefined
1181
1468
  const extExecutor = framework.pluginManager?.get('extension-executor');
1182
- if (extExecutor) {
1183
- extExecutor._extensions.delete('skill');
1469
+ if (extExecutor?._registry?.clearPlugin) {
1470
+ extExecutor._registry.clearPlugin('skill');
1184
1471
  }
1185
1472
 
1186
1473
  await this._loadAllSkills();
@@ -1192,8 +1479,8 @@ class SkillManagerPlugin extends Plugin {
1192
1479
  async onCwdChanged(oldCwd, newCwd, framework) {
1193
1480
  // 切换工作目录前清理旧 skill 命令
1194
1481
  const extExecutor = framework.pluginManager?.get('extension-executor');
1195
- if (extExecutor) {
1196
- extExecutor._extensions.delete('skill');
1482
+ if (extExecutor?._registry?.clearPlugin) {
1483
+ extExecutor._registry.clearPlugin('skill');
1197
1484
  }
1198
1485
  }
1199
1486
 
@@ -0,0 +1,10 @@
1
+ {{subagents}}
2
+
3
+ ### 子 Agent 调用规则
4
+
5
+ > **重要**:必须严格遵守以下规则
6
+
7
+ 1. 根据上述「子 Agent 匹配表」,将任务委托给最匹配的子 Agent 处理
8
+ 2. 使用 `subagent_call` 工具并指定 `agentName` 来委托任务
9
+ 3. 只有当没有匹配的子 Agent 时,才直接调用工具
10
+ 4. 子 Agent 是没有记忆的,有需要的时候,有需要的时候,需要传递给上下文给子 Agent