mocode-ai 0.6.5 → 0.6.7

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.
@@ -1,76 +1,202 @@
1
- /**
2
- * Code Encoder(read_file):折叠连续空行(≥3 → 1,保留首空行行号前缀)。
3
- *
4
- * 输入:read_file 返回的 ` N\t<content>` 行(6 宽右对齐行号 + tab + 内容),可能带尾部
5
- * `... (N 行未显示,共 M 行)`。
6
- * 输出:连续 ≥3 个空行(行号前缀 + 空 content)折叠为 1 个(保留首行前缀,丢后续空行);其余逐字保留。
7
- *
8
- * 不变量(离线脚本断言):
9
- * - 所有非空 content 行逐字保留(前缀 + 内容不变)→ edit_file 的 old_string 按内容匹配仍可用。
10
- * - content 行的行号前缀不变 → LLM 看到的行号与文件一致。
11
- * - 仅空行(前缀 + tab + 空 content)被折叠,且仅 ≥3 连续时;≤2 空行原样(常见,不动)。
12
- * - 尾部标记 `... (...)` 不匹配行号前缀,原样保留。
13
- *
14
- * ⚠️ 残余风险(可接受、可恢复):若 LLM 编辑一个含 ≥3 连续空行的区域,它看到的空行数比文件实际少,
15
- * old_string 的空行数可能不匹配 → edit_file 返"未找到" → LLM 重读重试(系统提示已要求编辑前 read_file)。
16
- * 真实代码极少 ≥3 连续空行(linter 通常强制 ≤2),故实际几乎不触发。出问题可设
17
- * MOCODE_CONTEXT_OPTIMIZE=false 全局回退,或调高阈值。
18
- *
19
- * 不做长度裁剪(cap 兜底);不动行号前缀;不删 content 行;不改内容(含尾随空白)。
20
- * 空行判定用前缀感知(前缀 + tab + 空 content),不能用 trim——read_file 空行 ` 2\t` trim 后剩 `2`。
21
- */
1
+ /** read_file line: right-aligned source line number + tab + exact content. */
22
2
  const LINE_RE = /^(\s*\d+)\t(.*)$/;
