@ryuu-reinzz/ryuu-cli 0.0.0-stage → 0.3.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1213 -2
  4. package/package.json +45 -4
  5. package/src/agent/agent.js +1194 -0
  6. package/src/agent/approvals.js +76 -0
  7. package/src/agent/context.js +106 -0
  8. package/src/agent/planner.js +237 -0
  9. package/src/agent/worklog.js +318 -0
  10. package/src/cli/app.js +463 -0
  11. package/src/cli/commands.js +1150 -0
  12. package/src/cli/input.js +287 -0
  13. package/src/cli/markdown.js +527 -0
  14. package/src/cli/prompts.js +255 -0
  15. package/src/cli/renderer.js +2155 -0
  16. package/src/cli/settings-editor.js +280 -0
  17. package/src/cli/settings-tui.js +669 -0
  18. package/src/cli/tui.js +1737 -0
  19. package/src/cli/width.js +48 -0
  20. package/src/config/load.js +132 -0
  21. package/src/config/scaffold.js +50 -0
  22. package/src/config/settings-registry.js +495 -0
  23. package/src/config/settings.js +304 -0
  24. package/src/config/validate.js +451 -0
  25. package/src/index.js +449 -0
  26. package/src/providers/catalog.js +282 -0
  27. package/src/providers/index.js +84 -0
  28. package/src/providers/nvidia.js +645 -0
  29. package/src/providers/registry.js +104 -0
  30. package/src/services/clipboard.js +38 -0
  31. package/src/services/diff.js +153 -0
  32. package/src/services/doctor.js +126 -0
  33. package/src/services/logger.js +190 -0
  34. package/src/services/preferences.js +249 -0
  35. package/src/services/processes.js +345 -0
  36. package/src/services/provider-settings.js +226 -0
  37. package/src/services/sessions.js +279 -0
  38. package/src/services/undo.js +133 -0
  39. package/src/services/ushell.js +146 -0
  40. package/src/services/websearch/manager.js +616 -0
  41. package/src/services/websearch/mcp.js +233 -0
  42. package/src/services/websearch/normalize.js +284 -0
  43. package/src/services/websearch/providers.js +178 -0
  44. package/src/services/websearch/serper.js +263 -0
  45. package/src/services/workspace.js +589 -0
  46. package/src/tools/filesystem.js +304 -0
  47. package/src/tools/git.js +114 -0
  48. package/src/tools/index.js +48 -0
  49. package/src/tools/project.js +160 -0
  50. package/src/tools/registry.js +118 -0
  51. package/src/tools/search.js +210 -0
  52. package/src/tools/shell.js +279 -0
  53. package/src/tools/url.js +542 -0
  54. package/src/tools/web.js +67 -0
  55. package/src/utils/errors.js +160 -0
  56. package/src/utils/network.js +129 -0
  57. package/src/utils/redaction.js +73 -0
  58. package/src/utils/validation.js +370 -0
  59. package/src/utils/version.js +24 -0
