foliko 1.0.0

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 (54) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/22.txt +10 -0
  3. package/README.md +218 -0
  4. package/SPEC.md +452 -0
  5. package/cli/bin/foliko.js +12 -0
  6. package/cli/src/commands/chat.js +75 -0
  7. package/cli/src/index.js +64 -0
  8. package/cli/src/ui/chat-ui.js +272 -0
  9. package/cli/src/utils/ansi.js +40 -0
  10. package/cli/src/utils/markdown.js +296 -0
  11. package/docs/quick-reference.md +131 -0
  12. package/docs/user-manual.md +1205 -0
  13. package/examples/basic.js +110 -0
  14. package/examples/bootstrap.js +93 -0
  15. package/examples/mcp-example.js +53 -0
  16. package/examples/skill-example.js +49 -0
  17. package/examples/workflow.js +158 -0
  18. package/package.json +36 -0
  19. package/plugins/ai-plugin.js +89 -0
  20. package/plugins/audit-plugin.js +187 -0
  21. package/plugins/default-plugins.js +412 -0
  22. package/plugins/file-system-plugin.js +344 -0
  23. package/plugins/install-plugin.js +93 -0
  24. package/plugins/python-executor-plugin.js +331 -0
  25. package/plugins/rules-plugin.js +292 -0
  26. package/plugins/scheduler-plugin.js +426 -0
  27. package/plugins/session-plugin.js +343 -0
  28. package/plugins/shell-executor-plugin.js +196 -0
  29. package/plugins/storage-plugin.js +237 -0
  30. package/plugins/subagent-plugin.js +395 -0
  31. package/plugins/think-plugin.js +329 -0
  32. package/plugins/tools-plugin.js +114 -0
  33. package/skills/mcp-usage/SKILL.md +198 -0
  34. package/skills/vb-agent-dev/AGENTS.md +162 -0
  35. package/skills/vb-agent-dev/SKILL.md +370 -0
  36. package/src/capabilities/index.js +11 -0
  37. package/src/capabilities/skill-manager.js +319 -0
  38. package/src/capabilities/workflow-engine.js +401 -0
  39. package/src/core/agent-chat.js +311 -0
  40. package/src/core/agent.js +573 -0
  41. package/src/core/framework.js +255 -0
  42. package/src/core/index.js +19 -0
  43. package/src/core/plugin-base.js +205 -0
  44. package/src/core/plugin-manager.js +392 -0
  45. package/src/core/provider.js +108 -0
  46. package/src/core/tool-registry.js +134 -0
  47. package/src/core/tool-router.js +216 -0
  48. package/src/executors/executor-base.js +58 -0
  49. package/src/executors/mcp-executor.js +728 -0
  50. package/src/index.js +37 -0
  51. package/src/utils/event-emitter.js +97 -0
  52. package/test-chat.js +129 -0
  53. package/test-mcp.js +79 -0
  54. package/test-reload.js +61 -0
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Python 执行器插件
3
+ * 使用 child_process 执行 Python 代码和脚本
4
+ */
5
+
6
+ const { Plugin } = require('../src/core/plugin-base')
7
+ const { spawn } = require('child_process')
8
+ const fs = require('fs')
9
+ const path = require('path')
10
+ const os = require('os')
11
+ const { z } = require('zod')
12
+
13
+ class PythonExecutorPlugin extends Plugin {
14
+ constructor(config = {}) {
15
+ super()
16
+ this.name = 'python-executor'
17
+ this.version = '1.0.0'
18
+ this.description = 'Python 执行器,用于运行 Python 代码和脚本'
19
+ this.priority = 15
20
+
21
+ this.config = {
22
+ pythonPath: config.pythonPath || 'python', // python 或 python3
23
+ timeout: config.timeout || 120000, // 2分钟超时
24
+ baseDir: config.baseDir || '.agent/python-scripts',
25
+ pipPath: config.pipPath || null // 可选,自定义 pip 路径
26
+ }
27
+
28
+ this._framework = null
29
+ this._tempDir = null
30
+ }
31
+
32
+ install(framework) {
33
+ this._framework = framework
34
+
35
+ // 确保临时目录存在
36
+ this._tempDir = path.join(os.tmpdir(), `vb-python-${process.pid}`)
37
+ if (!fs.existsSync(this._tempDir)) {
38
+ fs.mkdirSync(this._tempDir, { recursive: true })
39
+ }
40
+
41
+ return this
42
+ }
43
+
44
+ start(framework) {
45
+ // 注册 python 工具
46
+ framework.registerTool({
47
+ name: 'python',
48
+ description: '执行 Python 代码',
49
+ inputSchema: z.object({
50
+ code: z.string().describe('Python 代码'),
51
+ timeout: z.number().optional().describe('超时时间(毫秒),默认 120000')
52
+ }),
53
+ execute: async (args) => {
54
+ return this._executePython(args.code, args.timeout || this.config.timeout)
55
+ }
56
+ })
57
+
58
+ // 注册 python_script 工具
59
+ framework.registerTool({
60
+ name: 'python_script',
61
+ description: '执行 Python 脚本文件',
62
+ inputSchema: z.object({
63
+ scriptPath: z.string().describe('Python 脚本文件路径'),
64
+ args: z.string().optional().describe('传递给脚本的命令行参数'),
65
+ timeout: z.number().optional().describe('超时时间(毫秒)')
66
+ }),
67
+ execute: async (args) => {
68
+ return this._executeScript(args.scriptPath, args.args, args.timeout || this.config.timeout)
69
+ }
70
+ })
71
+
72
+ // 注册 pip_install 工具
73
+ framework.registerTool({
74
+ name: 'pip_install',
75
+ description: '安装 Python 包',
76
+ inputSchema: z.object({
77
+ package: z.string().describe('包名 (例如: numpy, pandas>=1.0)'),
78
+ upgrade: z.boolean().optional().describe('是否升级已存在的包'),
79
+ timeout: z.number().optional().describe('超时时间(毫秒)')
80
+ }),
81
+ execute: async (args) => {
82
+ return this._pipInstall(args.package, args.upgrade, args.timeout || this.config.timeout)
83
+ }
84
+ })
85
+
86
+ return this
87
+ }
88
+
89
+ /**
90
+ * 执行 Python 代码
91
+ * @private
92
+ */
93
+ _executePython(code, timeout) {
94
+ return new Promise((resolve) => {
95
+ const startTime = Date.now()
96
+ let output = ''
97
+ let errorOutput = ''
98
+
99
+ // 创建临时脚本文件
100
+ const tempFile = path.join(this._tempDir, `temp_${Date.now()}.py`)
101
+
102
+ // 包装代码,添加错误处理
103
+ const wrappedCode = `
104
+ import sys
105
+ import traceback
106
+ try:
107
+ ${code.split('\n').map(line => ' ' + line).join('\n')}
108
+ except SystemExit:
109
+ pass
110
+ except Exception:
111
+ traceback.print_exc()
112
+ sys.exit(1)
113
+ `
114
+
115
+ fs.writeFileSync(tempFile, wrappedCode, 'utf-8')
116
+
117
+ const proc = spawn(this.config.pythonPath, [tempFile], {
118
+ timeout,
119
+ env: { ...process.env }
120
+ })
121
+
122
+ proc.stdout.on('data', (data) => {
123
+ output += data.toString()
124
+ })
125
+
126
+ proc.stderr.on('data', (data) => {
127
+ errorOutput += data.toString()
128
+ })
129
+
130
+ proc.on('close', (code) => {
131
+ // 清理临时文件
132
+ try {
133
+ fs.unlinkSync(tempFile)
134
+ } catch (e) { }
135
+
136
+ const elapsed = Date.now() - startTime
137
+ resolve({
138
+ success: code === 0,
139
+ exitCode: code,
140
+ stdout: output,
141
+ stderr: errorOutput,
142
+ elapsed,
143
+ timedOut: code === null
144
+ })
145
+ })
146
+
147
+ proc.on('error', (err) => {
148
+ // 清理临时文件
149
+ try {
150
+ fs.unlinkSync(tempFile)
151
+ } catch (e) { }
152
+
153
+ resolve({
154
+ success: false,
155
+ error: err.message
156
+ })
157
+ })
158
+
159
+ setTimeout(() => {
160
+ if (!proc.killed) {
161
+ proc.kill('SIGTERM')
162
+ try {
163
+ fs.unlinkSync(tempFile)
164
+ } catch (e) { }
165
+ resolve({
166
+ success: false,
167
+ error: `Python execution timed out after ${timeout}ms`,
168
+ stdout: output,
169
+ stderr: errorOutput,
170
+ timedOut: true
171
+ })
172
+ }
173
+ }, timeout)
174
+ })
175
+ }
176
+
177
+ /**
178
+ * 执行 Python 脚本文件
179
+ * @private
180
+ */
181
+ _executeScript(scriptPath, args, timeout) {
182
+ return new Promise((resolve) => {
183
+ const startTime = Date.now()
184
+ let output = ''
185
+ let errorOutput = ''
186
+
187
+ // 检查文件是否存在
188
+ if (!fs.existsSync(scriptPath)) {
189
+ resolve({
190
+ success: false,
191
+ error: `Script file not found: ${scriptPath}`
192
+ })
193
+ return
194
+ }
195
+
196
+ const procArgs = [scriptPath]
197
+ if (args) {
198
+ procArgs.push(...args.split(' '))
199
+ }
200
+
201
+ const proc = spawn(this.config.pythonPath, procArgs, {
202
+ timeout,
203
+ env: { ...process.env }
204
+ })
205
+
206
+ proc.stdout.on('data', (data) => {
207
+ output += data.toString()
208
+ })
209
+
210
+ proc.stderr.on('data', (data) => {
211
+ errorOutput += data.toString()
212
+ })
213
+
214
+ proc.on('close', (code) => {
215
+ const elapsed = Date.now() - startTime
216
+ resolve({
217
+ success: code === 0,
218
+ exitCode: code,
219
+ stdout: output,
220
+ stderr: errorOutput,
221
+ elapsed,
222
+ timedOut: code === null
223
+ })
224
+ })
225
+
226
+ proc.on('error', (err) => {
227
+ resolve({
228
+ success: false,
229
+ error: err.message
230
+ })
231
+ })
232
+
233
+ setTimeout(() => {
234
+ if (!proc.killed) {
235
+ proc.kill('SIGTERM')
236
+ resolve({
237
+ success: false,
238
+ error: `Script execution timed out after ${timeout}ms`,
239
+ stdout: output,
240
+ stderr: errorOutput,
241
+ timedOut: true
242
+ })
243
+ }
244
+ }, timeout)
245
+ })
246
+ }
247
+
248
+ /**
249
+ * 安装 Python 包
250
+ * @private
251
+ */
252
+ _pipInstall(packageName, upgrade, timeout) {
253
+ return new Promise((resolve) => {
254
+ const startTime = Date.now()
255
+ let output = ''
256
+ let errorOutput = ''
257
+
258
+ const pipPath = this.config.pipPath || this.config.pythonPath.replace('python', 'pip')
259
+ const args = [packageName]
260
+ if (upgrade) args.unshift('-U')
261
+ if (upgrade === undefined && packageName.includes('>=')) args.unshift('-U')
262
+
263
+ const proc = spawn(pipPath, ['install', ...args], {
264
+ timeout,
265
+ env: { ...process.env }
266
+ })
267
+
268
+ proc.stdout.on('data', (data) => {
269
+ output += data.toString()
270
+ })
271
+
272
+ proc.stderr.on('data', (data) => {
273
+ errorOutput += data.toString()
274
+ })
275
+
276
+ proc.on('close', (code) => {
277
+ const elapsed = Date.now() - startTime
278
+ resolve({
279
+ success: code === 0,
280
+ exitCode: code,
281
+ stdout: output,
282
+ stderr: errorOutput,
283
+ elapsed,
284
+ package: packageName,
285
+ upgraded: upgrade || packageName.includes('>=')
286
+ })
287
+ })
288
+
289
+ proc.on('error', (err) => {
290
+ resolve({
291
+ success: false,
292
+ error: err.message,
293
+ package: packageName
294
+ })
295
+ })
296
+
297
+ setTimeout(() => {
298
+ if (!proc.killed) {
299
+ proc.kill('SIGTERM')
300
+ resolve({
301
+ success: false,
302
+ error: `pip install timed out after ${timeout}ms`,
303
+ stdout: output,
304
+ stderr: errorOutput,
305
+ timedOut: true,
306
+ package: packageName
307
+ })
308
+ }
309
+ }, timeout)
310
+ })
311
+ }
312
+
313
+ reload(framework) {
314
+ this._framework = framework
315
+ }
316
+
317
+ uninstall(framework) {
318
+ // 清理临时目录
319
+ if (this._tempDir && fs.existsSync(this._tempDir)) {
320
+ try {
321
+ fs.rmSync(this._tempDir, { recursive: true, force: true })
322
+ } catch (e) {
323
+ console.warn(`[PythonExecutor] Failed to cleanup temp dir: ${e.message}`)
324
+ }
325
+ }
326
+ this._framework = null
327
+ this._tempDir = null
328
+ }
329
+ }
330
+
331
+ module.exports = { PythonExecutorPlugin }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Rules 规则引擎插件
3
+ * 控制工具调用权限、内容过滤、触发动作
4
+ */
5
+
6
+ const { Plugin } = require('../src/core/plugin-base')
7
+ const { z } = require('zod')
8
+ const fs = require('fs')
9
+ const path = require('path')
10
+
11
+ /**
12
+ * 内置规则类型
13
+ */
14
+ const RuleType = {
15
+ ALLOW: 'allow', // 允许
16
+ DENY: 'deny', // 拒绝
17
+ TRANSFORM: 'transform', // 转换
18
+ RATE_LIMIT: 'rate_limit' // 限流
19
+ }
20
+
21
+ /**
22
+ * 规则评估结果
23
+ */
24
+ class RuleResult {
25
+ constructor(allowed, rule = null, transform = null) {
26
+ this.allowed = allowed
27
+ this.rule = rule
28
+ this.transform = transform
29
+ }
30
+ }
31
+
32
+ class RulesPlugin extends Plugin {
33
+ constructor(config = {}) {
34
+ super()
35
+ this.name = 'rules'
36
+ this.version = '1.0.0'
37
+ this.description = '规则引擎插件,用于控制工具调用权限、内容过滤、触发动作'
38
+ this.priority = 25
39
+
40
+ this.config = {
41
+ rulesDir: config.rulesDir || '.agent/rules',
42
+ autoLoad: config.autoLoad !== false
43
+ }
44
+
45
+ this._framework = null
46
+ this._rules = []
47
+ this._ruleStats = {
48
+ total: 0,
49
+ allowed: 0,
50
+ denied: 0,
51
+ transformed: 0
52
+ }
53
+ }
54
+
55
+ install(framework) {
56
+ this._framework = framework
57
+ return this
58
+ }
59
+
60
+ start(framework) {
61
+ // 自动加载规则文件
62
+ if (this.config.autoLoad && this.config.rulesDir) {
63
+ this.loadFromDirectory(this.config.rulesDir)
64
+ }
65
+
66
+ // 注册规则管理工具
67
+ framework.registerTool({
68
+ name: 'rules_list',
69
+ description: '列出所有规则',
70
+ inputSchema: z.object({}),
71
+ execute: async () => {
72
+ return {
73
+ success: true,
74
+ rules: this._rules.map(r => ({
75
+ name: r.name,
76
+ type: r.type,
77
+ target: r.target,
78
+ description: r.description,
79
+ enabled: r.enabled
80
+ })),
81
+ total: this._rules.length
82
+ }
83
+ }
84
+ })
85
+
86
+ framework.registerTool({
87
+ name: 'rules_add',
88
+ description: '添加规则',
89
+ inputSchema: z.object({
90
+ name: z.string().describe('规则名称'),
91
+ type: z.enum(['allow', 'deny', 'transform']).describe('规则类型'),
92
+ target: z.string().describe('目标 (工具名, *, @event-type)'),
93
+ pattern: z.string().optional().describe('匹配模式 (正则表达式)'),
94
+ description: z.string().optional().describe('规则描述'),
95
+ action: z.object({}).optional().describe('规则动作 (transform 时使用)')
96
+ }),
97
+ execute: async (args) => {
98
+ const rule = {
99
+ name: args.name,
100
+ type: args.type,
101
+ target: args.target,
102
+ pattern: args.pattern ? new RegExp(args.pattern) : null,
103
+ description: args.description || '',
104
+ action: args.action || {},
105
+ enabled: true
106
+ }
107
+ this._rules.push(rule)
108
+ return { success: true, rule: rule.name }
109
+ }
110
+ })
111
+
112
+ framework.registerTool({
113
+ name: 'rules_remove',
114
+ description: '移除规则',
115
+ inputSchema: z.object({
116
+ name: z.string().describe('规则名称')
117
+ }),
118
+ execute: async (args) => {
119
+ const index = this._rules.findIndex(r => r.name === args.name)
120
+ if (index === -1) {
121
+ return { success: false, error: 'Rule not found' }
122
+ }
123
+ this._rules.splice(index, 1)
124
+ return { success: true, removed: args.name }
125
+ }
126
+ })
127
+
128
+ framework.registerTool({
129
+ name: 'rules_stats',
130
+ description: '获取规则统计',
131
+ inputSchema: z.object({}),
132
+ execute: async () => {
133
+ return {
134
+ success: true,
135
+ stats: { ...this._ruleStats }
136
+ }
137
+ }
138
+ })
139
+
140
+ framework.registerTool({
141
+ name: 'rules_test',
142
+ description: '测试规则匹配',
143
+ inputSchema: z.object({
144
+ target: z.string().describe('目标 (工具名或事件类型)'),
145
+ input: z.object({}).optional().describe('输入参数')
146
+ }),
147
+ execute: async (args) => {
148
+ const result = this.evaluate(args.target, args.input || {})
149
+ return {
150
+ success: true,
151
+ target: args.target,
152
+ result: {
153
+ allowed: result.allowed,
154
+ ruleName: result.rule?.name || null,
155
+ ruleType: result.rule?.type || null
156
+ }
157
+ }
158
+ }
159
+ })
160
+
161
+ return this
162
+ }
163
+
164
+ /**
165
+ * 从目录加载规则文件
166
+ */
167
+ loadFromDirectory(rulesDir) {
168
+ const resolvedDir = path.resolve(process.cwd(), rulesDir)
169
+
170
+ if (!fs.existsSync(resolvedDir)) {
171
+ console.log(`[RulesPlugin] Rules directory not found: ${resolvedDir}`)
172
+ return
173
+ }
174
+
175
+ try {
176
+ const files = fs.readdirSync(resolvedDir).filter(f => f.endsWith('.json'))
177
+
178
+ for (const file of files) {
179
+ const filePath = path.join(resolvedDir, file)
180
+ const content = fs.readFileSync(filePath, 'utf-8')
181
+ const rules = JSON.parse(content)
182
+
183
+ for (const rule of rules) {
184
+ if (rule.pattern) {
185
+ rule.pattern = new RegExp(rule.pattern)
186
+ }
187
+ rule.enabled = rule.enabled !== false
188
+ this._rules.push(rule)
189
+ }
190
+
191
+ console.log(`[RulesPlugin] Loaded ${rules.length} rules from ${file}`)
192
+ }
193
+ } catch (err) {
194
+ console.error(`[RulesPlugin] Failed to load rules: ${err.message}`)
195
+ }
196
+ }
197
+
198
+ /**
199
+ * 评估目标是否符合规则
200
+ * @param {string} target - 目标 (工具名, *, @event-type)
201
+ * @param {Object} input - 输入参数
202
+ * @returns {RuleResult}
203
+ */
204
+ evaluate(target, input = {}) {
205
+ this._ruleStats.total++
206
+
207
+ // 按优先级排序规则 (先加载的在前)
208
+ for (const rule of this._rules) {
209
+ if (!rule.enabled) continue
210
+
211
+ // 匹配目标
212
+ if (rule.target !== '*' && rule.target !== target) continue
213
+
214
+ // 匹配模式 (如果有)
215
+ if (rule.pattern) {
216
+ const targetStr = typeof input === 'string' ? input : JSON.stringify(input)
217
+ if (!rule.pattern.test(targetStr)) continue
218
+ }
219
+
220
+ // 评估规则
221
+ if (rule.type === RuleType.DENY) {
222
+ this._ruleStats.denied++
223
+ return new RuleResult(false, rule)
224
+ }
225
+
226
+ if (rule.type === RuleType.ALLOW) {
227
+ this._ruleStats.allowed++
228
+ return new RuleResult(true, rule)
229
+ }
230
+
231
+ if (rule.type === RuleType.TRANSFORM) {
232
+ this._ruleStats.transformed++
233
+ return new RuleResult(true, rule, rule.action)
234
+ }
235
+ }
236
+
237
+ // 没有匹配规则,默认允许
238
+ this._ruleStats.allowed++
239
+ return new RuleResult(true)
240
+ }
241
+
242
+ /**
243
+ * 检查工具调用是否允许
244
+ */
245
+ checkToolCall(toolName, args) {
246
+ return this.evaluate(toolName, args)
247
+ }
248
+
249
+ /**
250
+ * 检查消息是否允许
251
+ */
252
+ checkMessage(message, context) {
253
+ return this.evaluate('@message', { message, context })
254
+ }
255
+
256
+ /**
257
+ * 添加规则
258
+ */
259
+ addRule(rule) {
260
+ this._rules.push(rule)
261
+ }
262
+
263
+ /**
264
+ * 移除规则
265
+ */
266
+ removeRule(name) {
267
+ const index = this._rules.findIndex(r => r.name === name)
268
+ if (index !== -1) {
269
+ this._rules.splice(index, 1)
270
+ return true
271
+ }
272
+ return false
273
+ }
274
+
275
+ reload(framework) {
276
+ this._framework = framework
277
+ this._rules = []
278
+ this._ruleStats = { total: 0, allowed: 0, denied: 0, transformed: 0 }
279
+
280
+ if (this.config.autoLoad && this.config.rulesDir) {
281
+ this.loadFromDirectory(this.config.rulesDir)
282
+ }
283
+ }
284
+
285
+ uninstall(framework) {
286
+ this._rules = []
287
+ this._ruleStats = { total: 0, allowed: 0, denied: 0, transformed: 0 }
288
+ this._framework = null
289
+ }
290
+ }
291
+
292
+ module.exports = { RulesPlugin }