23
- function isBlankCodeLine(l) {
24
- const m = LINE_RE.exec(l);
25
- return m ? m[2] === '' : false;
3
+ const JS_LIKE_PATH_RE = /\.(?:[cm]?js|jsx|ts|tsx)$/i;
4
+ function parseLine(raw) {
5
+ const match = LINE_RE.exec(raw);
6
+ if (!match)
7
+ return null;
8
+ return { raw, line: Number(match[1].trim()), content: match[2] };
26
9
  }
27
- export const codeEncoder = {
28
- kind: 'code',
29
- encode({ output }) {
30
- const lines = output.split('\n');
31
- const out = [];
32
- let i = 0;
33
- let collapsedRuns = 0;
34
- while (i < lines.length) {
35
- if (isBlankCodeLine(lines[i])) {
36
- let j = i;
37
- while (j < lines.length && isBlankCodeLine(lines[j]))
38
- j++;
39
- const run = j - i;
40
- if (run >= 3) {
41
- out.push(lines[i]); // 保留首空行(含其行号前缀)
42
- collapsedRuns++;
43
- }
44
- else {
45
- for (let k = 0; k < run; k++)
46
- out.push(lines[i]);
47
- }
48
- i = j;
10
+ function collapseBlankCodeRuns(output) {
11
+ const lines = output.split('\n');
12
+ const out = [];
13
+ let collapsed = 0;
14
+ let i = 0;
15
+ while (i < lines.length) {
16
+ if (parseLine(lines[i])?.content === '') {
17
+ let j = i;
18
+ while (j < lines.length && parseLine(lines[j])?.content === '')
19
+ j++;
20
+ if (j - i >= 3) {
21
+ out.push(lines[i]);
22
+ collapsed++;
49
23
  }
50
24
  else {
51
- out.push(lines[i]);
25
+ for (let k = i; k < j; k++)
26
+ out.push(lines[k]);
27
+ }
28
+ i = j;
29
+ continue;
30
+ }
31
+ out.push(lines[i++]);
32
+ }
33
+ return { text: out.join('\n'), collapsed };
34
+ }
35
+ function isJsLikePath(args) {
36
+ return typeof args?.path === 'string' && JS_LIKE_PATH_RE.test(args.path);
37
+ }
38
+ function isSingleLineImport(content) {
39
+ const line = content.trim();
40
+ return /^import\b/.test(line) && /(?:['"][^'"]+['"]\s*;?|;)$/.test(line);
41
+ }
42
+ function collapseImportBlocks(text) {
43
+ const lines = text.split('\n');
44
+ const out = [];
45
+ let collapsed = 0;
46
+ let i = 0;
47
+ while (i < lines.length) {
48
+ const first = parseLine(lines[i]);
49
+ if (!first || !isSingleLineImport(first.content)) {
50
+ out.push(lines[i++]);
51
+ continue;
52
+ }
53
+ let j = i + 1;
54
+ while (j < lines.length) {
55
+ const next = parseLine(lines[j]);
56
+ if (!next || !isSingleLineImport(next.content))
57
+ break;
58
+ j++;
59
+ }
60
+ const run = j - i;
61
+ if (run >= 4) {
62
+ const last = parseLine(lines[j - 1]);
63
+ out.push(lines[i]);
64
+ out.push(`… ${run - 2} import lines folded (source lines ${first.line + 1}–${last.line - 1}; cold read)`);
65
+ out.push(lines[j - 1]);
66
+ collapsed++;
67
+ }
68
+ else {
69
+ for (let k = i; k < j; k++)
70
+ out.push(lines[k]);
71
+ }
72
+ i = j;
73
+ }
74
+ const result = out.join('\n');
75
+ return result.length < text.length
76
+ ? { text: result, collapsed }
77
+ : { text, collapsed: 0 };
78
+ }
79
+ function braceDelta(line, state) {
80
+ let delta = 0;
81
+ for (let i = 0; i < line.length; i++) {
82
+ const ch = line[i];
83
+ const next = line[i + 1];
84
+ if (state.blockComment) {
85
+ if (ch === '*' && next === '/') {
86
+ state.blockComment = false;
52
87
  i++;
53
88
  }
89
+ continue;
90
+ }
91
+ if (state.quote) {
92
+ if (state.escaped) {
93
+ state.escaped = false;
94
+ }
95
+ else if (ch === '\\') {
96
+ state.escaped = true;
97
+ }
98
+ else if (ch === state.quote) {
99
+ state.quote = null;
100
+ }
101
+ continue;
102
+ }
103
+ if (ch === '/' && next === '/')
104
+ break;
105
+ if (ch === '/' && next === '*') {
106
+ state.blockComment = true;
107
+ i++;
108
+ continue;
109
+ }
110
+ if (ch === "'" || ch === '"' || ch === '`') {
111
+ state.quote = ch;
112
+ state.escaped = false;
113
+ continue;
114
+ }
115
+ if (ch === '{')
116
+ delta++;
117
+ else if (ch === '}')
118
+ delta--;
119
+ }
120
+ state.escaped = false;
121
+ return delta;
122
+ }
123
+ function isFunctionStart(content) {
124
+ return (/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function(?:\s+[\w$]+)?\s*\(/.test(content) ||
125
+ /^\s*(?:export\s+)?(?:const|let|var)\s+[\w$]+\s*=.*=>\s*\{\s*$/.test(content) ||
126
+ /^\s*(?:(?:public|private|protected|static|abstract|override|async|get|set)\s+)*(?:constructor|[\w$]+)\s*\([^;]*\)\s*(?::[^={]+)?\s*\{\s*$/.test(content));
127
+ }
128
+ function collapseFunctionBodies(text) {
129
+ const lines = text.split('\n');
130
+ const out = [];
131
+ let collapsed = 0;
132
+ let i = 0;
133
+ while (i < lines.length) {
134
+ const start = parseLine(lines[i]);
135
+ if (!start || !isFunctionStart(start.content)) {
136
+ out.push(lines[i++]);
137
+ continue;
54
138
  }
55
- if (collapsedRuns === 0) {
56
- return {
57
- text: output,
58
- meta: {
59
- kind: 'code',
60
- originalLen: output.length,
61
- encodedLen: output.length,
62
- note: 'no ≥3 blank runs passthrough',
63
- },
64
- };
139
+ const state = { quote: null, escaped: false, blockComment: false };
140
+ let depth = braceDelta(start.content, state);
141
+ if (depth <= 0) {
142
+ out.push(lines[i++]);
143
+ continue;
144
+ }
145
+ let j = i + 1;
146
+ for (; j < lines.length && depth > 0; j++) {
147
+ const current = parseLine(lines[j]);
148
+ if (!current)
149
+ break;
150
+ depth += braceDelta(current.content, state);
151
+ }
152
+ const endIndex = j - 1;
153
+ const end = parseLine(lines[endIndex] ?? '');
154
+ const bodyLines = endIndex - i - 1;
155
+ if (depth === 0 && end && bodyLines >= 8) {
156
+ out.push(lines[i]);
157
+ out.push(`… ${bodyLines} function-body lines folded (source lines ${start.line + 1}–${end.line - 1}; cold read)`);
158
+ out.push(lines[endIndex]);
159
+ collapsed++;
160
+ i = j;
161
+ }
162
+ else {
163
+ out.push(lines[i++]);
164
+ }
165
+ }
166
+ const result = out.join('\n');
167
+ return result.length < text.length
168
+ ? { text: result, collapsed }
169
+ : { text, collapsed: 0 };
170
+ }
171
+ export const codeEncoder = {
172
+ kind: 'code',
173
+ encode(input) {
174
+ const blankResult = collapseBlankCodeRuns(input.output);
175
+ let text = blankResult.text;
176
+ const notes = [];
177
+ if (blankResult.collapsed)
178
+ notes.push(`collapsed ${blankResult.collapsed} blank runs`);
179
+ if (input.phase === 'sweep' &&
180
+ input.isCold === true &&
181
+ (input.age ?? 0) >= 2 &&
182
+ input.isFirstRead === false &&
183
+ isJsLikePath(input.args)) {
184
+ const imports = collapseImportBlocks(text);
185
+ text = imports.text;
186
+ if (imports.collapsed)
187
+ notes.push(`folded ${imports.collapsed} import blocks`);
188
+ const functions = collapseFunctionBodies(text);
189
+ text = functions.text;
190
+ if (functions.collapsed)
191
+ notes.push(`folded ${functions.collapsed} function bodies`);
65
192
  }
66
- const text = out.join('\n');
67
193
  return {
68
194
  text,
69
195
  meta: {
70
196
  kind: 'code',
71
- originalLen: output.length,
197
+ originalLen: input.output.length,
72
198
  encodedLen: text.length,
73
- note: `collapsed ${collapsedRuns} blank runs (≥3 → 1)`,
199
+ note: notes.join(', ') || 'no change',
74
200
  },
75
201
  };
76
202
  },
@@ -0,0 +1,201 @@
1
+ import { stripAnsi } from './_util.js';
2
+ const PAREN_DIAGNOSTIC = /^(.*)\((\d+),(\d+)\):\s*(error|warning|warn|fatal|note)\b\s*:?\s*(.*)$/i;
3
+ const COL_DIAGNOSTIC = /^(.*):(\d+):(\d+):\s*(error|warning|warn|fatal|note)\b\s*:?\s*(.*)$/i;
4
+ const LINE_DIAGNOSTIC = /^(.*):(\d+):\s*(error|warning|warn|fatal|note)\b\s*:?\s*(.*)$/i;
5
+ const PASS_LINE = /^\s*(?:PASS\b|PASSED\b|✓|✔|✅|ok\b)/i;
6
+ const FAIL_LINE = /^\s*(?:FAIL\b|FAILED\b|ERROR\b|✗|×|❌|not ok\b|●)/i;
7
+ const TEST_SUMMARY = /^\s*(?:Test Suites?|Tests?|Ran\s+\d+|=+\s|\d+\s+(?:passed|failed|errors?))/i;
8
+ const IMPORTANT_LINE = /\b(?:error|failed|failure|fatal|exception|panic)\b|not ok|[✗×❌]/i;
9
+ const TEST_COMMAND = /\b(?:test|vitest|jest|pytest|mocha|ava|tap|cargo\s+test|go\s+test|dotnet\s+test)\b/i;
10
+ function parseDiagnostic(line) {
11
+ const m = PAREN_DIAGNOSTIC.exec(line) ?? COL_DIAGNOSTIC.exec(line);
12
+ if (m) {
13
+ return { file: m[1], line: m[2], column: m[3], severity: m[4], message: m[5], continuation: [] };
14
+ }
15
+ const lineOnly = LINE_DIAGNOSTIC.exec(line);
16
+ if (!lineOnly)
17
+ return null;
18
+ return {
19
+ file: lineOnly[1],
20
+ line: lineOnly[2],
21
+ severity: lineOnly[3],
22
+ message: lineOnly[4],
23
+ continuation: [],
24
+ };
25
+ }
26
+ function splitStatus(text) {
27
+ const lines = text.split('\n');
28
+ const status = /^\[(?:退出码 [^\]]+|已中断|超时,已终止)\]$/.test(lines[0] ?? '')
29
+ ? lines.shift()
30
+ : null;
31
+ return { status, lines };
32
+ }
33
+ function formatDiagnostics(lines) {
34
+ const diagnostics = [];
35
+ const other = [];
36
+ let current = null;
37
+ for (const line of lines) {
38
+ const diagnostic = parseDiagnostic(line);
39
+ if (diagnostic) {
40
+ diagnostics.push(diagnostic);
41
+ current = diagnostic;
42
+ }
43
+ else if (current && (/^\s+/.test(line) || /^[\^~|]/.test(line))) {
44
+ current.continuation.push(line);
45
+ }
46
+ else {
47
+ current = null;
48
+ other.push(line);
49
+ }
50
+ }
51
+ if (diagnostics.length === 0)
52
+ return null;
53
+ const groups = new Map();
54
+ for (const diagnostic of diagnostics) {
55
+ const group = groups.get(diagnostic.file) ?? [];
56
+ group.push(diagnostic);
57
+ groups.set(diagnostic.file, group);
58
+ }
59
+ const errors = diagnostics.filter((d) => /^(?:error|fatal)$/i.test(d.severity)).length;
60
+ const warnings = diagnostics.filter((d) => /^(?:warning|warn)$/i.test(d.severity)).length;
61
+ const out = [`# Build diagnostics · ${diagnostics.length} issues · ${groups.size} files · command-encoded`];
62
+ if (errors || warnings)
63
+ out.push(`# ${errors} errors · ${warnings} warnings`);
64
+ for (const [file, items] of groups) {
65
+ out.push(`${file}:`);
66
+ for (const item of items) {
67
+ const location = item.column ? `${item.line}:${item.column}` : item.line;
68
+ out.push(` ${location}: ${item.severity}${item.message ? `: ${item.message}` : ''}`);
69
+ for (const continuation of item.continuation)
70
+ out.push(` ${continuation}`);
71
+ }
72
+ }
73
+ if (other.some((line) => line.length > 0))
74
+ out.push('# Other output', ...other);
75
+ return { text: out.join('\n'), count: diagnostics.length };
76
+ }
77
+ function formatTests(lines, command) {
78
+ const blocks = [];
79
+ const other = [];
80
+ let current = null;
81
+ let markers = 0;
82
+ for (const line of lines) {
83
+ const kind = FAIL_LINE.test(line) ? 'fail' : PASS_LINE.test(line) ? 'pass' : null;
84
+ if (kind) {
85
+ current = { kind, lines: [line] };
86
+ blocks.push(current);
87
+ markers++;
88
+ }
89
+ else if (current && line.length > 0 && /^\s+/.test(line) && !TEST_SUMMARY.test(line)) {
90
+ current.lines.push(line);
91
+ }
92
+ else {
93
+ current = null;
94
+ other.push(line);
95
+ }
96
+ }
97
+ if (markers === 0 || (markers < 2 && !TEST_COMMAND.test(command)))
98
+ return null;
99
+ const failed = blocks.filter((block) => block.kind === 'fail');
100
+ const passed = blocks.filter((block) => block.kind === 'pass');
101
+ const out = [`# Test results · ${passed.length} passed · ${failed.length} failed · command-encoded`];
102
+ if (failed.length) {
103
+ out.push(`# Failed tests (${failed.length})`);
104
+ for (const block of failed)
105
+ out.push(...block.lines);
106
+ }
107
+ if (passed.length) {
108
+ out.push(`# Passed tests (${passed.length})`);
109
+ for (const block of passed)
110
+ out.push(...block.lines);
111
+ }
112
+ if (other.some((line) => line.length > 0))
113
+ out.push('# Test summary / other output', ...other);
114
+ return { text: out.join('\n'), count: markers };
115
+ }
116
+ function collapseDuplicateLines(text) {
117
+ const lines = text.split('\n');
118
+ const out = [];
119
+ let runs = 0;
120
+ for (let i = 0; i < lines.length;) {
121
+ let end = i + 1;
122
+ while (end < lines.length && lines[end] === lines[i])
123
+ end++;
124
+ const count = end - i;
125
+ if (count >= 3) {
126
+ out.push(`${lines[i]} [×${count}]`);
127
+ runs++;
128
+ }
129
+ else {
130
+ for (let j = i; j < end; j++)
131
+ out.push(lines[j]);
132
+ }
133
+ i = end;
134
+ }
135
+ return { text: out.join('\n'), runs };
136
+ }
137
+ function errorTail(text, max) {
138
+ if (max <= 0)
139
+ return '';
140
+ const lines = text.split('\n');
141
+ let important = -1;
142
+ for (let i = lines.length - 1; i >= 0; i--) {
143
+ if (IMPORTANT_LINE.test(lines[i])) {
144
+ important = i;
145
+ break;
146
+ }
147
+ }
148
+ if (important < 0)
149
+ return text.slice(-max);
150
+ const fromError = lines.slice(Math.max(0, important - 1)).join('\n');
151
+ if (fromError.length <= max)
152
+ return fromError;
153
+ const joiner = '\n…[错误详情中段省略]…\n';
154
+ const errorHead = Math.max(0, Math.floor((max - joiner.length) * 0.65));
155
+ const finalTail = Math.max(0, max - joiner.length - errorHead);
156
+ return fromError.slice(0, errorHead) + joiner + fromError.slice(-finalTail);
157
+ }
158
+ function truncateCommand(text, budget) {
159
+ if (!budget || text.length <= budget)
160
+ return { text, truncated: false };
161
+ const marker = '\n…[command 输出已结构化截断;保留开头与错误尾部]…\n';
162
+ const available = budget - marker.length;
163
+ if (available <= 0)
164
+ return { text: marker.slice(0, budget), truncated: true };
165
+ const headSize = Math.floor(available * 0.45);
166
+ const tailSize = available - headSize;
167
+ return {
168
+ text: text.slice(0, headSize) + marker + errorTail(text, tailSize),
169
+ truncated: true,
170
+ };
171
+ }
172
+ /** run_command 专用 encoder:构建诊断分文件、测试结果分 pass/fail,并按错误尾部优先截断。 */
173
+ export const commandEncoder = {
174
+ kind: 'log',
175
+ encode({ output, args, budget }) {
176
+ const stripped = stripAnsi(output).replace(/\r\n?/g, '\n');
177
+ const { status, lines } = splitStatus(stripped);
178
+ const command = typeof args?.command === 'string' ? args.command : '';
179
+ const diagnostics = formatDiagnostics(lines);
180
+ const tests = diagnostics ? null : formatTests(lines, command);
181
+ const structured = diagnostics?.text ?? tests?.text ?? lines.join('\n');
182
+ const withStatus = status ? `${status}\n${structured}` : structured;
183
+ const collapsed = collapseDuplicateLines(withStatus);
184
+ const fitted = truncateCommand(collapsed.text, budget);
185
+ const mode = diagnostics ? `build:${diagnostics.count}` : tests ? `tests:${tests.count}` : 'generic';
186
+ const notes = [mode, 'ANSI stripped'];
187
+ if (collapsed.runs)
188
+ notes.push(`${collapsed.runs} dup runs collapsed`);
189
+ if (fitted.truncated)
190
+ notes.push('head+error-tail truncated');
191
+ return {
192
+ text: fitted.text,
193
+ meta: {
194
+ kind: 'log',
195
+ originalLen: output.length,
196
+ encodedLen: fitted.text.length,
197
+ note: notes.join(', '),
198
+ },
199
+ };
200
+ },
201
+ };
@@ -1,30 +1,80 @@
1
1
  import { stripAnsi, collapseBlankRuns } from './_util.js';
2
- /**
3
- * Graph Encoder(codegraph):去 ANSI + 折叠连续空行(≥3 1)
4
- *
5
- * 输入:codegraph CLI 返回的 `[退出码 N]\n<源码 + 调用路径>`(可能含 ANSI 颜色、多余空行)。
6
- * 输出:去 ANSI + 空行折叠;结构(调用路径块、源码段)不动。
7
- *
8
- * 不变量:退出码行保留;源码与调用路径文本逐字保留(仅去颜色码 + 折叠空行);不删内容行。
9
- * 保守:不重构调用路径 / 不去重源码(codegraph CLI 输出格式未稳定,需先采样真实输出定不变量,留后续)
10
- */
2
+ function isAgedCold(input) {
3
+ return input.phase === 'sweep' && input.isCold === true && (input.age ?? 0) >= 2;
4
+ }
5
+ function normalizeBlock(block) {
6
+ return block
7
+ .replace(/\r\n/g, '\n')
8
+ .split('\n')
9
+ .map((line) => line.trimEnd())
10
+ .join('\n')
11
+ .trim();
12
+ }
13
+ function dedupeFencedBlocks(text) {
14
+ const seen = new Set();
15
+ let removed = 0;
16
+ const result = text.replace(/```[^\n]*\n[\s\S]*?\n```/g, (block) => {
17
+ const key = normalizeBlock(block);
18
+ if (key.length < 120 || !seen.has(key)) {
19
+ seen.add(key);
20
+ return block;
21
+ }
22
+ removed++;
23
+ return '… duplicate source block omitted (cold graph)';
24
+ });
25
+ return { text: result, removed };
26
+ }
27
+ function looksLikeSourceBlock(block) {
28
+ const lines = block.split('\n');
29
+ const sourceLines = lines.filter((line) => /^\s*\d+\t/.test(line) ||
30
+ /^\s*(?:L)?\d+[:|]\s/.test(line) ||
31
+ /^.*:\d+(?::\d+)?:\s/.test(line)).length;
32
+ return block.length >= 120 && sourceLines >= 3;
33
+ }
34
+ function dedupeParagraphSourceBlocks(text) {
35
+ const parts = text.split(/(\n{2,})/);
36
+ const seen = new Set();
37
+ let removed = 0;
38
+ for (let i = 0; i < parts.length; i += 2) {
39
+ const block = parts[i];
40
+ if (!looksLikeSourceBlock(block))
41
+ continue;
42
+ const key = normalizeBlock(block);
43
+ if (seen.has(key)) {
44
+ parts[i] = '… duplicate source block omitted (cold graph)';
45
+ removed++;
46
+ }
47
+ else {
48
+ seen.add(key);
49
+ }
50
+ }
51
+ return { text: parts.join(''), removed };
52
+ }
11
53
  export const graphEncoder = {
12
54
  kind: 'graph',
13
- encode({ output }) {
14
- const stripped = stripAnsi(output);
15
- const text = collapseBlankRuns(stripped, 3);
16
- const hadAnsi = /\x1b/.test(output);
17
- const hadBlanks = text !== stripped;
18
- const note = [hadAnsi ? 'ANSI stripped' : '', hadBlanks ? 'blank runs collapsed' : '']
19
- .filter(Boolean)
20
- .join(', ') || 'no change';
55
+ encode(input) {
56
+ const stripped = stripAnsi(input.output);
57
+ let text = stripped;
58
+ let duplicates = 0;
59
+ if (isAgedCold(input)) {
60
+ const fenced = dedupeFencedBlocks(text);
61
+ const paragraphs = dedupeParagraphSourceBlocks(fenced.text);
62
+ text = paragraphs.text;
63
+ duplicates = fenced.removed + paragraphs.removed;
64
+ }
65
+ text = collapseBlankRuns(text, 3);
66
+ const notes = [
67
+ /\x1b/.test(input.output) ? 'ANSI stripped' : '',
68
+ text !== stripped && duplicates === 0 ? 'blank runs collapsed' : '',
69
+ duplicates > 0 ? `${duplicates} duplicate source blocks omitted` : '',
70
+ ].filter(Boolean);
21
71
  return {
22
72
  text,
23
73
  meta: {
24
74
  kind: 'graph',
25
- originalLen: output.length,
75
+ originalLen: input.output.length,
26
76
  encodedLen: text.length,
27
- note,
77
+ note: notes.join(', ') || 'no change',
28
78
  },
29
79
  };
30
80
  },
@@ -11,7 +11,7 @@
11
11
  import { passthroughEncoder } from './passthrough.js';
12
12
  import { treeEncoder } from './tree.js';
13
13
  import { searchEncoder } from './search.js';
14
- import { logEncoder } from './log.js';
14
+ import { commandEncoder } from './command.js';
15
15
  import { tableEncoder } from './table.js';
16
16
  import { memoryEncoder } from './memory.js';
17
17
  import { codeEncoder } from './code.js';
@@ -22,7 +22,7 @@ export const builtinEncoders = [
22
22
  passthroughEncoder,
23
23
  treeEncoder,
24
24
  searchEncoder,
25
- logEncoder,
25
+ commandEncoder,
26
26
  tableEncoder,
27
27
  memoryEncoder,
28
28
  codeEncoder,
@@ -1,52 +1,2 @@
1
- /**
2
- * Log Encoder(run_command):去 ANSI 颜色码 + 折叠连续重复行(≥3 → 单行 + [×N])。
3
- *
4
- * 输入:run_command 返回的 `[退出码 N]\n<合并 stdout+stderr>`,可能含 ANSI(tsc --pretty / 测试框架)、
5
- * 重复行(构建 / 编译日志)、尾部 `...(输出已截断)`、`[已中断]` / `[超时,已终止]` 前缀。
6
- * 输出:去 ANSI CSI 序列 + 连续重复行折叠;行顺序不变(退出码头恒在首、错误行与尾部原位保留)。
7
- *
8
- * 不变量(离线脚本断言):退出码行 `[退出码 N]` / `[已中断]` / `[超时,已终止]` 保留;
9
- * 重复行以 `[×N]` 标注计数(语义不丢——LLM 仍知该行重复 N 次);ANSI 去除语义无损(颜色码不含信息)。
10
- * 不做长度裁剪(由 pipeline 末尾 capToolResultForHistory 兜底 head+标记+tail,与改造前一致)。
11
- * run ≤2 原样保留(常见输出不必标注,避免噪音);run ≥3 才折叠(真正的大规模重复才省)。
12
- */
13
- const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
14
- export const logEncoder = {
15
- kind: 'log',
16
- encode({ output }) {
17
- // 去 ANSI CSI 序列(颜色 / 光标 / 清屏等),保留所有可见文本。
18
- const stripped = output.replace(ANSI_RE, '');
19
- const lines = stripped.split('\n');
20
- // 折叠连续重复行:run ≥3 → 单行 + [×N];run ≤2 原样。
21
- const out = [];
22
- let i = 0;
23
- let collapsedRuns = 0;
24
- while (i < lines.length) {
25
- let j = i;
26
- while (j < lines.length && lines[j] === lines[i])
27
- j++;
28
- const run = j - i;
29
- if (run >= 3) {
30
- out.push(`${lines[i]} [×${run}]`);
31
- collapsedRuns++;
32
- }
33
- else {
34
- for (let k = 0; k < run; k++)
35
- out.push(lines[i]);
36
- }
37
- i = j;
38
- }
39
- const text = out.join('\n');
40
- return {
41
- text,
42
- meta: {
43
- kind: 'log',
44
- originalLen: output.length,
45
- encodedLen: text.length,
46
- note: collapsedRuns > 0
47
- ? `ANSI stripped, ${collapsedRuns} dup runs collapsed`
48
- : 'ANSI stripped',
49
- },
50
- };
51
- },
52
- };
1
+ /** @deprecated 使用 commandEncoder;保留别名避免破坏已有直接导入。 */
2
+ export { commandEncoder as logEncoder } from './command.js';