foliko 1.0.52 → 1.0.54

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.
@@ -125,7 +125,17 @@
125
125
  "Bash(node -c src/core/plugin-manager.js && node -c plugins/tools-plugin.js 2>&1)",
126
126
  "Bash(node -c plugins/default-plugins.js && node -c cli/src/commands/chat.js 2>&1)",
127
127
  "Bash(node -c plugins/telegram-plugin.js && node -c plugins/feishu-plugin.js && node -c plugins/weixin-plugin.js 2>&1)",
128
- "Bash(node -c src/core/agent.js 2>&1)"
128
+ "Bash(node -c src/core/agent.js 2>&1)",
129
+ "Bash(cd D:/code/vb-agent && node examples/bootstrap.js 2>&1 | head -50)",
130
+ "Bash(node -c plugins/proactive-agent-plugin.js 2>&1)",
131
+ "Bash(node -c src/index.js && node -c examples/proactive-example.js && node -c examples/proactive-advanced.js && echo \"All syntax OK\")",
132
+ "Bash(node -c plugins/proactive-agent-plugin.js && node -c plugins/default-plugins.js && echo \"All OK\")",
133
+ "Bash(node examples/basic.js 2>&1 | head -50)",
134
+ "Bash(node examples/ambient-example.js 2>&1 | head -80)",
135
+ "Bash(ls -la .agent/data/ambient/ 2>/dev/null && cat .agent/data/ambient/*.json 2>/dev/null | head -50)",
136
+ "Bash(node examples/basic.js 2>&1 | head -30)",
137
+ "Bash(node examples/bootstrap.js 2>&1 | head -40)",
138
+ "Bash(node test-debug.js 2>&1)"
129
139
  ]
130
140
  }
131
141
  }
