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
@@ -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
  }
@@ -146,9 +155,18 @@ class PluginManager {
146
155
  async load(plugin, options = {}) {
147
156
  let pluginInstance = plugin;
148
157
 
149
- // 如果是类(Plugin 子类),实例化
150
- if (typeof plugin === 'function' && plugin.prototype instanceof Plugin) {
151
- pluginInstance = new plugin();
158
+ // 处理 function(Plugin) { return class... } 格式
159
+ if (typeof plugin === 'function') {
160
+ if (plugin.prototype instanceof Plugin) {
161
+ // Plugin 子类
162
+ pluginInstance = new plugin();
163
+ } else {
164
+ // Factory function
165
+ pluginInstance = plugin(Plugin);
166
+ if (pluginInstance.prototype instanceof Plugin) {
167
+ pluginInstance = new pluginInstance();
168
+ }
169
+ }
152
170
  }
153
171
 
154
172
  const name = pluginInstance.name;
@@ -187,7 +205,7 @@ class PluginManager {
187
205
  this._log.info(`Loading plugin: ${name}`);
188
206
 
189
207
  if (typeof pluginInstance.install === 'function') {
190
- pluginInstance.install(this.framework);
208
+ await pluginInstance.install(this.framework);
191
209
  }
192
210
 
193
211
  entry.status = 'loaded';
@@ -250,10 +268,10 @@ class PluginManager {
250
268
  await plugin.reload(this.framework);
251
269
  } else {
252
270
  if (typeof plugin.uninstall === 'function') {
253
- plugin.uninstall(this.framework);
271
+ await plugin.uninstall(this.framework);
254
272
  }
255
273
  if (typeof plugin.install === 'function') {
256
- plugin.install(this.framework);
274
+ await plugin.install(this.framework);
257
275
  }
258
276
  if (typeof plugin.start === 'function') {
259
277
  await plugin.start(this.framework);
@@ -342,10 +360,10 @@ class PluginManager {
342
360
  } else {
343
361
  // 没有 reload,走完整 uninstall + install + start
344
362
  if (typeof entry.instance.uninstall === 'function') {
345
- entry.instance.uninstall(this.framework);
363
+ await entry.instance.uninstall(this.framework);
346
364
  }
347
365
  if (typeof entry.instance.install === 'function') {
348
- entry.instance.install(this.framework);
366
+ await entry.instance.install(this.framework);
349
367
  }
350
368
  if (typeof entry.instance.start === 'function') {
351
369
  await entry.instance.start(this.framework);
@@ -405,6 +423,9 @@ class PluginManager {
405
423
  }
406
424
  }
407
425
 
426
+ // 清理该插件注册的提示词
427
+ entry.instance._cleanupPrompts?.();
428
+
408
429
  this.framework?.emit('plugin:disabled', entry.instance);
409
430
  this._saveState();
410
431
  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
 
@@ -17,7 +17,7 @@ describe('Plugin declarative prompts', () => {
17
17
  prompts = [
18
18
  {
19
19
  name: 'demo',
20
- slot: 'BEHAVIOR',
20
+ priority: 50,
21
21
  description: 'demo part',
22
22
  provider: function () {
23
23
  return 'demo-content';
@@ -43,7 +43,7 @@ describe('Plugin declarative prompts', () => {
43
43
  prompts = [
44
44
  {
45
45
  name: 'foo',
46
- slot: 'CAPABILITY',
46
+ scope: 'global',
47
47
  provider: function () {
48
48
  return 'foo';
49
49
  },
@@ -70,7 +70,7 @@ describe('Plugin declarative prompts', () => {
70
70
  { provider: () => 'no-name' },
71
71
  {
72
72
  name: 'valid',
73
- slot: 'CUSTOM',
73
+ priority: 50,
74
74
  provider: function () {
75
75
  return 'valid-content';
76
76
  },
@@ -99,7 +99,7 @@ describe('Plugin declarative prompts', () => {
99
99
  prompts = [
100
100
  {
101
101
  name: 'capture',
102
- slot: 'BEHAVIOR',
102
+ scope: 'global',
103
103
  provider: function () {
104
104
  capturedThis = this;
105
105
  return 'x';
@@ -122,8 +122,8 @@ describe('Plugin declarative prompts', () => {
122
122
  return this;
123
123
  }
124
124
  prompts = [
125
- { name: 'a', slot: 'BEHAVIOR', provider: () => 'a' },
126
- { name: 'b', slot: 'BEHAVIOR', provider: () => 'b' },
125
+ { name: 'a', priority: 50, provider: () => 'a' },
126
+ { name: 'b', priority: 60, provider: () => 'b' },
127
127
  ];
128
128
  }
129
129
  const plugin = new CleanupPlugin();
@@ -146,7 +146,7 @@ describe('Plugin declarative prompts', () => {
146
146
  prompts = [
147
147
  {
148
148
  name: 'reloadable',
149
- slot: 'CUSTOM',
149
+ priority: 50,
150
150
  provider: function () {
151
151
  return 'v1';
152
152
  },
@@ -177,7 +177,7 @@ describe('Plugin declarative prompts', () => {
177
177
  prompts = [
178
178
  {
179
179
  name: 'demo',
180
- slot: 'CUSTOM',
180
+ priority: 50,
181
181
  provider: function () {
182
182
  return 'demo-content';
183
183
  },
@@ -192,7 +192,7 @@ describe('Plugin declarative prompts', () => {
192
192
  expect(fw.prompts.getPart('test-owner', 'demo').get()).toBe('demo-content');
193
193
  });
194
194
 
195
- it('renders in slot order', () => {
195
+ it('renders in priority order', () => {
196
196
  class MultiPlugin extends Plugin {
197
197
  name = 'multi';
198
198
  install(f) {
@@ -200,9 +200,9 @@ describe('Plugin declarative prompts', () => {
200
200
  return this;
201
201
  }
202
202
  prompts = [
203
- { name: 'behavior', slot: 'BEHAVIOR', provider: () => 'BEHAVIOR_CONTENT' },
204
- { name: 'identity', slot: 'IDENTITY', provider: () => 'IDENTITY_CONTENT' },
205
- { name: 'capability', slot: 'CAPABILITY', provider: () => 'CAPABILITY_CONTENT' },
203
+ { name: 'behavior', priority: 50, provider: () => 'BEHAVIOR_CONTENT' },
204
+ { name: 'identity', priority: 10, provider: () => 'IDENTITY_CONTENT' },
205
+ { name: 'capability', priority: 30, provider: () => 'CAPABILITY_CONTENT' },
206
206
  ];
207
207
  }
208
208
  const plugin = new MultiPlugin();
@@ -216,4 +216,4 @@ describe('Plugin declarative prompts', () => {
216
216
  expect(identityIdx).toBeLessThan(capabilityIdx);
217
217
  expect(capabilityIdx).toBeLessThan(behaviorIdx);
218
218
  });
219
- });
219
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { PromptRegistry, PROMPT_SLOT, PROMPT_PRIORITY_TO_SLOT } from '../../src/index';
2
+ import { PromptRegistry } from '../../src/index';
3
3
 
4
4
  describe('PromptRegistry', () => {
5
5
  let reg;
@@ -10,13 +10,20 @@ describe('PromptRegistry', () => {
10
10
 
11
11
  describe('basic API', () => {
12
12
  it('registers and retrieves a part', () => {
13
- reg.register('plugin-a', 'foo', () => 'hello', { slot: 'IDENTITY' });
13
+ reg.register('plugin-a', 'foo', () => 'hello');
14
14
  const part = reg.getPart('plugin-a', 'foo');
15
15
  expect(part).toBeTruthy();
16
16
  expect(part.get()).toBe('hello');
17
17
  expect(reg.count()).toBe(1);
18
18
  });
19
19
 
20
+ it('registers with scope and priority', () => {
21
+ reg.register('plugin-a', 'foo', () => 'hello', { scope: 'global', priority: 50 });
22
+ const part = reg.getPart('plugin-a', 'foo');
23
+ expect(part.scope).toBe('global');
24
+ expect(part.priority).toBe(50);
25
+ });
26
+
20
27
  it('requires owner/name/provider', () => {
21
28
  expect(() => reg.register('', 'foo', () => 'x')).toThrow(/owner/);
22
29
  expect(() => reg.register('p', '', () => 'x')).toThrow(/name/);
@@ -47,46 +54,53 @@ describe('PromptRegistry', () => {
47
54
  });
48
55
  });
49
56
 
50
- describe('slot ordering', () => {
51
- it('orders by slot order', () => {
52
- reg.register('p', 'a', () => 'BEHAVIOR', { slot: 'BEHAVIOR' });
53
- reg.register('p', 'b', () => 'IDENTITY', { slot: 'IDENTITY' });
54
- reg.register('p', 'c', () => 'CAPABILITY', { slot: 'CAPABILITY' });
55
- const order = reg.list().map((p) => p.slot);
56
- expect(order).toEqual(['IDENTITY', 'CAPABILITY', 'BEHAVIOR']);
57
- });
58
-
59
- it('uses explicit order within same slot', () => {
60
- reg.register('p', 'late', () => 'late', { slot: 'IDENTITY', order: 5 });
61
- reg.register('p', 'early', () => 'early', { slot: 'IDENTITY', order: 1 });
62
- const names = reg.list().map((p) => p.name);
63
- expect(names).toEqual(['early', 'late']);
57
+ describe('priority ordering', () => {
58
+ it('orders by priority (smaller first)', () => {
59
+ reg.register('p', 'a', () => 'a', { priority: 100 });
60
+ reg.register('p', 'b', () => 'b', { priority: 10 });
61
+ reg.register('p', 'c', () => 'c', { priority: 50 });
62
+ const order = reg.list().map((p) => p.name);
63
+ expect(order).toEqual(['b', 'c', 'a']);
64
64
  });
65
65
 
66
- it('falls back to alphabetical when order is equal', () => {
67
- reg.register('zeta', 'z', () => 'z', { slot: 'IDENTITY' });
68
- reg.register('alpha', 'a', () => 'a', { slot: 'IDENTITY' });
66
+ it('falls back to owner alphabetical when priority is equal', () => {
67
+ reg.register('zeta', 'z', () => 'z', { priority: 50 });
68
+ reg.register('alpha', 'a', () => 'a', { priority: 50 });
69
69
  const owners = reg.list().map((p) => p.owner);
70
70
  expect(owners).toEqual(['alpha', 'zeta']);
71
71
  });
72
72
 
73
- it('PROMPT_SLOT exposes 7 named slots', () => {
74
- const names = Object.keys(PROMPT_SLOT);
75
- expect(names).toContain('IDENTITY');
76
- expect(names).toContain('ENVIRONMENT');
77
- expect(names).toContain('USER');
78
- expect(names).toContain('CAPABILITY');
79
- expect(names).toContain('BEHAVIOR');
80
- expect(names).toContain('CONTEXT');
81
- expect(names).toContain('CUSTOM');
73
+ it('defaults to priority 100', () => {
74
+ reg.register('p', 'a', () => 'a');
75
+ reg.register('p', 'b', () => 'b', { priority: 50 });
76
+ const order = reg.list().map((p) => p.name);
77
+ expect(order).toEqual(['b', 'a']);
78
+ });
79
+ });
80
+
81
+ describe('scope filtering', () => {
82
+ it('listByScope filters correctly', () => {
83
+ reg.register('p', 'a', () => 'a', { scope: 'global' });
84
+ reg.register('p', 'b', () => 'b', { scope: 'main' });
85
+ reg.register('p', 'c', () => 'c', { scope: 'sub-1' });
86
+ expect(reg.listByScope('global').map((p) => p.name)).toEqual(['a']);
87
+ expect(reg.listByScope('main').map((p) => p.name)).toEqual(['b']);
88
+ expect(reg.listByScope('sub-1').map((p) => p.name)).toEqual(['c']);
82
89
  });
83
90
 
84
- it('PROMPT_PRIORITY_TO_SLOT maps all legacy priorities', () => {
85
- expect(PROMPT_PRIORITY_TO_SLOT.DATETIME).toBe('ENVIRONMENT');
86
- expect(PROMPT_PRIORITY_TO_SLOT.ORIGINAL_PROMPT).toBe('IDENTITY');
87
- expect(PROMPT_PRIORITY_TO_SLOT.SHARED_PROMPT).toBe('USER');
88
- expect(PROMPT_PRIORITY_TO_SLOT.CAPABILITIES).toBe('CAPABILITY');
89
- expect(PROMPT_PRIORITY_TO_SLOT.TOOL_CORE_RULES).toBe('BEHAVIOR');
91
+ it('listForAgent includes global and matching scope', () => {
92
+ reg.register('p', 'global', () => 'g', { scope: 'global' });
93
+ reg.register('p', 'main', () => 'm', { scope: 'main' });
94
+ reg.register('p', 'sub1', () => 's1', { scope: 'sub-1' });
95
+ reg.register('p', 'sub2', () => 's2', { scope: 'sub-2' });
96
+
97
+ // Main agent sees global + main
98
+ const mainParts = reg.listForAgent(null).map((p) => p.name);
99
+ expect(mainParts).toEqual(['global', 'main']);
100
+
101
+ // sub-1 agent sees global + sub1
102
+ const sub1Parts = reg.listForAgent('sub-1').map((p) => p.name);
103
+ expect(sub1Parts).toEqual(['global', 'sub1']);
90
104
  });
91
105
  });
92
106
 
@@ -97,6 +111,13 @@ describe('PromptRegistry', () => {
97
111
  expect(reg.build()).toBe('AAA\n\nBBB');
98
112
  });
99
113
 
114
+ it('builds only parts for specific agent', () => {
115
+ reg.register('p', 'global', () => 'G', { scope: 'global' });
116
+ reg.register('p', 'main', () => 'M', { scope: 'main' });
117
+ reg.register('p', 'sub1', () => 'S1', { scope: 'sub-1' });
118
+ expect(reg.buildForAgent('sub-1')).toBe('G\n\nS1');
119
+ });
120
+
100
121
  it('skips null/empty content', () => {
101
122
  reg.register('p', 'a', () => 'AAA');
102
123
  reg.register('p', 'b', () => null);
@@ -116,7 +137,6 @@ describe('PromptRegistry', () => {
116
137
  console.warn = (msg) => warnings.push(msg);
117
138
  try {
118
139
  const result = reg.build();
119
- // 排序:after → bad(fail) → good → "BBB\n\nAAA"
120
140
  expect(result).toBe('BBB\n\nAAA');
121
141
  expect(warnings.length).toBe(1);
122
142
  } finally {
@@ -170,14 +190,14 @@ describe('PromptRegistry', () => {
170
190
 
171
191
  describe('inspect', () => {
172
192
  it('returns structured per-part metadata', () => {
173
- reg.register('plugin-a', 'foo', () => 'hello world', { slot: 'IDENTITY' });
193
+ reg.register('plugin-a', 'foo', () => 'hello world', { scope: 'global', priority: 50 });
174
194
  const out = reg.inspect();
175
195
  expect(out).toHaveLength(1);
176
196
  expect(out[0]).toMatchObject({
177
197
  owner: 'plugin-a',
178
198
  name: 'foo',
179
- slot: 'IDENTITY',
180
- order: 10,
199
+ scope: 'global',
200
+ priority: 50,
181
201
  hasContent: true,
182
202
  length: 11,
183
203
  });
@@ -190,20 +210,8 @@ describe('PromptRegistry', () => {
190
210
  const { Framework } = require('../../src/index');
191
211
  const fw = new Framework({ silent: true });
192
212
  expect(fw.prompts).toBeDefined();
193
- expect(fw.prompts).toBe(fw.promptRegistry);
194
213
  expect(typeof fw.prompts.register).toBe('function');
195
214
  expect(typeof fw.prompts.preview).toBe('function');
196
215
  });
197
-
198
- it('each agent has its own promptRegistry via BaseAgent', () => {
199
- const { BaseAgent } = require('../../src/agent/base');
200
- const { Framework } = require('../../src/index');
201
- const fw = new Framework({ silent: true });
202
- const agent = new BaseAgent(fw, { name: 'Test' });
203
- expect(agent.promptRegistry).toBeDefined();
204
- expect(agent.promptRegistry).toBe(agent._promptRegistry);
205
- agent.promptRegistry.register('test', 'foo', () => 'x', { slot: 'CUSTOM' });
206
- expect(agent.promptRegistry.count()).toBe(1);
207
- });
208
216
  });
209
- });
217
+ });