mocode-ai 0.1.4 → 0.1.6
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 -188
- package/dist/agent/core.js +309 -0
- package/dist/agent/index.js +62 -249
- 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 +81 -36
- package/dist/context/classifier.js +83 -0
- package/dist/context/encoders/index.js +10 -0
- package/dist/context/encoders/passthrough.js +23 -0
- package/dist/context/index.js +10 -0
- package/dist/context/pipeline.js +84 -0
- package/dist/context/registry.js +30 -0
- package/dist/context/types.js +12 -0
- package/dist/index.js +12 -2
- package/dist/llm/index.js +11 -9
- package/dist/memory/index.js +2 -2
- package/dist/memory/reflect.js +11 -11
- package/dist/memory/store.js +4 -4
- package/dist/repl/index.js +269 -109
- package/dist/sandbox/command.js +41 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/jail.js +75 -0
- package/dist/sandbox/policy.js +58 -0
- package/dist/sandbox/root.js +19 -0
- package/dist/session/compact.js +1 -1
- package/dist/skills/index.js +2 -2
- package/dist/tools/builtins/ask-human.js +7 -7
- package/dist/tools/builtins/codegraph.js +113 -0
- package/dist/tools/builtins/edit-file.js +3 -3
- package/dist/tools/builtins/glob.js +10 -5
- package/dist/tools/builtins/grep.js +14 -9
- package/dist/tools/builtins/index.js +7 -1
- package/dist/tools/builtins/memory-forget.js +2 -2
- package/dist/tools/builtins/memory-list.js +2 -2
- package/dist/tools/builtins/memory-save.js +7 -7
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/memory-update.js +6 -6
- package/dist/tools/builtins/read-file.js +5 -4
- package/dist/tools/builtins/run-command.js +47 -8
- 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 +16 -0
- package/dist/tools/registry.js +18 -4
- package/dist/ui/content.js +25 -17
- package/dist/ui/diff.js +28 -26
- package/dist/ui/intervention.js +24 -3
- package/dist/ui/layout.js +295 -81
- package/dist/ui/markdown.js +607 -0
- package/dist/ui/mouse.js +93 -0
- package/dist/ui/prompt.js +259 -5
- package/dist/ui/render.js +1 -1
- package/dist/ui/theme.js +146 -13
- package/package.json +2 -2
package/dist/repl/index.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config } from '../config/index.js';
|
|
4
|
+
import { config, PLAN_MODE_SUFFIX } from '../config/index.js';
|
|
5
|
+
import { updateConfigKey } from '../config/file.js';
|
|
5
6
|
import { runAgent } from '../agent/index.js';
|
|
6
|
-
import {
|
|
7
|
+
import { getAgentMode, setAgentMode, onModeChange } from '../agent/mode.js';
|
|
8
|
+
import { setSandboxRoot } from '../sandbox/root.js';
|
|
9
|
+
import { ui, setTheme, getTheme, listThemes, themeExists } from '../ui/theme.js';
|
|
7
10
|
import { bannerString, displayWidth, padEndDisplay, summarizeToolCall, summarizeToolResult } from '../ui/render.js';
|
|
8
11
|
import * as layout from '../ui/layout.js';
|
|
9
|
-
import
|
|
12
|
+
import * as mouse from '../ui/mouse.js';
|
|
13
|
+
import { promptWithSlashMenu, promptTurnPicker, promptSessionPicker, promptThemePicker, promptRevertChoice, } from '../ui/prompt.js';
|
|
14
|
+
import { promptIntervention } from '../ui/intervention.js';
|
|
10
15
|
import { tools } from '../tools/registry.js';
|
|
11
16
|
import { estimateMessagesTokens, estimateToolSchemaTokens, } from '../llm/index.js';
|
|
12
17
|
import { compactHistory, contextState, newSessionId, saveSession, loadSession, listSessions, } from '../session/index.js';
|
|
@@ -26,40 +31,50 @@ const SLASH_COMMANDS = [
|
|
|
26
31
|
{ name: '/skills', desc: '列出已发现的 skill' },
|
|
27
32
|
{ name: '/compact', desc: '压缩历史(可带焦点 /compact …)' },
|
|
28
33
|
{ name: '/resume', desc: '续接已保存的会话' },
|
|
29
|
-
{ name: '/think', desc: '展开折叠思考段(/think N)' },
|
|
30
34
|
{ name: '/rollback', desc: '菜单选轮次回滚(↑↓·Enter)' },
|
|
31
35
|
{ name: '/memory', desc: '记忆库:条目计数与近期索引' },
|
|
32
36
|
{ name: '/reflect', desc: '手动触发后台记忆反思 pass' },
|
|
33
37
|
{ name: '/init', desc: '扫描项目生成 MOCODE.md 项目记忆' },
|
|
38
|
+
{ name: '/theme', desc: '切换颜色主题(↑↓·Enter)' },
|
|
39
|
+
{ name: '/plan', desc: '切到 plan 模式(只读探查+产出计划)' },
|
|
40
|
+
{ name: '/auto', desc: '切回 auto 模式(全工具执行)' },
|
|
34
41
|
];
|
|
42
|
+
/** 主题名 → 一句描述(供 /theme 菜单 / 列表显示)。新增主题时在 src/ui/theme.ts THEMES 加键后于此补一句。 */
|
|
43
|
+
const THEME_DESCRIPTIONS = {
|
|
44
|
+
default: '16 色原版(深底)',
|
|
45
|
+
light: '浅底终端',
|
|
46
|
+
solarized: 'Solarized 强调色',
|
|
47
|
+
gruvbox: 'Gruvbox 暖色',
|
|
48
|
+
nord: 'Nord 冷色',
|
|
49
|
+
};
|
|
35
50
|
/**
|
|
36
51
|
* /init 指令:发给 agent 扫描项目并生成 MOCODE.md(对标 Claude Code /init 生成 CLAUDE.md,
|
|
37
52
|
* 但 mocode 读 MOCODE.md)。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
|
|
38
53
|
*/
|
|
39
|
-
const INIT_PROMPT = `分析当前项目(process.cwd()),生成 MOCODE.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
|
|
40
|
-
|
|
41
|
-
先探查(尽量少调用拿全貌):
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
- 若 MOCODE.md 已存在:read_file 读它,在其基础上更新(补缺、修正过时),不丢已有准确事实。
|
|
46
|
-
|
|
47
|
-
MOCODE.md 按以下结构写(每节简短,只写稳定、非显然的事实):
|
|
48
|
-
## 项目
|
|
49
|
-
一两句:是什么、技术栈、运行环境。
|
|
50
|
-
## 命令
|
|
51
|
-
install / dev / build / test / typecheck / lint 等——从 package.json scripts 提炼,写原样命令行(如 \`npm run typecheck\`);没有的注明"无测试"/"无 lint"。
|
|
52
|
-
## 目录结构
|
|
53
|
-
顶层各目录与子系统职责,一句话/个;不逐文件列。
|
|
54
|
-
## 约定
|
|
55
|
-
从代码与现有文档提炼的硬约定:模块系统(ESM?)、命名、错误处理、工具/函数契约、易踩坑点。只写非显然、会让人踩坑的;不写"保持简洁"这种正确废话。
|
|
56
|
-
## 扩展点
|
|
57
|
-
加工具/命令/provider/模块的接缝(改哪个文件、加在哪)。
|
|
58
|
-
|
|
59
|
-
硬要求:
|
|
60
|
-
- 从实际代码提炼,引用具体文件名/命令/符号;不编造、不泛泛。
|
|
61
|
-
- 总长 ≤ 1500 字;只写后续会话有用的稳定事实,不写易变项(当前 bug、临时文件、未决 TODO)。
|
|
62
|
-
- 用 write_file 写入项目根 MOCODE.md。
|
|
54
|
+
const INIT_PROMPT = `分析当前项目(process.cwd()),生成 MOCODE.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
|
|
55
|
+
|
|
56
|
+
先探查(尽量少调用拿全貌):
|
|
57
|
+
- 若有 .codegraph/:用 codegraph 工具(explore "<架构或入口符号>")一次拿相关源码+调用路径,别逐文件读!!!
|
|
58
|
+
- read_file package.json(或 Cargo.toml/pyproject.toml/go.mod 等):scripts、依赖、入口、模块类型。
|
|
59
|
+
- glob 顶层目录;read_file 入口文件 + 各子系统 index.ts/README。
|
|
60
|
+
- 若 MOCODE.md 已存在:read_file 读它,在其基础上更新(补缺、修正过时),不丢已有准确事实。
|
|
61
|
+
|
|
62
|
+
MOCODE.md 按以下结构写(每节简短,只写稳定、非显然的事实):
|
|
63
|
+
## 项目
|
|
64
|
+
一两句:是什么、技术栈、运行环境。
|
|
65
|
+
## 命令
|
|
66
|
+
install / dev / build / test / typecheck / lint 等——从 package.json scripts 提炼,写原样命令行(如 \`npm run typecheck\`);没有的注明"无测试"/"无 lint"。
|
|
67
|
+
## 目录结构
|
|
68
|
+
顶层各目录与子系统职责,一句话/个;不逐文件列。
|
|
69
|
+
## 约定
|
|
70
|
+
从代码与现有文档提炼的硬约定:模块系统(ESM?)、命名、错误处理、工具/函数契约、易踩坑点。只写非显然、会让人踩坑的;不写"保持简洁"这种正确废话。
|
|
71
|
+
## 扩展点
|
|
72
|
+
加工具/命令/provider/模块的接缝(改哪个文件、加在哪)。
|
|
73
|
+
|
|
74
|
+
硬要求:
|
|
75
|
+
- 从实际代码提炼,引用具体文件名/命令/符号;不编造、不泛泛。
|
|
76
|
+
- 总长 ≤ 1500 字;只写后续会话有用的稳定事实,不写易变项(当前 bug、临时文件、未决 TODO)。
|
|
77
|
+
- 用 write_file 写入项目根 MOCODE.md。
|
|
63
78
|
- 写完简述:写了哪几节 + 从代码里发现的 2-3 条非显然关键约定(供用户校验)。`;
|
|
64
79
|
/** 临时 readline 读一行(cooked,用于子提问;主输入走 promptWithSlashMenu)。 */
|
|
65
80
|
async function askLine(prompt) {
|
|
@@ -80,7 +95,7 @@ function renderContextBar(history) {
|
|
|
80
95
|
const pct = Math.min(1, est / win);
|
|
81
96
|
const W = 10;
|
|
82
97
|
const filled = Math.round(pct * W);
|
|
83
|
-
const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
|
|
98
|
+
const bar = filled === 0 ? ' '.repeat(W) : '█'.repeat(filled) + '░'.repeat(W - filled);
|
|
84
99
|
const src = contextState.lastUsage ? '实测' : '估算';
|
|
85
100
|
const k = (n) => `${Math.round(n / 1000)}k`;
|
|
86
101
|
const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.cyan;
|
|
@@ -95,17 +110,18 @@ function renderContextBarInline(history) {
|
|
|
95
110
|
const pct = Math.min(1, est / win);
|
|
96
111
|
const W = 10;
|
|
97
112
|
const filled = Math.round(pct * W);
|
|
98
|
-
const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
|
|
113
|
+
const bar = filled === 0 ? ' '.repeat(W) : '█'.repeat(filled) + '░'.repeat(W - filled);
|
|
99
114
|
const k = (n) => `${Math.round(n / 1000)}k`;
|
|
100
115
|
const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.cyan;
|
|
101
116
|
return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${pctCol}${Math.round(pct * 100)}%${ui.reset} ${ui.dim}${k(est)}/${k(win)}${ui.reset}`;
|
|
102
117
|
}
|
|
103
|
-
/** 状态行基线:模型 / context / cwd
|
|
118
|
+
/** 状态行基线:模型 / context / cwd / 模式标识。repl 在轮次边界与切模式时调,刷新 context 用量与 mode chip。 */
|
|
104
119
|
function refreshStatusBase(history) {
|
|
105
120
|
layout.setStatusBase({
|
|
106
121
|
model: config.model,
|
|
107
122
|
contextBar: renderContextBarInline(history),
|
|
108
123
|
cwd: process.cwd(),
|
|
124
|
+
modeTag: getAgentMode() === 'plan' ? 'plan' : 'auto',
|
|
109
125
|
});
|
|
110
126
|
}
|
|
111
127
|
/** 命令 → 运行态状态文字 + 底栏 dim 占位。 */
|
|
@@ -119,12 +135,19 @@ function runningStateFor(cmd) {
|
|
|
119
135
|
return { status: '回滚', placeholder: '选择轮次…' };
|
|
120
136
|
case '/init':
|
|
121
137
|
return { status: '初始化', placeholder: '生成 MOCODE.md…' };
|
|
138
|
+
case '/plan':
|
|
139
|
+
return { status: '切 plan', placeholder: '…' };
|
|
140
|
+
case '/auto':
|
|
141
|
+
return { status: '切 auto', placeholder: '…' };
|
|
122
142
|
case '/clear':
|
|
123
143
|
return { status: '清空', placeholder: '…' };
|
|
144
|
+
case '/theme':
|
|
145
|
+
return { status: '切主题', placeholder: '选择主题…' };
|
|
124
146
|
default:
|
|
125
|
-
// 输入框留空(运行中可 typeahead 打字,dim 回显)
|
|
126
|
-
//
|
|
127
|
-
|
|
147
|
+
// 输入框留空(运行中可 typeahead 打字,dim 回显);运行状态由内联 spinner 承载(思考中/执行…),
|
|
148
|
+
// 状态行只显走时——故常态 status 留空,不塞「处理」这种与内联重复的泛标签。
|
|
149
|
+
// 不把「思考中… Ctrl+C 中断」塞进输入框占位——避免看起来像已有输入、妨碍正常输入。
|
|
150
|
+
return { status: '', placeholder: '' };
|
|
128
151
|
}
|
|
129
152
|
}
|
|
130
153
|
const emitter = stdin;
|
|
@@ -134,11 +157,20 @@ let runningInput = ''; // 运行中已打字缓冲(单行;agent 结束后预填
|
|
|
134
157
|
let runningPlaceholder = '';
|
|
135
158
|
let currentAbort = null;
|
|
136
159
|
let pendingPrefill = null; // /rollback 选中后预填的 user 输入(下轮 INPUT 态消费)
|
|
160
|
+
// agent 模式状态已提到 src/agent/mode.ts(共享叶子:switch_mode 工具可写、agent 每步读、repl 注册 onModeChange 监听器)。
|
|
137
161
|
/** 运行态按键:滚动优先,再 Ctrl+C 中断,再 typeahead 编辑(单行,Enter=无操作)。 */
|
|
138
162
|
function onRunningKey(_str, key) {
|
|
139
163
|
if (!key)
|
|
140
164
|
return;
|
|
141
|
-
//
|
|
165
|
+
// SGR 鼠标滚轮:readline 把 \x1B[<btn;col;rowM 拆成多 fragment,经 mouse.consumeMouse 重组;
|
|
166
|
+
// 滚轮 → scrollBy(±5)(不调 setUserActive——与键盘滚动一致;流式 flushTimer 在 scrollOffset>0 时不重画,安全)。
|
|
167
|
+
const _m = mouse.consumeMouse(key.sequence ?? '');
|
|
168
|
+
if (_m.suppress) {
|
|
169
|
+
if (_m.wheel)
|
|
170
|
+
layout.scrollBy(_m.wheel * 5);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// 滚动回看键(优先;不触发回尾):PgUp/PgDn 翻页,↑/↓ 每次 5 行(键盘;鼠标滚轮已由上方守卫处理)。
|
|
142
174
|
// 运行态无输入光标,↑/↓ 无其他用途,直接作滚动。
|
|
143
175
|
if (key.name === 'pageup' ||
|
|
144
176
|
key.name === 'pagedown' ||
|
|
@@ -150,17 +182,27 @@ function onRunningKey(_str, key) {
|
|
|
150
182
|
else if (key.name === 'pagedown')
|
|
151
183
|
layout.scrollBy(-pageH);
|
|
152
184
|
else if (key.name === 'up')
|
|
153
|
-
layout.scrollBy(
|
|
185
|
+
layout.scrollBy(5);
|
|
154
186
|
else
|
|
155
|
-
layout.scrollBy(-
|
|
187
|
+
layout.scrollBy(-5);
|
|
156
188
|
return;
|
|
157
189
|
}
|
|
190
|
+
// 用户在交互(非滚动键)→ 暂停流式物理写,避免光标去 contentRow 扰动 IME 候选窗(停手后自动 flush)
|
|
191
|
+
layout.setUserActive();
|
|
158
192
|
// 其他键:若处于滚动回看,先回尾再处理(打字即回底)
|
|
159
193
|
if (layout.isScrolled())
|
|
160
194
|
layout.resetScroll();
|
|
161
|
-
// Ctrl+C
|
|
195
|
+
// Ctrl+C 4 层语义(RUNNING 态):有 typeahead → 清空(层 1,不中断);空 → abort(层 2,中断 agent)。
|
|
196
|
+
// 两次 Ctrl+C 才中断(先清 typeahead 再 abort),与 INPUT 态 onCtrlC 的 fish 式一致。
|
|
197
|
+
// raw 模式下 Ctrl+C 是按键不触发 SIGINT;signal 经 executeTool 串进工具,run_command/web_fetch 即时被杀。
|
|
162
198
|
if (key.ctrl && key.name === 'c') {
|
|
163
|
-
|
|
199
|
+
if (runningInput.length > 0) {
|
|
200
|
+
runningInput = '';
|
|
201
|
+
layout.paintRunningInputEcho(runningInput, runningPlaceholder);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
currentAbort?.abort();
|
|
205
|
+
}
|
|
164
206
|
return;
|
|
165
207
|
}
|
|
166
208
|
const s = key.sequence ?? '';
|
|
@@ -214,6 +256,7 @@ function stopRunningListener() {
|
|
|
214
256
|
* 把用户消息格式化为带满宽背景色的文本(上滑时易辨认用户消息)。
|
|
215
257
|
* 每行用 padEndDisplay 填充到终端宽度(含 ❯ / 缩进),背景色 SGR 包裹整行 + 行末 reset。
|
|
216
258
|
* 满宽 pad 使终端背景色覆盖整行(含行尾空单元格),上滑滚动时用户消息呈连续色块、与 assistant 正文区分。
|
|
259
|
+
* 末尾多留一空行(\n\n 收尾):用户消息与后续(agent 流式输出 / 下条消息)之间空一行,仿 Claude Code。
|
|
217
260
|
*/
|
|
218
261
|
function formatUserMessage(lines) {
|
|
219
262
|
const cols = layout.getGeo().cols;
|
|
@@ -227,7 +270,7 @@ function formatUserMessage(lines) {
|
|
|
227
270
|
const padded = padEndDisplay(full, cols);
|
|
228
271
|
return `${userBg}${padded}${reset}`;
|
|
229
272
|
})
|
|
230
|
-
.join('\n') + '\n');
|
|
273
|
+
.join('\n') + '\n\n');
|
|
231
274
|
}
|
|
232
275
|
/** 把多行提交输入回显进内容区(❯ 首行,续行按 prompt 宽度缩进)。仅 TUI 态回显(非 TTY 由 readline 自带回显)。 */
|
|
233
276
|
function echoInput(lines) {
|
|
@@ -267,7 +310,7 @@ export function renderHistory(history) {
|
|
|
267
310
|
if (m.role === 'assistant') {
|
|
268
311
|
const text = textOf(m.content);
|
|
269
312
|
if (text) {
|
|
270
|
-
layout.
|
|
313
|
+
layout.contentWriteMdOnce(text);
|
|
271
314
|
if (!text.endsWith('\n'))
|
|
272
315
|
layout.contentWrite('\n');
|
|
273
316
|
}
|
|
@@ -299,17 +342,27 @@ export function renderHistory(history) {
|
|
|
299
342
|
* contentWrite 落入内容区(滚动区域内自动滚动,底栏不动)。history 由本模块持有,在轮次间持久;
|
|
300
343
|
* agent 只读取并追加(+ 经 session/ 压缩)。每轮成功结束后自动落盘,退出后可用 --resume / /resume 续接。
|
|
301
344
|
*/
|
|
302
|
-
export async function startRepl(initialHistory, sessionId, updateNotice = null) {
|
|
345
|
+
export async function startRepl(initialHistory, sessionId, updateNotice = null, sandboxRootOverride) {
|
|
346
|
+
// 模式重置:agentMode 不落盘,每个 REPL 会话从 auto 开始(/resume / --resume 亦重置)。
|
|
347
|
+
setAgentMode('auto');
|
|
348
|
+
// 沙箱根:文件操作边界。优先级 --sandbox-root > SANDBOX_ROOT env > process.cwd()。
|
|
349
|
+
// 纯边界记录(不 chdir),jail.ts 内部 resolve。子 agent 同进程继承全局 root。
|
|
350
|
+
setSandboxRoot(sandboxRootOverride ?? config.sandboxRoot ?? process.cwd());
|
|
351
|
+
// 构造系统提示:auto 用 base;plan 在 config.systemPrompt 后追加 PLAN_MODE_SUFFIX。
|
|
352
|
+
// 切模式时 applyMode 重算 history[0](history[0] 恒 system,compaction 保它,不破坏)。
|
|
353
|
+
const buildSystemMessage = (planMode) => effectiveSystemPrompt(config.systemPrompt +
|
|
354
|
+
(planMode ? PLAN_MODE_SUFFIX : '') +
|
|
355
|
+
buildMemorySection() +
|
|
356
|
+
buildMemoryIndexSection());
|
|
303
357
|
// 有预加载(--resume)则用它,并把 history[0] 刷成当前 system prompt(config 可能已变);
|
|
304
|
-
// 否则新会话只塞 system
|
|
305
|
-
const systemPrompt = effectiveSystemPrompt(config.systemPrompt + buildMemorySection() + buildMemoryIndexSection());
|
|
358
|
+
// 否则新会话只塞 system 提示(默认 auto)。
|
|
306
359
|
const history = initialHistory && initialHistory.length
|
|
307
360
|
? initialHistory
|
|
308
|
-
: [{ role: 'system', content:
|
|
361
|
+
: [{ role: 'system', content: buildSystemMessage(false) }];
|
|
309
362
|
if (initialHistory &&
|
|
310
363
|
initialHistory.length &&
|
|
311
364
|
history[0]?.role === 'system') {
|
|
312
|
-
history[0] = { role: 'system', content:
|
|
365
|
+
history[0] = { role: 'system', content: buildSystemMessage(false) };
|
|
313
366
|
}
|
|
314
367
|
// --resume:读回该会话的轮次/快照;无文件则从 history 重建 turns(无快照→旧轮次文件改动不可撤销)
|
|
315
368
|
if (sessionId && initialHistory && initialHistory.length) {
|
|
@@ -319,8 +372,6 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
|
|
|
319
372
|
let currentSessionId = sessionId;
|
|
320
373
|
// 反思 cadence 计数:每 reflectEveryN 轮 fire-and-forget 一次后台反思 pass。
|
|
321
374
|
let turnCount = 0;
|
|
322
|
-
// 本会话累积的折叠思考段,供 /think N 重打原文。
|
|
323
|
-
const collapsedThinkings = [];
|
|
324
375
|
const toolsLine = tools.map((t) => t.name).join(' · ');
|
|
325
376
|
const banner = () => ({
|
|
326
377
|
model: config.model,
|
|
@@ -328,7 +379,9 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
|
|
|
328
379
|
cwd: process.cwd(),
|
|
329
380
|
tools: toolsLine,
|
|
330
381
|
});
|
|
331
|
-
//
|
|
382
|
+
// 开场:按 config.theme 切主题(横幅 / 状态行 / 后续渲染皆用新色),再进 alt screen + 状态基线 + 清内容区。
|
|
383
|
+
// --resume 有历史则渲染对话(仿 Claude Code),否则横幅。
|
|
384
|
+
setTheme(config.theme);
|
|
332
385
|
layout.enterAltScreen();
|
|
333
386
|
refreshStatusBase(history);
|
|
334
387
|
layout.clearContent();
|
|
@@ -343,10 +396,29 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
|
|
|
343
396
|
// 自更新提示:开场静态段(进 INPUT 态前),dim 一行,不与流式 / 输入争用。
|
|
344
397
|
layout.contentWrite(` ${ui.gray}↳ ${updateNotice}${ui.reset}\n`);
|
|
345
398
|
}
|
|
399
|
+
layout.contentWrite(`${ui.dim} /plan · /auto · Shift+Tab 切换模式(plan:只读探查 + 产出计划,审批后切 auto 执行)${ui.reset}\n`);
|
|
400
|
+
/**
|
|
401
|
+
* 切换 agent 模式(Shift+Tab 触发,经 prompt.ts 的 onCycleMode 回调)。
|
|
402
|
+
* cycleMode 翻 agentMode + applyMode 重写 history[0] + refreshStatusBase 设状态行 modeTag;
|
|
403
|
+
* 随后 prompt.ts 自调 redraw() 经 paintInput 把 chip 画出来(光标留输入框,不调 drawStatusBar)。
|
|
404
|
+
*/
|
|
405
|
+
const applyMode = (planMode) => {
|
|
406
|
+
history[0] = { role: 'system', content: buildSystemMessage(planMode) };
|
|
407
|
+
};
|
|
408
|
+
const cycleMode = () => {
|
|
409
|
+
setAgentMode(getAgentMode() === 'plan' ? 'auto' : 'plan'); // listener 接手 applyMode + refreshStatusBase
|
|
410
|
+
};
|
|
411
|
+
// 注册模式变更监听器:switch_mode 工具 / cycleMode / /plan / /auto / runTurn 调 setAgentMode 时同步触发,
|
|
412
|
+
// 重写 history[0] 系统提示(切 plan 追加 PLAN_MODE_SUFFIX)+ 刷状态行 modeTag。
|
|
413
|
+
// 不调 drawStatusBar:INPUT 态靠 prompt.ts redraw 画 chip;RUNNING 态(switch_mode 中途切)靠 200ms turnTimer 兜底。
|
|
414
|
+
onModeChange((m) => {
|
|
415
|
+
applyMode(m === 'plan');
|
|
416
|
+
refreshStatusBase(history);
|
|
417
|
+
});
|
|
346
418
|
/**
|
|
347
419
|
* 回滚子流程(由 /rollback 触发):菜单(↑/↓)选轮次 → 选中第 X 轮 = 删第 X 轮及之后 + 预填第 X 轮 user 输入
|
|
348
|
-
* (仿 Claude Code rewind,Enter 重新跑该轮)
|
|
349
|
-
*
|
|
420
|
+
* (仿 Claude Code rewind,Enter 重新跑该轮);被删轮次的文件改动走二选一菜单(promptRevertChoice:
|
|
421
|
+
* 撤销文件 / 只撤销消息)。选轮 + 方式菜单均走 raw mode。预填经 pendingPrefill 注入下轮 INPUT。
|
|
350
422
|
*/
|
|
351
423
|
const rollbackFlow = async () => {
|
|
352
424
|
const turnList = listTurns();
|
|
@@ -384,24 +456,25 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
|
|
|
384
456
|
dim: true,
|
|
385
457
|
});
|
|
386
458
|
const revertPaths = new Set();
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
459
|
+
// 文件撤销:二选一菜单(1=撤销文件改动 / 2=只撤销消息保留文件)
|
|
460
|
+
// 返回 true=撤销文件(把 changes 里所有可撤销的路径加入 revertPaths);
|
|
461
|
+
// false/null(取消/非 TTY/Ctrl+C)=只撤销消息,保留文件改动。
|
|
462
|
+
const changes = plan.changes;
|
|
463
|
+
const revertable = changes.filter((c) => c.snapshotAvailable);
|
|
464
|
+
let revertFiles = false;
|
|
465
|
+
if (changes.length > 0) {
|
|
390
466
|
try {
|
|
391
|
-
|
|
467
|
+
const choice = await promptRevertChoice(revertable.length);
|
|
468
|
+
revertFiles = choice === true;
|
|
392
469
|
}
|
|
393
470
|
catch {
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
if (ans.startsWith('u') || ans.startsWith('U')) {
|
|
397
|
-
if (c.snapshotAvailable) {
|
|
398
|
-
revertPaths.add(c.path);
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
layout.contentWrite(`${ui.dim} (无快照,无法撤销——保留)${ui.reset}\n`);
|
|
402
|
-
}
|
|
471
|
+
revertFiles = false; // Ctrl+C(SIGINT)→ 保留文件,只撤销消息
|
|
403
472
|
}
|
|
404
473
|
}
|
|
474
|
+
if (revertFiles) {
|
|
475
|
+
for (const c of revertable)
|
|
476
|
+
revertPaths.add(c.path);
|
|
477
|
+
}
|
|
405
478
|
applyRollback(plan, history, revertPaths);
|
|
406
479
|
if (!currentSessionId)
|
|
407
480
|
currentSessionId = newSessionId();
|
|
@@ -417,6 +490,52 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
|
|
|
417
490
|
renderHistory(history);
|
|
418
491
|
pendingPrefill = prefillText.split('\n');
|
|
419
492
|
};
|
|
493
|
+
/**
|
|
494
|
+
* 跑一轮 agent(enterRunningMode 已由调用方完成):startRunningListener → runAgent → autosave / reflect。
|
|
495
|
+
* plan 模式传 planMode=true(runAgent 用 planChatTools 只读子集)。返 ok=正常结束(未中断 / 未抛错),
|
|
496
|
+
* 供调用方决定是否弹审批面板。execute 轮的合成输入也走这里。
|
|
497
|
+
*/
|
|
498
|
+
const runTurn = async (input, planMode, placeholder) => {
|
|
499
|
+
let ok = false;
|
|
500
|
+
try {
|
|
501
|
+
const signal = startRunningListener(placeholder);
|
|
502
|
+
// 入口设定本轮初始模式(合成执行轮传 false→auto;用户轮传当前 mode)。
|
|
503
|
+
// setAgentMode 触发 listener 重写 history[0];LLM 可在轮中调 switch_mode 切模式,runAgent 每步读实时值。
|
|
504
|
+
setAgentMode(planMode ? 'plan' : 'auto');
|
|
505
|
+
// 运行中每步 chat() 返回后刷新状态行 context 用量条(用 fresh lastUsage / 估算),
|
|
506
|
+
// 否则整轮冻结在轮首 refreshStatusBase 的值,「执行 grep」时 2k/1000k 不动。
|
|
507
|
+
await runAgent(history, input, signal, () => {
|
|
508
|
+
refreshStatusBase(history);
|
|
509
|
+
layout.drawStatusBar();
|
|
510
|
+
});
|
|
511
|
+
ok = !signal.aborted; // 中断(Ctrl+C)→ runAgent 已还原 history,ok=false 不弹审批
|
|
512
|
+
// 成功轮次自动落盘(崩溃也保住上一轮);新会话首轮分配 id
|
|
513
|
+
if (!currentSessionId)
|
|
514
|
+
currentSessionId = newSessionId();
|
|
515
|
+
try {
|
|
516
|
+
saveSession(history, currentSessionId);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
// 落盘失败不阻断 REPL
|
|
520
|
+
}
|
|
521
|
+
persistSnapshots(currentSessionId); // 随会话落盘回滚快照(/resume 后仍可撤销)
|
|
522
|
+
// 后台反思:每 reflectEveryN 轮 fire-and-forget 一次(与下一轮 agent 并发,不阻塞)。
|
|
523
|
+
// 已有在飞任务 / autoReflect 关 → kickoff 内部自守卫。快照同步取(避免下一轮 mutate history 竞态)。
|
|
524
|
+
turnCount++;
|
|
525
|
+
if (turnCount % config.reflectEveryN === 0) {
|
|
526
|
+
kickoffReflection(snapshotTranscript(history, 20));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
ok = false;
|
|
531
|
+
layout.contentWrite(`${ui.red}[错误]${ui.reset} ${e instanceof Error ? e.message : String(e)}\n`);
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
stopRunningListener();
|
|
535
|
+
}
|
|
536
|
+
layout.contentWrite('\n'); // 轮次之间空行
|
|
537
|
+
return ok;
|
|
538
|
+
};
|
|
420
539
|
while (true) {
|
|
421
540
|
// INPUT 态:画底栏输入框 + 状态行,光标入输入框
|
|
422
541
|
refreshStatusBase(history);
|
|
@@ -433,6 +552,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
|
|
|
433
552
|
input = await promptWithSlashMenu({
|
|
434
553
|
prompt: PROMPT,
|
|
435
554
|
commands: SLASH_COMMANDS,
|
|
555
|
+
onCycleMode: cycleMode,
|
|
436
556
|
// /rollback 预填优先;否则上一轮运行中 typeahead 打的字 → 预填进输入框,用户可改可发
|
|
437
557
|
...(pendingPrefill
|
|
438
558
|
? { initialLines: pendingPrefill }
|
|
@@ -464,9 +584,30 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
|
|
|
464
584
|
// /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成 MOCODE.md),fall through 走 runAgent
|
|
465
585
|
joined = INIT_PROMPT;
|
|
466
586
|
}
|
|
587
|
+
if (line === '/plan') {
|
|
588
|
+
// /plan:切到 plan 模式(只读探查 + 产出计划)。Shift+Tab 的可靠后备——
|
|
589
|
+
// 中文 IME(微软拼音用 Shift 切中英)与部分终端会吞掉 Shift+Tab 的 \x1b[Z,命令不受影响。
|
|
590
|
+
if (getAgentMode() === 'plan') {
|
|
591
|
+
layout.contentWrite(`${ui.dim}(已在 plan 模式)${ui.reset}\n`);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
setAgentMode('plan'); // listener 接手 applyMode + refreshStatusBase
|
|
595
|
+
layout.contentWrite(`${ui.dim}(已切到 plan 模式:只读探查 + 产出计划,审批后切 auto 执行。/auto 或 Shift+Tab 切回)${ui.reset}\n`);
|
|
596
|
+
}
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (line === '/auto') {
|
|
600
|
+
if (getAgentMode() === 'auto') {
|
|
601
|
+
layout.contentWrite(`${ui.dim}(已在 auto 模式)${ui.reset}\n`);
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
setAgentMode('auto'); // listener 接手 applyMode + refreshStatusBase
|
|
605
|
+
layout.contentWrite(`${ui.dim}(已切回 auto 模式:全工具执行)${ui.reset}\n`);
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
467
609
|
if (line === '/clear') {
|
|
468
610
|
history.length = 1; // 保留 system 提示
|
|
469
|
-
collapsedThinkings.length = 0; // 同步清空折叠的思考段
|
|
470
611
|
resetState(); // 同步清空回滚轮次/快照
|
|
471
612
|
currentSessionId = undefined; // 下轮起新会话文件
|
|
472
613
|
turnCount = 0; // 反思 cadence 重新计数
|
|
@@ -565,37 +706,70 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
|
|
|
565
706
|
continue;
|
|
566
707
|
}
|
|
567
708
|
if (loaded.history[0]?.role === 'system') {
|
|
568
|
-
loaded.history[0] = { role: 'system', content:
|
|
709
|
+
loaded.history[0] = { role: 'system', content: buildSystemMessage(false) };
|
|
569
710
|
}
|
|
570
711
|
history.length = 0;
|
|
571
712
|
history.push(...loaded.history);
|
|
713
|
+
setAgentMode('auto'); // /resume 重置为 auto(mode 不落盘;listener 重写 history[0] 回 auto,与 loaded 幂等)
|
|
572
714
|
currentSessionId = loaded.id;
|
|
573
715
|
// 读回该会话的轮次/快照;无文件则从 history 重建 turns(无快照→旧轮次文件改动不可撤销)
|
|
574
716
|
if (!loadSnapshots(loaded.id))
|
|
575
717
|
rebuildFromHistory(history);
|
|
576
718
|
contextState.lastUsage = undefined;
|
|
577
|
-
collapsedThinkings.length = 0;
|
|
578
719
|
layout.clearContent();
|
|
579
720
|
renderHistory(history);
|
|
580
721
|
layout.contentWrite(`${ui.dim}(已续接会话 ${loaded.id})${ui.reset}\n`);
|
|
581
722
|
continue;
|
|
582
723
|
}
|
|
583
|
-
if (line === '/
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
724
|
+
if (line === '/theme' || line.startsWith('/theme ')) {
|
|
725
|
+
// /theme:无参开菜单(↑↓ 选,Enter 切换,Esc 取消);/theme <name> 直切;/theme list 或未知名 → 列出。
|
|
726
|
+
const arg = line.startsWith('/theme ') ? line.slice('/theme '.length).trim() : '';
|
|
727
|
+
let name;
|
|
728
|
+
if (arg === '') {
|
|
729
|
+
// 无参:菜单选(仿 /resume /rollback 的 picker)
|
|
730
|
+
const items = listThemes().map((t) => ({
|
|
731
|
+
id: t,
|
|
732
|
+
title: t,
|
|
733
|
+
subtitle: THEME_DESCRIPTIONS[t] ?? '',
|
|
734
|
+
}));
|
|
735
|
+
let pick;
|
|
736
|
+
try {
|
|
737
|
+
pick = await promptThemePicker(items);
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
continue; // Ctrl+C(SIGINT)→ 取消
|
|
741
|
+
}
|
|
742
|
+
name = pick?.id ?? null;
|
|
588
743
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
744
|
+
else if (arg === 'list' || !themeExists(arg)) {
|
|
745
|
+
layout.contentWrite(`${ui.dim}可用主题:${ui.reset}\n`);
|
|
746
|
+
for (const t of listThemes()) {
|
|
747
|
+
layout.contentWrite(` ${ui.cyan}${t}${ui.reset} ${ui.dim}${THEME_DESCRIPTIONS[t] ?? ''}${ui.reset}\n`);
|
|
748
|
+
}
|
|
749
|
+
layout.contentWrite(`${ui.dim}(当前:${getTheme()};用 /theme <名称> 切换)${ui.reset}\n`);
|
|
592
750
|
continue;
|
|
593
751
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (
|
|
598
|
-
|
|
752
|
+
else {
|
|
753
|
+
name = arg;
|
|
754
|
+
}
|
|
755
|
+
if (name === null)
|
|
756
|
+
continue; // Esc / Ctrl+D 取消
|
|
757
|
+
// 切:setTheme → 重算状态行(新色)→ 清内容重绘(历史 / 横幅,镜像启动 + /resume)→ 确认 → 持久化。
|
|
758
|
+
// markdown MEMO 按 themeVersion 自动失效,故 renderHistory 取新色;状态栏 / 输入框由 continue 回 INPUT 态时读 getter 刷。
|
|
759
|
+
setTheme(name);
|
|
760
|
+
refreshStatusBase(history);
|
|
761
|
+
layout.clearContent();
|
|
762
|
+
if (history.some((m) => m.role === 'user')) {
|
|
763
|
+
renderHistory(history);
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
layout.contentWrite(bannerString(banner()));
|
|
767
|
+
}
|
|
768
|
+
layout.contentWrite(`${ui.dim}(已切换主题 ${name})${ui.reset}\n`);
|
|
769
|
+
updateConfigKey('MOCODE_THEME', name);
|
|
770
|
+
if (config.themeFromShell) {
|
|
771
|
+
layout.contentWrite(`${ui.dim}(shell 环境变量 MOCODE_THEME 已设,文件写入下次启动被其覆盖;取消该 shell 设置后生效)${ui.reset}\n`);
|
|
772
|
+
}
|
|
599
773
|
continue;
|
|
600
774
|
}
|
|
601
775
|
if (line === '/rollback' || line.startsWith('/rollback ')) {
|
|
@@ -604,38 +778,24 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
|
|
|
604
778
|
await rollbackFlow();
|
|
605
779
|
continue;
|
|
606
780
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
781
|
+
const initialPlan = getAgentMode() === 'plan'; // 轮首模式(在 runTurn 之前读)
|
|
782
|
+
const ok = await runTurn(joined, initialPlan, placeholder);
|
|
783
|
+
// plan 轮正常结束(未中断 / 未抛错)→ 看轮末模式决定:
|
|
784
|
+
// - 仍 plan:LLM 没自切(只产计划就 STOP)→ 弹审批面板(原行为)。
|
|
785
|
+
// - 已 auto:LLM 调了 switch_mode('auto') 在同轮自主执行了 → 跳过审批,不重复打扰。
|
|
786
|
+
if (initialPlan && ok && getAgentMode() === 'plan') {
|
|
787
|
+
const res = await promptIntervention({
|
|
788
|
+
type: 'choice',
|
|
789
|
+
title: '计划已就绪',
|
|
790
|
+
detail: '切换到 auto 模式按上述计划执行?(plan 模式只读探查,执行需切 auto)',
|
|
791
|
+
options: ['切 auto 执行', '留 plan 细化'],
|
|
614
792
|
});
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
saveSession(history, currentSessionId);
|
|
620
|
-
}
|
|
621
|
-
catch {
|
|
622
|
-
// 落盘失败不阻断 REPL
|
|
793
|
+
if (res.action === 'selected' && res.value === '切 auto 执行') {
|
|
794
|
+
// setAgentMode('auto') 由 runTurn 入口做(listener 重写 history[0] 回 auto);这里只切运行态 + 合成执行轮。
|
|
795
|
+
layout.enterRunningMode('执行', '按计划执行…');
|
|
796
|
+
await runTurn('请按上述计划执行。', false, '按计划执行…');
|
|
623
797
|
}
|
|
624
|
-
persistSnapshots(currentSessionId); // 随会话落盘回滚快照(/resume 后仍可撤销)
|
|
625
|
-
// 后台反思:每 reflectEveryN 轮 fire-and-forget 一次(与下一轮 agent 并发,不阻塞)。
|
|
626
|
-
// 已有在飞任务 / autoReflect 关 → kickoff 内部自守卫。快照同步取(避免下一轮 mutate history 竞态)。
|
|
627
|
-
turnCount++;
|
|
628
|
-
if (turnCount % config.reflectEveryN === 0) {
|
|
629
|
-
kickoffReflection(snapshotTranscript(history, 20));
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
catch (e) {
|
|
633
|
-
layout.contentWrite(`${ui.red}[错误]${ui.reset} ${e instanceof Error ? e.message : String(e)}\n`);
|
|
634
|
-
}
|
|
635
|
-
finally {
|
|
636
|
-
stopRunningListener();
|
|
637
798
|
}
|
|
638
|
-
layout.contentWrite('\n'); // 轮次之间空行
|
|
639
799
|
}
|
|
640
800
|
// 退出前等在飞反思收尾(Ctrl+C 走 SIGINT 直退不等;fire-and-forget 不承诺中断时完成)。
|
|
641
801
|
await drainMemoryBackground();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// run_command 的 best-effort 命令层原语。**非安全边界**——只挡误操作与低水平 prompt 注入,
|
|
2
|
+
// 真隔离需 OS jailer(bwrap/firejail/sandbox-exec/docker,本次不做)。cwd 钉死由 run_command
|
|
3
|
+
// 自己用 getSandboxRoot() 做;本模块只提供 env 脱敏与灾难命令 denylist。
|
|
4
|
+
/** env 脱敏 denylist:删敏感键(*KEY / *TOKEN / *SECRET / *CREDENTIAL / *PASSWORD / LLM_*)。
|
|
5
|
+
* 防 LLM_API_KEY / ANYSEARCH_API_KEY 泄给子进程。用 denylist——白名单会误杀 npm/git 需要的
|
|
6
|
+
* PATH/HOME/USERPROFILE 等 vars,不稳。 */
|
|
7
|
+
const ENV_DENY = /^(.*_?KEY|.*TOKEN|.*SECRET|.*CREDENTIAL|.*PASSWORD|LLM_.*)$/i;
|
|
8
|
+
/** 返回 env 浅拷贝,删敏感键。 */
|
|
9
|
+
export function filterEnv(env) {
|
|
10
|
+
const out = {};
|
|
11
|
+
for (const [k, v] of Object.entries(env)) {
|
|
12
|
+
if (ENV_DENY.test(k))
|
|
13
|
+
continue;
|
|
14
|
+
out[k] = v;
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 灾难性文件操作 denylist:仅挡最明显的 fs 破坏(rm -rf /、rm -rf ~、rm -rf /*、fork bomb、
|
|
20
|
+
* mkfs、dd of=/dev/、chmod -R 777 /)。**不挡 curl/wget/sudo**——用户目标是 fs 隔离,不是网络/提权;
|
|
21
|
+
* curl 写到 cwd 不算逃逸。大小写无关、粗匹配。刻意者可绕($IFS / base64 / env var 拼接),
|
|
22
|
+
* 故文档明确:非安全边界。返 string = 拒绝原因;null = 放行。
|
|
23
|
+
*/
|
|
24
|
+
const CMD_DENY = [
|
|
25
|
+
// rm -rf / (flags 含 r 与 f,任序;目标 = 根、根*、家目录)
|
|
26
|
+
/\brm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(\s|$|\*)/i,
|
|
27
|
+
/\brm\s+-[a-z]*f[a-z]*r[a-z]*\s+\/(\s|$|\*)/i,
|
|
28
|
+
/\brm\s+-[a-z]*r[a-z]*f[a-z]*\s+~(\s|$)/i,
|
|
29
|
+
/\brm\s+-[a-z]*f[a-z]*r[a-z]*\s+~(\s|$)/i,
|
|
30
|
+
/:\s*\(\)\s*\{/i, // :(){ fork bomb
|
|
31
|
+
/\bmkfs\b/i,
|
|
32
|
+
/\bdd\b.*of=\/dev\//i,
|
|
33
|
+
/\bchmod\s+-R\s+777\s+\/(\s|$)/i,
|
|
34
|
+
];
|
|
35
|
+
export function isCommandDenied(cmd) {
|
|
36
|
+
for (const re of CMD_DENY) {
|
|
37
|
+
if (re.test(cmd))
|
|
38
|
+
return `命令被沙箱拒绝(灾难性文件操作): ${cmd}`;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// 沙箱子系统 barrel。叶子:仅 node:path / node:fs,不反向依赖业务。
|
|
2
|
+
export { getSandboxRoot, setSandboxRoot } from './root.js';
|
|
3
|
+
export { jailResolve, jailGlobPattern, isInsideRoot } from './jail.js';
|
|
4
|
+
export { filterEnv, isCommandDenied } from './command.js';
|
|
5
|
+
export { SANDBOX_EXEMPT_TOOLS, SANDBOX_PATH_TOOLS, enforceSandbox, } from './policy.js';
|