@@ -0,0 +1,527 @@
1
+ import { Chalk } from 'chalk';
2
+
3
+ const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
4
+ const ANSI_AT = /\x1b\[[0-?]*[ -/]*[@-~]/y;
5
+ const CONTROL_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
6
+
7
+ const INLINE_RE = /(\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|~~[^~\n]+~~|\[[^\]\n]+\]\([^)\n]+\)|\*[^*\n]+\*|_[^_\n]+_)/g;
8
+ const TOKEN_RE = /(\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|~~[^~\n]+~~|\[[^\]\n]+\]\([^)\n]+\)|\*[^*\n]+\*|_[^_\n]+_)/;
9
+
10
+ const CODE_TOKEN_RE = /(\/\/.*|#.*|\/\*.*?\*\/|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`|\b\d+(?:\.\d+)?\b|\b(?:const|let|var|function|return|if|else|for|while|import|from|export|default|class|extends|new|async|await|try|catch|finally|throw|def|fn|pub|struct|impl|use|mod|package|func|type|interface|select|go|defer|map|range|switch|case|break|continue|true|false|null|undefined|None|True|False|and|or|not|in|is)\b)/g;
11
+
12
+ const CYAN = '#22d3ee';
13
+ const CYAN_LIGHT = '#67e8f9';
14
+ const GREEN = '#86efac';
15
+ const AMBER = '#fbbf24';
16
+ const VIOLET = '#c4b5fd';
17
+ const SLATE = '#94a3b8';
18
+
19
+ function visibleLength(text) {
20
+ return String(text ?? '').replace(ANSI_RE, '').length;
21
+ }
22
+
23
+ function toChalk(c, color) {
24
+ if (!color) return new Chalk({ level: 0 });
25
+ return c || new Chalk({ level: 1 });
26
+ }
27
+
28
+ /**
29
+ * The accent colour used for rules, tables, headings, links and list markers.
30
+ * Callers pass a chalk colour function (e.g. `c.hex('#38bdf8')`) to theme the
31
+ * whole document; the default keeps the classic cyan look.
32
+ */
33
+ function toAccent(accent, c, fallback = CYAN) {
34
+ if (typeof accent === 'function') return accent;
35
+ return (text) => c.hex(fallback)(text);
36
+ }
37
+
38
+ /** Remove model-provided ANSI escapes and unsafe control characters. */
39
+ function sanitize(text) {
40
+ return String(text ?? '')
41
+ .replace(ANSI_RE, '')
42
+ .replace(/\r\n?/g, '\n')
43
+ .replace(CONTROL_RE, '');
44
+ }
45
+
46
+ function plainInline(atom) {
47
+ const value = String(atom ?? '');
48
+ if (/^\*\*[\s\S]+\*\*$/.test(value) || /^__[\s\S]+__$/.test(value)) return value.slice(2, -2);
49
+ if (/^`[\s\S]+`$/.test(value)) return value.slice(1, -1);
50
+ if (/^~~[\s\S]+~~$/.test(value)) return value.slice(2, -2);
51
+ const link = value.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
52
+ if (link) return link[1] === link[2] ? link[1] : `${link[1]} (${link[2]})`;
53
+ if (/^\*[^*\n]+\*$/.test(value) || /^_[^_\n]+_$/.test(value)) return value.slice(1, -1);
54
+ return value;
55
+ }
56
+
57
+ /**
58
+ * Paint one inline Markdown atom. `base` is an optional chalk style (e.g.
59
+ * `c.white`) applied to plain and style-wrapped atoms, so a caller can set the
60
+ * default text colour without losing to mid-line ANSI resets.
61
+ */
62
+ function paintInline(atom, c, base = null, accent = null) {
63
+ const value = String(atom ?? '');
64
+ const plain = (text) => (base ? base(text) : text);
65
+ const accentPaint = toAccent(accent, c, CYAN_LIGHT);
66
+ if (/^\*\*[\s\S]+\*\*$/.test(value) || /^__[\s\S]+__$/.test(value)) {
67
+ return plain(c.bold(value.slice(2, -2)));
68
+ }
69
+ if (/^`[\s\S]+`$/.test(value)) {
70
+ return c.bgHex('#1e293b')(accentPaint(value.slice(1, -1)));
71
+ }
72
+ if (/^~~[\s\S]+~~$/.test(value)) {
73
+ return plain(c.strikethrough(value.slice(2, -2)));
74
+ }
75
+ const link = value.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
76
+ if (link) {
77
+ const label = c.underline(accentPaint(link[1]));
78
+ return link[1] === link[2] ? label : `${label}${c.dim(` (${link[2]})`)}`;
79
+ }
80
+ if (/^\*[^*\n]+\*$/.test(value) || /^_[^_\n]+_$/.test(value)) {
81
+ return plain(c.italic(value.slice(1, -1)));
82
+ }
83
+ return plain(value);
84
+ }
85
+
86
+ function splitWords(segment) {
87
+ return String(segment).split(/(\s+)/).filter((part) => part !== '');
88
+ }
89
+
90
+ /** Break a long token at a natural boundary when one exists. */
91
+ function splitLongPlain(text, width) {
92
+ const chunks = [];
93
+ let remaining = String(text);
94
+ while (remaining.length > width) {
95
+ const window = remaining.slice(0, width);
96
+ const boundary = Math.max(
97
+ window.lastIndexOf('/'),
98
+ window.lastIndexOf('.'),
99
+ window.lastIndexOf('-'),
100
+ window.lastIndexOf('_'),
101
+ );
102
+ const cut = boundary > 0 && boundary < width - 1 ? boundary + 1 : width;
103
+ chunks.push(remaining.slice(0, cut));
104
+ remaining = remaining.slice(cut);
105
+ }
106
+ if (remaining) chunks.push(remaining);
107
+ return chunks;
108
+ }
109
+
110
+ function splitAtoms(text) {
111
+ const atoms = [];
112
+ let last = 0;
113
+ for (const match of String(text).matchAll(INLINE_RE)) {
114
+ if (match.index > last) atoms.push(...splitWords(String(text).slice(last, match.index)));
115
+ atoms.push(match[0]);
116
+ last = match.index + match[0].length;
117
+ }
118
+ if (last < text.length) atoms.push(...splitWords(String(text).slice(last)));
119
+ return atoms;
120
+ }
121
+
122
+ /** Apply inline Markdown without wrapping. */
123
+ export function renderInline(text, c, base = null, accent = null) {
124
+ return splitAtoms(text).map((atom) => paintInline(atom, c, base, accent)).join('');
125
+ }
126
+
127
+ /** Wrap inline Markdown to a visible width, preserving styles. */
128
+ export function wrapInline(text, width, c, base = null, accent = null) {
129
+ const max = Math.max(1, width);
130
+ const atoms = splitAtoms(text);
131
+ const lines = [];
132
+ let line = '';
133
+ let used = 0;
134
+
135
+ for (const atom of atoms) {
136
+ if (/^\s+$/.test(atom)) {
137
+ if (!line) continue;
138
+ if (used + 1 > max) {
139
+ lines.push(line);
140
+ line = '';
141
+ used = 0;
142
+ continue;
143
+ }
144
+ line += ' ';
145
+ used += 1;
146
+ continue;
147
+ }
148
+ const painted = paintInline(atom, c, base, accent);
149
+ const plain = plainInline(atom);
150
+ if (plain.length > max) {
151
+ if (line) {
152
+ lines.push(line);
153
+ line = '';
154
+ used = 0;
155
+ }
156
+ const chunks = splitLongPlain(plain, max);
157
+ for (const chunk of chunks.slice(0, -1)) lines.push(chunk);
158
+ line = chunks[chunks.length - 1] || '';
159
+ used = line.length;
160
+ continue;
161
+ }
162
+ if (used > 0 && used + plain.length > max) {
163
+ lines.push(line);
164
+ line = '';
165
+ used = 0;
166
+ }
167
+ line += painted;
168
+ used += plain.length;
169
+ }
170
+ if (line || lines.length === 0) lines.push(line);
171
+ return lines;
172
+ }
173
+
174
+ /** Truncate a styled line without cutting an ANSI escape sequence. */
175
+ function truncateStyled(text, width, c) {
176
+ const value = String(text ?? '');
177
+ if (visibleLength(value) <= width) return value;
178
+ let output = '';
179
+ let visible = 0;
180
+ let index = 0;
181
+ while (index < value.length && visible < width - 1) {
182
+ if (value[index] === '\x1b') {
183
+ ANSI_AT.lastIndex = index;
184
+ const match = ANSI_AT.exec(value);
185
+ if (match) {
186
+ output += match[0];
187
+ index = ANSI_AT.lastIndex;
188
+ continue;
189
+ }
190
+ }
191
+ output += value[index];
192
+ visible += 1;
193
+ index += 1;
194
+ }
195
+ return `${output}${c.dim('…')}${c.level > 0 ? '\x1b[0m' : ''}`;
196
+ }
197
+
198
+ function padStyled(text, width, align, c) {
199
+ const padding = Math.max(0, width - visibleLength(text));
200
+ if (padding === 0) return text;
201
+ if (align === 'right') return `${' '.repeat(padding)}${text}`;
202
+ if (align === 'center') {
203
+ const left = Math.floor(padding / 2);
204
+ return `${' '.repeat(left)}${text}${' '.repeat(padding - left)}`;
205
+ }
206
+ return `${text}${' '.repeat(padding)}`;
207
+ }
208
+
209
+ function splitRow(line) {
210
+ let value = String(line).trim();
211
+ if (value.startsWith('|')) value = value.slice(1);
212
+ if (value.endsWith('|')) value = value.slice(0, -1);
213
+ return value
214
+ .replace(/\\\|/g, '\u0000')
215
+ .split('|')
216
+ .map((cell) => cell.replace(/\u0000/g, '|').trim());
217
+ }
218
+
219
+ function parseTable(lines, start) {
220
+ if (start + 1 >= lines.length) return null;
221
+ if (!String(lines[start]).includes('|') || !String(lines[start + 1]).includes('|')) return null;
222
+ const header = splitRow(lines[start]);
223
+ const separator = splitRow(lines[start + 1]);
224
+ if (header.length === 0 || header.length !== separator.length) return null;
225
+ const aligns = [];
226
+ for (const cell of separator) {
227
+ const value = cell.trim();
228
+ if (!/^:?-{3,}:?$/.test(value)) return null;
229
+ if (value.startsWith(':') && value.endsWith(':')) aligns.push('center');
230
+ else if (value.endsWith(':')) aligns.push('right');
231
+ else aligns.push('left');
232
+ }
233
+ const rows = [];
234
+ let index = start + 2;
235
+ while (index < lines.length) {
236
+ const line = lines[index];
237
+ if (!String(line).includes('|') || !String(line).trim()) break;
238
+ const cells = splitRow(line);
239
+ if (cells.length !== header.length) break;
240
+ rows.push(cells);
241
+ index += 1;
242
+ }
243
+ return { header, aligns, rows, next: index };
244
+ }
245
+
246
+ function renderVerticalTable(header, rows, { width, c, base = null, accent = null }) {
247
+ const output = [];
248
+ const inner = Math.max(8, width - 4);
249
+ const paint = toAccent(accent, c);
250
+ output.push(paint(`╭─ table ${'─'.repeat(Math.max(1, width - 10))}╮`));
251
+ for (const row of rows) {
252
+ for (let index = 0; index < header.length; index += 1) {
253
+ const label = truncateStyled(renderInline(header[index] || `column ${index + 1}`, c, base, paint), inner, c);
254
+ const value = renderInline(row[index] || '', c, base, paint);
255
+ output.push(`${paint('│')} ${c.bold(label)}`);
256
+ for (const line of wrapInline(value, inner, c, base, paint)) {
257
+ output.push(`${paint('│')} ${truncateStyled(line, inner, c)}`);
258
+ }
259
+ }
260
+ output.push(paint('│'));
261
+ }
262
+ output.push(paint(`╰${'─'.repeat(Math.max(1, width - 2))}╯`));
263
+ return output;
264
+ }
265
+
266
+ /** Fit a Markdown table into the available width. */
267
+ export function renderTable(header, aligns, rows, { width, c, base = null, accent = null }) {
268
+ const columns = header.length;
269
+ if (columns === 0) return [];
270
+ const paint = toAccent(accent, c);
271
+ const natural = [];
272
+ for (let index = 0; index < columns; index += 1) {
273
+ const candidates = [header[index] || '', ...rows.map((row) => row[index] || '')];
274
+ const longest = Math.max(
275
+ 3,
276
+ ...candidates.map((cell) => visibleLength(renderInline(cell, c, base, paint))),
277
+ );
278
+ natural.push(Math.min(32, longest));
279
+ }
280
+
281
+ const borderWidth = 3 * columns + 1;
282
+ const budget = width - borderWidth;
283
+ if (budget < columns * 3) {
284
+ return renderVerticalTable(header, rows, { width, c, base, accent: paint });
285
+ }
286
+
287
+ const widths = natural.map((value) => Math.max(3, value));
288
+ let total = widths.reduce((sum, value) => sum + value, 0);
289
+ while (total > budget) {
290
+ let widest = 0;
291
+ for (let index = 1; index < widths.length; index += 1) {
292
+ if (widths[index] > widths[widest]) widest = index;
293
+ }
294
+ if (widths[widest] <= 3) break;
295
+ widths[widest] -= 1;
296
+ total -= 1;
297
+ }
298
+ if (total > budget) return renderVerticalTable(header, rows, { width, c, base, accent: paint });
299
+
300
+ const wrapCell = (cell, colWidth) =>
301
+ wrapInline(cell || '', colWidth, c, base, paint).map((line) => truncateStyled(line, colWidth, c));
302
+ const headerLines = widths.map((colWidth, index) => wrapCell(header[index], colWidth));
303
+ const bodyLines = rows.map((row) =>
304
+ widths.map((colWidth, index) => wrapCell(row[index] || '', colWidth)),
305
+ );
306
+ const border = (left, middle, right) =>
307
+ paint(`${left}${widths.map((colWidth) => '─'.repeat(colWidth + 2)).join(middle)}${right}`);
308
+
309
+ const output = [border('╭', '┬', '╮')];
310
+ const pushRow = (cells, isHeader) => {
311
+ const height = Math.max(...cells.map((lines) => lines.length));
312
+ for (let lineIndex = 0; lineIndex < height; lineIndex += 1) {
313
+ const rendered = cells.map((lines, column) => {
314
+ const content = lines[lineIndex] ?? '';
315
+ const padded = padStyled(content, widths[column], aligns[column], c);
316
+ return isHeader ? c.bold(padded) : padded;
317
+ });
318
+ output.push(`${paint('│')} ${rendered.join(` ${paint('│')} `)} ${paint('│')}`);
319
+ }
320
+ };
321
+
322
+ pushRow(headerLines, true);
323
+ output.push(border('├', '┼', '┤'));
324
+ for (const row of bodyLines) pushRow(row, false);
325
+ output.push(border('╰', '┴', '╯'));
326
+ return output;
327
+ }
328
+
329
+ function highlightCode(line, lang, c, color) {
330
+ if (!color) return line;
331
+ if (/^\+\+\+|^---/.test(line)) return c.bold(line);
332
+ if (/^@@/.test(line)) return c.hex(CYAN_LIGHT)(line);
333
+ if (/^\+(?!\+\+)/.test(line)) return c.hex(GREEN)(line);
334
+ if (/^-(?!--)/.test(line)) return c.hex('#f87171')(line);
335
+ if (/^diff --git/.test(line)) return c.bold(c.hex(CYAN)(line));
336
+
337
+ let output = '';
338
+ let last = 0;
339
+ for (const match of line.matchAll(CODE_TOKEN_RE)) {
340
+ output += line.slice(last, match.index);
341
+ const token = match[0];
342
+ if (/^(\/\/|#|\/\*)/.test(token)) output += c.dim(token);
343
+ else if (/^['"`]/.test(token)) output += c.hex(GREEN)(token);
344
+ else if (/^\d/.test(token)) output += c.hex(AMBER)(token);
345
+ else output += c.hex(VIOLET)(token);
346
+ last = match.index + token.length;
347
+ }
348
+ output += line.slice(last);
349
+ return output;
350
+ }
351
+
352
+ function renderCodeBlock(lines, lang, { width, c, color, accent = null, copy = true }) {
353
+ const label = lang ? ` ${lang} ` : ' code ';
354
+ // The chip is the visible `⧉ copy` button: the TUI and the line renderer
355
+ // register the whole block as its hit target. It shrinks away on a block too
356
+ // narrow to read as a button instead of eating the whole rule.
357
+ const chip = copy ? ' ⧉ copy ' : '';
358
+ const showChip = Boolean(chip) && width - 2 - visibleLength(label) - visibleLength(chip) >= 4;
359
+ const rule = Math.max(1, width - 2 - visibleLength(label) - (showChip ? visibleLength(chip) : 0));
360
+ const paint = toAccent(accent, c);
361
+ const chipPaint = showChip ? c.bgHex('#1e293b')(c.hex(SLATE)(chip)) : '';
362
+ const output = [
363
+ `${paint('╭─')}${c.bold(label)}${c.dim('─'.repeat(rule))}${chipPaint}`,
364
+ ];
365
+ const body = lines.length > 0 ? lines : [''];
366
+ const codeWidth = Math.max(8, width - 2);
367
+ for (const line of body) {
368
+ const chunks = [];
369
+ for (let index = 0; index < line.length; index += codeWidth) {
370
+ chunks.push(line.slice(index, index + codeWidth));
371
+ }
372
+ if (chunks.length === 0) chunks.push('');
373
+ for (const chunk of chunks) {
374
+ output.push(`${paint('│')} ${highlightCode(chunk, lang, c, color)}`);
375
+ }
376
+ }
377
+ output.push(paint(`╰${'─'.repeat(Math.max(1, width - 2))}`));
378
+ return output;
379
+ }
380
+
381
+ function isHorizontalRule(line) {
382
+ return /^\s*([-*_])(?:\s*\1){2,}\s*$/.test(line);
383
+ }
384
+
385
+ function isBlockStart(lines, index) {
386
+ const line = lines[index] || '';
387
+ if (!line.trim()) return true;
388
+ if (/^\s*```/.test(line)) return true;
389
+ if (/^\s{0,3}#{1,6}\s+/.test(line)) return true;
390
+ if (/^\s*>\s?/.test(line)) return true;
391
+ if (isHorizontalRule(line)) return true;
392
+ if (/^(\s*)([-*+]|\d+[.)])\s+/.test(line)) return true;
393
+ if (parseTable(lines, index)) return true;
394
+ return false;
395
+ }
396
+
397
+ /**
398
+ * Render a Markdown message for a terminal.
399
+ *
400
+ * @param {string} text
401
+ * @param {object} options
402
+ * @param {number} [options.width] visible columns available
403
+ * @param {object} [options.c] chalk instance
404
+ * @param {boolean} [options.color] paint ANSI styles
405
+ * @param {Function} [options.base] default inline style
406
+ * @param {Function} [options.accent] accent paint for rules and tables
407
+ * @param {boolean} [options.copy] show the `⧉ copy` chip on code blocks
408
+ * @param {Function} [options.onCodeBlock] receives `{ lang, code, start, end }`
409
+ * where `start`/`end` are the rendered row range of the block, so a caller
410
+ * can make that range a clipboard hit target
411
+ * @returns {string[]} lines without a trailing newline and without outer indent
412
+ */
413
+ export function renderMarkdown(
414
+ text,
415
+ { width = 80, c = null, color = true, base = null, accent = null, copy = true, onCodeBlock = null } = {},
416
+ ) {
417
+ const chalk = toChalk(c, color);
418
+ const paint = toAccent(accent, chalk);
419
+ const lines = sanitize(text).split('\n');
420
+ const output = [];
421
+ let index = 0;
422
+
423
+ const pushBlank = () => {
424
+ if (output.length > 0 && output[output.length - 1] !== '') output.push('');
425
+ };
426
+
427
+ while (index < lines.length) {
428
+ const line = lines[index];
429
+ if (!line.trim()) {
430
+ pushBlank();
431
+ index += 1;
432
+ continue;
433
+ }
434
+
435
+ const fence = line.match(/^\s*```\s*([A-Za-z0-9_+#.-]*)\s*$/);
436
+ if (fence) {
437
+ const code = [];
438
+ index += 1;
439
+ while (index < lines.length && !/^\s*```\s*$/.test(lines[index])) {
440
+ code.push(lines[index]);
441
+ index += 1;
442
+ }
443
+ if (index < lines.length) index += 1;
444
+ pushBlank();
445
+ const start = output.length;
446
+ output.push(...renderCodeBlock(code, fence[1], { width, c: chalk, color, accent: paint, copy }));
447
+ if (typeof onCodeBlock === 'function') {
448
+ onCodeBlock({ lang: fence[1], code: code.join('\n'), start, end: output.length - 1 });
449
+ }
450
+ continue;
451
+ }
452
+
453
+ const table = parseTable(lines, index);
454
+ if (table) {
455
+ pushBlank();
456
+ output.push(...renderTable(table.header, table.aligns, table.rows, { width, c: chalk, base, accent: paint }));
457
+ index = table.next;
458
+ continue;
459
+ }
460
+
461
+ const heading = line.match(/^\s{0,3}(#{1,6})\s+(.*)$/);
462
+ if (heading) {
463
+ const level = heading[1].length;
464
+ const prefix = level === 1 ? '◆ ' : level === 2 ? '◇ ' : '· ';
465
+ const content = truncateStyled(renderInline(heading[2], chalk, base, paint), Math.max(4, width - 2), chalk);
466
+ output.push(`${paint(prefix)}${chalk.bold(content)}`);
467
+ if (level === 1) output.push(chalk.dim('─'.repeat(width)));
468
+ index += 1;
469
+ continue;
470
+ }
471
+
472
+ if (isHorizontalRule(line)) {
473
+ pushBlank();
474
+ output.push(chalk.dim('─'.repeat(width)));
475
+ pushBlank();
476
+ index += 1;
477
+ continue;
478
+ }
479
+
480
+ if (/^\s*>\s?/.test(line)) {
481
+ const quote = [];
482
+ while (index < lines.length && /^\s*>\s?/.test(lines[index])) {
483
+ quote.push(lines[index].replace(/^\s*>\s?/, ''));
484
+ index += 1;
485
+ }
486
+ pushBlank();
487
+ for (const wrapped of wrapInline(quote.join(' '), Math.max(4, width - 2), chalk, base, paint)) {
488
+ output.push(`${paint('│')} ${chalk.dim(chalk.italic(truncateStyled(wrapped, width - 2, chalk)))}`);
489
+ }
490
+ continue;
491
+ }
492
+
493
+ const list = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
494
+ if (list) {
495
+ const indent = list[1].replace(/\t/g, ' ');
496
+ const marker = list[2];
497
+ const ordered = /^\d/.test(marker);
498
+ const checkbox = list[3].match(/^\[([ xX])\]\s+(.*)$/);
499
+ const textContent = checkbox ? checkbox[2] : list[3];
500
+ const bullet = checkbox
501
+ ? (checkbox[1].toLowerCase() === 'x' ? chalk.hex(GREEN)('✓') : chalk.dim('○'))
502
+ : (ordered ? paint(marker) : paint('▸'));
503
+ const prefixWidth = indent.length + 2;
504
+ const wrapped = wrapInline(textContent, Math.max(4, width - prefixWidth), chalk, base, paint);
505
+ output.push(`${indent}${bullet} ${wrapped[0] || ''}`);
506
+ for (const continuation of wrapped.slice(1)) {
507
+ output.push(`${' '.repeat(prefixWidth)}${continuation}`);
508
+ }
509
+ index += 1;
510
+ continue;
511
+ }
512
+
513
+ const paragraph = [line.trim()];
514
+ index += 1;
515
+ while (index < lines.length && !isBlockStart(lines, index)) {
516
+ paragraph.push(lines[index].trim());
517
+ index += 1;
518
+ }
519
+ pushBlank();
520
+ for (const wrapped of wrapInline(paragraph.join(' '), width, chalk, base, paint)) {
521
+ output.push(wrapped);
522
+ }
523
+ }
524
+
525
+ while (output.length > 0 && output[output.length - 1] === '') output.pop();
526
+ return output;
527
+ }