mocode-ai 1.1.10 → 1.2.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/dist/rollback/index.js +10 -3
- package/dist/skills/runner.js +43 -0
- package/dist/tools/builtins/grep.js +8 -0
- package/dist/tools/constants.js +3 -1
- package/dist/ui/batch.js +34 -6
- package/dist/ui/layout.js +3 -1
- package/package.json +1 -1
package/dist/rollback/index.js
CHANGED
|
@@ -163,11 +163,18 @@ export function endPathMutation(capture, op) {
|
|
|
163
163
|
if (changed)
|
|
164
164
|
mutationVersion += 1;
|
|
165
165
|
}
|
|
166
|
+
// 构建产物 / 临时 / 缓存目录:可再生运行时状态,扫描它们既昂贵(dist/ 含大量 .js bundle,
|
|
167
|
+
// 全量 readFileSync 会同步卡死事件循环,表现为 run_command 期间滚轮划不动、spinner 冻结),
|
|
168
|
+
// 也易把后台 daemon / 打包器的写入误判成模型改动。回滚本就只应覆盖源码,构建产物可再生。
|
|
169
|
+
const EXCLUDED_WORKSPACE_DIRS = new Set([
|
|
170
|
+
'.git', '.codegraph', 'node_modules',
|
|
171
|
+
'dist', 'build', 'out', 'coverage', '.tmp', 'tmp',
|
|
172
|
+
'.output', '.next', '.vite', '.turbo', '.svelte-kit',
|
|
173
|
+
]);
|
|
166
174
|
function isWorkspaceExcluded(full) {
|
|
167
175
|
const base = path.basename(full).toLowerCase();
|
|
168
|
-
// Git 元数据必须永久排除以保护 index
|
|
169
|
-
|
|
170
|
-
if (base === '.git' || base === '.codegraph' || base === 'node_modules')
|
|
176
|
+
// Git 元数据必须永久排除以保护 index;依赖树/代码索引/构建产物/临时目录是可再生运行时状态。
|
|
177
|
+
if (EXCLUDED_WORKSPACE_DIRS.has(base))
|
|
171
178
|
return true;
|
|
172
179
|
const sessionDir = path.resolve(config.sessionDir);
|
|
173
180
|
return isInside(sessionDir, full);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// 可执行 skill runner:把 skill 的「工作流」封装成隔离子 agent 执行。
|
|
2
|
+
// 对齐 Claude Code Agent Skills 的 context: fork 模型——skill 内容成为驱动子 agent
|
|
3
|
+
// 的 prompt(协议/操作规范),子 agent 用受控工具子集在隔离上下文里执行,结果摘要回灌。
|
|
4
|
+
//
|
|
5
|
+
// 依赖:agent/spawn.ts 的 spawnAgent(已具备隔离 history / 工具白名单 / maxSteps /
|
|
6
|
+
// read-write overlay / abort 透传 / usage 统计),这里只做「渲染 + 参数映射」,不重复造执行器。
|
|
7
|
+
import { spawnAgent } from '../agent/spawn.js';
|
|
8
|
+
/** 子 agent(Explore/Plan 等)类型 → 只读/写模式。缺省按 read 保守处理。 */
|
|
9
|
+
const AGENT_READ_MODE = new Set(['explore', 'plan', 'read', 'research']);
|
|
10
|
+
/**
|
|
11
|
+
* 把参数渲染进 skill 正文:替换 $ARGUMENTS / ${} / $1 / ${1} 等占位符。
|
|
12
|
+
* 仅替换存在的占位符,无占位符正文原样返回(兼容纯文本 skill)。
|
|
13
|
+
*/
|
|
14
|
+
export function renderBody(skill, args) {
|
|
15
|
+
const body = skill.body?.trim() || '';
|
|
16
|
+
if (!body)
|
|
17
|
+
return body;
|
|
18
|
+
const arg = args ?? {};
|
|
19
|
+
const named = JSON.stringify(arg, null, 2) || '{}';
|
|
20
|
+
const positional = Array.isArray(arg)
|
|
21
|
+
? arg.map((v) => String(v))
|
|
22
|
+
: (Object.values(arg).map((v) => String(v)));
|
|
23
|
+
const at = (i) => positional[i] ?? '';
|
|
24
|
+
return body
|
|
25
|
+
.replace(/\$ARGUMENTS\b/gi, named)
|
|
26
|
+
.replace(/\$\{?(\d+)\}?/g, (_m, idx) => at(Number(idx) - 1))
|
|
27
|
+
.replace(/\$\{ARGUMENTS\}/gi, named);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* fork 执行:把 skill 作为隔离子 agent 的协议,派生受控子 agent 执行其工作流。
|
|
31
|
+
* skill.allowed_tools → 工具白名单;skill.agent → 只读/写模式;args 序列化进用户 prompt。
|
|
32
|
+
*/
|
|
33
|
+
export async function runSkillForked(skill, args, ctx) {
|
|
34
|
+
const rendered = renderBody(skill, args);
|
|
35
|
+
const mode = skill.agent && AGENT_READ_MODE.has(skill.agent.trim().toLowerCase()) ? 'read' : 'write';
|
|
36
|
+
return spawnAgent({
|
|
37
|
+
prompt: `Execute the "${skill.name}" skill workflow now. Follow its protocol exactly, use the available tools, and when done return a concise summary of what you did, key findings, any files changed, and blockers.\n\n--- Skill protocol ---\n\n${rendered || '(skill body is empty; follow the skill contract described in the list above)'}`,
|
|
38
|
+
tools: skill.allowed_tools,
|
|
39
|
+
mode,
|
|
40
|
+
signal: ctx?.signal,
|
|
41
|
+
context: args ? `Skill arguments:\n${JSON.stringify(args, null, 2)}` : undefined,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import fg from 'fast-glob';
|
|
3
3
|
import { MAX_RESULTS, IGNORE } from '../constants.js';
|
|
4
|
+
// 二进制文件探测:头部 4KB 含 C0 控制字符(NUL/BEL 等)即视为二进制,跳过。
|
|
5
|
+
// 否则 grep 扫到 SQLite/压缩文件等会产出「单行数 KB + 控制字符」的匹配行,
|
|
6
|
+
// 这类行进 TUI 展开后被终端 auto-wrap,物理行与缓冲行失配导致整屏错乱。
|
|
7
|
+
const BINARY_PROBE_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
|
|
4
8
|
import { getSandboxRoot, isInsideRoot, jailResolve } from '../../sandbox/index.js';
|
|
5
9
|
// ---------- grep ----------
|
|
6
10
|
export const grepTool = {
|
|
@@ -55,6 +59,10 @@ export const grepTool = {
|
|
|
55
59
|
continue; // 跳过无法读的文件(二进制/权限/沙箱越界)
|
|
56
60
|
}
|
|
57
61
|
scanned++;
|
|
62
|
+
// 二进制文件(如 .codegraph/codegraph.db 这类 SQLite)跳过:其「行」是数 KB 的
|
|
63
|
+
// 序列化记录 + 控制字符,匹配行对 LLM 无意义,还会污染 TUI 展开渲染。
|
|
64
|
+
if (BINARY_PROBE_RE.test(content.slice(0, 4096)))
|
|
65
|
+
continue;
|
|
58
66
|
const lines = content.split(/\r?\n/);
|
|
59
67
|
const lineNos = [];
|
|
60
68
|
for (let i = 0; i < lines.length; i++) {
|
package/dist/tools/constants.js
CHANGED
|
@@ -22,7 +22,9 @@ export const DECAY_DAYS = 30;
|
|
|
22
22
|
export const GC_DAYS = 90;
|
|
23
23
|
/** memory_search 结果(召回的记忆正文)的放宽上限:指令性内容,中截破坏语义,对齐 use_skill。 */
|
|
24
24
|
export const MAX_MEMORY_RESULT = 64000;
|
|
25
|
-
|
|
25
|
+
// .codegraph:codegraph 索引目录(codegraph.db 是 SQLite 二进制 + daemon.log),
|
|
26
|
+
// grep/glob 扫它无意义且会产出数 KB 的超长「行」,污染 TUI 展开渲染。
|
|
27
|
+
export const IGNORE = ['**/node_modules/**', '**/.git/**', '**/.codegraph/**'];
|
|
26
28
|
// ── 前端工具簇(默认关闭,显式开启)─────────────────────────────────────────
|
|
27
29
|
/**
|
|
28
30
|
* 前端开发相关工具簇:browser / dev_server 依赖 playwright 二进制且拉起长驻进程,
|
package/dist/ui/batch.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { ui } from './theme.js';
|
|
15
15
|
import { t } from '../i18n/index.js';
|
|
16
|
+
import { truncateAnsi } from './render.js';
|
|
16
17
|
const batches = new Map();
|
|
17
18
|
/** 绝对行索引 → 所属 batch id(仅记录 summary 行;用于鼠标点击反查)。
|
|
18
19
|
* buffer 行数变化时本表可能漂移——但只在 insertAfter/deleteFrom 后由本模块同步更新,
|
|
@@ -24,6 +25,31 @@ const absLineToEntry = new Map();
|
|
|
24
25
|
const expandedBatches = new Set();
|
|
25
26
|
/** 展开时完整输出的最大行数;超出截断,避免巨型输出撑爆 viewport。 */
|
|
26
27
|
const MAX_EXPAND_LINES = 200;
|
|
28
|
+
/** 自洽行允许的最大显示宽(= 终端 cols)。buffer 行超 cols 会被终端 auto-wrap,
|
|
29
|
+
* 物理行与缓冲行失配 → repaintViewport 的 CUP 寻址全错(屏幕错乱)。 */
|
|
30
|
+
let maxCols = 200;
|
|
31
|
+
/** layout 在进 alt 屏 / SIGWINCH 时调,同步当前终端列宽供行宽钳制。 */
|
|
32
|
+
export function setMaxCols(n) {
|
|
33
|
+
if (Number.isFinite(n) && n >= 8)
|
|
34
|
+
maxCols = Math.floor(n);
|
|
35
|
+
}
|
|
36
|
+
/** 行内控制字符(NUL/BEL/TAB 等,常见于 grep 扫到二进制)替换为可见替代符。
|
|
37
|
+
* 必须保护 SGR 序列:行已带 ui.* 颜色码,裸 replace 会把 \x1B 一并替换、
|
|
38
|
+
* 毁掉转义序列(显示成字面 "[90m")。按 SGR 切分后只清洗文本段。 */
|
|
39
|
+
function visibleControl(s) {
|
|
40
|
+
return s
|
|
41
|
+
.split(/(\x1b\[[0-9;]*m)/)
|
|
42
|
+
.map((part, i) => (i % 2 === 1 ? part : part.replace(/[\x00-\x1f\x7f]/g, '·')))
|
|
43
|
+
.join('');
|
|
44
|
+
}
|
|
45
|
+
/** 自洽行统一收尾:行宽钳到 maxCols + 行末补 reset。
|
|
46
|
+
* 超宽行若直接入 rows[],repaintViewport 逐行 cup+clearLine 直出时终端会
|
|
47
|
+
* auto-wrap 成多条物理行,把后续所有行的屏位打乱(用户报告:展开含超长行的
|
|
48
|
+
* 工具输出后整屏错乱)。truncateAnsi 保留行内 SGR 且断尾补 reset,再统一 \x1B[0m 收尾。 */
|
|
49
|
+
function sanitizeRow(s) {
|
|
50
|
+
const cleaned = visibleControl(s);
|
|
51
|
+
return truncateAnsi(cleaned, maxCols) + '\x1B[0m';
|
|
52
|
+
}
|
|
27
53
|
export function isMutationToolName(name) {
|
|
28
54
|
return name === 'write_file' || name === 'edit_file';
|
|
29
55
|
}
|
|
@@ -129,23 +155,25 @@ function buildEntryDetailLines(e, indent = ' ') {
|
|
|
129
155
|
if (line === '' && lines.length > 0)
|
|
130
156
|
continue; // 跳过首尾空行(diff 头/尾换行)
|
|
131
157
|
const prefixed = `${ui.dim}${indent}${ui.reset}${line}`;
|
|
132
|
-
lines.push(
|
|
158
|
+
lines.push(sanitizeRow(prefixed));
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
161
|
else if (e.fullOutput) {
|
|
136
|
-
// 完整工具输出(纯文本):按行展开,每行缩进 + dim 样式;长输出截断到 MAX_EXPAND_LINES
|
|
162
|
+
// 完整工具输出(纯文本):按行展开,每行缩进 + dim 样式;长输出截断到 MAX_EXPAND_LINES 行。
|
|
163
|
+
// 每行经 sanitizeRow 钳宽:fullOutput 可能含 grep 扫二进制(db/压缩文件)得到的
|
|
164
|
+
// 超长行 + 控制字符,不钳会让终端 auto-wrap 打乱屏位。
|
|
137
165
|
const rawLines = e.fullOutput.split('\n');
|
|
138
166
|
const truncated = rawLines.length > MAX_EXPAND_LINES;
|
|
139
167
|
const displayLines = truncated ? rawLines.slice(0, MAX_EXPAND_LINES) : rawLines;
|
|
140
168
|
for (const line of displayLines) {
|
|
141
|
-
lines.push(`${indent}${ui.gray}${line}${ui.reset}
|
|
169
|
+
lines.push(sanitizeRow(`${indent}${ui.gray}${line}${ui.reset}`));
|
|
142
170
|
}
|
|
143
171
|
if (truncated) {
|
|
144
|
-
lines.push(`${indent}${ui.dim}… (${rawLines.length - MAX_EXPAND_LINES} more lines)${ui.reset}
|
|
172
|
+
lines.push(sanitizeRow(`${indent}${ui.dim}… (${rawLines.length - MAX_EXPAND_LINES} more lines)${ui.reset}`));
|
|
145
173
|
}
|
|
146
174
|
}
|
|
147
175
|
else if (e.resultSummary) {
|
|
148
|
-
lines.push(`${indent}${ui.gray}↳ ${e.resultSummary}${ui.reset}
|
|
176
|
+
lines.push(sanitizeRow(`${indent}${ui.gray}↳ ${e.resultSummary}${ui.reset}`));
|
|
149
177
|
}
|
|
150
178
|
return lines;
|
|
151
179
|
}
|
|
@@ -155,7 +183,7 @@ function buildExpandedLines(entries) {
|
|
|
155
183
|
const result = e.resultSummary ? ` ${ui.gray}↳ ${e.resultSummary}${ui.reset}` : '';
|
|
156
184
|
const branch = index === entries.length - 1 ? '└─' : '├─';
|
|
157
185
|
const failure = e.failed ? `${ui.red}×${ui.reset} ` : '';
|
|
158
|
-
return ` ${ui.dim}${branch}${ui.reset} ${failure}${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}${result}
|
|
186
|
+
return sanitizeRow(` ${ui.dim}${branch}${ui.reset} ${failure}${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}${result}`);
|
|
159
187
|
});
|
|
160
188
|
}
|
|
161
189
|
function entryDetailIndent(entries, index) {
|
package/dist/ui/layout.js
CHANGED
|
@@ -4,7 +4,7 @@ import { charWidth, displayWidth, truncateDisplay, truncateDisplayHead, truncate
|
|
|
4
4
|
import { ui, applyTerminalBackground, resetTerminalBackground } from './theme.js';
|
|
5
5
|
import * as content from './content.js';
|
|
6
6
|
import * as mouse from './mouse.js';
|
|
7
|
-
import { reset as resetBatches, shiftBatchesAfter } from './batch.js';
|
|
7
|
+
import { reset as resetBatches, shiftBatchesAfter, setMaxCols } from './batch.js';
|
|
8
8
|
import { copyToClipboard, readClipboard } from './clipboard.js';
|
|
9
9
|
import { renderMarkdown } from './markdown.js';
|
|
10
10
|
import { t } from '../i18n/index.js';
|
|
@@ -2064,6 +2064,7 @@ export function enterAltScreen() {
|
|
|
2064
2064
|
if (active || !ui.isTTY)
|
|
2065
2065
|
return;
|
|
2066
2066
|
active = true;
|
|
2067
|
+
setMaxCols(getGeo().cols); // 同步 batch 展开行宽钳制,防超宽行 auto-wrap 打乱屏位
|
|
2067
2068
|
stdout.write(esc.altOn);
|
|
2068
2069
|
applyTerminalBackground();
|
|
2069
2070
|
stdout.write(esc.mouseOn); // 完整鼠标追踪(按下/拖动/释放/滚轮)→ mouse.swallow 重组 → handleMouseEvent
|
|
@@ -2092,6 +2093,7 @@ export function enterAltScreen() {
|
|
|
2092
2093
|
// contentRow 停在旧值、区域未更新,spinner/contentWrite 画到旧行号(「思考中在消息堆里」根因)。
|
|
2093
2094
|
// 重画 repaintViewport 防抖(下面 timer),避免连续拖动闪烁;但行号/区域必须立即正确。
|
|
2094
2095
|
const g = getGeo(footerH);
|
|
2096
|
+
setMaxCols(g.cols); // 列宽变 → 展开行钳宽上限同步(后续新展开行生效)
|
|
2095
2097
|
const total = content.totalRows();
|
|
2096
2098
|
const committed = content.committedRows();
|
|
2097
2099
|
// 缩小:contentRow > 新 bottom → 钳到新 bottom
|