@dreb/coding-agent 2.3.0 → 2.4.1
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 +4 -5
- package/dist/core/keybindings.d.ts +3 -3
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +3 -3
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +6 -3
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/terminal-render.d.ts +37 -0
- package/dist/core/tools/terminal-render.d.ts.map +1 -0
- package/dist/core/tools/terminal-render.js +151 -0
- package/dist/core/tools/terminal-render.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/extensions.md +2 -2
- package/docs/keybindings.md +3 -3
- package/docs/sdk.md +1 -1
- package/docs/settings.md +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { TerminalTextRender } from "terminal-render";
|
|
2
|
+
/**
|
|
3
|
+
* Maximum row or column value allowed in ANSI cursor positioning sequences.
|
|
4
|
+
* Anything larger gets capped to this value to prevent memory exhaustion from
|
|
5
|
+
* malicious sequences like `ESC[9999999;1H`.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_CURSOR_POSITION = 5000;
|
|
8
|
+
/**
|
|
9
|
+
* Sanitize ANSI cursor positioning sequences to prevent memory exhaustion.
|
|
10
|
+
*
|
|
11
|
+
* Sequences like `ESC[9999999;1H` can cause TerminalTextRender to allocate
|
|
12
|
+
* millions of empty lines. This function caps row/column values in:
|
|
13
|
+
* - CUP (cursor position): ESC[<row>;<col>H or ESC[<row>;<col>f
|
|
14
|
+
* - CUU/CUD/CUF/CUB (cursor movement): ESC[<n>A/B/C/D
|
|
15
|
+
* - CNL (cursor next line): ESC[<n>E
|
|
16
|
+
* - VPA (vertical position absolute): ESC[<row>d
|
|
17
|
+
* - HPA (horizontal position absolute): ESC[<col>G or ESC[<col>`
|
|
18
|
+
*
|
|
19
|
+
* In addition to per-sequence caps, this tracks cumulative cursor position
|
|
20
|
+
* to prevent accumulation attacks where many sequences each at the per-sequence
|
|
21
|
+
* cap combine to push the cursor to millions of rows/columns, triggering OOM
|
|
22
|
+
* via array allocation in the terminal renderer.
|
|
23
|
+
*/
|
|
24
|
+
export function sanitizeCursorPositioning(input) {
|
|
25
|
+
// Track cumulative cursor position across sequences. An attacker can send
|
|
26
|
+
// thousands of ESC[5000B sequences, each passing the per-sequence cap but
|
|
27
|
+
// accumulating to ~55M rows. By tracking position, we clamp movement that
|
|
28
|
+
// would exceed the limit.
|
|
29
|
+
let cursorRow = 0;
|
|
30
|
+
let cursorCol = 0;
|
|
31
|
+
const parsePart = (p) => {
|
|
32
|
+
if (p === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
const n = Number.parseInt(p, 10);
|
|
35
|
+
return Number.isNaN(n) ? null : n;
|
|
36
|
+
};
|
|
37
|
+
// Cap all numeric params individually and rebuild the param string
|
|
38
|
+
const capParams = (rawParts) => rawParts
|
|
39
|
+
.map((p) => {
|
|
40
|
+
const n = Number.parseInt(p, 10);
|
|
41
|
+
if (Number.isNaN(n))
|
|
42
|
+
return p;
|
|
43
|
+
return String(Math.min(n, MAX_CURSOR_POSITION));
|
|
44
|
+
})
|
|
45
|
+
.join(";");
|
|
46
|
+
// Match CSI sequences: ESC[ followed by params and a final byte
|
|
47
|
+
// Covers H, f (CUP), A/B/C/D (movement), E (CNL), d (VPA), G/` (HPA), r (scroll region)
|
|
48
|
+
return input.replace(/\x1b\[([0-9;]*)([ABCDEGHfdr`])/g, (_match, params, cmd) => {
|
|
49
|
+
const rawParts = params.split(";");
|
|
50
|
+
switch (cmd) {
|
|
51
|
+
case "B": // Cursor down by n (default 1)
|
|
52
|
+
case "E": {
|
|
53
|
+
// Cursor next line by n (default 1)
|
|
54
|
+
const n = parsePart(rawParts[0]) ?? 1;
|
|
55
|
+
const capped = Math.min(n, MAX_CURSOR_POSITION);
|
|
56
|
+
const allowed = Math.max(0, MAX_CURSOR_POSITION - cursorRow);
|
|
57
|
+
const clamped = Math.min(capped, allowed);
|
|
58
|
+
cursorRow += clamped;
|
|
59
|
+
if (cmd === "E")
|
|
60
|
+
cursorCol = 0;
|
|
61
|
+
return `\x1b[${clamped}${cmd}`;
|
|
62
|
+
}
|
|
63
|
+
case "A": {
|
|
64
|
+
// Cursor up by n (default 1)
|
|
65
|
+
const n = parsePart(rawParts[0]) ?? 1;
|
|
66
|
+
const capped = Math.min(n, MAX_CURSOR_POSITION);
|
|
67
|
+
cursorRow = Math.max(0, cursorRow - capped);
|
|
68
|
+
return `\x1b[${capped}A`;
|
|
69
|
+
}
|
|
70
|
+
case "C": {
|
|
71
|
+
// Cursor forward by n (default 1)
|
|
72
|
+
const n = parsePart(rawParts[0]) ?? 1;
|
|
73
|
+
const capped = Math.min(n, MAX_CURSOR_POSITION);
|
|
74
|
+
const allowed = Math.max(0, MAX_CURSOR_POSITION - cursorCol);
|
|
75
|
+
const clamped = Math.min(capped, allowed);
|
|
76
|
+
cursorCol += clamped;
|
|
77
|
+
return `\x1b[${clamped}C`;
|
|
78
|
+
}
|
|
79
|
+
case "D": {
|
|
80
|
+
// Cursor back by n (default 1)
|
|
81
|
+
const n = parsePart(rawParts[0]) ?? 1;
|
|
82
|
+
const capped = Math.min(n, MAX_CURSOR_POSITION);
|
|
83
|
+
cursorCol = Math.max(0, cursorCol - capped);
|
|
84
|
+
return `\x1b[${capped}D`;
|
|
85
|
+
}
|
|
86
|
+
case "H":
|
|
87
|
+
case "f": {
|
|
88
|
+
// Absolute cursor position: ESC[row;colH
|
|
89
|
+
const row = parsePart(rawParts[0]);
|
|
90
|
+
const col = parsePart(rawParts[1]);
|
|
91
|
+
cursorRow = row != null ? Math.min(row, MAX_CURSOR_POSITION) : 1;
|
|
92
|
+
cursorCol = col != null ? Math.min(col, MAX_CURSOR_POSITION) : 1;
|
|
93
|
+
return `\x1b[${capParams(rawParts)}${cmd}`;
|
|
94
|
+
}
|
|
95
|
+
case "d": {
|
|
96
|
+
// VPA - vertical position absolute
|
|
97
|
+
const row = parsePart(rawParts[0]);
|
|
98
|
+
cursorRow = row != null ? Math.min(row, MAX_CURSOR_POSITION) : 1;
|
|
99
|
+
return `\x1b[${capParams(rawParts)}d`;
|
|
100
|
+
}
|
|
101
|
+
case "G":
|
|
102
|
+
case "`": {
|
|
103
|
+
// HPA - horizontal position absolute
|
|
104
|
+
const col = parsePart(rawParts[0]);
|
|
105
|
+
cursorCol = col != null ? Math.min(col, MAX_CURSOR_POSITION) : 1;
|
|
106
|
+
return `\x1b[${capParams(rawParts)}${cmd}`;
|
|
107
|
+
}
|
|
108
|
+
case "r": {
|
|
109
|
+
// Set scroll region - cap params, no position tracking
|
|
110
|
+
return `\x1b[${capParams(rawParts)}r`;
|
|
111
|
+
}
|
|
112
|
+
default: {
|
|
113
|
+
return `\x1b[${capParams(rawParts)}${cmd}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Process raw terminal output through a terminal renderer, producing the clean
|
|
120
|
+
* text a human would actually see on screen.
|
|
121
|
+
*
|
|
122
|
+
* This handles:
|
|
123
|
+
* - Carriage returns (`\r`) — progress bars overwrite the current line
|
|
124
|
+
* - ANSI cursor movement — up, down, forward, backward, absolute positioning
|
|
125
|
+
* - Backspace (`\b`) — moves cursor back one position
|
|
126
|
+
* - Line clearing / screen clearing escape sequences
|
|
127
|
+
* - Tab stops
|
|
128
|
+
*
|
|
129
|
+
* The result is the final rendered state of the terminal — identical to what
|
|
130
|
+
* a human would see in a real terminal after the output completes.
|
|
131
|
+
*
|
|
132
|
+
* Safety:
|
|
133
|
+
* - Cursor positioning values are capped to prevent memory exhaustion
|
|
134
|
+
* - Errors fall back to returning the raw input
|
|
135
|
+
*/
|
|
136
|
+
export function renderTerminalOutput(raw) {
|
|
137
|
+
if (!raw)
|
|
138
|
+
return raw;
|
|
139
|
+
try {
|
|
140
|
+
const sanitized = sanitizeCursorPositioning(raw);
|
|
141
|
+
const renderer = new TerminalTextRender();
|
|
142
|
+
renderer.write(sanitized);
|
|
143
|
+
return renderer.render();
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
147
|
+
console.error(`[dreb] terminal-render fallback: TerminalTextRender failed (${detail}), returning raw output`);
|
|
148
|
+
return raw;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=terminal-render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-render.js","sourceRoot":"","sources":["../../../src/core/tools/terminal-render.ts"],"names":[],"mappings":"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","sourcesContent":["import { TerminalTextRender } from \"terminal-render\";\n\n/**\n * Maximum row or column value allowed in ANSI cursor positioning sequences.\n * Anything larger gets capped to this value to prevent memory exhaustion from\n * malicious sequences like `ESC[9999999;1H`.\n */\nconst MAX_CURSOR_POSITION = 5000;\n\n/**\n * Sanitize ANSI cursor positioning sequences to prevent memory exhaustion.\n *\n * Sequences like `ESC[9999999;1H` can cause TerminalTextRender to allocate\n * millions of empty lines. This function caps row/column values in:\n * - CUP (cursor position): ESC[<row>;<col>H or ESC[<row>;<col>f\n * - CUU/CUD/CUF/CUB (cursor movement): ESC[<n>A/B/C/D\n * - CNL (cursor next line): ESC[<n>E\n * - VPA (vertical position absolute): ESC[<row>d\n * - HPA (horizontal position absolute): ESC[<col>G or ESC[<col>`\n *\n * In addition to per-sequence caps, this tracks cumulative cursor position\n * to prevent accumulation attacks where many sequences each at the per-sequence\n * cap combine to push the cursor to millions of rows/columns, triggering OOM\n * via array allocation in the terminal renderer.\n */\nexport function sanitizeCursorPositioning(input: string): string {\n\t// Track cumulative cursor position across sequences. An attacker can send\n\t// thousands of ESC[5000B sequences, each passing the per-sequence cap but\n\t// accumulating to ~55M rows. By tracking position, we clamp movement that\n\t// would exceed the limit.\n\tlet cursorRow = 0;\n\tlet cursorCol = 0;\n\n\tconst parsePart = (p: string | undefined): number | null => {\n\t\tif (p === undefined) return null;\n\t\tconst n = Number.parseInt(p, 10);\n\t\treturn Number.isNaN(n) ? null : n;\n\t};\n\n\t// Cap all numeric params individually and rebuild the param string\n\tconst capParams = (rawParts: string[]) =>\n\t\trawParts\n\t\t\t.map((p: string) => {\n\t\t\t\tconst n = Number.parseInt(p, 10);\n\t\t\t\tif (Number.isNaN(n)) return p;\n\t\t\t\treturn String(Math.min(n, MAX_CURSOR_POSITION));\n\t\t\t})\n\t\t\t.join(\";\");\n\n\t// Match CSI sequences: ESC[ followed by params and a final byte\n\t// Covers H, f (CUP), A/B/C/D (movement), E (CNL), d (VPA), G/` (HPA), r (scroll region)\n\treturn input.replace(/\\x1b\\[([0-9;]*)([ABCDEGHfdr`])/g, (_match, params: string, cmd: string) => {\n\t\tconst rawParts = params.split(\";\");\n\n\t\tswitch (cmd) {\n\t\t\tcase \"B\": // Cursor down by n (default 1)\n\t\t\tcase \"E\": {\n\t\t\t\t// Cursor next line by n (default 1)\n\t\t\t\tconst n = parsePart(rawParts[0]) ?? 1;\n\t\t\t\tconst capped = Math.min(n, MAX_CURSOR_POSITION);\n\t\t\t\tconst allowed = Math.max(0, MAX_CURSOR_POSITION - cursorRow);\n\t\t\t\tconst clamped = Math.min(capped, allowed);\n\t\t\t\tcursorRow += clamped;\n\t\t\t\tif (cmd === \"E\") cursorCol = 0;\n\t\t\t\treturn `\\x1b[${clamped}${cmd}`;\n\t\t\t}\n\t\t\tcase \"A\": {\n\t\t\t\t// Cursor up by n (default 1)\n\t\t\t\tconst n = parsePart(rawParts[0]) ?? 1;\n\t\t\t\tconst capped = Math.min(n, MAX_CURSOR_POSITION);\n\t\t\t\tcursorRow = Math.max(0, cursorRow - capped);\n\t\t\t\treturn `\\x1b[${capped}A`;\n\t\t\t}\n\t\t\tcase \"C\": {\n\t\t\t\t// Cursor forward by n (default 1)\n\t\t\t\tconst n = parsePart(rawParts[0]) ?? 1;\n\t\t\t\tconst capped = Math.min(n, MAX_CURSOR_POSITION);\n\t\t\t\tconst allowed = Math.max(0, MAX_CURSOR_POSITION - cursorCol);\n\t\t\t\tconst clamped = Math.min(capped, allowed);\n\t\t\t\tcursorCol += clamped;\n\t\t\t\treturn `\\x1b[${clamped}C`;\n\t\t\t}\n\t\t\tcase \"D\": {\n\t\t\t\t// Cursor back by n (default 1)\n\t\t\t\tconst n = parsePart(rawParts[0]) ?? 1;\n\t\t\t\tconst capped = Math.min(n, MAX_CURSOR_POSITION);\n\t\t\t\tcursorCol = Math.max(0, cursorCol - capped);\n\t\t\t\treturn `\\x1b[${capped}D`;\n\t\t\t}\n\t\t\tcase \"H\":\n\t\t\tcase \"f\": {\n\t\t\t\t// Absolute cursor position: ESC[row;colH\n\t\t\t\tconst row = parsePart(rawParts[0]);\n\t\t\t\tconst col = parsePart(rawParts[1]);\n\t\t\t\tcursorRow = row != null ? Math.min(row, MAX_CURSOR_POSITION) : 1;\n\t\t\t\tcursorCol = col != null ? Math.min(col, MAX_CURSOR_POSITION) : 1;\n\t\t\t\treturn `\\x1b[${capParams(rawParts)}${cmd}`;\n\t\t\t}\n\t\t\tcase \"d\": {\n\t\t\t\t// VPA - vertical position absolute\n\t\t\t\tconst row = parsePart(rawParts[0]);\n\t\t\t\tcursorRow = row != null ? Math.min(row, MAX_CURSOR_POSITION) : 1;\n\t\t\t\treturn `\\x1b[${capParams(rawParts)}d`;\n\t\t\t}\n\t\t\tcase \"G\":\n\t\t\tcase \"`\": {\n\t\t\t\t// HPA - horizontal position absolute\n\t\t\t\tconst col = parsePart(rawParts[0]);\n\t\t\t\tcursorCol = col != null ? Math.min(col, MAX_CURSOR_POSITION) : 1;\n\t\t\t\treturn `\\x1b[${capParams(rawParts)}${cmd}`;\n\t\t\t}\n\t\t\tcase \"r\": {\n\t\t\t\t// Set scroll region - cap params, no position tracking\n\t\t\t\treturn `\\x1b[${capParams(rawParts)}r`;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\treturn `\\x1b[${capParams(rawParts)}${cmd}`;\n\t\t\t}\n\t\t}\n\t});\n}\n\n/**\n * Process raw terminal output through a terminal renderer, producing the clean\n * text a human would actually see on screen.\n *\n * This handles:\n * - Carriage returns (`\\r`) — progress bars overwrite the current line\n * - ANSI cursor movement — up, down, forward, backward, absolute positioning\n * - Backspace (`\\b`) — moves cursor back one position\n * - Line clearing / screen clearing escape sequences\n * - Tab stops\n *\n * The result is the final rendered state of the terminal — identical to what\n * a human would see in a real terminal after the output completes.\n *\n * Safety:\n * - Cursor positioning values are capped to prevent memory exhaustion\n * - Errors fall back to returning the raw input\n */\nexport function renderTerminalOutput(raw: string): string {\n\tif (!raw) return raw;\n\ttry {\n\t\tconst sanitized = sanitizeCursorPositioning(raw);\n\t\tconst renderer = new TerminalTextRender();\n\t\trenderer.write(sanitized);\n\t\treturn renderer.render();\n\t} catch (err) {\n\t\tconst detail = err instanceof Error ? err.message : String(err);\n\t\tconsole.error(`[dreb] terminal-render fallback: TerminalTextRender failed (${detail}), returning raw output`);\n\t\treturn raw;\n\t}\n}\n"]}
|