foliko 2.0.1 → 2.0.3

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 (79) hide show
  1. package/README.md +6 -6
  2. package/docs/public-api.md +91 -0
  3. package/docs/system-prompt.md +219 -0
  4. package/docs/usage.md +6 -6
  5. package/package.json +1 -1
  6. package/plugins/ambient/ExplorerLoop.js +1 -0
  7. package/plugins/ambient/index.js +5 -0
  8. package/plugins/core/coordinator/index.js +11 -6
  9. package/plugins/core/default/bootstrap.js +21 -3
  10. package/plugins/core/default/config.js +12 -3
  11. package/plugins/core/python-loader/index.js +3 -3
  12. package/plugins/core/scheduler/index.js +26 -2
  13. package/plugins/core/skill-manager/index.js +198 -151
  14. package/plugins/core/sub-agent/index.js +34 -15
  15. package/plugins/core/think/index.js +1 -0
  16. package/plugins/core/workflow/index.js +5 -4
  17. package/plugins/executors/data-splitter/index.js +14 -1
  18. package/plugins/executors/extension/index.js +51 -37
  19. package/plugins/executors/python/index.js +3 -3
  20. package/plugins/executors/shell/index.js +6 -4
  21. package/plugins/io/web/index.js +2 -1
  22. package/plugins/memory/index.js +29 -74
  23. package/plugins/messaging/email/handlers.js +1 -19
  24. package/plugins/messaging/email/monitor.js +2 -17
  25. package/plugins/messaging/email/reply.js +2 -1
  26. package/plugins/messaging/email/utils.js +20 -1
  27. package/plugins/messaging/feishu/index.js +1 -1
  28. package/plugins/messaging/qq/index.js +1 -1
  29. package/plugins/messaging/telegram/index.js +5 -1
  30. package/plugins/messaging/weixin/index.js +1 -1
  31. package/plugins/plugin-manager/index.js +1 -33
  32. package/plugins/tools/index.js +14 -1
  33. package/skills/plugins/SKILL.md +316 -0
  34. package/skills/skill-guide/SKILL.md +5 -5
  35. package/skills/{subagent-guide → subagents}/SKILL.md +1 -1
  36. package/skills/{workflow-guide → workflows}/SKILL.md +3 -3
  37. package/skills/{workflow-troubleshooting → workflows/workflow-troubleshooting}/DEBUGGING.md +197 -197
  38. package/skills/{workflow-troubleshooting → workflows/workflow-troubleshooting}/SKILL.md +391 -391
  39. package/src/agent/base.js +5 -20
  40. package/src/agent/index.js +20 -8
  41. package/src/agent/main.js +100 -23
  42. package/src/agent/prompt-registry.js +296 -0
  43. package/src/agent/sub-config.js +1 -78
  44. package/src/agent/sub.js +19 -24
  45. package/src/cli/commands/plugin.js +1 -60
  46. package/src/cli/ui/chat-ui-old.js +1 -1
  47. package/src/cli/ui/chat-ui.js +1 -1
  48. package/src/cli/ui/components/agent-mention-provider.js +1 -1
  49. package/src/common/constants.js +42 -0
  50. package/src/common/id.js +13 -0
  51. package/src/common/queue.js +2 -2
  52. package/src/context/compressor.js +17 -9
  53. package/src/framework/framework.js +185 -0
  54. package/src/framework/index.js +1 -2
  55. package/src/framework/lifecycle.js +102 -1
  56. package/src/framework/loader.js +1 -55
  57. package/src/index.js +11 -2
  58. package/src/plugin/base.js +69 -55
  59. package/src/plugin/loader.js +1 -57
  60. package/src/plugin/manager.js +13 -4
  61. package/src/session/entry.js +1 -11
  62. package/src/storage/manager.js +1 -12
  63. package/src/tool/executor.js +2 -1
  64. package/src/tool/router.js +5 -5
  65. package/src/utils/data-splitter.js +2 -1
  66. package/src/utils/index.js +150 -0
  67. package/src/utils/message-validator.js +21 -17
  68. package/src/utils/plugin-helpers.js +19 -5
  69. package/subagent.md +2 -2
  70. package/tests/core/plugin-prompts.test.js +219 -0
  71. package/tests/core/prompt-registry.test.js +209 -0
  72. package/src/cli/utils/plugin-config.js +0 -50
  73. package/src/config/plugin-config.js +0 -50
  74. /package/skills/{ambient-agent → ambient}/SKILL.md +0 -0
  75. /package/skills/{foliko-dev → foliko}/AGENTS.md +0 -0
  76. /package/skills/{foliko-dev → foliko}/SKILL.md +0 -0
  77. /package/skills/{mcp-usage → mcp}/SKILL.md +0 -0
  78. /package/skills/{plugin-guide → plugins-guide}/SKILL.md +0 -0
  79. /package/skills/{python-plugin-dev → python}/SKILL.md +0 -0
