@hncode/hncode 0.5.5 → 0.6.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.
Files changed (56) hide show
  1. package/package.json +37 -38
  2. package/src/agent.js +67 -2
  3. package/src/image-row.js +111 -0
  4. package/src/latex.js +1557 -0
  5. package/src/llm.js +19 -4
  6. package/src/mermaid/ansi.js +55 -0
  7. package/src/mermaid/canvas.js +449 -0
  8. package/src/mermaid/class-style.js +57 -0
  9. package/src/mermaid/css-colors.js +153 -0
  10. package/src/mermaid/diagrams/class.js +289 -0
  11. package/src/mermaid/diagrams/er.js +201 -0
  12. package/src/mermaid/diagrams/flowchart.js +488 -0
  13. package/src/mermaid/diagrams/gitgraph.js +205 -0
  14. package/src/mermaid/diagrams/mindmap.js +101 -0
  15. package/src/mermaid/diagrams/pie.js +104 -0
  16. package/src/mermaid/diagrams/sequence.js +282 -0
  17. package/src/mermaid/diagrams/state.js +292 -0
  18. package/src/mermaid/diagrams/timeline.js +99 -0
  19. package/src/mermaid/graph.js +138 -0
  20. package/src/mermaid/index.js +59 -0
  21. package/src/mermaid/labels.js +339 -0
  22. package/src/mermaid/layout-seq.js +225 -0
  23. package/src/mermaid/layout.js +1454 -0
  24. package/src/mermaid/registry.js +54 -0
  25. package/src/mermaid/source-box.js +102 -0
  26. package/src/mermaid/statements.js +235 -0
  27. package/src/mermaid/types.js +1 -0
  28. package/src/mermaid/width.js +29 -0
  29. package/src/screen-diff.js +512 -0
  30. package/src/screen-paint.js +128 -0
  31. package/src/screen.js +277 -0
  32. package/src/subagent-types.js +136 -147
  33. package/src/term-caps.js +10 -4
  34. package/src/tex/font.js +292 -0
  35. package/src/tex/fonts/LICENSE.txt +11 -0
  36. package/src/tex/fonts/STIX2Math.ttf +0 -0
  37. package/src/tex/index.js +293 -0
  38. package/src/tex/kitty.js +186 -0
  39. package/src/tex/layout.js +446 -0
  40. package/src/tex/parse.js +331 -0
  41. package/src/tex/raster.js +216 -0
  42. package/src/tex/render.js +167 -0
  43. package/src/tex/sixel.js +326 -0
  44. package/src/tex/svg.js +237 -0
  45. package/src/tool-result.js +2 -2
  46. package/src/tools/agent-swarm.js +3 -15
  47. package/src/tools/agent.js +454 -464
  48. package/src/tools/ask-user-question.js +195 -206
  49. package/src/tools/bash.js +39 -3
  50. package/src/tools/edit.js +10 -112
  51. package/src/tools/fetch-url.js +130 -120
  52. package/src/tools/memory.js +80 -88
  53. package/src/tools/read.js +27 -23
  54. package/src/tools/todo.js +8 -13
  55. package/src/tools/validate-args.js +78 -0
  56. package/src/tui.js +1346 -496
@@ -1,206 +1,195 @@
1
- // AskUserQuestion tool — ask the user 1-4 structured multiple-choice questions
2
- // mid-turn and block until they answer. Mirrors kimi-code's AskUserQuestion:
3
- // the same input shape, the same duplicate validation, and the same result
4
- // JSON, so a model trained on either behaves the same.
5
- //
6
- // The TUI supplies the actual UI through `ctx.askQuestion` (a function injected
7
- // by the Agent). When it is absent — headless `-p` mode, a plugin harness, or a
8
- // client that cannot prompt — the tool FAILS with a message telling the model to
9
- // ask in plain text instead of silently returning an empty answer, which would
10
- // look like "the user declined".
11
-
12
- const MAX_QUESTIONS = 4;
13
- const MIN_OPTIONS = 2;
14
- const MAX_OPTIONS = 4;
15
-
16
- // The user always gets an escape hatch for free input, so the model must not
17
- // waste one of its 2-4 option slots on an "Other" of its own.
18
- export const OTHER_LABEL = 'Other';
19
-
20
- // A SECOND free-text row, shown once after the LAST question: not an answer to
21
- // anything, just somewhere to add context that no option covered. Reported as
22
- // `additional` on the result (NOT `note`, which already means "dismissed").
23
- export const SUPPLEMENT_LABEL = 'Add a note';
24
-
25
- const UNIQUENESS_HINT =
26
- 'Question texts must be unique across questions, and option labels must be unique within each question.';
27
-
28
- // Validate the two uniqueness rules kimi enforces. Returns an error string, or
29
- // null when the input is acceptable. Checks run BEFORE any UI is shown so a
30
- // malformed call never interrupts the user.
31
- export function questionUniquenessError(questions) {
32
- const texts = new Set();
33
- for (const q of questions) {
34
- if (texts.has(q.question)) {
35
- return `Invalid questions: duplicate question text ${JSON.stringify(q.question)}. ${UNIQUENESS_HINT} Rephrase the duplicates and call the tool again.`;
36
- }
37
- texts.add(q.question);
38
- const labels = new Set();
39
- for (const o of q.options) {
40
- if (labels.has(o.label)) {
41
- return `Invalid questions: duplicate option label ${JSON.stringify(o.label)} in question ${JSON.stringify(q.question)}. ${UNIQUENESS_HINT} Rephrase the duplicates and call the tool again.`;
42
- }
43
- labels.add(o.label);
44
- }
45
- }
46
- return null;
47
- }
48
-
49
- const DESCRIPTION = `Use this tool when you need to ask the user questions with structured options during execution. This allows you to:
50
- 1. Collect user preferences or requirements before proceeding
51
- 2. Resolve ambiguous or underspecified instructions
52
- 3. Let the user decide between implementation approaches as you work
53
- 4. Present concrete options when multiple valid directions exist
54
-
55
- When NOT to use:
56
- - When you can infer the answer from context — be decisive and proceed
57
- - Trivial decisions that don't materially affect the outcome
58
-
59
- Overusing this tool interrupts the user's flow. Only use it when the user's input genuinely changes your next action.
60
-
61
- Usage notes:
62
- - Users always have an "Other" option for custom input — don't create one yourself
63
- - Use multi_select to allow multiple answers to be selected for a question
64
- - Keep option labels concise (1-5 words), use descriptions for trade-offs and details
65
- - Each question should have 2-4 meaningful, distinct options
66
- - Question texts must be unique across the call, and option labels must be unique within each question
67
- - You can ask 1-4 questions at a time; group related questions to minimize interruptions
68
- - If you recommend a specific option, list it first and append "(Recommended)" to its label
69
- - The result is JSON with an \`answers\` object keyed by question text; each value is the chosen option's label (comma-separated labels for multi_select, or the user's own words if they picked "Other"); if \`answers\` is empty and a \`note\` says the user dismissed it, they chose not to answer — do not treat this as selecting the recommended option; decide based on context and do not re-ask the same question
70
- - The user may also leave a free-form \`additional\` note covering the whole request; the field appears only when they wrote one. Treat it as extra context, not as the answer to a specific question`;
71
-
72
- export const spec = {
73
- name: 'AskUserQuestion',
74
- description: DESCRIPTION,
75
- parameters: {
76
- type: 'object',
77
- properties: {
78
- questions: {
79
- type: 'array',
80
- minItems: 1,
81
- maxItems: MAX_QUESTIONS,
82
- description: 'The questions to ask the user (1-4 questions).',
83
- items: {
84
- type: 'object',
85
- properties: {
86
- question: {
87
- type: 'string',
88
- description: "A specific, actionable question. End with '?'.",
89
- },
90
- header: {
91
- type: 'string',
92
- description: "Short category tag (max 12 chars, e.g. 'Auth', 'Style').",
93
- },
94
- options: {
95
- type: 'array',
96
- minItems: MIN_OPTIONS,
97
- maxItems: MAX_OPTIONS,
98
- description:
99
- "2-4 meaningful, distinct options. Do NOT include an 'Other' option — the system adds one automatically.",
100
- items: {
101
- type: 'object',
102
- properties: {
103
- label: {
104
- type: 'string',
105
- description: "Concise display text (1-5 words). If recommended, append '(Recommended)'.",
106
- },
107
- description: {
108
- type: 'string',
109
- description: 'Brief explanation of trade-offs or implications.',
110
- },
111
- },
112
- required: ['label'],
113
- },
114
- },
115
- multi_select: {
116
- type: 'boolean',
117
- description: 'Whether the user can select multiple options.',
118
- },
119
- },
120
- required: ['question', 'options'],
121
- },
122
- },
123
- },
124
- required: ['questions'],
125
- },
126
-
127
- async execute(args, ctx) {
128
- // AUTO ("Never Ask") forbids asking by definition — the whole point of the
129
- // mode is that the agent decides for itself. Checked FIRST so the model gets
130
- // a clear instruction back instead of a dialog the user never wanted. The
131
- // wording mirrors kimi-code's `auto-mode-ask-user-question-deny` policy; the
132
- // `Error:` prefix is ours — it is what marks the call as failed to the TUI, so
133
- // the status bullet turns RED instead of staying green on a refusal.
134
- if (ctx && ctx.permissionMode === 'auto') {
135
- return 'Error: AskUserQuestion is disabled while auto permission mode is active. Make a reasonable decision and continue without asking the user.';
136
- }
137
-
138
- const raw = (args && args.questions) || [];
139
- if (!Array.isArray(raw) || raw.length === 0) {
140
- return 'Error: `questions` must be a non-empty array (1-4 questions).';
141
- }
142
- if (raw.length > MAX_QUESTIONS) {
143
- return `Error: at most ${MAX_QUESTIONS} questions per call (got ${raw.length}). Group related questions to minimize interruptions.`;
144
- }
145
-
146
- // Normalize + validate BEFORE showing any UI: a malformed call must not
147
- // interrupt the user, it must come straight back to the model.
148
- const questions = [];
149
- for (const q of raw) {
150
- const text = String((q && q.question) || '').trim();
151
- if (!text) return 'Error: every question needs a non-empty `question` string.';
152
- const opts = Array.isArray(q && q.options) ? q.options : [];
153
- if (opts.length < MIN_OPTIONS || opts.length > MAX_OPTIONS) {
154
- return `Error: question ${JSON.stringify(text)} needs ${MIN_OPTIONS}-${MAX_OPTIONS} options (got ${opts.length}).`;
155
- }
156
- const options = [];
157
- for (const o of opts) {
158
- const label = String((o && o.label) || '').trim();
159
- if (!label) return `Error: every option in ${JSON.stringify(text)} needs a non-empty \`label\`.`;
160
- options.push({ label, description: String((o && o.description) || '') });
161
- }
162
- questions.push({
163
- question: text,
164
- header: String((q && q.header) || '').trim().slice(0, 12),
165
- options,
166
- multiSelect: !!(q && q.multi_select),
167
- });
168
- }
169
-
170
- const dup = questionUniquenessError(questions);
171
- if (dup) return dup;
172
-
173
- // No interactive client: say so explicitly. Returning `{answers:{}}` would be
174
- // read as "the user refused to answer", which is a different signal.
175
- if (typeof ctx.askQuestion !== 'function') {
176
- return 'The connected client does not support interactive questions. Do NOT call this tool again. Ask the user directly in your text response instead.';
177
- }
178
-
179
- let result;
180
- try {
181
- result = await ctx.askQuestion(questions, ctx);
182
- } catch (err) {
183
- // An aborted turn (Esc / Ctrl-C) is not a dismissal — let the agent see the
184
- // interruption instead of a fabricated empty answer.
185
- if (ctx.signal && ctx.signal.aborted) throw err;
186
- return `Error: could not ask the user: ${(err && err.message) || String(err)}`;
187
- }
188
-
189
- // Dismissed (Esc) or nothing answered: kimi's exact semantics — an empty
190
- // answers object plus a note telling the model NOT to treat it as consent.
191
- // A dismissal also drops any note the user had started typing.
192
- const answers = (result && typeof result === 'object' && result.answers) || null;
193
- const additional = (result && typeof result === 'object' && result.additional) || '';
194
- if (!answers || Object.keys(answers).length === 0) {
195
- return JSON.stringify({
196
- answers: {},
197
- note: 'User dismissed the question without answering.',
198
- });
199
- }
200
- // `additional` is OPTIONAL: omit the key entirely when the user left the
201
- // supplement blank, so the model does not see a meaningless empty field.
202
- const out = { answers };
203
- if (additional) out.additional = additional;
204
- return JSON.stringify(out);
205
- },
206
- };
1
+ // AskUserQuestion tool — ask the user 1-4 structured multiple-choice questions
2
+ // mid-turn and block until they answer. Mirrors kimi-code's AskUserQuestion:
3
+ // the same input shape, the same duplicate validation, and the same result
4
+ // JSON, so a model trained on either behaves the same.
5
+ //
6
+ // The TUI supplies the actual UI through `ctx.askQuestion` (a function injected
7
+ // by the Agent). When it is absent — headless `-p` mode, a plugin harness, or a
8
+ // client that cannot prompt — the tool FAILS with a message telling the model to
9
+ // ask in plain text instead of silently returning an empty answer, which would
10
+ // look like "the user declined".
11
+
12
+ const MAX_QUESTIONS = 4;
13
+ const MIN_OPTIONS = 2;
14
+ const MAX_OPTIONS = 4;
15
+
16
+ // The user always gets an escape hatch for free input, so the model must not
17
+ // waste one of its 2-4 option slots on an "Other" of its own.
18
+ export const OTHER_LABEL = 'Other';
19
+
20
+ // A SECOND free-text row, shown once after the LAST question: not an answer to
21
+ // anything, just somewhere to add context that no option covered. Reported as
22
+ // `additional` on the result (NOT `note`, which already means "dismissed").
23
+ export const SUPPLEMENT_LABEL = 'Add a note';
24
+
25
+ const UNIQUENESS_HINT =
26
+ 'Question texts must be unique across questions, and option labels must be unique within each question.';
27
+
28
+ // Validate the two uniqueness rules kimi enforces. Returns an error string, or
29
+ // null when the input is acceptable. Checks run BEFORE any UI is shown so a
30
+ // malformed call never interrupts the user.
31
+ export function questionUniquenessError(questions) {
32
+ const texts = new Set();
33
+ for (const q of questions) {
34
+ if (texts.has(q.question)) {
35
+ return `Invalid questions: duplicate question text ${JSON.stringify(q.question)}. ${UNIQUENESS_HINT} Rephrase the duplicates and call the tool again.`;
36
+ }
37
+ texts.add(q.question);
38
+ const labels = new Set();
39
+ for (const o of q.options) {
40
+ if (labels.has(o.label)) {
41
+ return `Invalid questions: duplicate option label ${JSON.stringify(o.label)} in question ${JSON.stringify(q.question)}. ${UNIQUENESS_HINT} Rephrase the duplicates and call the tool again.`;
42
+ }
43
+ labels.add(o.label);
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ const DESCRIPTION = `Ask the user a question with structured options, during execution. Use it when the answer
50
+ genuinely changes what you do next: a preference, an ambiguous instruction, a choice between approaches.
51
+
52
+ Do NOT use it for what you can infer from context, or for trivial decisions — overusing it interrupts the user's flow.
53
+
54
+ Usage:
55
+ - Users always get an "Other" option; do not create one.
56
+ - 2-4 distinct options per question, labels 1-5 words, descriptions carry the trade-offs. Labels unique within a question, question texts unique across the call.
57
+ - 1-4 questions at a time, related ones grouped. multi_select allows several answers.
58
+ - Put a recommended option first and append "(Recommended)" to its label.
59
+ - The result is JSON: an \`answers\` object keyed by question text, each value the chosen label (comma-separated for multi_select, or the user's own words for "Other"). An empty \`answers\` with a \`note\` means they dismissed it — that is NOT a vote for the recommended option; decide from context and do not re-ask. A free-form \`additional\` note, when present, is extra context for the whole request.`;
60
+
61
+ export const spec = {
62
+ name: 'AskUserQuestion',
63
+ description: DESCRIPTION,
64
+ parameters: {
65
+ type: 'object',
66
+ properties: {
67
+ questions: {
68
+ type: 'array',
69
+ minItems: 1,
70
+ maxItems: MAX_QUESTIONS,
71
+ description: 'The questions to ask the user (1-4 questions).',
72
+ items: {
73
+ type: 'object',
74
+ properties: {
75
+ question: {
76
+ type: 'string',
77
+ description: "A specific, actionable question. End with '?'.",
78
+ },
79
+ header: {
80
+ type: 'string',
81
+ description: "Short category tag (max 12 chars, e.g. 'Auth', 'Style').",
82
+ },
83
+ options: {
84
+ type: 'array',
85
+ minItems: MIN_OPTIONS,
86
+ maxItems: MAX_OPTIONS,
87
+ description:
88
+ "2-4 meaningful, distinct options. Do NOT include an 'Other' option — the system adds one automatically.",
89
+ items: {
90
+ type: 'object',
91
+ properties: {
92
+ label: {
93
+ type: 'string',
94
+ description: "Concise display text (1-5 words). If recommended, append '(Recommended)'.",
95
+ },
96
+ description: {
97
+ type: 'string',
98
+ description: 'Brief explanation of trade-offs or implications.',
99
+ },
100
+ },
101
+ required: ['label'],
102
+ },
103
+ },
104
+ multi_select: {
105
+ type: 'boolean',
106
+ description: 'Whether the user can select multiple options.',
107
+ },
108
+ },
109
+ required: ['question', 'options'],
110
+ },
111
+ },
112
+ },
113
+ required: ['questions'],
114
+ },
115
+
116
+ async execute(args, ctx) {
117
+ // AUTO ("Never Ask") forbids asking by definition — the whole point of the
118
+ // mode is that the agent decides for itself. Checked FIRST so the model gets
119
+ // a clear instruction back instead of a dialog the user never wanted. The
120
+ // wording mirrors kimi-code's `auto-mode-ask-user-question-deny` policy; the
121
+ // `Error:` prefix is ours — it is what marks the call as failed to the TUI, so
122
+ // the status bullet turns RED instead of staying green on a refusal.
123
+ if (ctx && ctx.permissionMode === 'auto') {
124
+ return 'Error: AskUserQuestion is disabled while auto permission mode is active. Make a reasonable decision and continue without asking the user.';
125
+ }
126
+
127
+ const raw = (args && args.questions) || [];
128
+ if (!Array.isArray(raw) || raw.length === 0) {
129
+ return 'Error: `questions` must be a non-empty array (1-4 questions).';
130
+ }
131
+ if (raw.length > MAX_QUESTIONS) {
132
+ return `Error: at most ${MAX_QUESTIONS} questions per call (got ${raw.length}). Group related questions to minimize interruptions.`;
133
+ }
134
+
135
+ // Normalize + validate BEFORE showing any UI: a malformed call must not
136
+ // interrupt the user, it must come straight back to the model.
137
+ const questions = [];
138
+ for (const q of raw) {
139
+ const text = String((q && q.question) || '').trim();
140
+ if (!text) return 'Error: every question needs a non-empty `question` string.';
141
+ const opts = Array.isArray(q && q.options) ? q.options : [];
142
+ if (opts.length < MIN_OPTIONS || opts.length > MAX_OPTIONS) {
143
+ return `Error: question ${JSON.stringify(text)} needs ${MIN_OPTIONS}-${MAX_OPTIONS} options (got ${opts.length}).`;
144
+ }
145
+ const options = [];
146
+ for (const o of opts) {
147
+ const label = String((o && o.label) || '').trim();
148
+ if (!label) return `Error: every option in ${JSON.stringify(text)} needs a non-empty \`label\`.`;
149
+ options.push({ label, description: String((o && o.description) || '') });
150
+ }
151
+ questions.push({
152
+ question: text,
153
+ header: String((q && q.header) || '').trim().slice(0, 12),
154
+ options,
155
+ multiSelect: !!(q && q.multi_select),
156
+ });
157
+ }
158
+
159
+ const dup = questionUniquenessError(questions);
160
+ if (dup) return dup;
161
+
162
+ // No interactive client: say so explicitly. Returning `{answers:{}}` would be
163
+ // read as "the user refused to answer", which is a different signal.
164
+ if (typeof ctx.askQuestion !== 'function') {
165
+ return 'The connected client does not support interactive questions. Do NOT call this tool again. Ask the user directly in your text response instead.';
166
+ }
167
+
168
+ let result;
169
+ try {
170
+ result = await ctx.askQuestion(questions, ctx);
171
+ } catch (err) {
172
+ // An aborted turn (Esc / Ctrl-C) is not a dismissal — let the agent see the
173
+ // interruption instead of a fabricated empty answer.
174
+ if (ctx.signal && ctx.signal.aborted) throw err;
175
+ return `Error: could not ask the user: ${(err && err.message) || String(err)}`;
176
+ }
177
+
178
+ // Dismissed (Esc) or nothing answered: kimi's exact semantics — an empty
179
+ // answers object plus a note telling the model NOT to treat it as consent.
180
+ // A dismissal also drops any note the user had started typing.
181
+ const answers = (result && typeof result === 'object' && result.answers) || null;
182
+ const additional = (result && typeof result === 'object' && result.additional) || '';
183
+ if (!answers || Object.keys(answers).length === 0) {
184
+ return JSON.stringify({
185
+ answers: {},
186
+ note: 'User dismissed the question without answering.',
187
+ });
188
+ }
189
+ // `additional` is OPTIONAL: omit the key entirely when the user left the
190
+ // supplement blank, so the model does not see a meaningless empty field.
191
+ const out = { answers };
192
+ if (additional) out.additional = additional;
193
+ return JSON.stringify(out);
194
+ },
195
+ };
package/src/tools/bash.js CHANGED
@@ -197,17 +197,53 @@ const command = args.command;
197
197
  // Line-by-line is also what keeps an escape sequence from being cut in half:
198
198
  // a sequence never contains a newline, so a complete line is always safe to
199
199
  // sanitize, and the trailing partial line waits for more input.
200
+ // THE LIVE STREAM MUST DECODE EXACTLY THE WAY THE FINAL RESULT DOES.
201
+ //
202
+ // The finished output goes through `decodeBuffer`, which tries strict UTF-8 and FALLS BACK TO GBK. The
203
+ // live sink used a plain non-fatal UTF-8 decoder, so a command whose output is not UTF-8 — PowerShell on a
204
+ // Chinese Windows writes GBK for native tools — streamed as replacement characters while it ran and then
205
+ // turned correct the moment it finished. That is the "garbled while running, fine afterwards" report.
206
+ //
207
+ // The live path cannot buffer the whole run, so it decides from the first chunk that proves itself: valid
208
+ // UTF-8 keeps the UTF-8 decoder, anything else pins GBK for the rest of the stream. A chunk boundary
209
+ // through the middle of a multi-byte character looks invalid too, so a failed probe is retried once with
210
+ // the remaining bytes before the encoding is pinned.
200
211
  const sink = typeof ctx.onOutput === 'function' ? ctx.onOutput : null;
201
- const liveDecoder = sink ? new TextDecoder('utf-8') : null;
212
+ let liveDecoder = sink ? new TextDecoder('utf-8') : null;
202
213
  let livePending = '';
203
- const push = (d) => {
214
+ let liveUndecided = !!sink; // the probe has not committed yet
215
+ const push = (d) => {
204
216
  chunks.push(d);
205
217
  const dropped = trimChunkRing(chunks, LIVE_RING_BYTES);
206
218
  if (dropped) chunksDropped += dropped;
207
219
 
208
220
  if (!sink || settled) return;
209
221
  try {
210
- livePending += liveDecoder.decode(d, { stream: true });
222
+ const buf = Buffer.isBuffer(d) ? d : Buffer.from(String(d));
223
+ if (liveUndecided) {
224
+ // A trailing partial character makes a valid UTF-8 chunk look invalid. Only a chunk that still
225
+ // fails after its own tail is set aside is evidence.
226
+ let valid = false;
227
+ try {
228
+ new TextDecoder('utf-8', { fatal: true }).decode(buf);
229
+ valid = true;
230
+ } catch {
231
+ // Trim up to three trailing bytes and try once more: that is the longest an incomplete UTF-8
232
+ // sequence can be.
233
+ for (let cut = 1; cut <= 3 && cut < buf.length; cut++) {
234
+ try {
235
+ new TextDecoder('utf-8', { fatal: true }).decode(buf.subarray(0, buf.length - cut));
236
+ valid = true;
237
+ break;
238
+ } catch { /* keep trimming */ }
239
+ }
240
+ }
241
+ if (!valid) liveDecoder = new TextDecoder('gbk');
242
+ liveUndecided = false;
243
+ }
244
+ // One decoder for the whole stream, in streaming mode: a GBK character is two bytes and will be split
245
+ // across chunks just as often as a UTF-8 one.
246
+ livePending += liveDecoder.decode(buf, { stream: true });
211
247
  const nl = livePending.lastIndexOf('\n');
212
248
  if (nl < 0) return;
213
249
  const complete = livePending.slice(0, nl + 1);