mocode-ai 0.1.3 → 0.1.5
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/README.md +196 -161
- package/dist/agent/core.js +302 -0
- package/dist/agent/index.js +85 -202
- package/dist/agent/mode.js +46 -0
- package/dist/agent/spawn.js +123 -0
- package/dist/commands/config.js +8 -26
- package/dist/config/file.js +48 -0
- package/dist/config/index.js +76 -22
- package/dist/llm/index.js +11 -9
- package/dist/memory/discover.js +49 -0
- package/dist/memory/index.js +45 -0
- package/dist/memory/reflect.js +265 -0
- package/dist/memory/store.js +339 -0
- package/dist/repl/index.js +334 -87
- package/dist/session/compact.js +51 -3
- package/dist/skills/index.js +2 -2
- package/dist/tools/builtins/ask-human.js +50 -0
- package/dist/tools/builtins/codegraph.js +113 -0
- package/dist/tools/builtins/edit-file.js +3 -3
- package/dist/tools/builtins/glob.js +3 -2
- package/dist/tools/builtins/grep.js +4 -3
- package/dist/tools/builtins/index.js +19 -1
- package/dist/tools/builtins/memory-forget.js +32 -0
- package/dist/tools/builtins/memory-list.js +34 -0
- package/dist/tools/builtins/memory-save.js +51 -0
- package/dist/tools/builtins/memory-search.js +42 -0
- package/dist/tools/builtins/memory-update.js +41 -0
- package/dist/tools/builtins/read-file.js +5 -4
- package/dist/tools/builtins/run-command.js +40 -7
- package/dist/tools/builtins/switch-mode.js +46 -0
- package/dist/tools/builtins/task.js +62 -0
- package/dist/tools/builtins/use-skill.js +2 -2
- package/dist/tools/builtins/web-fetch.js +16 -3
- package/dist/tools/builtins/web-search.js +6 -6
- package/dist/tools/builtins/write-file.js +3 -3
- package/dist/tools/constants.js +29 -0
- package/dist/tools/registry.js +11 -4
- package/dist/ui/content.js +25 -17
- package/dist/ui/diff.js +28 -26
- package/dist/ui/intervention.js +318 -0
- package/dist/ui/layout.js +402 -103
- package/dist/ui/markdown.js +607 -0
- package/dist/ui/mouse.js +93 -0
- package/dist/ui/prompt.js +469 -27
- package/dist/ui/render.js +19 -1
- package/dist/ui/spinner.js +15 -0
- package/dist/ui/theme.js +147 -13
- package/package.json +2 -2
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { stdin, stderr } from 'node:process';
|
|
3
|
+
import { ui } from './theme.js';
|
|
4
|
+
import { displayWidth, truncateDisplay } from './render.js';
|
|
5
|
+
import * as layout from './layout.js';
|
|
6
|
+
import * as mouse from './mouse.js';
|
|
7
|
+
import { Spinner } from './spinner.js';
|
|
8
|
+
const emitter = stdin;
|
|
9
|
+
/** choice 末尾的"自定义输入"项标签(纯 ASCII,宽度安全)。选中后切到 input 子态。 */
|
|
10
|
+
const CUSTOM_LABEL = '其他(自定义输入)';
|
|
11
|
+
/** 弹出介入面板,阻塞直到用户完成选择。 */
|
|
12
|
+
export async function promptIntervention(req) {
|
|
13
|
+
// 非 TTY 降级:不阻塞,自动选默认。日志走 stderr(不污染 stdout 内容流)。
|
|
14
|
+
if (!layout.isActive()) {
|
|
15
|
+
const kind = req.type === 'choice' ? '自动选默认项' : '自动返回空输入';
|
|
16
|
+
stderr.write(`[介入] ${req.title}(非交互环境,${kind})\n`);
|
|
17
|
+
if (req.type === 'choice') {
|
|
18
|
+
return { action: 'selected', value: req.options?.[0] ?? '' };
|
|
19
|
+
}
|
|
20
|
+
return { action: 'submitted', value: req.seed ?? '' };
|
|
21
|
+
}
|
|
22
|
+
const options = req.type === 'choice' && Array.isArray(req.options)
|
|
23
|
+
? req.options.map((o) => String(o)).filter((s) => s.length > 0)
|
|
24
|
+
: [];
|
|
25
|
+
// choice 但选项被滤空 → 降级 input(对齐设计文档 §8:ask_human 选项为空数组→input)。
|
|
26
|
+
const startMode = req.type === 'choice' && options.length > 0 ? 'choice' : 'input';
|
|
27
|
+
let mode = startMode;
|
|
28
|
+
// choice:选中下标(0..options.length-1 为各选项,options.length 为"自定义"项)。
|
|
29
|
+
let selected = 0;
|
|
30
|
+
// input:可编辑文本 + 光标(UTF-16 码元索引;显示位置由 displayWidth 算,见 paintInput)。
|
|
31
|
+
let text = req.seed ?? '';
|
|
32
|
+
let cursor = text.length;
|
|
33
|
+
// input 是否由 choice 的"自定义"项切入(决定 Esc 是返回 choice 还是取消)。
|
|
34
|
+
let cameFromChoice = false;
|
|
35
|
+
let resolved = false;
|
|
36
|
+
let resolve;
|
|
37
|
+
// 挂自己监听前快照的现有 keypress 监听(运行态即 onRunningKey),退出时按原序恢复。
|
|
38
|
+
let savedListeners = [];
|
|
39
|
+
/** choice 菜单行:标题(bold)+ detail(dim,行数上限保选项可见)+ 空行 + 选项(▸ 选中/dim)。 */
|
|
40
|
+
function menuLinesChoice() {
|
|
41
|
+
const g = layout.getGeo();
|
|
42
|
+
const cols = g.cols;
|
|
43
|
+
const items = [...options, CUSTOM_LABEL];
|
|
44
|
+
const optionCount = items.length;
|
|
45
|
+
// detail 上限:title(1)+detail(D)+空行(1)+options(O) ≤ contentBottom → D ≤ contentBottom-2-O
|
|
46
|
+
const detailCap = Math.max(0, g.contentBottom - 2 - optionCount);
|
|
47
|
+
const lines = [];
|
|
48
|
+
lines.push(`${ui.bold}${truncateDisplay(req.title, cols)}${ui.reset}`);
|
|
49
|
+
if (req.detail) {
|
|
50
|
+
const dl = req.detail.split('\n');
|
|
51
|
+
const shown = dl.slice(0, detailCap);
|
|
52
|
+
for (const d of shown) {
|
|
53
|
+
lines.push(`${ui.dim}${truncateDisplay(d, cols)}${ui.reset}`);
|
|
54
|
+
}
|
|
55
|
+
if (dl.length > detailCap)
|
|
56
|
+
lines.push(`${ui.dim}…${ui.reset}`);
|
|
57
|
+
}
|
|
58
|
+
lines.push(''); // 分隔空行
|
|
59
|
+
// 选项开窗:超屏高时以 selected 为中心取窗,保选中项可见。
|
|
60
|
+
const maxOptRows = Math.max(1, g.contentBottom - lines.length);
|
|
61
|
+
let start = 0;
|
|
62
|
+
if (optionCount > maxOptRows) {
|
|
63
|
+
start = Math.max(0, Math.min(selected - Math.floor(maxOptRows / 2), optionCount - maxOptRows));
|
|
64
|
+
}
|
|
65
|
+
const count = Math.min(maxOptRows, optionCount);
|
|
66
|
+
for (let i = 0; i < count; i++) {
|
|
67
|
+
const idx = start + i;
|
|
68
|
+
// 选中项:▸ 与正文均 cyan+bold(去 dim),未选中项保持 dim——选中行整体高亮。
|
|
69
|
+
const isSel = idx === selected;
|
|
70
|
+
const color = isSel ? `${ui.cyan}${ui.bold}` : ui.dim;
|
|
71
|
+
const marker = isSel ? `${ui.cyan}${ui.bold}▸${ui.reset}` : ' ';
|
|
72
|
+
const body = truncateDisplay(items[idx], cols - 2);
|
|
73
|
+
lines.push(`${marker} ${color}${body}${ui.reset}`);
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
/** input 菜单行:标题(bold)+ detail(dim)+ 提示行(dim)。可编辑文本在底栏输入框。 */
|
|
78
|
+
function menuLinesInput() {
|
|
79
|
+
const g = layout.getGeo();
|
|
80
|
+
const cols = g.cols;
|
|
81
|
+
const detailCap = Math.max(0, g.contentBottom - 2); // title(1)+提示(1)
|
|
82
|
+
const lines = [];
|
|
83
|
+
lines.push(`${ui.bold}${truncateDisplay(req.title, cols)}${ui.reset}`);
|
|
84
|
+
if (req.detail) {
|
|
85
|
+
const dl = req.detail.split('\n');
|
|
86
|
+
const shown = dl.slice(0, detailCap);
|
|
87
|
+
for (const d of shown) {
|
|
88
|
+
lines.push(`${ui.dim}${truncateDisplay(d, cols)}${ui.reset}`);
|
|
89
|
+
}
|
|
90
|
+
if (dl.length > detailCap)
|
|
91
|
+
lines.push(`${ui.dim}…${ui.reset}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push(`${ui.dim}Enter 提交 · Esc ${cameFromChoice ? '返回选项' : '取消'} · Ctrl+C 取消${ui.reset}`);
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
function redraw() {
|
|
97
|
+
if (mode === 'choice') {
|
|
98
|
+
const hint = '↑↓ 选择 · Enter 确认 · 数字键直选 · Esc 取消';
|
|
99
|
+
layout.paintInput({
|
|
100
|
+
prompt: '❯ ',
|
|
101
|
+
lines: [hint],
|
|
102
|
+
cursorLine: 0,
|
|
103
|
+
cursorCol: displayWidth(hint),
|
|
104
|
+
menu: { lines: menuLinesChoice() },
|
|
105
|
+
caret: false, // 纯导航(非文本输入):不画输入框块状光标,聚焦由选项 ▸ 标记
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
layout.paintInput({
|
|
110
|
+
prompt: '❯ ',
|
|
111
|
+
lines: [text],
|
|
112
|
+
cursorLine: 0,
|
|
113
|
+
cursorCol: displayWidth(text.slice(0, cursor)),
|
|
114
|
+
menu: { lines: menuLinesInput() },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function finish(result) {
|
|
119
|
+
if (resolved)
|
|
120
|
+
return;
|
|
121
|
+
resolved = true;
|
|
122
|
+
cleanup();
|
|
123
|
+
resolve(result);
|
|
124
|
+
}
|
|
125
|
+
/** 退出:摘自己的监听 + 恢复快照监听 + 擦菜单恢复内容区。不 setRawMode(false)/pause stdin(运行态由 repl 接管)。 */
|
|
126
|
+
function cleanup() {
|
|
127
|
+
emitter.removeListener('keypress', onKey);
|
|
128
|
+
for (const l of savedListeners)
|
|
129
|
+
emitter.on('keypress', l);
|
|
130
|
+
savedListeners = [];
|
|
131
|
+
// 先 paintInput(dim,menu:null):擦菜单(用 lastMenuRows)+ 画 dim 占位底栏 + 复位 lastMenuRows=0;
|
|
132
|
+
// 再处理滚动:面板内可能滚过滚轮(scrollOffset>0)→ resetScroll 回尾让 agent 后续输出可见;
|
|
133
|
+
// 未滚则 repaintViewport 从 content 缓冲重画内容区(恢复被菜单覆盖的 ● ask_human 行)。
|
|
134
|
+
// 顺序不可反——resetScroll/repaintViewport 在菜单还在(lastMenuRows>0)时会用内容覆盖菜单区,擦不掉菜单。
|
|
135
|
+
layout.paintInput({
|
|
136
|
+
prompt: '❯ ',
|
|
137
|
+
lines: [''],
|
|
138
|
+
cursorLine: 0,
|
|
139
|
+
cursorCol: 0,
|
|
140
|
+
menu: null,
|
|
141
|
+
dim: true,
|
|
142
|
+
});
|
|
143
|
+
if (layout.isScrolled())
|
|
144
|
+
layout.resetScroll();
|
|
145
|
+
else
|
|
146
|
+
layout.repaintViewport();
|
|
147
|
+
}
|
|
148
|
+
function onKey(_str, key) {
|
|
149
|
+
if (resolved || !key)
|
|
150
|
+
return;
|
|
151
|
+
// SGR 鼠标 fragment:readline 拆碎 \x1B[<…M,经 mouse.consumeMouse 重组;
|
|
152
|
+
// 滚轮 → scrollBy(±5) 滚动查看 agent 输出内容(菜单只靠 ↑↓ 选,滚轮不选菜单);
|
|
153
|
+
// fragment 期间 suppress 吞掉防砸进 onKeyInput 的可打印插入。菜单经 scrollBy 末尾
|
|
154
|
+
// repaint()→paintInput(lastView) 重画,不被 viewport 滚动覆盖。
|
|
155
|
+
const _m = mouse.consumeMouse(key.sequence ?? '');
|
|
156
|
+
if (_m.suppress) {
|
|
157
|
+
if (_m.wheel)
|
|
158
|
+
layout.scrollBy(_m.wheel * 5);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Ctrl+C → 取消(不 reject SIGINT——否则经 executeTool 的 try/catch 变成 tool 错误串)
|
|
162
|
+
if (key.ctrl && key.name === 'c') {
|
|
163
|
+
finish({ action: 'cancelled' });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (key.name === 'escape') {
|
|
167
|
+
if (mode === 'input' && cameFromChoice) {
|
|
168
|
+
// 由 choice"自定义"项切入的 input:Esc 返回选项(不取消整次提问)
|
|
169
|
+
mode = 'choice';
|
|
170
|
+
redraw();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
finish({ action: 'cancelled' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// PgUp/PgDn:翻页滚动查看 agent 输出内容(与 onRunningKey/prompt.ts 一致;↑↓ 仍归菜单导航,不抢)。
|
|
177
|
+
if (key.name === 'pageup' || key.name === 'pagedown') {
|
|
178
|
+
const pageH = layout.getGeo().contentBottom;
|
|
179
|
+
layout.scrollBy(key.name === 'pageup' ? pageH : -pageH);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (mode === 'choice') {
|
|
183
|
+
onKeyChoice(key);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
onKeyInput(key);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function onKeyChoice(key) {
|
|
190
|
+
const itemCount = options.length + 1; // +自定义项
|
|
191
|
+
switch (key.name) {
|
|
192
|
+
case 'up':
|
|
193
|
+
selected = (selected - 1 + itemCount) % itemCount;
|
|
194
|
+
redraw();
|
|
195
|
+
return;
|
|
196
|
+
case 'down':
|
|
197
|
+
selected = (selected + 1) % itemCount;
|
|
198
|
+
redraw();
|
|
199
|
+
return;
|
|
200
|
+
case 'return':
|
|
201
|
+
case 'enter':
|
|
202
|
+
if (selected === options.length) {
|
|
203
|
+
// 自定义项 → 切 input 子态(空文本起)
|
|
204
|
+
mode = 'input';
|
|
205
|
+
cameFromChoice = true;
|
|
206
|
+
text = '';
|
|
207
|
+
cursor = 0;
|
|
208
|
+
redraw();
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
finish({ action: 'selected', value: options[selected] });
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// 数字键 1-9 直选对应选项(自定义项不绑定数字)
|
|
216
|
+
const s = key.sequence ?? '';
|
|
217
|
+
if (s >= '1' && s <= '9') {
|
|
218
|
+
const n = Number(s) - 1;
|
|
219
|
+
if (n < options.length) {
|
|
220
|
+
finish({ action: 'selected', value: options[n] });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function onKeyInput(key) {
|
|
225
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
226
|
+
finish({ action: 'submitted', value: text });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (key.ctrl && key.name === 'a') {
|
|
230
|
+
cursor = 0;
|
|
231
|
+
redraw();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (key.ctrl && key.name === 'e') {
|
|
235
|
+
cursor = text.length;
|
|
236
|
+
redraw();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
switch (key.name) {
|
|
240
|
+
case 'backspace':
|
|
241
|
+
if (cursor > 0) {
|
|
242
|
+
text = text.slice(0, cursor - 1) + text.slice(cursor);
|
|
243
|
+
cursor--;
|
|
244
|
+
redraw();
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
case 'left':
|
|
248
|
+
if (cursor > 0) {
|
|
249
|
+
cursor--;
|
|
250
|
+
redraw();
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
case 'right':
|
|
254
|
+
if (cursor < text.length) {
|
|
255
|
+
cursor++;
|
|
256
|
+
redraw();
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
case 'home':
|
|
260
|
+
cursor = 0;
|
|
261
|
+
redraw();
|
|
262
|
+
return;
|
|
263
|
+
case 'end':
|
|
264
|
+
cursor = text.length;
|
|
265
|
+
redraw();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// 可打印字符(>= 空格,非 ctrl/meta)→ 插入光标处(\n < ' ' 自动排除,保单行)
|
|
269
|
+
const s = key.sequence ?? '';
|
|
270
|
+
if (s && s >= ' ' && !key.ctrl && !key.meta) {
|
|
271
|
+
text = text.slice(0, cursor) + s + text.slice(cursor);
|
|
272
|
+
cursor += s.length;
|
|
273
|
+
redraw();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return new Promise((res, rej) => {
|
|
277
|
+
resolve = res;
|
|
278
|
+
try {
|
|
279
|
+
// 进入面板:停 spinner(避免 onFrame 覆盖)+ 回尾(若用户正滚动回看)
|
|
280
|
+
Spinner.pauseCurrent();
|
|
281
|
+
layout.resetScroll();
|
|
282
|
+
// 快照现有 keypress 监听(运行态的 onRunningKey)并摘掉,挂自己的 onKey
|
|
283
|
+
savedListeners = emitter.listeners('keypress').slice();
|
|
284
|
+
for (const l of savedListeners)
|
|
285
|
+
emitter.removeListener('keypress', l);
|
|
286
|
+
readline.emitKeypressEvents(stdin); // 幂等(运行态已挂解析器,防御性再调)
|
|
287
|
+
// raw 模式:runAgent 期间 repl 已设 true;防御性确保(失败则按键不来,但不崩)
|
|
288
|
+
try {
|
|
289
|
+
stdin.setRawMode(true);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// 非 TTY / 不支持:忽略(实际非 TTY 已在上方 isActive 守卫返回)
|
|
293
|
+
}
|
|
294
|
+
stdin.resume();
|
|
295
|
+
emitter.on('keypress', onKey);
|
|
296
|
+
redraw();
|
|
297
|
+
}
|
|
298
|
+
catch (e) {
|
|
299
|
+
// 进入失败:必须恢复运行态监听,否则 onRunningKey 残留摘除 → 本 turns 的 Ctrl+C/滚动/typeahead 全废
|
|
300
|
+
try {
|
|
301
|
+
emitter.removeListener('keypress', onKey);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// 忽略
|
|
305
|
+
}
|
|
306
|
+
for (const l of savedListeners) {
|
|
307
|
+
try {
|
|
308
|
+
emitter.on('keypress', l);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// 忽略
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
savedListeners = [];
|
|
315
|
+
rej(e instanceof Error ? e : new Error(String(e)));
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|