dgxay-code-cli 0.0.0-stage → 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.
- package/LICENSE +674 -0
- package/README-zh.md +200 -0
- package/README.md +182 -2
- package/bin/dgxay.js +42 -0
- package/package.json +47 -4
- package/src/cli/ansi.js +509 -0
- package/src/cli/approval.js +186 -0
- package/src/cli/args.js +162 -0
- package/src/cli/command-table.js +52 -0
- package/src/cli/editor.js +574 -0
- package/src/cli/index.js +228 -0
- package/src/cli/print.js +148 -0
- package/src/cli/render.js +324 -0
- package/src/cli/repl-state.js +85 -0
- package/src/cli/repl.js +1167 -0
- package/src/cli/slash.js +74 -0
- package/src/cli/ui.js +52 -0
- package/src/commands/_shared.js +20 -0
- package/src/commands/chat.js +212 -0
- package/src/commands/config.js +363 -0
- package/src/commands/context.js +43 -0
- package/src/commands/doctor.js +192 -0
- package/src/commands/export.js +102 -0
- package/src/commands/mcp.js +257 -0
- package/src/commands/model.js +223 -0
- package/src/commands/oauth.js +246 -0
- package/src/commands/permission.js +108 -0
- package/src/commands/provider.js +332 -0
- package/src/commands/run.js +172 -0
- package/src/commands/skill.js +94 -0
- package/src/commands/workspace.js +100 -0
- package/src/core/agent.js +419 -0
- package/src/core/llm.js +709 -0
- package/src/core/mcp.js +271 -0
- package/src/core/oauth.js +737 -0
- package/src/core/prompt.js +269 -0
- package/src/core/sessions.js +208 -0
- package/src/core/tools.js +391 -0
- package/src/services/config.js +182 -0
- package/src/services/input.js +91 -0
- package/src/services/session-bus.js +104 -0
- package/src/services/settings.js +340 -0
- package/src/services/skills.js +125 -0
- package/src/services/terminal.js +124 -0
- package/src/services/version.js +25 -0
package/src/cli/slash.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 交互模式里的斜杠命令(对应桌面版输入框的 / 菜单)。
|
|
5
|
+
*
|
|
6
|
+
* 与桌面版的关系:
|
|
7
|
+
* 桌面版只有 /压缩 与 /技能;CLI 额外提供会话、模型、权限、MCP 等
|
|
8
|
+
* 「原本在界面按钮上」的操作,保证去掉 UI 后功能不丢。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SLASH_COMMANDS = [
|
|
12
|
+
{ name: '/help', desc: '查看所有命令', usage: '/help' },
|
|
13
|
+
{ name: '/new', desc: '开始新对话', usage: '/new' },
|
|
14
|
+
{ name: '/list', desc: '列出历史对话', usage: '/list' },
|
|
15
|
+
{ name: '/resume', desc: '恢复历史对话', usage: '/resume <序号|ID>' },
|
|
16
|
+
{ name: '/delete', desc: '删除一段对话', usage: '/delete <序号|ID>' },
|
|
17
|
+
{ name: '/rename', desc: '重命名当前对话', usage: '/rename <标题>' },
|
|
18
|
+
{ name: '/compress', desc: '压缩上下文(有损,不可撤销)', usage: '/compress', aliases: ['/压缩'] },
|
|
19
|
+
{ name: '/skill', desc: '列出或启用技能', usage: '/skill [名称] [提示词]', aliases: ['/技能'] },
|
|
20
|
+
{ name: '/model', desc: '查看或切换模型', usage: '/model [序号|ID]' },
|
|
21
|
+
{ name: '/provider', desc: '查看或切换供应商', usage: '/provider [序号|名称]' },
|
|
22
|
+
{ name: '/mode', desc: '切换对话 / 工作模式', usage: '/mode [chat|work]' },
|
|
23
|
+
{ name: '/permission', desc: '切换权限档位', usage: '/permission [none|normal|auto|full]' },
|
|
24
|
+
{ name: '/cd', desc: '切换工作空间', usage: '/cd <目录>' },
|
|
25
|
+
{ name: '/ls', desc: '查看工作空间文件', usage: '/ls [目录]' },
|
|
26
|
+
{ name: '/pwd', desc: '显示当前工作空间', usage: '/pwd' },
|
|
27
|
+
{ name: '/term', desc: '执行终端命令(不进模型上下文时用 ! 前缀)', usage: '/term <命令>' },
|
|
28
|
+
{ name: '/context', desc: '查看上下文用量', usage: '/context' },
|
|
29
|
+
{ name: '/mcp', desc: '管理 MCP:list/start/stop/logs/add/rm', usage: '/mcp [子命令]' },
|
|
30
|
+
{ name: '/theme', desc: '开关颜色输出', usage: '/theme [on|off]' },
|
|
31
|
+
{ name: '/clear', desc: '清屏(不删除会话)', usage: '/clear' },
|
|
32
|
+
{ name: '/exit', desc: '退出', usage: '/exit', aliases: ['/quit'] }
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function findCommand(text) {
|
|
36
|
+
const token = String(text || '').trim().split(/\s+/)[0].toLowerCase();
|
|
37
|
+
if (!token) return null;
|
|
38
|
+
return (
|
|
39
|
+
SLASH_COMMANDS.find((cmd) => cmd.name === token) ||
|
|
40
|
+
SLASH_COMMANDS.find((cmd) => (cmd.aliases || []).includes(token)) ||
|
|
41
|
+
null
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 行编辑器的补全器:只在输入以 / 开头且光标在首行时弹出。
|
|
47
|
+
* 返回 { items: [{ label, hint, apply }], title, hint }
|
|
48
|
+
*/
|
|
49
|
+
function completer(value, cursor) {
|
|
50
|
+
if (!value || !value.startsWith('/')) return null;
|
|
51
|
+
if (value.includes('\n')) return null;
|
|
52
|
+
const before = value.slice(0, cursor);
|
|
53
|
+
if (before.includes(' ')) return null;
|
|
54
|
+
const query = before.slice(1).toLowerCase();
|
|
55
|
+
const items = SLASH_COMMANDS.filter(
|
|
56
|
+
(cmd) =>
|
|
57
|
+
!query ||
|
|
58
|
+
cmd.name.slice(1).toLowerCase().startsWith(query) ||
|
|
59
|
+
(cmd.aliases || []).some((alias) => alias.slice(1).toLowerCase().startsWith(query))
|
|
60
|
+
).map((cmd) => ({
|
|
61
|
+
label: cmd.name,
|
|
62
|
+
hint: cmd.desc,
|
|
63
|
+
apply: `${cmd.name} `,
|
|
64
|
+
command: cmd
|
|
65
|
+
}));
|
|
66
|
+
if (!items.length) return null;
|
|
67
|
+
return {
|
|
68
|
+
title: '斜杠命令',
|
|
69
|
+
hint: '↑↓ 选择 · Tab 补全 · Enter 执行 · Esc 关闭',
|
|
70
|
+
items
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { SLASH_COMMANDS, findCommand, completer };
|
package/src/cli/ui.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 上下文用量(对应桌面版的上下文圆环)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { estimateChatTokens, contextLimit, formatTokens } = require('../core/sessions');
|
|
8
|
+
const { COLORS, ring, truncate, displayWidth, termWidth } = require('./ansi');
|
|
9
|
+
|
|
10
|
+
function contextInfo(chat, model) {
|
|
11
|
+
const used = estimateChatTokens(chat);
|
|
12
|
+
const limit = contextLimit(model);
|
|
13
|
+
const ratio = limit > 0 ? Math.min(1, used / limit) : 0;
|
|
14
|
+
const messageCount = (chat?.messages || []).filter((msg) => msg.role !== 'terminal').length;
|
|
15
|
+
return { used, limit, ratio, messageCount, model, skill: chat?.skill || '' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 输入框下方的上下文环(单行) */
|
|
19
|
+
function ringLine(info) {
|
|
20
|
+
const percent = Math.round(info.ratio * 100);
|
|
21
|
+
return `${ring(info.ratio)} ${COLORS.faint(`${formatTokens(info.used)}/${formatTokens(info.limit)} · ${percent}%`)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** /context 命令的详细输出 */
|
|
25
|
+
function contextReport(info) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
const percent = (info.ratio * 100).toFixed(info.ratio < 0.1 ? 1 : 0);
|
|
28
|
+
lines.push(`${COLORS.accent('上下文')} ${COLORS.bold(`${percent}%`)} ${COLORS.faint(
|
|
29
|
+
`${formatTokens(info.used)} / ${formatTokens(info.limit)}`
|
|
30
|
+
)}`);
|
|
31
|
+
const width = Math.max(10, Math.min(40, termWidth() - 12));
|
|
32
|
+
const filled = Math.round(info.ratio * width);
|
|
33
|
+
lines.push(`${COLORS.accent('█'.repeat(filled))}${COLORS.faint('░'.repeat(Math.max(0, width - filled)))}`);
|
|
34
|
+
lines.push('');
|
|
35
|
+
const rows = [
|
|
36
|
+
['对话消息', String(info.messageCount)],
|
|
37
|
+
['上下文上限', formatTokens(info.limit)],
|
|
38
|
+
['剩余可用', formatTokens(Math.max(0, info.limit - info.used))],
|
|
39
|
+
['当前模型', info.model?.name || '未选择'],
|
|
40
|
+
['供应商', info.model?.providerName || ''],
|
|
41
|
+
['技能', info.skill || '无']
|
|
42
|
+
];
|
|
43
|
+
const labelWidth = Math.max(...rows.map(([label]) => displayWidth(label)));
|
|
44
|
+
for (const [label, value] of rows) {
|
|
45
|
+
lines.push(` ${COLORS.faint(truncate(label, 12).padEnd(labelWidth))} ${value}`);
|
|
46
|
+
}
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push(COLORS.faint('按字符启发式估算,实际 token 以模型 usage 为准;接近上限时用 /compress 保留重要上下文。'));
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { contextInfo, ringLine, contextReport };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** 子命令之间共享的小工具。 */
|
|
4
|
+
|
|
5
|
+
const sessions = require('../core/sessions');
|
|
6
|
+
|
|
7
|
+
function resolveChatArg(opts = {}, runtime = {}) {
|
|
8
|
+
if (opts.resume || runtime.resume) return sessions.getChat(String(opts.resume || runtime.resume));
|
|
9
|
+
if (opts.continue || runtime.continueLast) return sessions.listChats()[0] || null;
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 失败时给出“下一步”提示而不是直接抛错 */
|
|
14
|
+
function requireProviders(config) {
|
|
15
|
+
if (!config.providers?.length) {
|
|
16
|
+
throw Object.assign(new Error('还没有配置供应商。运行 dgxay config init 开始配置'), { userError: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { resolveChatArg, requireProviders };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* chat:会话管理与在新会话中提问。
|
|
5
|
+
*
|
|
6
|
+
* dgxay chat new 新建会话
|
|
7
|
+
* dgxay chat list 列出历史会话
|
|
8
|
+
* dgxay chat show <序号|ID> 查看某个会话
|
|
9
|
+
* dgxay chat resume <序号|ID> 进入交互模式并恢复该会话
|
|
10
|
+
* dgxay chat rm <序号|ID> 删除会话
|
|
11
|
+
* dgxay chat rename <序号|ID> <标题>
|
|
12
|
+
* dgxay chat clear 清空全部会话
|
|
13
|
+
* dgxay chat "问题" 在最近会话上继续提问
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const print = require('../cli/print');
|
|
18
|
+
const { parseArgs, actionFirst } = require('../cli/args');
|
|
19
|
+
const { COLORS, renderMarkdown, termWidth } = require('../cli/ansi');
|
|
20
|
+
const sessions = require('../core/sessions');
|
|
21
|
+
|
|
22
|
+
const options = [
|
|
23
|
+
{ name: 'print', alias: 'p', type: 'string', valueName: 'text', desc: '直接提问(等同 dgxay run)' },
|
|
24
|
+
{ name: 'mode', alias: 'm', type: 'string', valueName: 'mode', desc: 'chat / work' },
|
|
25
|
+
{ name: 'permission', type: 'string', valueName: 'level', desc: 'none / normal / auto / full' },
|
|
26
|
+
{ name: 'workspace', alias: 'C', type: 'string', valueName: 'dir', desc: '工作空间目录' },
|
|
27
|
+
{ name: 'yes', alias: 'y', type: 'boolean', desc: '自动同意授权请求' },
|
|
28
|
+
{ name: 'yes-all', type: 'boolean', desc: '清空会话时跳过确认' }
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function relativeTime(ts) {
|
|
32
|
+
const diff = Date.now() - Number(ts || 0);
|
|
33
|
+
if (diff < 60000) return '刚刚';
|
|
34
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
|
|
35
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
|
|
36
|
+
const date = new Date(ts);
|
|
37
|
+
return `${date.getMonth() + 1} 月 ${date.getDate()} 日`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function listChats() {
|
|
41
|
+
const chats = sessions.listChats();
|
|
42
|
+
print.blank();
|
|
43
|
+
if (!chats.length) {
|
|
44
|
+
print.hint('还没有保存的会话');
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
print.heading(`历史会话(${chats.length},最多保留 ${sessions.MAX_CHATS} 段)`);
|
|
48
|
+
const rows = chats.map((chat, index) => {
|
|
49
|
+
const messages = chat.messages.filter((m) => m.role !== 'terminal');
|
|
50
|
+
const last = messages[messages.length - 1];
|
|
51
|
+
return [
|
|
52
|
+
String(index + 1),
|
|
53
|
+
chat.title || '新对话',
|
|
54
|
+
String(messages.length),
|
|
55
|
+
relativeTime(chat.updatedAt),
|
|
56
|
+
chat.mode === 'work' ? '工作' : '对话',
|
|
57
|
+
chat.skill || '',
|
|
58
|
+
last ? String(last.content || '').replace(/\s+/g, ' ').slice(0, 40) : ''
|
|
59
|
+
];
|
|
60
|
+
});
|
|
61
|
+
print.table(
|
|
62
|
+
[
|
|
63
|
+
{ title: '#', max: 3 },
|
|
64
|
+
{ title: '标题', max: 30 },
|
|
65
|
+
{ title: '消息', max: 4 },
|
|
66
|
+
{ title: '更新时间', max: 10 },
|
|
67
|
+
{ title: '模式', max: 4 },
|
|
68
|
+
{ title: '技能', max: 12 },
|
|
69
|
+
{ title: '最后内容', max: 40 }
|
|
70
|
+
],
|
|
71
|
+
rows
|
|
72
|
+
);
|
|
73
|
+
print.blank();
|
|
74
|
+
print.hint('dgxay chat resume 1 恢复第 1 个会话');
|
|
75
|
+
print.hint('dgxay -r 1 直接恢复并进入交互模式');
|
|
76
|
+
print.hint('dgxay chat show 1 只查看内容');
|
|
77
|
+
print.blank();
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function showChat(ref) {
|
|
82
|
+
const chat = sessions.resolveChat(ref);
|
|
83
|
+
if (!chat) {
|
|
84
|
+
print.fail(`找不到会话:${ref}`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
print.blank();
|
|
88
|
+
print.write(`${COLORS.bold(chat.title)} ${COLORS.faint(`${chat.id} · ${relativeTime(chat.updatedAt)}`)}`);
|
|
89
|
+
for (const message of chat.messages) {
|
|
90
|
+
if (message.role === 'user') {
|
|
91
|
+
print.write(`\n${COLORS.accent('▍你')}`);
|
|
92
|
+
print.write(message.content || '');
|
|
93
|
+
} else if (message.role === 'assistant') {
|
|
94
|
+
print.write(`\n${COLORS.accent('▍DgxayCode')}`);
|
|
95
|
+
process.stdout.write(renderMarkdown(message.content || '', { width: termWidth() }));
|
|
96
|
+
if (message.error) print.write(COLORS.red(message.error));
|
|
97
|
+
if (message.tools?.length) {
|
|
98
|
+
for (const tool of message.tools) {
|
|
99
|
+
print.write(COLORS.faint(` · ${tool.name} ${tool.ok ? '成功' : '失败'}`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else if (message.role === 'terminal') {
|
|
103
|
+
print.write(`\n${COLORS.faint(`▍终端 $ ${message.command}`)}`);
|
|
104
|
+
const output = `${message.output || ''}${message.stderr ? `\n${message.stderr}` : ''}`;
|
|
105
|
+
print.write(output.slice(0, 4000));
|
|
106
|
+
print.write(COLORS.faint(` exit=${message.exitCode ?? '?'}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
print.blank();
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function run({ argv, runtime }) {
|
|
114
|
+
const { action: rawAction, rest, options: opts } = actionFirst(argv, options);
|
|
115
|
+
const action = rawAction || 'list';
|
|
116
|
+
|
|
117
|
+
switch (action) {
|
|
118
|
+
case 'list':
|
|
119
|
+
case 'ls':
|
|
120
|
+
return listChats();
|
|
121
|
+
|
|
122
|
+
case 'show':
|
|
123
|
+
case 'view': {
|
|
124
|
+
if (!rest[0]) throw Object.assign(new Error('用法:dgxay chat show <序号|ID>'), { userError: true });
|
|
125
|
+
return showChat(rest[0]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'new': {
|
|
129
|
+
print.ok(opts.print ? '新会话已建立(单次提问,不会保存到历史)' : '进入交互模式后将自动创建会话');
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'resume': {
|
|
134
|
+
if (!rest[0]) throw Object.assign(new Error('用法:dgxay chat resume <序号|ID>'), { userError: true });
|
|
135
|
+
const chat = sessions.resolveChat(rest[0]);
|
|
136
|
+
if (!chat) {
|
|
137
|
+
print.fail(`找不到会话:${rest[0]}`);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
const repl = require('../cli/repl');
|
|
141
|
+
return repl.start({ ...runtime, resume: chat.id, single: false });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'rm':
|
|
145
|
+
case 'delete': {
|
|
146
|
+
if (!rest[0]) throw Object.assign(new Error('用法:dgxay chat rm <序号|ID>'), { userError: true });
|
|
147
|
+
const chat = sessions.resolveChat(rest[0]);
|
|
148
|
+
if (!chat) {
|
|
149
|
+
print.fail(`找不到会话:${rest[0]}`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
sessions.deleteChat(chat.id);
|
|
153
|
+
print.ok(`已删除「${chat.title}」`);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case 'rename': {
|
|
158
|
+
const ref = rest[0];
|
|
159
|
+
const title = rest.slice(1).join(' ').trim();
|
|
160
|
+
if (!ref || !title) throw Object.assign(new Error('用法:dgxay chat rename <序号|ID> <新标题>'), { userError: true });
|
|
161
|
+
const chat = sessions.resolveChat(ref);
|
|
162
|
+
if (!chat) {
|
|
163
|
+
print.fail(`找不到会话:${ref}`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
chat.title = title;
|
|
167
|
+
chat.updatedAt = Date.now();
|
|
168
|
+
sessions.saveChat(chat);
|
|
169
|
+
print.ok(`已重命名为「${title}」`);
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'clear': {
|
|
174
|
+
if (!opts.yesAll && process.stdin.isTTY) {
|
|
175
|
+
const { confirm } = require('../services/input');
|
|
176
|
+
const ok = await confirm('清空全部会话记录?(不可恢复)', false);
|
|
177
|
+
if (!ok) {
|
|
178
|
+
print.hint('已取消');
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
sessions.clearChats();
|
|
183
|
+
print.ok('全部会话已清空');
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
default: {
|
|
188
|
+
// 非子命令:当作提示词,在最近会话上继续提问
|
|
189
|
+
const prompt = positionals.join(' ').trim();
|
|
190
|
+
const repl = require('../cli/repl');
|
|
191
|
+
return repl.start({ ...runtime, initialPrompt: prompt, single: true, continueLast: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
name: 'chat',
|
|
198
|
+
aliases: ['c'],
|
|
199
|
+
summary: '会话管理:列表、查看、恢复、删除',
|
|
200
|
+
usage: 'dgxay chat <list|show|resume|rm|rename|clear>',
|
|
201
|
+
options,
|
|
202
|
+
subcommands: [
|
|
203
|
+
['list', '列出历史会话'],
|
|
204
|
+
['show <序号|ID>', '查看会话内容'],
|
|
205
|
+
['resume <序号|ID>', '进入交互模式并恢复'],
|
|
206
|
+
['rm <序号|ID>', '删除会话'],
|
|
207
|
+
['rename <序号|ID> <标题>', '重命名会话'],
|
|
208
|
+
['clear', '清空全部会话']
|
|
209
|
+
],
|
|
210
|
+
examples: ['dgxay chat list', 'dgxay chat resume 1', 'dgxay chat rm 2', 'dgxay chat show 1'],
|
|
211
|
+
run
|
|
212
|
+
};
|