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,272 @@
1
+ /**
2
+ * 聊天界面组件
3
+ * 处理用户输入、粘贴检测、输出渲染
4
+ */
5
+
6
+ const readline = require('readline')
7
+ const { EventEmitter } = require('events')
8
+ const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../utils/ansi')
9
+ const { render, renderLine } = require('../utils/markdown')
10
+
11
+ class ChatUI extends EventEmitter {
12
+ constructor(agent) {
13
+ super()
14
+ this.agent = agent
15
+ this.rl = null
16
+ this.currentLine = ''
17
+ this.lines = []
18
+ this.pasteId = 0
19
+ this.isFirstLine = true
20
+
21
+ // 粘贴检测
22
+ this.lastKeyTime = 0
23
+ this.pasteBuffer = ''
24
+ this.isPasting = false
25
+ }
26
+
27
+ /**
28
+ * 启动聊天界面
29
+ */
30
+ start() {
31
+ this.setupReadline()
32
+ this.printWelcome()
33
+ this.prompt()
34
+ }
35
+
36
+ /**
37
+ * 设置 readline
38
+ */
39
+ setupReadline() {
40
+ this.rl = readline.createInterface({
41
+ input: process.stdin,
42
+ output: process.stdout,
43
+ crlfDelay: Infinity
44
+ })
45
+
46
+ // 使用 keypress 事件支持特殊按键
47
+ readline.emitKeypressEvents(process.stdin)
48
+
49
+ if (process.stdin.isTTY) {
50
+ process.stdin.setRawMode(true)
51
+ }
52
+
53
+ // 监听按键
54
+ process.stdin.on('keypress', this.handleKey.bind(this))
55
+
56
+ // 退出时清理
57
+ process.on('exit', () => {
58
+ if (process.stdin.isTTY) {
59
+ process.stdin.setRawMode(false)
60
+ }
61
+ })
62
+
63
+ process.on('SIGINT', () => {
64
+ if (process.stdin.isTTY) {
65
+ process.stdin.setRawMode(false)
66
+ }
67
+ process.exit(0)
68
+ })
69
+ }
70
+
71
+ /**
72
+ * 打印欢迎信息
73
+ */
74
+ printWelcome() {
75
+ console.log(`${colored('Foliko', CYAN)} - 持续对话聊天`)
76
+ console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('Ctrl+Enter', DIM)} 换行 | ${colored('Enter', DIM)} 发送\n`)
77
+ }
78
+
79
+ /**
80
+ * 显示提示符
81
+ */
82
+ prompt() {
83
+ const prefix = this.isFirstLine ? colored('You: ', GREEN) : colored('> ', DIM)
84
+ process.stdout.write(`\r${CLEAR_LINE}${prefix}${this.currentLine}`)
85
+ }
86
+
87
+ /**
88
+ * 处理按键
89
+ */
90
+ handleKey(str, key) {
91
+ // Ctrl+C 退出
92
+ if (key.ctrl && key.name === 'c') {
93
+ console.log('\n再见!')
94
+ if (process.stdin.isTTY) {
95
+ process.stdin.setRawMode(false)
96
+ }
97
+ process.exit(0)
98
+ return
99
+ }
100
+
101
+ // Enter 发送
102
+ if (key.name === 'return') {
103
+ this.handleSubmit()
104
+ return
105
+ }
106
+
107
+ // Ctrl+Enter 换行
108
+ if (key.ctrl && key.name === 'return') {
109
+ this.lines.push(this.currentLine)
110
+ this.currentLine = ''
111
+ this.isFirstLine = false
112
+ this.prompt()
113
+ return
114
+ }
115
+
116
+ // Backspace 退格
117
+ if (key.name === 'backspace') {
118
+ if (this.currentLine.length > 0) {
119
+ this.currentLine = this.currentLine.slice(0, -1)
120
+ this.prompt()
121
+ } else if (this.lines.length > 0) {
122
+ this.currentLine = this.lines.pop()
123
+ this.isFirstLine = this.lines.length === 0
124
+ this.prompt()
125
+ }
126
+ return
127
+ }
128
+
129
+ // 普通字符
130
+ if (str && !key.ctrl && !key.meta) {
131
+ // 直接添加到当前行
132
+ this.currentLine += str
133
+ this.prompt()
134
+ }
135
+ }
136
+
137
+ /**
138
+ * 处理粘贴
139
+ */
140
+ handlePaste() {
141
+ if (this.pasteBuffer.includes('\n')) {
142
+ const allLines = this.pasteBuffer.split('\n')
143
+ this.lines.push(...allLines.slice(0, -1))
144
+ this.currentLine = allLines[allLines.length - 1]
145
+ this.isFirstLine = false
146
+ this.pasteId++
147
+
148
+ console.log(`\n${CLEAR_LINE}${colored(`[Pasted text #${this.pasteId} +${this.lines.length + 1} lines]`, CYAN)}`)
149
+ } else {
150
+ this.currentLine = this.pasteBuffer
151
+ this.lines = []
152
+ }
153
+ this.pasteBuffer = ''
154
+ }
155
+
156
+ /**
157
+ * 处理发送
158
+ */
159
+ async handleSubmit() {
160
+ const content = this.lines.length > 0
161
+ ? [...this.lines, this.currentLine].join('\n')
162
+ : this.currentLine
163
+
164
+ const trimmed = content.trim()
165
+
166
+ // 显示空行
167
+ console.log()
168
+
169
+ // 退出命令
170
+ if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
171
+ console.log('再见!')
172
+ if (process.stdin.isTTY) {
173
+ process.stdin.setRawMode(false)
174
+ }
175
+ process.exit(0)
176
+ return
177
+ }
178
+
179
+ if (!trimmed) {
180
+ this.resetInput()
181
+ this.prompt()
182
+ return
183
+ }
184
+
185
+ // 重置输入
186
+ this.resetInput()
187
+
188
+ // 发送消息
189
+ await this.sendMessage(trimmed)
190
+ }
191
+
192
+ /**
193
+ * 重置输入状态
194
+ */
195
+ resetInput() {
196
+ this.currentLine = ''
197
+ this.lines = []
198
+ this.isFirstLine = true
199
+ this.pasteId = 0
200
+ this.isPasting = false
201
+ this.pasteBuffer = ''
202
+ }
203
+
204
+ /**
205
+ * 发送消息并显示响应
206
+ */
207
+ async sendMessage(message) {
208
+ console.log(colored('Agent:', GREEN))
209
+ console.log()
210
+
211
+ try {
212
+ let lineBuffer = ''
213
+ // 渲染状态追踪
214
+ const renderState = { inThink: false, inCodeBlock: false }
215
+
216
+ // 检查是否还有孤立的代理对
217
+ const hasOrphanedSurrogate = (str) => {
218
+ if (!str || str.length === 0) return false
219
+ let i = 0
220
+ while (i < str.length) {
221
+ const code = str.charCodeAt(i)
222
+ if (code >= 0xD800 && code <= 0xDBFF) {
223
+ const nextCode = str.charCodeAt(i + 1)
224
+ if (nextCode >= 0xDC00 && nextCode <= 0xDFFF) {
225
+ i += 2
226
+ continue
227
+ }
228
+ return true // 孤立的高代理
229
+ }
230
+ i++
231
+ }
232
+ return false
233
+ }
234
+
235
+ for await (const chunk of this.agent.chatStream(message)) {
236
+ if (chunk.type === 'text') {
237
+ lineBuffer += chunk.text
238
+
239
+ // 如果有孤立代理对,等待下一个chunk补充
240
+ if (hasOrphanedSurrogate(lineBuffer)) {
241
+ continue
242
+ }
243
+
244
+ // 当有一行完整内容时,渲染并输出
245
+ while (lineBuffer.includes('\n')) {
246
+ const nlIndex = lineBuffer.indexOf('\n')
247
+ const line = lineBuffer.substring(0, nlIndex)
248
+ lineBuffer = lineBuffer.substring(nlIndex + 1)
249
+ if (line.trim()) {
250
+ console.log(renderLine(line, renderState))
251
+ }
252
+ }
253
+ } else if (chunk.type === 'tool-call') {
254
+ console.log(`\n${colored('[工具调用]', YELLOW)} ${chunk.toolName}`)
255
+ } else if (chunk.type === 'error') {
256
+ console.error(`\n${colored('[错误]', RED)} ${chunk.error}`)
257
+ }
258
+ }
259
+
260
+ // 输出剩余内容
261
+ if (lineBuffer.trim()) {
262
+ console.log(renderLine(lineBuffer, renderState))
263
+ }
264
+ } catch (err) {
265
+ console.error(`\n${colored('[错误]', RED)} ${err.message}\n`)
266
+ }
267
+
268
+ this.prompt()
269
+ }
270
+ }
271
+
272
+ module.exports = { ChatUI }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * ANSI 转义序列工具
3
+ */
4
+
5
+ const CLEAR = '\x1B[0m'
6
+ const CLEAR_LINE = '\x1B[2K'
7
+
8
+ // 颜色
9
+ const RED = '\x1B[31m'
10
+ const GREEN = '\x1B[32m'
11
+ const YELLOW = '\x1B[33m'
12
+ const BLUE = '\x1B[34m'
13
+ const MAGENTA = '\x1B[35m'
14
+ const CYAN = '\x1B[36m'
15
+ const WHITE = '\x1B[37m'
16
+ const DIM = '\x1B[2m'
17
+ const BOLD = '\x1B[1m'
18
+
19
+ /**
20
+ * 创建带颜色的字符串
21
+ */
22
+ function colored(text, color) {
23
+ if (!color) return text
24
+ return `${color}${text}${CLEAR}`
25
+ }
26
+
27
+ module.exports = {
28
+ CLEAR,
29
+ CLEAR_LINE,
30
+ RED,
31
+ GREEN,
32
+ YELLOW,
33
+ BLUE,
34
+ MAGENTA,
35
+ CYAN,
36
+ WHITE,
37
+ DIM,
38
+ BOLD,
39
+ colored
40
+ }
@@ -0,0 +1,296 @@
1
+ /**
2
+ * 简单的流式 Markdown 渲染器
3
+ * 支持: **粗体**, *斜体*, `代码`, ```代码块```, # 标题, 列表, 引用, <think>思考
4
+ */
5
+
6
+ const {
7
+ BOLD, DIM, CLEAR, RED, GREEN, YELLOW, CYAN, BLUE, MAGENTA, WHITE
8
+ } = require('./ansi')
9
+
10
+ // Markdown 标记对应的 ANSI 样式
11
+ const STYLES = {
12
+ bold: { prefix: BOLD, suffix: CLEAR },
13
+ italic: { prefix: DIM, suffix: CLEAR },
14
+ code: { prefix: CYAN, suffix: CLEAR }, // dim = 暗色
15
+ codeBlock: { prefix: YELLOW, suffix: CLEAR }, // yellow = 黄色
16
+ h1: { prefix: BOLD, suffix: CLEAR }, // bold white
17
+ h2: { prefix: BOLD, suffix: CLEAR }, // bold white
18
+ h3: { prefix: BOLD, suffix: CLEAR },
19
+ link: { prefix: '\x1B[34m\x1B[4m', suffix: CLEAR }, // blue underline
20
+ list: { prefix: DIM, suffix: CLEAR }, // dim = 暗色
21
+ blockquote: { prefix: DIM, suffix: CLEAR }, // dim = 暗色
22
+ think: { prefix: `\x1B[38;5;240m`, suffix: CLEAR }, // yellow = 黄色
23
+ }
24
+
25
+ /**
26
+ * 验证并修复UTF-16代理对
27
+ */
28
+ function fixSurrogates(text) {
29
+ if (!text) return ''
30
+ const result = []
31
+ let i = 0
32
+ while (i < text.length) {
33
+ const char = text[i]
34
+ const code = text.charCodeAt(i)
35
+ // 高代理
36
+ if (code >= 0xD800 && code <= 0xDBFF) {
37
+ const nextCode = text.charCodeAt(i + 1)
38
+ // 如果有低代理,配对
39
+ if (nextCode >= 0xDC00 && nextCode <= 0xDFFF) {
40
+ result.push(char + text[i + 1])
41
+ i += 2
42
+ continue
43
+ }
44
+ // 孤立的高代理,替换为方框
45
+ result.push('\uFFFD')
46
+ i += 1
47
+ continue
48
+ }
49
+ // 低代理(不应该单独出现)
50
+ if (code >= 0xDC00 && code <= 0xDFFF) {
51
+ result.push('\uFFFD')
52
+ i += 1
53
+ continue
54
+ }
55
+ result.push(char)
56
+ i++
57
+ }
58
+ return result.join('')
59
+ }
60
+
61
+ /**
62
+ * 渲染单行 markdown(流式友好,简单处理)
63
+ */
64
+ function renderInline(text) {
65
+ if (!text) return ''
66
+
67
+ // 修复不完整的代理对
68
+ const fixed = fixSurrogates(text)
69
+ let result = fixed
70
+
71
+
72
+ // 处理行内代码 `code`
73
+ result = result.replace(/`([^`]+)`/g, (match, code) => {
74
+ return `${STYLES.code.prefix}${code}${STYLES.code.suffix}`
75
+ })
76
+
77
+ // 处理 **粗体**
78
+ result = result.replace(/\*\*([^*]+)\*\*/g, (match, bold) => {
79
+ return `${STYLES.bold.prefix}${bold}${STYLES.bold.suffix}`
80
+ })
81
+
82
+ // 处理 *斜体*(不匹配已处理的粗体内部)
83
+ result = result.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, (match, italic) => {
84
+ return `${STYLES.italic.prefix}${italic}${STYLES.italic.suffix}`
85
+ })
86
+
87
+ // 处理 ~~删除线~~ (简单处理)
88
+ result = result.replace(/~~([^~]+)~~/g, (match, strikethrough) => {
89
+ return `${DIM}${strikethrough}${CLEAR}`
90
+ })
91
+
92
+ // 处理行内链接 [text](url) -> text
93
+ result = result.replace(/\[([^\]]+)\]\([^)]+\)/g, (match, text) => {
94
+ return `${STYLES.link.prefix}${text}${STYLES.link.suffix}`
95
+ })
96
+
97
+ return result
98
+ }
99
+
100
+ /**
101
+ * 渲染包含思考标签的行
102
+ */
103
+ function renderThink(line, state = { inThink: false }) {
104
+ // <think>思考内容</think> -> 思考内容用暗色显示
105
+ let result = ''
106
+
107
+ let i = 0
108
+ let lastEnd = 0
109
+
110
+ // 如果在思考块中但这行没有开始标签,在行首添加黄色前缀
111
+ if (state.inThink && !line.includes('<think>')) {
112
+ result += STYLES.think.prefix
113
+ }
114
+
115
+ while (i < line.length) {
116
+ // 检查 <think> (7个字符)
117
+ if (line.substring(i, i + 7) === '<think>') {
118
+ if (i > lastEnd) {
119
+ result += renderInline(line.substring(lastEnd, i))
120
+ }
121
+ result += STYLES.think.prefix + '<think>'
122
+ i += 7
123
+ lastEnd = i
124
+ state.inThink = true
125
+ // 如果这行只有<think>标签,不要在这里添加CLEAR
126
+ // 后续行会继续使用黄色
127
+ if (i >= line.length) {
128
+ // 没有更多内容,return时不添加CLEAR,保持黄色状态
129
+ return result
130
+ }
131
+ continue
132
+ }
133
+
134
+ // 检查</think> (8个字符)
135
+ if (line.substring(i, i + 8) === '</think>') {
136
+ if (i > lastEnd) {
137
+ result += line.substring(lastEnd, i) + STYLES.think.suffix
138
+ }
139
+ result += '</think>' + CLEAR
140
+ i += 8
141
+ lastEnd = i
142
+ state.inThink = false
143
+ continue
144
+ }
145
+
146
+ i++
147
+ }
148
+
149
+ if (lastEnd < line.length) {
150
+ result += renderInline(line.substring(lastEnd))
151
+ }
152
+
153
+ return result
154
+ }
155
+
156
+ /**
157
+ * 渲染一行 markdown(处理标题、列表等)
158
+ * state: { inThink: boolean, inCodeBlock: boolean } - 状态,会被修改
159
+ */
160
+ function renderLine(line, state = { inThink: false, inCodeBlock: false }) {
161
+ // 思考标签 - 保留首尾换行符
162
+ const leadingNL = line.match(/^\n*/)[0]
163
+ const trailingNL = line.match(/\n*$/)[0]
164
+ const trimmed = line.trim()
165
+
166
+ // 代码块处理
167
+ if (trimmed.startsWith('```')) {
168
+ if (state.inCodeBlock) {
169
+ // 结束代码块
170
+ state.inCodeBlock = false
171
+ return `\`\`\`${trimmed.slice(3) ? ' ' + trimmed.slice(3) : ''}${STYLES.codeBlock.suffix}`
172
+ } else {
173
+ // 开始代码块
174
+ state.inCodeBlock = true
175
+ const lang = trimmed.slice(3).trim()
176
+ return `${STYLES.codeBlock.prefix}\`\`\`${lang ? ' ' + lang : ''}`
177
+ }
178
+ }
179
+
180
+ // 如果在代码块中,整行用代码样式
181
+ if (state.inCodeBlock) {
182
+ return `${STYLES.codeBlock.prefix}${line}${STYLES.codeBlock.suffix}`
183
+ }
184
+
185
+ // 如果在思考块中或者有思考标签
186
+ if (state.inThink || trimmed.includes('<think>') || trimmed.includes('</think>')) {
187
+ const result = leadingNL + renderThink(trimmed, state) + trailingNL
188
+ return result
189
+ }
190
+
191
+ // 标题
192
+ if (line.startsWith('### ')) {
193
+ return `${STYLES.h3.prefix}${renderInline(line.substring(4))}${STYLES.h3.suffix}`
194
+ }
195
+ if (line.startsWith('## ')) {
196
+ return `${STYLES.h2.prefix}${renderInline(line.substring(3))}${STYLES.h2.suffix}`
197
+ }
198
+ if (line.startsWith('# ')) {
199
+ return `${STYLES.h1.prefix}${renderInline(line.substring(2))}${STYLES.h1.suffix}`
200
+ }
201
+
202
+ // 无序列表
203
+ if (line.match(/^[-*+] /)) {
204
+ return line.replace(/^([-*+] )(.*)/, (match, bullet, content) => {
205
+ return `${STYLES.list.prefix}${bullet}${CLEAR}${renderInline(content)}`
206
+ })
207
+ }
208
+
209
+ // 有序列表
210
+ if (line.match(/^\d+\. /)) {
211
+ return line.replace(/^(\d+\. )(.*)/, (match, bullet, content) => {
212
+ return `${STYLES.list.prefix}${bullet}${CLEAR}${renderInline(content)}`
213
+ })
214
+ }
215
+
216
+ // 引用
217
+ if (line.startsWith('> ')) {
218
+ return line.replace(/^> (.*)/, (match, content) => {
219
+ return `${STYLES.blockquote.prefix}| ${renderInline(content)}${STYLES.blockquote.suffix}`
220
+ })
221
+ }
222
+
223
+ return renderInline(line)
224
+ }
225
+
226
+ /**
227
+ * 渲染完整的 markdown 文本
228
+ */
229
+ function render(text) {
230
+ if (!text) return ''
231
+
232
+ const lines = text.split('\n')
233
+ const rendered = []
234
+
235
+ let inCodeBlock = false
236
+
237
+ for (const line of lines) {
238
+ // 代码块
239
+ if (line.startsWith('```')) {
240
+ if (inCodeBlock) {
241
+ rendered.push(`\`\`\`${STYLES.codeBlock.suffix}`)
242
+ inCodeBlock = false
243
+ } else {
244
+ const lang = line.substring(3).trim() || ''
245
+ rendered.push(`${STYLES.codeBlock.prefix}\`\`\`${lang ? ' ' + lang : ''}`)
246
+ inCodeBlock = true
247
+ }
248
+ continue
249
+ }
250
+
251
+ if (inCodeBlock) {
252
+ rendered.push(`${STYLES.codeBlock.prefix}${line}${STYLES.codeBlock.suffix}`)
253
+ } else {
254
+ rendered.push(renderLine(line))
255
+ }
256
+ }
257
+
258
+ return rendered.join('\n')
259
+ }
260
+
261
+ /**
262
+ * 流式渲染 - 每次返回一个完整的行
263
+ * 返回 { done: boolean, line: string }
264
+ */
265
+ function* streamRender(text) {
266
+ if (!text) return
267
+
268
+ const lines = text.split('\n')
269
+ let inCodeBlock = false
270
+
271
+ for (const line of lines) {
272
+ // 代码块
273
+ if (line.startsWith('```')) {
274
+ if (inCodeBlock) {
275
+ yield { done: false, line: `${STYLES.codeBlock.suffix}` }
276
+ inCodeBlock = false
277
+ } else {
278
+ const lang = line.substring(3).trim() || ''
279
+ yield { done: false, line: `${STYLES.codeBlock.prefix}\`\`\`${lang ? ' ' + lang : ''}` }
280
+ inCodeBlock = true
281
+ }
282
+ continue
283
+ }
284
+
285
+
286
+ if (inCodeBlock) {
287
+ yield { done: false, line: `${STYLES.codeBlock.prefix}${line}${STYLES.codeBlock.suffix}` }
288
+ } else {
289
+ yield { done: false, line: renderLine(line) }
290
+ }
291
+ }
292
+
293
+ yield { done: true }
294
+ }
295
+
296
+ module.exports = { render, renderInline, renderLine, streamRender, STYLES }