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
@@ -42,6 +42,15 @@ class PluginManager {
42
42
  * 注册插件(不加载,只记录状态)
43
43
  */
44
44
  register(plugin, options = {}) {
45
+ // 处理 function(Plugin) { return class... } 格式
46
+ if (typeof plugin === 'function' && !(plugin instanceof Plugin)) {
47
+ try {
48
+ plugin = plugin(Plugin);
49
+ } catch (err) {
50
+ throw new PluginError(`Plugin factory function failed: ${err.message}`);
51
+ }
52
+ }
53
+
45
54
  if (!(plugin instanceof Plugin)) {
46
55
  throw new PluginError('Plugin must be an instance of Plugin');
47
56
  }
@@ -69,7 +78,7 @@ class PluginManager {
69
78
  instance: plugin,
70
79
  status: 'registered',
71
80
  enabled,
72
- sourcePath: plugin.sourcePath || null,
81
+ sourcePath: this._detectPluginSourcePath(plugin, plugin.sourcePath),
73
82
  });
74
83
  this._knownPlugins.add(plugin.name);
75
84
  this._log.debug(`Plugin registered: ${plugin.name} (enabled: ${enabled})`);
@@ -80,6 +89,71 @@ class PluginManager {
80
89
  return this._plugins.has(name);
81
90
  }
82
91
 
92
+ /**
93
+ * Find the source file path for a plugin instance by scanning plugin
94
+ * class names in candidate directories (used as a fallback when the
95
+ * plugin was created directly via `new SomePlugin(...)` instead of going
96
+ * through `loadPluginFromPath`).
97
+ */
98
+ _detectPluginSourcePath(plugin, declared) {
99
+ if (declared) return declared;
100
+
101
+ // For system plugins registered in plugins/core/*, the class name often
102
+ // matches the on-disk filename (e.g. `WorkflowPlugin` => `index.js` of
103
+ // `plugins/core/workflow`, `SubAgentManagerPlugin` => `.../sub-agent/index.js`).
104
+ const candidates = [];
105
+ const fs = require('fs');
106
+ const path = require('path');
107
+ try {
108
+ const projectRoot = path.resolve(__dirname, '..', '..');
109
+ candidates.push(path.resolve(projectRoot, 'plugins'));
110
+ } catch (_) {}
111
+
112
+ const className =
113
+ (plugin.constructor && plugin.constructor.name) || plugin.name;
114
+ if (!className) return null;
115
+
116
+ for (const base of candidates) {
117
+ // exact-match: <base>/<dir>/index.js with class declared inside
118
+ // heuristic: walk top-level plugin dirs and look for index.js exports
119
+ // a class named `className`.
120
+ let dirs = [];
121
+ try {
122
+ dirs = fs.readdirSync(base);
123
+ } catch (_) {
124
+ continue;
125
+ }
126
+ for (const sub of dirs) {
127
+ const subDir = path.join(base, sub);
128
+ let files = [];
129
+ try {
130
+ files = fs.statSync(subDir).isDirectory()
131
+ ? fs.readdirSync(subDir)
132
+ : [path.basename(subDir)];
133
+ subDir = fs.statSync(subDir).isDirectory()
134
+ ? subDir
135
+ : path.dirname(subDir);
136
+ } catch (_) {
137
+ continue;
138
+ }
139
+ for (const file of files) {
140
+ if (!file.endsWith('.js')) continue;
141
+ const full = path.join(subDir, file);
142
+ try {
143
+ const src = fs.readFileSync(full, 'utf-8');
144
+ // Heuristic class declaration
145
+ if (
146
+ new RegExp(`class\\s+${className}\\s+extends\\s+Plugin`).test(src)
147
+ ) {
148
+ return full;
149
+ }
150
+ } catch (_) {}
151
+ }
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+
83
157
  get(name) {
84
158
  const entry = this._plugins.get(name);
85
159
  return entry ? entry.instance : null;
@@ -146,9 +220,18 @@ class PluginManager {
146
220
  async load(plugin, options = {}) {
147
221
  let pluginInstance = plugin;
148
222
 
149
- // 如果是类(Plugin 子类),实例化
150
- if (typeof plugin === 'function' && plugin.prototype instanceof Plugin) {
151
- pluginInstance = new plugin();
223
+ // 处理 function(Plugin) { return class... } 格式
224
+ if (typeof plugin === 'function') {
225
+ if (plugin.prototype instanceof Plugin) {
226
+ // Plugin 子类
227
+ pluginInstance = new plugin();
228
+ } else {
229
+ // Factory function
230
+ pluginInstance = plugin(Plugin);
231
+ if (pluginInstance.prototype instanceof Plugin) {
232
+ pluginInstance = new pluginInstance();
233
+ }
234
+ }
152
235
  }
153
236
 
154
237
  const name = pluginInstance.name;
@@ -187,7 +270,7 @@ class PluginManager {
187
270
  this._log.info(`Loading plugin: ${name}`);
188
271
 
189
272
  if (typeof pluginInstance.install === 'function') {
190
- pluginInstance.install(this.framework);
273
+ await pluginInstance.install(this.framework);
191
274
  }
192
275
 
193
276
  entry.status = 'loaded';
@@ -246,14 +329,43 @@ class PluginManager {
246
329
  this._log.info(`Reloading plugin: ${name}`);
247
330
  const plugin = entry.instance;
248
331
 
332
+ // Refresh the plugin's prototype with the latest source code BEFORE invoking
333
+ // its reload hook. Without this, plugin.reload() still runs against the
334
+ // prototype cached at construction time (and Node.js' require cache means
335
+ // the same source file is returned on disk), so changes to plugin code
336
+ // (e.g., workflow/_loadWorkflows) won't take effect until the process
337
+ // restarts.
338
+ if (entry.sourcePath) {
339
+ try {
340
+ delete require.cache[require.resolve(entry.sourcePath)];
341
+ const mod = require(entry.sourcePath);
342
+ // The module may export a single class or an object map.
343
+ const PluginClass =
344
+ (mod && (mod.default || mod.WorkflowPlugin || mod)) ||
345
+ null;
346
+ if (
347
+ PluginClass &&
348
+ PluginClass.prototype &&
349
+ Object.getPrototypeOf(plugin) !== PluginClass.prototype
350
+ ) {
351
+ Object.setPrototypeOf(plugin, PluginClass.prototype);
352
+ }
353
+ } catch (err) {
354
+ this._log.warn(
355
+ `Failed to refresh prototype for plugin '${name}' from ${entry.sourcePath}:`,
356
+ err.message
357
+ );
358
+ }
359
+ }
360
+
249
361
  if (typeof plugin.reload === 'function') {
250
362
  await plugin.reload(this.framework);
251
363
  } else {
252
364
  if (typeof plugin.uninstall === 'function') {
253
- plugin.uninstall(this.framework);
365
+ await plugin.uninstall(this.framework);
254
366
  }
255
367
  if (typeof plugin.install === 'function') {
256
- plugin.install(this.framework);
368
+ await plugin.install(this.framework);
257
369
  }
258
370
  if (typeof plugin.start === 'function') {
259
371
  await plugin.start(this.framework);
@@ -342,10 +454,10 @@ class PluginManager {
342
454
  } else {
343
455
  // 没有 reload,走完整 uninstall + install + start
344
456
  if (typeof entry.instance.uninstall === 'function') {
345
- entry.instance.uninstall(this.framework);
457
+ await entry.instance.uninstall(this.framework);
346
458
  }
347
459
  if (typeof entry.instance.install === 'function') {
348
- entry.instance.install(this.framework);
460
+ await entry.instance.install(this.framework);
349
461
  }
350
462
  if (typeof entry.instance.start === 'function') {
351
463
  await entry.instance.start(this.framework);
@@ -405,6 +517,9 @@ class PluginManager {
405
517
  }
406
518
  }
407
519
 
520
+ // 清理该插件注册的提示词
521
+ entry.instance._cleanupPrompts?.();
522
+
408
523
  this.framework?.emit('plugin:disabled', entry.instance);
409
524
  this._saveState();
410
525
  this._log.info(`Plugin '${name}' disabled`);
@@ -13,15 +13,36 @@ const { z } = require('zod');
13
13
  * @param {boolean} [required] - Whether this schema is required (optional when false)
14
14
  * @returns {z.ZodType}
15
15
  */
16
+ /**
17
+ * Convert JSON Schema to Zod schema
18
+ * @param {Object} jsonSchema
19
+ * @param {boolean} [required] - Whether the root schema is required (default true)
20
+ * @returns {z.ZodType}
21
+ */
16
22
  function jsonSchemaToZod(jsonSchema, required) {
17
23
  if (!jsonSchema || typeof jsonSchema !== 'object') {
18
24
  return z.any();
19
25
  }
20
26
 
21
- return _convertSchema(jsonSchema, required);
27
+ // 根级默认 required=true,optional() 在对象字段级别应用
28
+ const isRequired = required !== false;
29
+ return _applyOptional(_convertSchema(jsonSchema, true), isRequired, jsonSchema);
30
+ }
31
+
32
+ /**
33
+ * 在对象字段构建时调用:根据 required 包装
34
+ */
35
+ function _applyOptional(zodType, isRequired, schema) {
36
+ if (isRequired) return zodType;
37
+ if (schema && schema.default !== undefined) return zodType;
38
+ return zodType.optional();
22
39
  }
23
40
 
24
- function _convertSchema(schema, required = false) {
41
+ /**
42
+ * 内部转换:只负责类型转换,不做 optional 包装
43
+ * optional 包装由调用方根据 required 参数决定
44
+ */
45
+ function _convertSchema(schema, _required = true) {
25
46
  if (!schema) return z.any();
26
47
 
27
48
  let type = schema.type;
@@ -55,8 +76,10 @@ function _convertSchema(schema, required = false) {
55
76
  zodType = z.boolean();
56
77
  break;
57
78
  case 'array': {
79
+ // 数组项的 required 由 _required 决定(通常为 true,因为数组存在即代表"有数组")
80
+ const itemRequired = _required && schema.items?.required !== false;
58
81
  if (schema.items) {
59
- zodType = z.array(_convertSchema(schema.items, true));
82
+ zodType = z.array(_convertSchema(schema.items, itemRequired));
60
83
  } else {
61
84
  zodType = z.array(z.any());
62
85
  }
@@ -68,12 +91,14 @@ function _convertSchema(schema, required = false) {
68
91
  break;
69
92
  }
70
93
  const shape = {};
71
- const req = schema.required || [];
94
+ const req = new Set(schema.required || []);
72
95
  for (const [key, prop] of Object.entries(schema.properties)) {
73
- shape[key] = _convertSchema(prop, req.includes(key));
96
+ const isRequired = req.has(key);
97
+ // 子字段的 optional() 在这里统一包装
98
+ const fieldSchema = _convertSchema(prop, isRequired);
99
+ shape[key] = _applyOptional(fieldSchema, isRequired, prop);
74
100
  }
75
- const obj = z.object(shape);
76
- zodType = req.length === Object.keys(shape).length ? obj : obj.partial();
101
+ zodType = z.object(shape);
77
102
  break;
78
103
  }
79
104
  case 'null':
@@ -84,8 +109,6 @@ function _convertSchema(schema, required = false) {
84
109
  }
85
110
 
86
111
  if (schema.nullable) zodType = zodType.nullable();
87
- if (!required && schema.default === undefined) zodType = zodType.optional();
88
-
89
112
  return zodType;
90
113
  }
91
114
 
@@ -29,7 +29,7 @@ function createSandboxContext(customContext = {}, options = {}) {
29
29
  Number: { isNaN: Number.isNaN, isFinite: Number.isFinite, parseInt: Number.parseInt, parseFloat: Number.parseFloat },
30
30
  Boolean,
31
31
  Object: { keys: Object.keys, values: Object.values, entries: Object.entries, assign: Object.assign, create: Object.create, freeze: Object.freeze },
32
- Date: { now: Date.now },
32
+ Date,
33
33
  RegExp,
34
34
  Error,
35
35
  Promise,