@@ -53,24 +53,28 @@ function validateMessagesPairing(messages, options = {}) {
53
53
  let removedToolResultCount = 0;
54
54
 
55
55
  for (const msg of messages) {
56
- if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
56
+ if (msg.role !== 'tool') continue;
57
57
 
58
- const originalLength = msg.content.length;
59
- msg.content = msg.content.filter((item) => {
60
- if (
61
- item &&
62
- (item.type === 'tool-result' || item.type === 'tool_result') &&
63
- item.toolCallId &&
64
- !validToolCallIds.has(item.toolCallId)
65
- ) {
66
- removedToolResultCount++;
67
- return false;
58
+ if (Array.isArray(msg.content)) {
59
+ const originalLength = msg.content.length;
60
+ msg.content = msg.content.filter((item) => {
61
+ if (
62
+ item &&
63
+ (item.type === 'tool-result' || item.type === 'tool_result') &&
64
+ item.toolCallId &&
65
+ !validToolCallIds.has(item.toolCallId)
66
+ ) {
67
+ removedToolResultCount++;
68
+ return false;
69
+ }
70
+ return true;
71
+ });
72
+ // content 全被删除了,标记整个消息待删除
73
+ if (msg.content.length === 0 && originalLength > 0) {
74
+ msg._orphaned = true;
68
75
  }
69
- return true;
70
- });
71
-
72
- // content 全被删除了,标记整个消息待删除
73
- if (msg.content.length === 0 && originalLength > 0) {
76
+ } else if (msg.tool_call_id && !validToolCallIds.has(msg.tool_call_id)) {
77
+ // OpenAI 兼容格式:整条消息无对应 tool_calls,标记为 orphaned
74
78
  msg._orphaned = true;
75
79
  }
76
80
  }
@@ -280,4 +284,4 @@ module.exports = {
280
284
  validateToolCalls,
281
285
  validateAll,
282
286
  filterPairedMessages,
283
- };
287
+ };
@@ -70,15 +70,20 @@ function resolvePluginPath(pluginsDir, name, options = {}) {
70
70
  }
71
71
 
72
72
  /**
73
- * 扫描插件目录,返回所有插件名称
73
+ * 扫描插件目录,返回所有插件名称(支持嵌套目录)
74
74
  * @param {string} pluginsDir - 插件目录
75
+ * @param {string} baseDir - 基础目录(用于计算相对路径)
75
76
  * @returns {string[]} 插件名称列表
76
77
  */
77
- function scanPluginNames(pluginsDir) {
78
+ function scanPluginNames(pluginsDir, baseDir = null) {
78
79
  if (!fs.existsSync(pluginsDir)) {
79
80
  return [];
80
81
  }
81
82
 
83
+ if (baseDir === null) {
84
+ baseDir = pluginsDir;
85
+ }
86
+
82
87
  // 忽略的目录/文件(无意义或系统文件)
83
88
  const IGNORE_PATTERNS = new Set([
84
89
  '__pycache__',
@@ -105,13 +110,22 @@ function scanPluginNames(pluginsDir) {
105
110
  const isDir = entry.isSymbolicLink() ? isDirectory(fullPath) : entry.isDirectory();
106
111
 
107
112
  if (isDir) {
108
- // 文件夹插件(包括 Junction Symbolic Link)
109
- names.add(entry.name);
113
+ const indexPath = path.join(fullPath, 'index.js');
114
+ if (fs.existsSync(indexPath)) {
115
+ // 这是一个插件目录
116
+ const relativePath = path.relative(baseDir, fullPath);
117
+ names.add(relativePath.replace(/\\/g, '/'));
118
+ } else {
119
+ // 不是插件目录,递归扫描子目录
120
+ const subNames = scanPluginNames(fullPath, baseDir);
121
+ subNames.forEach(n => names.add(n));
122
+ }
110
123
  } else if (entry.isFile() && entry.name.endsWith('.js')) {
111
124
  // 单文件插件(排除与文件夹同名的)
112
125
  const baseName = entry.name.replace(/\.js$/, '');
113
126
  if (!names.has(baseName)) {
114
- names.add(baseName);
127
+ const relativePath = path.relative(baseDir, fullPath);
128
+ names.add(relativePath.replace(/\\/g, '/').replace(/\.js$/, ''));
115
129
  }
116
130
  }
117
131
  }
package/subagent.md CHANGED
@@ -111,7 +111,7 @@
111
111
  ## 【Extensions】扩展插件
112
112
 
113
113
  **使用流程(必须按顺序执行):**
114
- 1. 调用 `ext_skill({ plugin: "<plugin_name>" })` 或 `loadSkill({ skill: "<skill_name>" })` 获取目标扩展的详细工具参数
114
+ 1. 调用 `ext_skill({ plugin: "<plugin_name>" })` 或 `skill_load({ skill: "<skill_name>" })` 获取目标扩展的详细工具参数
115
115
  2. 根据返回的参数定义,使用 `ext_call({ plugin, tool, args })` 调用扩展
116
116
 
117
117
  > **警告**:禁止在未执行第1步获取参数的情况下直接调用 `ext_call`!
@@ -167,4 +167,4 @@ MCP (Model Context Protocol) 执行器
167
167
  **工具:** `designmd_search_design_kits`, `designmd_get_design_kit`, `designmd_download_design_kit`, `designmd_upload_design_kit`, `designmd_delete_design_kit`, `designmd_list_popular_kits`, `designmd_list_tags`
168
168
 
169
169
  ## 禁止事项
170
- - 不先调用 `ext_skill/loadSkill` 获取参数就直接使用 `ext_call`
170
+ - 不先调用 `ext_skill/skill_load` 获取参数就直接使用 `ext_call`
@@ -0,0 +1,219 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { Framework, Plugin } from '../../src/index';
3
+
4
+ describe('Plugin declarative prompts', () => {
5
+ let fw;
6
+
7
+ beforeEach(() => {
8
+ fw = new Framework({ silent: true });
9
+ });
10
+
11
+ it('auto-registers prompts declared in Plugin.prompts array', () => {
12
+ class DemoPlugin extends Plugin {
13
+ install(f) {
14
+ this._framework = f;
15
+ return this;
16
+ }
17
+ prompts = [
18
+ {
19
+ name: 'demo',
20
+ slot: 'BEHAVIOR',
21
+ description: 'demo part',
22
+ provider: function () {
23
+ return 'demo-content';
24
+ },
25
+ },
26
+ ];
27
+ }
28
+ const plugin = new DemoPlugin();
29
+ plugin.install(fw);
30
+ plugin.start(fw);
31
+
32
+ expect(fw.prompts.has('unnamed-plugin', 'demo')).toBe(true);
33
+ expect(fw.prompts.getPart('unnamed-plugin', 'demo').get()).toBe('demo-content');
34
+ });
35
+
36
+ it('uses plugin.name as owner', () => {
37
+ class NamedPlugin extends Plugin {
38
+ name = 'my-plugin';
39
+ install(f) {
40
+ this._framework = f;
41
+ return this;
42
+ }
43
+ prompts = [
44
+ {
45
+ name: 'foo',
46
+ slot: 'CAPABILITY',
47
+ provider: function () {
48
+ return 'foo';
49
+ },
50
+ },
51
+ ];
52
+ }
53
+ const plugin = new NamedPlugin();
54
+ plugin.install(fw);
55
+ plugin.start(fw);
56
+
57
+ expect(fw.prompts.has('my-plugin', 'foo')).toBe(true);
58
+ });
59
+
60
+ it('skips invalid entries without breaking others', () => {
61
+ class MixedPlugin extends Plugin {
62
+ name = 'mixed';
63
+ install(f) {
64
+ this._framework = f;
65
+ return this;
66
+ }
67
+ prompts = [
68
+ null,
69
+ { name: 'no-provider' },
70
+ { provider: () => 'no-name' },
71
+ {
72
+ name: 'valid',
73
+ slot: 'CUSTOM',
74
+ provider: function () {
75
+ return 'valid-content';
76
+ },
77
+ },
78
+ ];
79
+ }
80
+ const plugin = new MixedPlugin();
81
+ plugin.install(fw);
82
+ plugin.start(fw);
83
+
84
+ // 合法条目注册成功
85
+ expect(fw.prompts.has('mixed', 'valid')).toBe(true);
86
+ // 非法条目被跳过(不抛错,不影响合法条目)
87
+ expect(fw.prompts.has('mixed', 'no-provider')).toBe(false);
88
+ expect(fw.prompts.has('mixed', 'no-name')).toBe(false);
89
+ });
90
+
91
+ it('binds this to the plugin instance inside provider', () => {
92
+ let capturedThis;
93
+ class CapturePlugin extends Plugin {
94
+ name = 'capture';
95
+ install(f) {
96
+ this._framework = f;
97
+ return this;
98
+ }
99
+ prompts = [
100
+ {
101
+ name: 'capture',
102
+ slot: 'BEHAVIOR',
103
+ provider: function () {
104
+ capturedThis = this;
105
+ return 'x';
106
+ },
107
+ },
108
+ ];
109
+ }
110
+ const plugin = new CapturePlugin();
111
+ plugin.install(fw);
112
+ plugin.start(fw);
113
+ fw.prompts.getPart('capture', 'capture').get();
114
+ expect(capturedThis).toBe(plugin);
115
+ });
116
+
117
+ it('cleanup on uninstall removes all parts owned by plugin', () => {
118
+ class CleanupPlugin extends Plugin {
119
+ name = 'cleanup';
120
+ install(f) {
121
+ this._framework = f;
122
+ return this;
123
+ }
124
+ prompts = [
125
+ { name: 'a', slot: 'BEHAVIOR', provider: () => 'a' },
126
+ { name: 'b', slot: 'BEHAVIOR', provider: () => 'b' },
127
+ ];
128
+ }
129
+ const plugin = new CleanupPlugin();
130
+ plugin.install(fw);
131
+ plugin.start(fw);
132
+ expect(fw.prompts.count()).toBe(2);
133
+
134
+ plugin.uninstall(fw);
135
+ expect(fw.prompts.has('cleanup', 'a')).toBe(false);
136
+ expect(fw.prompts.has('cleanup', 'b')).toBe(false);
137
+ });
138
+
139
+ it('reload re-registers declarative prompts', () => {
140
+ class ReloadPlugin extends Plugin {
141
+ name = 'reload-test';
142
+ install(f) {
143
+ this._framework = f;
144
+ return this;
145
+ }
146
+ prompts = [
147
+ {
148
+ name: 'reloadable',
149
+ slot: 'CUSTOM',
150
+ provider: function () {
151
+ return 'v1';
152
+ },
153
+ },
154
+ ];
155
+ }
156
+ const plugin = new ReloadPlugin();
157
+ plugin.install(fw);
158
+ plugin.start(fw);
159
+ expect(fw.prompts.getPart('reload-test', 'reloadable').get()).toBe('v1');
160
+
161
+ // 修改后 reload
162
+ plugin.prompts[0].provider = function () {
163
+ return 'v2';
164
+ };
165
+ plugin.reload(fw);
166
+ fw.prompts.invalidate('reload-test', 'reloadable');
167
+ expect(fw.prompts.getPart('reload-test', 'reloadable').get()).toBe('v2');
168
+ });
169
+
170
+ it('declarative prompts with owner matching plugin name', () => {
171
+ class NamedPlugin extends Plugin {
172
+ name = 'test-owner';
173
+ install(f) {
174
+ this._framework = f;
175
+ return this;
176
+ }
177
+ prompts = [
178
+ {
179
+ name: 'demo',
180
+ slot: 'CUSTOM',
181
+ provider: function () {
182
+ return 'demo-content';
183
+ },
184
+ },
185
+ ];
186
+ }
187
+ const plugin = new NamedPlugin();
188
+ plugin.install(fw);
189
+ plugin.start(fw);
190
+
191
+ expect(fw.prompts.has('test-owner', 'demo')).toBe(true);
192
+ expect(fw.prompts.getPart('test-owner', 'demo').get()).toBe('demo-content');
193
+ });
194
+
195
+ it('renders in slot order', () => {
196
+ class MultiPlugin extends Plugin {
197
+ name = 'multi';
198
+ install(f) {
199
+ this._framework = f;
200
+ return this;
201
+ }
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' },
206
+ ];
207
+ }
208
+ const plugin = new MultiPlugin();
209
+ plugin.install(fw);
210
+ plugin.start(fw);
211
+
212
+ const built = fw.prompts.build();
213
+ const identityIdx = built.indexOf('IDENTITY_CONTENT');
214
+ const capabilityIdx = built.indexOf('CAPABILITY_CONTENT');
215
+ const behaviorIdx = built.indexOf('BEHAVIOR_CONTENT');
216
+ expect(identityIdx).toBeLessThan(capabilityIdx);
217
+ expect(capabilityIdx).toBeLessThan(behaviorIdx);
218
+ });
219
+ });
@@ -0,0 +1,209 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PromptRegistry, PROMPT_SLOT, PROMPT_PRIORITY_TO_SLOT } from '../../src/index';
3
+
4
+ describe('PromptRegistry', () => {
5
+ let reg;
6
+
7
+ beforeEach(() => {
8
+ reg = new PromptRegistry();
9
+ });
10
+
11
+ describe('basic API', () => {
12
+ it('registers and retrieves a part', () => {
13
+ reg.register('plugin-a', 'foo', () => 'hello', { slot: 'IDENTITY' });
14
+ const part = reg.getPart('plugin-a', 'foo');
15
+ expect(part).toBeTruthy();
16
+ expect(part.get()).toBe('hello');
17
+ expect(reg.count()).toBe(1);
18
+ });
19
+
20
+ it('requires owner/name/provider', () => {
21
+ expect(() => reg.register('', 'foo', () => 'x')).toThrow(/owner/);
22
+ expect(() => reg.register('p', '', () => 'x')).toThrow(/name/);
23
+ expect(() => reg.register('p', 'n', 'not-fn')).toThrow(/provider/);
24
+ });
25
+
26
+ it('unregisters a part', () => {
27
+ reg.register('p', 'n', () => 'x');
28
+ expect(reg.unregister('p', 'n')).toBe(true);
29
+ expect(reg.unregister('p', 'n')).toBe(false);
30
+ expect(reg.count()).toBe(0);
31
+ });
32
+
33
+ it('clears all parts of an owner', () => {
34
+ reg.register('p', 'a', () => '1');
35
+ reg.register('p', 'b', () => '2');
36
+ reg.register('q', 'c', () => '3');
37
+ const cleared = reg.clearOwner('p');
38
+ expect(cleared).toBe(2);
39
+ expect(reg.count()).toBe(1);
40
+ });
41
+
42
+ it('isolates namespaces by owner', () => {
43
+ reg.register('x', 'foo', () => 'x-foo');
44
+ reg.register('y', 'foo', () => 'y-foo');
45
+ expect(reg.getPart('x', 'foo').get()).toBe('x-foo');
46
+ expect(reg.getPart('y', 'foo').get()).toBe('y-foo');
47
+ });
48
+ });
49
+
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']);
64
+ });
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' });
69
+ const owners = reg.list().map((p) => p.owner);
70
+ expect(owners).toEqual(['alpha', 'zeta']);
71
+ });
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');
82
+ });
83
+
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');
90
+ });
91
+ });
92
+
93
+ describe('build', () => {
94
+ it('joins non-empty parts with double newline', () => {
95
+ reg.register('p', 'a', () => 'AAA');
96
+ reg.register('p', 'b', () => 'BBB');
97
+ expect(reg.build()).toBe('AAA\n\nBBB');
98
+ });
99
+
100
+ it('skips null/empty content', () => {
101
+ reg.register('p', 'a', () => 'AAA');
102
+ reg.register('p', 'b', () => null);
103
+ reg.register('p', 'c', () => ' ');
104
+ reg.register('p', 'd', () => 'DDD');
105
+ expect(reg.build()).toBe('AAA\n\nDDD');
106
+ });
107
+
108
+ it('does not break when provider throws', () => {
109
+ reg.register('p', 'good', () => 'AAA');
110
+ reg.register('p', 'bad', () => {
111
+ throw new Error('boom');
112
+ });
113
+ reg.register('p', 'after', () => 'BBB');
114
+ const original = console.warn;
115
+ const warnings = [];
116
+ console.warn = (msg) => warnings.push(msg);
117
+ try {
118
+ const result = reg.build();
119
+ // 排序:after → bad(fail) → good → "BBB\n\nAAA"
120
+ expect(result).toBe('BBB\n\nAAA');
121
+ expect(warnings.length).toBe(1);
122
+ } finally {
123
+ console.warn = original;
124
+ }
125
+ });
126
+
127
+ it('preview is an alias for build', () => {
128
+ reg.register('p', 'a', () => 'AAA');
129
+ expect(reg.preview()).toBe(reg.build());
130
+ });
131
+ });
132
+
133
+ describe('caching & invalidation', () => {
134
+ it('caches provider result until invalidated', () => {
135
+ let calls = 0;
136
+ reg.register('p', 'a', () => {
137
+ calls++;
138
+ return 'v' + calls;
139
+ });
140
+ expect(reg.getPart('p', 'a').get()).toBe('v1');
141
+ expect(reg.getPart('p', 'a').get()).toBe('v1');
142
+ reg.invalidate('p', 'a');
143
+ expect(reg.getPart('p', 'a').get()).toBe('v2');
144
+ });
145
+
146
+ it('invalidateAll resets all caches', () => {
147
+ let n = 0;
148
+ reg.register('p', 'a', () => ++n);
149
+ reg.register('p', 'b', () => ++n);
150
+ reg.build();
151
+ reg.invalidateAll();
152
+ reg.build();
153
+ expect(n).toBeGreaterThanOrEqual(4);
154
+ });
155
+ });
156
+
157
+ describe('events', () => {
158
+ it('emits register / unregister / clear-owner', () => {
159
+ const events = [];
160
+ reg.on('register', (e) => events.push('reg:' + e.owner));
161
+ reg.on('unregister', (e) => events.push('unreg:' + e.owner));
162
+ reg.on('clear-owner', (e) => events.push('clear-owner:' + e.owner));
163
+ reg.register('p', 'a', () => 'x');
164
+ reg.register('q', 'b', () => 'x');
165
+ reg.unregister('p', 'a');
166
+ reg.clearOwner('q');
167
+ expect(events).toEqual(['reg:p', 'reg:q', 'unreg:p', 'clear-owner:q']);
168
+ });
169
+ });
170
+
171
+ describe('inspect', () => {
172
+ it('returns structured per-part metadata', () => {
173
+ reg.register('plugin-a', 'foo', () => 'hello world', { slot: 'IDENTITY' });
174
+ const out = reg.inspect();
175
+ expect(out).toHaveLength(1);
176
+ expect(out[0]).toMatchObject({
177
+ owner: 'plugin-a',
178
+ name: 'foo',
179
+ slot: 'IDENTITY',
180
+ order: 10,
181
+ hasContent: true,
182
+ length: 11,
183
+ });
184
+ expect(out[0].contentPreview).toContain('hello');
185
+ });
186
+ });
187
+
188
+ describe('Framework integration', () => {
189
+ it('Framework exposes prompts registry', () => {
190
+ const { Framework } = require('../../src/index');
191
+ const fw = new Framework({ silent: true });
192
+ expect(fw.prompts).toBeDefined();
193
+ expect(fw.prompts).toBe(fw.promptRegistry);
194
+ expect(typeof fw.prompts.register).toBe('function');
195
+ expect(typeof fw.prompts.preview).toBe('function');
196
+ });
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
+ });
209
+ });
@@ -1,50 +0,0 @@
1
- /**
2
- * 插件管理器公共配置
3
- */
4
-
5
- // 默认插件仓库
6
- const DEFAULT_REPO = 'https://github.com/chnak/foliko-plugins.git';
7
-
8
- // 发布时忽略的文件和目录
9
- const IGNORE_PATTERNS = [
10
- 'node_modules',
11
- '.git',
12
- '.env',
13
- '.DS_Store',
14
- 'Thumbs.db',
15
- '*.log',
16
- '*.lock',
17
- '*.bak',
18
- '.claude',
19
- '.foliko',
20
- 'examples',
21
- 'dist',
22
- 'build',
23
- 'coverage',
24
- 'tests',
25
- '__tests__',
26
- '*.test.js',
27
- '*.spec.js',
28
- 'package-lock.json',
29
- 'yarn.lock',
30
- 'pnpm-lock.yaml',
31
- ];
32
-
33
- /**
34
- * 检查文件/目录是否应该被忽略
35
- */
36
- function shouldIgnore(name) {
37
- return IGNORE_PATTERNS.some((pattern) => {
38
- if (pattern.startsWith('*.')) {
39
- const ext = pattern.slice(1);
40
- return name.endsWith(ext);
41
- }
42
- return name === pattern;
43
- });
44
- }
45
-
46
- module.exports = {
47
- DEFAULT_REPO,
48
- IGNORE_PATTERNS,
49
- shouldIgnore,
50
- };
@@ -1,50 +0,0 @@
1
- /**
2
- * 插件管理器公共配置
3
- */
4
-
5
- // 默认插件仓库
6
- const DEFAULT_REPO = 'https://github.com/chnak/foliko-plugins.git';
7
-
8
- // 发布时忽略的文件和目录
9
- const IGNORE_PATTERNS = [
10
- 'node_modules',
11
- '.git',
12
- '.env',
13
- '.DS_Store',
14
- 'Thumbs.db',
15
- '*.log',
16
- '*.lock',
17
- '*.bak',
18
- '.claude',
19
- '.foliko',
20
- 'examples',
21
- 'dist',
22
- 'build',
23
- 'coverage',
24
- 'tests',
25
- '__tests__',
26
- '*.test.js',
27
- '*.spec.js',
28
- 'package-lock.json',
29
- 'yarn.lock',
30
- 'pnpm-lock.yaml',
31
- ];
32
-
33
- /**
34
- * 检查文件/目录是否应该被忽略
35
- */
36
- function shouldIgnore(name) {
37
- return IGNORE_PATTERNS.some((pattern) => {
38
- if (pattern.startsWith('*.')) {
39
- const ext = pattern.slice(1);
40
- return name.endsWith(ext);
41
- }
42
- return name === pattern;
43
- });
44
- }
45
-
46
- module.exports = {
47
- DEFAULT_REPO,
48
- IGNORE_PATTERNS,
49
- shouldIgnore,
50
- };
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes