aether-code 0.43.0 → 0.43.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/LICENSE +21 -21
- package/bin/aether-code.js +6 -4
- package/package.json +4 -2
- package/scripts/postinstall.js +182 -0
- package/skills/adult-creative-writing.md +60 -60
- package/skills/debugging.md +51 -51
- package/skills/game-modding.md +73 -73
- package/skills/reverse-engineering.md +41 -41
- package/skills/scraping-automation.md +77 -77
- package/skills/security-research.md +67 -67
- package/src/agent.js +168 -267
- package/src/api.js +22 -32
- package/src/box-input.js +141 -36
- package/src/config.js +38 -38
- package/src/diff.js +49 -49
- package/src/ink-input.js +91 -91
- package/src/mcp-cli.js +94 -94
- package/src/mcp-registry.js +266 -266
- package/src/mcp.js +259 -260
- package/src/menu.js +83 -83
- package/src/render.js +288 -254
- package/src/repl.js +408 -444
- package/src/setup.js +6 -8
- package/src/tools.js +16 -393
- package/src/update-check.js +64 -7
- package/src/project-context.js +0 -712
- package/src/sessions.js +0 -167
- package/src/version.js +0 -15
package/src/render.js
CHANGED
|
@@ -1,254 +1,288 @@
|
|
|
1
|
-
// ANSI helpers — no chalk dependency.
|
|
2
|
-
|
|
3
|
-
const isTty = process.stdout.isTTY;
|
|
4
|
-
const noColor = !!process.env.NO_COLOR || !isTty;
|
|
5
|
-
|
|
6
|
-
function wrap(open, close) {
|
|
7
|
-
return (s) => (noColor ? String(s) : `\x1b[${open}m${s}\x1b[${close}m`);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const c = {
|
|
11
|
-
bold: wrap(1, 22),
|
|
12
|
-
dim: wrap(2, 22),
|
|
13
|
-
red: wrap(31, 39),
|
|
14
|
-
green: wrap(32, 39),
|
|
15
|
-
yellow: wrap(33, 39),
|
|
16
|
-
blue: wrap(34, 39),
|
|
17
|
-
magenta: wrap(35, 39),
|
|
18
|
-
cyan: wrap(36, 39),
|
|
19
|
-
gray: wrap(90, 39),
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export function divider() {
|
|
23
|
-
const w = process.stdout.columns || 60;
|
|
24
|
-
return c.gray("─".repeat(Math.min(60, w)));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
case "
|
|
81
|
-
case "
|
|
82
|
-
case "
|
|
83
|
-
case "
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
1
|
+
// ANSI helpers — no chalk dependency.
|
|
2
|
+
|
|
3
|
+
const isTty = process.stdout.isTTY;
|
|
4
|
+
const noColor = !!process.env.NO_COLOR || !isTty;
|
|
5
|
+
|
|
6
|
+
function wrap(open, close) {
|
|
7
|
+
return (s) => (noColor ? String(s) : `\x1b[${open}m${s}\x1b[${close}m`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const c = {
|
|
11
|
+
bold: wrap(1, 22),
|
|
12
|
+
dim: wrap(2, 22),
|
|
13
|
+
red: wrap(31, 39),
|
|
14
|
+
green: wrap(32, 39),
|
|
15
|
+
yellow: wrap(33, 39),
|
|
16
|
+
blue: wrap(34, 39),
|
|
17
|
+
magenta: wrap(35, 39),
|
|
18
|
+
cyan: wrap(36, 39),
|
|
19
|
+
gray: wrap(90, 39),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function divider() {
|
|
23
|
+
const w = process.stdout.columns || 60;
|
|
24
|
+
return c.gray("─".repeat(Math.min(60, w)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
28
|
+
|
|
29
|
+
export function boxLines(lines, { color = c.gray, padX = 2, minInner = 0 } = {}) {
|
|
30
|
+
const inner = Math.max(minInner, 0, ...lines.map((l) => stripAnsi(l).length));
|
|
31
|
+
const w = inner + padX * 2;
|
|
32
|
+
const top = color("╭" + "─".repeat(w) + "╮");
|
|
33
|
+
const bot = color("╰" + "─".repeat(w) + "╯");
|
|
34
|
+
const mid = lines.map(
|
|
35
|
+
(l) => color("│") + " ".repeat(padX) + l + " ".repeat(inner - stripAnsi(l).length + padX) + color("│"),
|
|
36
|
+
);
|
|
37
|
+
return [top, ...mid, bot];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function box(lines, opts) {
|
|
41
|
+
return boxLines(lines, opts).join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function sideBySide(a, b, gap = 3) {
|
|
45
|
+
const wa = Math.max(0, ...a.map((l) => stripAnsi(l).length));
|
|
46
|
+
const h = Math.max(a.length, b.length);
|
|
47
|
+
const out = [];
|
|
48
|
+
for (let i = 0; i < h; i++) {
|
|
49
|
+
const la = a[i] ?? "";
|
|
50
|
+
const lb = b[i] ?? "";
|
|
51
|
+
out.push(la + " ".repeat(wa - stripAnsi(la).length + gap) + lb);
|
|
52
|
+
}
|
|
53
|
+
return out.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function turn(n) {
|
|
57
|
+
return c.gray(`turn ${n}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function toolHeader(name, args) {
|
|
61
|
+
const compact = JSON.stringify(args);
|
|
62
|
+
const trimmed = compact.length > 120 ? compact.slice(0, 117) + "..." : compact;
|
|
63
|
+
return `${c.cyan(c.bold(name))}${c.gray("(")}${c.gray(trimmed)}${c.gray(")")}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ellip = (s, n) => (s && s.length > n ? s.slice(0, n - 1) + "…" : s || "");
|
|
67
|
+
|
|
68
|
+
export function toolLabel(name, args) {
|
|
69
|
+
const a = args || {};
|
|
70
|
+
const verb = (v) => c.cyan(c.bold(v));
|
|
71
|
+
const arg = (s) => c.gray(ellip(s == null ? "" : String(s), 72));
|
|
72
|
+
switch (name) {
|
|
73
|
+
case "read_file": return `${verb("Read")} ${arg(a.path)}`;
|
|
74
|
+
case "write_file": return `${verb("Write")} ${arg(a.path)}`;
|
|
75
|
+
case "edit_file": return `${verb("Edit")} ${arg(a.path)}`;
|
|
76
|
+
case "list_dir": return `${verb("List")} ${arg(a.path)}`;
|
|
77
|
+
case "glob_files": return `${verb("Glob")} ${arg(a.pattern)}`;
|
|
78
|
+
case "search_files": return `${verb("Grep")} ${arg(`/${a.pattern}/ in ${a.path ?? "."}`)}`;
|
|
79
|
+
case "run_shell": return `${verb("Bash")} ${arg(a.command)}`;
|
|
80
|
+
case "web_search": return `${verb("WebSearch")} ${arg(a.query ?? "")}`;
|
|
81
|
+
case "web_fetch": return `${verb("WebFetch")} ${arg(a.url)}`;
|
|
82
|
+
case "todo_write": return "";
|
|
83
|
+
case "ask_user": return "";
|
|
84
|
+
default: return `${verb(name)} ${arg(JSON.stringify(a))}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function setTerminalTitle(title) {
|
|
89
|
+
if (process.stdout.isTTY) process.stdout.write(`\x1b]0;${title}\x07`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fun thinking words — like Claude Code's "Hullaballooing...", "Cogitating..."
|
|
93
|
+
const THINKING_WORDS = [
|
|
94
|
+
"Thinking",
|
|
95
|
+
"Pondering",
|
|
96
|
+
"Mulling",
|
|
97
|
+
"Cogitating",
|
|
98
|
+
"Ruminating",
|
|
99
|
+
"Deliberating",
|
|
100
|
+
"Contemplating",
|
|
101
|
+
"Noodling",
|
|
102
|
+
"Musing",
|
|
103
|
+
"Brainstorming",
|
|
104
|
+
"Percolating",
|
|
105
|
+
"Brainwaving",
|
|
106
|
+
"Daydreaming",
|
|
107
|
+
"Hullaballooing",
|
|
108
|
+
"Churning",
|
|
109
|
+
"Brewing",
|
|
110
|
+
"Conjuring",
|
|
111
|
+
"Scheming",
|
|
112
|
+
"Hatching",
|
|
113
|
+
"Concocting",
|
|
114
|
+
"Simmering",
|
|
115
|
+
"Marinating",
|
|
116
|
+
"Fermenting",
|
|
117
|
+
"Stewing",
|
|
118
|
+
"Whittling",
|
|
119
|
+
"Tinkering",
|
|
120
|
+
"Puzzling",
|
|
121
|
+
"Deciphering",
|
|
122
|
+
"Unraveling",
|
|
123
|
+
"Distilling",
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
// Pick a random thinking word (seeded by current second so it varies per call)
|
|
127
|
+
function pickThinkingWord() {
|
|
128
|
+
const idx = (Date.now() % (THINKING_WORDS.length * 1000)) / 1000 | 0;
|
|
129
|
+
return THINKING_WORDS[idx % THINKING_WORDS.length];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function startSpinner(label) {
|
|
133
|
+
if (!process.stdout.isTTY) return { stop: () => {}, update: () => {} };
|
|
134
|
+
// Ping-pong animation — cmd.exe-safe chars inspired by Claude Code's sparkle cycle
|
|
135
|
+
const frames = ["\xB7", "*", "+", "*", "\xB7", ".", "\xB7", "*", "+", "*", "\xB7", "."];
|
|
136
|
+
const t0 = Date.now();
|
|
137
|
+
let i = 0;
|
|
138
|
+
let tokenCount = 0;
|
|
139
|
+
let statusText = "";
|
|
140
|
+
const isThinking = label === "thinking";
|
|
141
|
+
const thinkWord = isThinking ? pickThinkingWord() : null;
|
|
142
|
+
process.stdout.write("\x1b[?25l");
|
|
143
|
+
const render = () => {
|
|
144
|
+
const ms = Date.now() - t0;
|
|
145
|
+
const secs = Math.floor(ms / 1000);
|
|
146
|
+
const timeFmt = secs >= 60
|
|
147
|
+
? `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`
|
|
148
|
+
: `${secs}s`;
|
|
149
|
+
const parts = [];
|
|
150
|
+
if (secs > 0) parts.push(timeFmt);
|
|
151
|
+
if (tokenCount > 0) parts.push(`${fmtTokens(tokenCount)} tokens`);
|
|
152
|
+
if (statusText) parts.push(statusText);
|
|
153
|
+
const metrics = parts.length > 0 ? c.gray(` (${parts.join(" \xB7 ")})`) : "";
|
|
154
|
+
const displayLabel = isThinking ? thinkWord : label;
|
|
155
|
+
process.stdout.write(`\r\x1b[K${c.cyan(frames[i % frames.length])} ${c.dim(displayLabel + "...")}${metrics}`);
|
|
156
|
+
i++;
|
|
157
|
+
};
|
|
158
|
+
render();
|
|
159
|
+
const timer = setInterval(render, 120);
|
|
160
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
161
|
+
return {
|
|
162
|
+
update(data) {
|
|
163
|
+
if (data.tokens != null) tokenCount = data.tokens;
|
|
164
|
+
if (data.status != null) statusText = data.status;
|
|
165
|
+
},
|
|
166
|
+
stop() {
|
|
167
|
+
clearInterval(timer);
|
|
168
|
+
process.stdout.write("\r\x1b[K\x1b[?25h");
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function fmtTokens(n) {
|
|
174
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
175
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + "k";
|
|
176
|
+
return String(n);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function turnStats({ elapsed, tokensIn, tokensOut, tools, credits }) {
|
|
180
|
+
const parts = [];
|
|
181
|
+
if (elapsed > 0) {
|
|
182
|
+
const s = Math.floor(elapsed / 1000);
|
|
183
|
+
parts.push(s >= 60 ? `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s` : `${s}s`);
|
|
184
|
+
}
|
|
185
|
+
if (tokensIn > 0 || tokensOut > 0) {
|
|
186
|
+
parts.push(`${fmtTokens(tokensIn)}->${fmtTokens(tokensOut)} tokens`);
|
|
187
|
+
}
|
|
188
|
+
if (tools > 0) parts.push(`${tools} tool call${tools === 1 ? "" : "s"}`);
|
|
189
|
+
if (credits > 0) parts.push(`${credits} credit${credits === 1 ? "" : "s"}`);
|
|
190
|
+
return parts.length > 0 ? c.dim(` ${parts.join(" \xB7 ")}`) : "";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function toolResult(text, ok = true) {
|
|
194
|
+
const prefix = ok ? c.green(" ● ") : c.red(" × ");
|
|
195
|
+
const lines = text.split("\n");
|
|
196
|
+
const head = lines[0].slice(0, 200);
|
|
197
|
+
const rest = lines.slice(1, 6).join("\n").slice(0, 600);
|
|
198
|
+
return `${prefix}${head}${rest ? "\n" + c.dim(rest) : ""}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function assistant(text) {
|
|
202
|
+
return text.split("\n").map((l) => ` ${l}`).join("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function errorLine(msg) {
|
|
206
|
+
return `${c.red(c.bold("Error:"))} ${msg}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function note(msg) {
|
|
210
|
+
return c.dim(msg);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function toolSummary(name, result) {
|
|
214
|
+
const ok = result.ok;
|
|
215
|
+
const branch = ok ? c.green("└─") : c.red("└─");
|
|
216
|
+
const out = result.output ?? "";
|
|
217
|
+
const firstLine = out.split("\n").find((l) => l.trim()) ?? "";
|
|
218
|
+
|
|
219
|
+
if (name === "run_shell") {
|
|
220
|
+
let code = null;
|
|
221
|
+
let stderr = "";
|
|
222
|
+
try {
|
|
223
|
+
const j = JSON.parse(out);
|
|
224
|
+
code = j.exit_code;
|
|
225
|
+
stderr = (j.stderr || "").split("\n")[0] || "";
|
|
226
|
+
} catch { /* ignore */ }
|
|
227
|
+
const status = code === null ? (ok ? "done" : "failed") : code === 0 ? "done" : `exit ${code}`;
|
|
228
|
+
const detail = !ok && stderr ? ` ${ellip(stderr, 60)}` : "";
|
|
229
|
+
return ` ${branch} ${c.gray(status + detail)}`;
|
|
230
|
+
}
|
|
231
|
+
if (name === "todo_write") return "";
|
|
232
|
+
if (name === "ask_user") return "";
|
|
233
|
+
if (name === "write_file") {
|
|
234
|
+
return ` ${branch} ${c.gray("wrote file")}`;
|
|
235
|
+
}
|
|
236
|
+
if (name === "edit_file") {
|
|
237
|
+
return ` ${branch} ${c.gray(ellip(firstLine, 100))}`;
|
|
238
|
+
}
|
|
239
|
+
if (name === "web_search") {
|
|
240
|
+
let count = 0;
|
|
241
|
+
try { const j = JSON.parse(out); if (Array.isArray(j)) count = j.length; } catch { /* ignore */ }
|
|
242
|
+
return ` ${branch} ${c.gray(count > 0 ? `${count} results` : "no results")}`;
|
|
243
|
+
}
|
|
244
|
+
if (name === "web_fetch") {
|
|
245
|
+
const lines = out.split("\n").length;
|
|
246
|
+
return ` ${branch} ${c.gray(`${lines} lines fetched`)}`;
|
|
247
|
+
}
|
|
248
|
+
let summary = "";
|
|
249
|
+
try {
|
|
250
|
+
const j = JSON.parse(out);
|
|
251
|
+
if (Array.isArray(j)) summary = `${j.length} result${j.length === 1 ? "" : "s"}`;
|
|
252
|
+
else if (Array.isArray(j.files)) summary = `${j.files.length} file${j.files.length === 1 ? "" : "s"}`;
|
|
253
|
+
else if (Array.isArray(j.matches)) summary = `${j.matches.length} match${j.matches.length === 1 ? "" : "es"}`;
|
|
254
|
+
} catch { /* not JSON */ }
|
|
255
|
+
if (!summary) {
|
|
256
|
+
summary = name === "read_file" ? `${out.split("\n").length} lines` : ellip(firstLine, 100);
|
|
257
|
+
}
|
|
258
|
+
return ` ${branch} ${c.gray(summary)}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const MODEL_TOKEN_RE = /<\|[a-z_]*\|?>|<[a-z_]+\|>/gi;
|
|
262
|
+
|
|
263
|
+
export function stripModelTokens(text) {
|
|
264
|
+
return text.replace(MODEL_TOKEN_RE, "");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function makeTokenStripper() {
|
|
268
|
+
let buf = "";
|
|
269
|
+
return {
|
|
270
|
+
push(text) {
|
|
271
|
+
buf = (buf + text).replace(MODEL_TOKEN_RE, "");
|
|
272
|
+
const partial = buf.match(/<[|a-z_/]*$/i);
|
|
273
|
+
if (partial) {
|
|
274
|
+
const emit = buf.slice(0, partial.index);
|
|
275
|
+
buf = buf.slice(partial.index);
|
|
276
|
+
return emit;
|
|
277
|
+
}
|
|
278
|
+
const emit = buf;
|
|
279
|
+
buf = "";
|
|
280
|
+
return emit;
|
|
281
|
+
},
|
|
282
|
+
flush() {
|
|
283
|
+
const out = buf.replace(MODEL_TOKEN_RE, "");
|
|
284
|
+
buf = "";
|
|
285
|
+
return out;
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|