package/CLAUDE.md ADDED
@@ -0,0 +1,106 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ **Foliko** is a minimalist plugin-based Agent framework in pure JavaScript (no TypeScript). It provides a lightweight core with extensible plugins for AI conversation, tool execution, workflow automation, and more.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Run basic example
13
+ npm start
14
+
15
+ # CLI chat mode
16
+ npm run chat
17
+
18
+ # Proactive agent examples
19
+ npm run proactive
20
+ npm run proactive:advanced
21
+
22
+ # No test suite yet
23
+ npm test
24
+ ```
25
+
26
+ ## Architecture
27
+
28
+ ```
29
+ ┌─────────────────────────────────────────────────┐
30
+ │ Foliko Framework │
31
+ ├─────────────────────────────────────────────────┤
32
+ │ Framework (Container Layer) │
33
+ │ ├── pluginManager - Plugin lifecycle │
34
+ │ ├── toolRegistry - Tool registration │
35
+ │ ├── skillManager - Skill management │
36
+ │ └── eventEmitter - Event bus │
37
+ ├─────────────────────────────────────────────────┤
38
+ │ Agent (Dialogue Layer) │
39
+ │ ├── chat() - Send messages │
40
+ │ ├── chatStream() - Streaming responses │
41
+ │ ├── tools - From Framework │
42
+ │ └── events - Message/tool events │
43
+ └─────────────────────────────────────────────────┘
44
+ ```
45
+
46
+ ### Core Classes
47
+
48
+ - **Framework** (`src/core/framework.js`) - Container managing plugins, tools, events, and agent creation
49
+ - **Agent** (`src/core/agent.js`) - Handles conversation, tool calls, subAgent delegation, and message queuing
50
+ - **Plugin** (`src/core/plugin-base.js`) - Base class with `install()`, `start()`, `reload()`, `uninstall()` lifecycle
51
+ - **PluginManager** (`src/core/plugin-manager.js`) - Manages plugin loading, priority ordering, and hot reload
52
+ - **ToolRegistry** (`src/core/tool-registry.js`) - Registers and executes tools with Zod schemas
53
+ - **AgentChatHandler** (`src/core/agent-chat.js`) - Handles AI provider communication and tool call loops
54
+
55
+ ### Directory Structure
56
+
57
+ ```
58
+ src/
59
+ ├── core/ # Framework, Agent, PluginManager, ToolRegistry
60
+ ├── capabilities/ # SkillManager, WorkflowEngine
61
+ ├── executors/ # MCPExecutor for MCP server integration
62
+ └── utils/ # EventEmitter
63
+
64
+ plugins/ # Built-in plugins (ai-plugin, tools-plugin, shell/python executors, session, scheduler, etc.)
65
+ cli/bin/ # CLI entry point
66
+ .agent/ # User configuration (plugins, skills, agents, mcp_config.json)
67
+ examples/ # Usage examples
68
+ ```
69
+
70
+ ### Plugin System
71
+
72
+ Plugins are registered via `framework.registerPlugin()` and loaded with `framework.loadPlugin()`. Each plugin:
73
+ - Has a `name`, `version`, `description`, and `priority`
74
+ - Implements `install(framework)` to register tools/events
75
+ - Implements `start(framework)` after initialization
76
+ - Can implement `reload(framework)` for hot reload
77
+ - Can implement `uninstall(framework)` for cleanup
78
+
79
+ ### AI Integration
80
+
81
+ Uses Vercel AI SDK (`ai` package) with support for multiple providers:
82
+ - Anthropic, DeepSeek, MiniMax, OpenAI, OpenAI-Compatible
83
+
84
+ Provider selection via `config.provider` and AI settings in `.agent/ai.json`.
85
+
86
+ ### Key Patterns
87
+
88
+ 1. **Context Isolation**: `framework.runWithContext(context, fn)` uses AsyncLocalStorage for true context isolation
89
+ 2. **Tool Execution**: Tools receive `(args, framework)` and return results; errors should be caught and returned as `{error: message}`
90
+ 3. **Event System**: Framework emits events like `framework:ready`, `plugin:loaded`, `agent:message`, `tool-call`
91
+ 4. **Hot Reload**: Manual only via `framework.reloadPlugin(name)` or `framework.reloadAllPlugins()` - no file watching
92
+
93
+ ### Built-in Plugins
94
+
95
+ - **ai-plugin** - AI conversation via Vercel AI SDK
96
+ - **tools-plugin** - Plugin management tools (list, reload, enable/disable)
97
+ - **shell-executor-plugin** - Shell command execution
98
+ - **python-executor-plugin** - Python code/script execution
99
+ - **session-plugin** - Multi-session management
100
+ - **scheduler-plugin** - Cron-based task scheduling
101
+ - **subagent-plugin** - Child agent isolation
102
+ - **email.js** - SMTP/IMAP email
103
+ - **telegram-plugin.js** - Telegram bot integration
104
+ - **audit-plugin.js** - Operation logging
105
+ - **rules-plugin.js** - Permission/content rules
106
+ - **storage-plugin.js** - Key-value persistence
package/Dockerfile CHANGED
@@ -49,7 +49,7 @@ ENV PATH="/root/.local/bin:$PATH"
49
49
  WORKDIR /app
50
50
 
51
51
  # 全局安装 foliko CLI
52
- RUN npm install -g foliko
52
+ # RUN npm install -g foliko
53
53
 
54
54
  # 暴露端口
55
55
  # 3000: Web 服务端口
@@ -60,4 +60,4 @@ ENV NODE_ENV=production
60
60
 
61
61
 
62
62
  # 默认命令:运行聊天界面
63
- CMD ["foliko", "chat"]
63
+ # CMD ["foliko", "chat"]
package/cli/src/index.js CHANGED
@@ -4,14 +4,16 @@
4
4
 
5
5
  const { chatCommand } = require('./commands/chat')
6
6
  const { listCommand } = require('./commands/list')
7
-
7
+ const fs = require('fs');
8
+ const path = require('path');
8
9
  /**
9
10
  * CLI 主入口
10
11
  */
11
12
  async function cli() {
12
13
  const args = process.argv.slice(2)
13
14
  const command = args[0] || 'chat'
14
-
15
+ const packageJsonPath = path.join(__dirname, '../../package.json');
16
+
15
17
  switch (command) {
16
18
  case 'chat':
17
19
  await chatCommand(args.slice(1))
@@ -31,7 +33,8 @@ async function cli() {
31
33
  case 'version':
32
34
  case '--version':
33
35
  case '-v':
34
- console.log('foliko v1.0.0')
36
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
37
+ console.log(`${packageJson.name} v${packageJson.version}`)
35
38
  break
36
39
 
37
40
  default:
@@ -147,7 +147,7 @@ class ChatUI {
147
147
  const renderState = { inThink: false, inCodeBlock: false }
148
148
 
149
149
  console.log(colored('● ', GREEN))
150
- console.log()
150
+ // console.log()
151
151
 
152
152
  const runWithContext = this.agent.framework?.runWithContext.bind(this.agent.framework)
153
153
  const { sessionId } = this
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Ambient Agent Example
3
+ * Demonstrates how to use the Ambient Agent plugin for autonomous monitoring and actions
4
+ */
5
+
6
+ const { Framework } = require('../src/core/framework')
7
+
8
+ async function main() {
9
+ console.log('=== Ambient Agent Example ===\n')
10
+
11
+ // Create framework
12
+ const framework = new Framework({
13
+ debug: true
14
+ })
15
+
16
+ // Bootstrap with default plugins (including ambient)
17
+ await framework.bootstrap({
18
+ agentDir: './.agent',
19
+ aiConfig: {
20
+ provider: 'deepseek',
21
+ model: 'deepseek-chat',
22
+ apiKey: process.env.DEEPSEEK_API_KEY || 'your-api-key'
23
+ }
24
+ })
25
+
26
+ console.log('\n=== Framework Ready ===\n')
27
+
28
+ // Wait for plugins to start
29
+ await new Promise(resolve => setTimeout(resolve, 1000))
30
+
31
+ // Check if ambient plugin is loaded
32
+ const ambientPlugin = framework.pluginManager.get('ambient')
33
+ if (!ambientPlugin) {
34
+ console.error('Ambient plugin not loaded!')
35
+ return
36
+ }
37
+
38
+ console.log('=== Ambient Plugin Tools Available ===')
39
+ const tools = framework.getTools()
40
+ const ambientTools = tools.filter(t => t.name.startsWith('ambient_'))
41
+ console.log('Ambient tools:', ambientTools.map(t => t.name).join(', '))
42
+
43
+ console.log('\n=== Example 1: Create a Goal ===')
44
+ // Use ambient_goals to create a simple goal
45
+ const createResult = await framework.executeTool('ambient_goals', {
46
+ action: 'create',
47
+ title: 'Monitor System Health',
48
+ description: 'Periodically check system information and alert on issues',
49
+ priority: 8,
50
+ actions: [
51
+ { id: 'check_1', type: 'tool', name: 'system_info', args: {} }
52
+ ]
53
+ })
54
+ console.log('Create goal result:', JSON.stringify(createResult, null, 2))
55
+
56
+ console.log('\n=== Example 2: Create a Thinking Goal ===')
57
+ // Create a goal that triggers LLM thinking
58
+ const thinkGoal = await framework.executeTool('ambient_goals', {
59
+ action: 'create',
60
+ title: 'Periodic Reflection',
61
+ description: 'Regularly reflect on system state and suggest improvements',
62
+ priority: 5,
63
+ actions: [
64
+ { id: 'reflect_1', type: 'think', topic: 'System state review', mode: 'reflect', depth: 3 }
65
+ ]
66
+ })
67
+ console.log('Create thinking goal result:', JSON.stringify(thinkGoal, null, 2))
68
+
69
+ console.log('\n=== Example 3: List All Goals ===')
70
+ const listResult = await framework.executeTool('ambient_goals', {
71
+ action: 'list'
72
+ })
73
+ console.log('Goals list:', JSON.stringify(listResult, null, 2))
74
+
75
+ console.log('\n=== Example 4: Get Ambient Status ===')
76
+ const statusResult = await framework.executeTool('ambient_status', {})
77
+ console.log('Status:', JSON.stringify(statusResult, null, 2))
78
+
79
+ console.log('\n=== Example 5: Store a Memory ===')
80
+ const storeResult = await framework.executeTool('ambient_remember', {
81
+ action: 'store',
82
+ content: 'Important: System check showed high CPU usage at 14:30',
83
+ key: 'cpu_alert_1'
84
+ })
85
+ console.log('Store memory result:', JSON.stringify(storeResult, null, 2))
86
+
87
+ console.log('\n=== Example 6: Retrieve Memories ===')
88
+ const retrieveResult = await framework.executeTool('ambient_remember', {
89
+ action: 'retrieve',
90
+ limit: 5
91
+ })
92
+ console.log('Retrieve memories result:', JSON.stringify(retrieveResult, null, 2))
93
+
94
+ console.log('\n=== Example 7: Search Memories ===')
95
+ const searchResult = await framework.executeTool('ambient_remember', {
96
+ action: 'search',
97
+ query: 'CPU'
98
+ })
99
+ console.log('Search memories result:', JSON.stringify(searchResult, null, 2))
100
+
101
+ console.log('\n=== Example 8: Trigger Thinking ===')
102
+ const thinkResult = await framework.executeTool('ambient_think', {
103
+ mode: 'brainstorm',
104
+ topic: 'What improvements could be made to the current system?',
105
+ depth: 3
106
+ })
107
+ console.log('Think result:', JSON.stringify(thinkResult, null, 2))
108
+
109
+ console.log('\n=== Example 9: Control Loop ===')
110
+ console.log('Pausing loop...')
111
+ const pauseResult = await framework.executeTool('ambient_control', {
112
+ action: 'pause'
113
+ })
114
+ console.log('Pause result:', JSON.stringify(pauseResult, null, 2))
115
+
116
+ await new Promise(resolve => setTimeout(resolve, 1000))
117
+
118
+ console.log('Resuming loop...')
119
+ const resumeResult = await framework.executeTool('ambient_control', {
120
+ action: 'resume'
121
+ })
122
+ console.log('Resume result:', JSON.stringify(resumeResult, null, 2))
123
+
124
+ console.log('\n=== Example 10: Adjust Loop Settings ===')
125
+ const adjustResult = await framework.executeTool('ambient_control', {
126
+ action: 'adjust',
127
+ tickInterval: 10000, // 10 seconds
128
+ cooldownPeriod: 5000 // 5 seconds
129
+ })
130
+ console.log('Adjust result:', JSON.stringify(adjustResult, null, 2))
131
+
132
+ console.log('\n=== Example 11: Get Final Status ===')
133
+ const finalStatus = await framework.executeTool('ambient_status', {})
134
+ console.log('Final status:', JSON.stringify(finalStatus, null, 2))
135
+
136
+ console.log('\n=== Example 12: Activate a Pending Goal ===')
137
+ // First create a pending goal (without auto-activation conditions)
138
+ const newGoal = await framework.executeTool('ambient_goals', {
139
+ action: 'create',
140
+ title: 'Inactive Test Goal',
141
+ description: 'A goal that starts inactive',
142
+ priority: 3,
143
+ actions: []
144
+ })
145
+ console.log('Created inactive goal:', newGoal.goal ? newGoal.goal.id : 'N/A')
146
+
147
+ if (newGoal.goal) {
148
+ console.log('Activating goal...')
149
+ const activateResult = await framework.executeTool('ambient_goals', {
150
+ action: 'activate',
151
+ goalId: newGoal.goal.id
152
+ })
153
+ console.log('Activate result:', JSON.stringify(activateResult, null, 2))
154
+ }
155
+
156
+ console.log('\n=== Example 13: Update a Goal ===')
157
+ const goals = await framework.executeTool('ambient_goals', { action: 'list' })
158
+ if (goals.goals && goals.goals.length > 0) {
159
+ const goalToUpdate = goals.goals[0]
160
+ console.log(`Updating goal ${goalToUpdate.id}...`)
161
+ const updateResult = await framework.executeTool('ambient_goals', {
162
+ action: 'update',
163
+ goalId: goalToUpdate.id,
164
+ priority: 10
165
+ })
166
+ console.log('Update result:', JSON.stringify(updateResult, null, 2))
167
+ }
168
+
169
+ console.log('\n=== Example 14: Delete a Goal ===')
170
+ const deleteGoal = await framework.executeTool('ambient_goals', {
171
+ action: 'create',
172
+ title: 'Goal to Delete',
173
+ description: 'This will be deleted',
174
+ priority: 1
175
+ })
176
+ if (deleteGoal.goal) {
177
+ console.log(`Created goal to delete: ${deleteGoal.goal.id}`)
178
+ const deleteResult = await framework.executeTool('ambient_goals', {
179
+ action: 'delete',
180
+ goalId: deleteGoal.goal.id
181
+ })
182
+ console.log('Delete result:', JSON.stringify(deleteResult, null, 2))
183
+ }
184
+
185
+ console.log('\n=== All Examples Completed ===')
186
+ console.log('Check .agent/data/ambient/ for persisted data')
187
+
188
+ // Cleanup
189
+ console.log('\nShutting down...')
190
+ await framework.destroy()
191
+ }
192
+
193
+ main().catch(err => {
194
+ console.error('Example error:', err)
195
+ process.exit(1)
196
+ })
package/examples/basic.js CHANGED
@@ -1,110 +1,110 @@
1
- /**
2
- * 基础示例
3
- * 展示如何使用 Framework 和 Agent
4
- */
5
-
6
- const { Framework } = require('../src')
7
- const { AIPlugin } = require('../plugins/ai-plugin')
8
- const { z } = require('zod')
9
-
10
- async function main() {
11
- // 创建框架实例
12
- const framework = new Framework({ debug: true })
13
-
14
- // 加载 AI 插件
15
- await framework.loadPlugin(new AIPlugin({
16
- provider: 'deepseek',
17
- model: 'deepseek-chat',
18
- apiKey: process.env.DEEPSEEK_API_KEY || 'your-api-key'
19
- }))
20
-
21
- // 注册自定义工具(使用 inputSchema 格式)
22
- framework.registerTool({
23
- name: 'hello',
24
- description: '打招呼工具',
25
- inputSchema: z.object({
26
- name: z.string().optional().describe('姓名')
27
- }),
28
- execute: async (args) => {
29
- return `Hello, ${args.name || 'World'}!`
30
- }
31
- })
32
-
33
- // 注册计算器工具
34
- framework.registerTool({
35
- name: 'calculate',
36
- description: '简单的计算器',
37
- inputSchema: z.object({
38
- expression: z.string().describe('数学表达式,如 2+3*4')
39
- }),
40
- execute: async (args) => {
41
- try {
42
- // 安全计算(仅支持基本运算)
43
- const result = Function(`"use strict"; return (${args.expression})`)()
44
- return { result }
45
- } catch (e) {
46
- return { error: e.message }
47
- }
48
- }
49
- })
50
-
51
- console.log('[Framework] Ready!')
52
- console.log('[Tools]', framework.getTools().map(t => t.name))
53
-
54
- // 创建 Agent
55
- const agent = framework.createAgent({
56
- name: 'MyAgent',
57
- systemPrompt: '你是一个有帮助的助手。当需要计算时,使用 calculate 工具。'
58
- })
59
-
60
- // 监听事件
61
- agent.on('tool-call', (tool) => {
62
- console.log('[Agent] Tool call:', tool.name, tool.args)
63
- })
64
-
65
- agent.on('tool-result', (result) => {
66
- console.log('[Agent] Tool result:', result.name, result.result)
67
- })
68
-
69
- // AI 对话示例
70
- console.log('\n=== AI Chat Example ===')
71
- try {
72
- const response = await agent.chat('你好!')
73
- console.log('[Agent] Response:', response.message)
74
- } catch (err) {
75
- console.error('[Agent] Error:', err.message)
76
- }
77
-
78
- // 使用工具的对话示例
79
- console.log('\n=== AI Chat with Tool Call ===')
80
- try {
81
- const response = await agent.chat('请帮我计算 (15 + 25) * 2 等于多少?')
82
- console.log('[Agent] Response:', response.message)
83
- } catch (err) {
84
- console.error('[Agent] Error:', err.message)
85
- }
86
-
87
- // 流式对话示例
88
- console.log('\n=== Streaming Chat ===')
89
- try {
90
- for await (const chunk of agent.chatStream('请用中文介绍一下你自己')) {
91
- if (chunk.type === 'text') {
92
- process.stdout.write(chunk.text)
93
- }
94
- }
95
- console.log('\n')
96
- } catch (err) {
97
- console.error('[Agent] Stream Error:', err.message)
98
- }
99
-
100
- // 热重载示例
101
- console.log('\n=== Hot Reload ===')
102
- await framework.reloadPlugin('ai')
103
- console.log('AI plugin reloaded!')
104
-
105
- // 清理
106
- await framework.destroy()
107
- console.log('\n[Done]')
108
- }
109
-
110
- main().catch(console.error)
1
+ /**
2
+ * 基础示例
3
+ * 展示如何使用 Framework 和 Agent
4
+ */
5
+
6
+ const { Framework } = require('../src')
7
+ const { AIPlugin } = require('../plugins/ai-plugin')
8
+ const { z } = require('zod')
9
+
10
+ async function main() {
11
+ // 创建框架实例
12
+ const framework = new Framework({ debug: true })
13
+
14
+ // 加载 AI 插件
15
+ await framework.loadPlugin(new AIPlugin({
16
+ provider: 'deepseek',
17
+ model: 'deepseek-chat',
18
+ apiKey: process.env.DEEPSEEK_API_KEY || 'your-api-key'
19
+ }))
20
+
21
+ // 注册自定义工具(使用 inputSchema 格式)
22
+ framework.registerTool({
23
+ name: 'hello',
24
+ description: '打招呼工具',
25
+ inputSchema: z.object({
26
+ name: z.string().optional().describe('姓名')
27
+ }),
28
+ execute: async (args) => {
29
+ return `Hello, ${args.name || 'World'}!`
30
+ }
31
+ })
32
+
33
+ // 注册计算器工具
34
+ framework.registerTool({
35
+ name: 'calculate',
36
+ description: '简单的计算器',
37
+ inputSchema: z.object({
38
+ expression: z.string().describe('数学表达式,如 2+3*4')
39
+ }),
40
+ execute: async (args) => {
41
+ try {
42
+ // 安全计算(仅支持基本运算)
43
+ const result = Function(`"use strict"; return (${args.expression})`)()
44
+ return { result }
45
+ } catch (e) {
46
+ return { error: e.message }
47
+ }
48
+ }
49
+ })
50
+
51
+ console.log('[Framework] Ready!')
52
+ console.log('[Tools]', framework.getTools().map(t => t.name))
53
+
54
+ // 创建 Agent
55
+ const agent = framework.createAgent({
56
+ name: 'MyAgent',
57
+ systemPrompt: '你是一个有帮助的助手。当需要计算时,使用 calculate 工具。'
58
+ })
59
+
60
+ // 监听事件
61
+ agent.on('tool-call', (tool) => {
62
+ console.log('[Agent] Tool call:', tool.name, tool.args)
63
+ })
64
+
65
+ agent.on('tool-result', (result) => {
66
+ console.log('[Agent] Tool result:', result.name, result.result)
67
+ })
68
+
69
+ // AI 对话示例
70
+ console.log('\n=== AI Chat Example ===')
71
+ try {
72
+ const response = await agent.chat('你好!')
73
+ console.log('[Agent] Response:', response.message)
74
+ } catch (err) {
75
+ console.error('[Agent] Error:', err.message)
76
+ }
77
+
78
+ // 使用工具的对话示例
79
+ console.log('\n=== AI Chat with Tool Call ===')
80
+ try {
81
+ const response = await agent.chat('请帮我计算 (15 + 25) * 2 等于多少?')
82
+ console.log('[Agent] Response:', response.message)
83
+ } catch (err) {
84
+ console.error('[Agent] Error:', err.message)
85
+ }
86
+
87
+ // 流式对话示例
88
+ console.log('\n=== Streaming Chat ===')
89
+ try {
90
+ for await (const chunk of agent.chatStream('请用中文介绍一下你自己')) {
91
+ if (chunk.type === 'text') {
92
+ process.stdout.write(chunk.text)
93
+ }
94
+ }
95
+ console.log('\n')
96
+ } catch (err) {
97
+ console.error('[Agent] Stream Error:', err.message)
98
+ }
99
+
100
+ // 热重载示例
101
+ console.log('\n=== Hot Reload ===')
102
+ await framework.reloadPlugin('ai')
103
+ console.log('AI plugin reloaded!')
104
+
105
+ // 清理
106
+ await framework.destroy()
107
+ console.log('\n[Done]')
108
+ }
109
+
110
+ main().catch(console.error)