osai-agent 4.0.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 (86) hide show
  1. package/LICENSE +7 -0
  2. package/package.json +72 -0
  3. package/src/agent/context.js +141 -0
  4. package/src/agent/loop/context-summary.js +196 -0
  5. package/src/agent/loop/directory-utils.js +102 -0
  6. package/src/agent/loop/local.js +196 -0
  7. package/src/agent/loop/loop-detection.js +288 -0
  8. package/src/agent/loop/stream-parser.js +515 -0
  9. package/src/agent/loop/tool-executor.js +470 -0
  10. package/src/agent/loop/verification.js +263 -0
  11. package/src/agent/loop/websocket.js +80 -0
  12. package/src/agent/prompt.js +259 -0
  13. package/src/agent/react-loop.js +697 -0
  14. package/src/agent/subagent.js +263 -0
  15. package/src/commands/config.js +53 -0
  16. package/src/commands/connect.js +190 -0
  17. package/src/commands/devices.js +121 -0
  18. package/src/commands/login.js +77 -0
  19. package/src/commands/logout.js +31 -0
  20. package/src/commands/mcp.js +258 -0
  21. package/src/commands/provider.js +633 -0
  22. package/src/commands/register.js +74 -0
  23. package/src/commands/run.js +150 -0
  24. package/src/commands/search.js +64 -0
  25. package/src/commands/session.js +57 -0
  26. package/src/commands/skills.js +54 -0
  27. package/src/commands/stop-subagent.js +58 -0
  28. package/src/index.js +208 -0
  29. package/src/llm/direct.js +317 -0
  30. package/src/memory/store.js +215 -0
  31. package/src/mock-readline.js +27 -0
  32. package/src/parser/dependencies.js +71 -0
  33. package/src/parser/markdown.js +505 -0
  34. package/src/parser/stream.js +96 -0
  35. package/src/prompts/modes/CODING.js +160 -0
  36. package/src/prompts/modes/GENERAL.js +105 -0
  37. package/src/prompts/modes/NETWORK.js +69 -0
  38. package/src/prompts/modes/SSH.js +53 -0
  39. package/src/prompts/systemPrompt.js +85 -0
  40. package/src/safety/check.js +210 -0
  41. package/src/services/crypto.js +78 -0
  42. package/src/services/executor.js +68 -0
  43. package/src/services/history.js +58 -0
  44. package/src/services/server-url.js +11 -0
  45. package/src/services/session.js +194 -0
  46. package/src/services/ssh.js +176 -0
  47. package/src/services/websocket.js +112 -0
  48. package/src/skills/loader.js +231 -0
  49. package/src/tools/browser.js +434 -0
  50. package/src/tools/local.js +1254 -0
  51. package/src/tools/mcp-client.js +209 -0
  52. package/src/tools/registry.js +132 -0
  53. package/src/tools/search-providers.js +237 -0
  54. package/src/tools/ssh.js +74 -0
  55. package/src/ui/App.js +2031 -0
  56. package/src/ui/animation.js +47 -0
  57. package/src/ui/components/AskUserDialog.js +33 -0
  58. package/src/ui/components/ConfirmationDialog.js +45 -0
  59. package/src/ui/components/DiffView.js +201 -0
  60. package/src/ui/components/Header.js +157 -0
  61. package/src/ui/components/HistoryPicker.js +130 -0
  62. package/src/ui/components/InputShell.js +22 -0
  63. package/src/ui/components/MessageHistory.js +1200 -0
  64. package/src/ui/components/ModalPanel.js +40 -0
  65. package/src/ui/components/ModePicker.js +161 -0
  66. package/src/ui/components/PlanDialog.js +48 -0
  67. package/src/ui/components/ProviderMenu.js +1095 -0
  68. package/src/ui/components/SavePicker.js +106 -0
  69. package/src/ui/components/SelectMenu.js +194 -0
  70. package/src/ui/components/SlashMenu.js +168 -0
  71. package/src/ui/components/SubagentPanel.js +138 -0
  72. package/src/ui/components/TextInputSafe.js +117 -0
  73. package/src/ui/components/TodoPanel.js +54 -0
  74. package/src/ui/components/ToolExecution.js +261 -0
  75. package/src/ui/components/TranscriptViewport.js +99 -0
  76. package/src/ui/diff.js +249 -0
  77. package/src/ui/h.js +7 -0
  78. package/src/ui/mouse-scroll.js +63 -0
  79. package/src/ui/slash-picker.js +58 -0
  80. package/src/ui/terminal.js +41 -0
  81. package/src/ui/theme.js +5 -0
  82. package/src/ui/welcome.js +12 -0
  83. package/src/utils/constants.js +231 -0
  84. package/src/utils/helpers.js +154 -0
  85. package/src/utils/logger.js +81 -0
  86. package/src/utils/sound.js +33 -0
@@ -0,0 +1,505 @@
1
+ // =============================================================================
2
+ // OS AI Agent — Markdown Renderer v4.0
3
+ // =============================================================================
4
+ // Two modes:
5
+ // renderMarkdown(text) — full buffered render (après réception complète)
6
+ // StreamMarkdown — classe stateful pour le streaming token-by-token
7
+ // =============================================================================
8
+
9
+ import chalk from 'chalk';
10
+
11
+ // ─── Tokyo Night color tokens ─────────────────────────────────
12
+ const C = {
13
+ text: '#c0caf5',
14
+ bright: '#e2e8f0',
15
+ blue: '#7aa2f7',
16
+ cyan: '#7dcfff',
17
+ teal: '#2dd4bf',
18
+ green: '#9ece6a',
19
+ emerald: '#34d399',
20
+ amber: '#e0af68',
21
+ red: '#f7768e',
22
+ soft: '#9aa5ce',
23
+ subtle: '#565f89',
24
+ dim: '#414868',
25
+ muted: '#3b3f52',
26
+ surface: '#1a1b26',
27
+ border: '#2a2e3f',
28
+ };
29
+
30
+ const ELLIPSIS = '…';
31
+ const FORCE_ASCII_TABLES =
32
+ process.env.OSAI_TABLE_ASCII === '1' ||
33
+ process.env.NO_UNICODE_TABLES === '1' ||
34
+ (process.env.TERM || '').toLowerCase() === 'dumb';
35
+
36
+ // ─── Tool names (for stripping tool JSON from output) ────────
37
+ const ALL_TOOLS = 'LOCAL_CMD|SSH_CMD|READ_FILE|WRITE_FILE|EDIT_FILE|APPEND_FILE|DELETE_FILE|LIST_DIR|SEARCH_FILE|CREATE_DIR|TREE_VIEW|RUN_SCRIPT|MOVE_FILE|COPY_FILE|FILE_INFO|FETCH_URL|WEB_SEARCH|TODO_ADD|TODO_COMPLETE|TODO_UPDATE|TODO_LIST|TODO_CLEAR|POWERSHELL|GLOB|GREP|GIT|DIAG_POST_EDIT|ASK_USER|PLAN_MODE|SKILL_LIST|LOAD_SKILL|CREATE_SKILL|TASK';
38
+
39
+ const stripAnsi = (str) => (str || '')
40
+ .replace(/\x1b\[[0-9;]*m/g, '')
41
+ .replace(/\x1b\]8;;[^\x1b]*\x1b\\/g, '')
42
+ .replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
43
+
44
+ const visibleLen = (str) => stripAnsi(str).length;
45
+
46
+ export const stripToolJSON = (text) => {
47
+ if (!text) return '';
48
+ return text
49
+ .replace(new RegExp(`\\{[^{}]*"tool"\\s*:\\s*"(?:${ALL_TOOLS})"[^{}]*\\}`, 'g'), '')
50
+ .replace(new RegExp(`\\{[\\s\\S]*?"tool"\\s*:\\s*"(?:${ALL_TOOLS})"[\\s\\S]*?\\}`, 'g'), '')
51
+ .replace(/```(?:json)?\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/g, '')
52
+ .replace(/\[TOOL_RESULT\][\s\S]*?(?=\n\n|\n[A-Z\u00C0-\u024F]|$)/g, '')
53
+ .replace(/\[DONE\]/gi, '').replace(/\[INCOMPLETE\]/gi, '').replace(/\[BLOCKED\]/gi, '')
54
+ .replace(/\[TOOL_CALL\]\s*/g, '')
55
+ .replace(/THE TOOL\s*CALL[\s\S]*?(?=\n\n|$)/gi, '')
56
+ .replace(/\n{3,}/g, '\n\n')
57
+ .trim();
58
+ };
59
+
60
+ export const stripToolXML = (text) => {
61
+ if (!text) return '';
62
+ return text
63
+ .replace(/<tool\b[^>]*>[\s\S]*?<\/tool\s*>/g, '')
64
+ .replace(/<tool\b[^>]*\/>/g, '')
65
+ .replace(/\n{3,}/g, '\n\n')
66
+ .trim();
67
+ };
68
+
69
+ // ─── Inline style renderer ────────────────────────────────────
70
+ export const renderInline = (text) => {
71
+ // Strip any raw ANSI escape sequences the LLM may have included in its output
72
+ const clean = text.replace(/\x1b\[[0-9;]*[mA-Za-z]/g, '').replace(/\x1b\][^\x1b]*\x1b\\/g, '');
73
+ return clean
74
+ .replace(/\*\*\*([^*]+)\*\*\*/g, (_, t) => chalk.bold.italic.hex(C.bright)(t))
75
+ .replace(/\*\*([^*]+)\*\*/g, (_, t) => chalk.bold.hex(C.bright)(t))
76
+ .replace(/\*([^*\n]+)\*/g, (_, t) => chalk.italic.hex(C.soft)(t))
77
+ .replace(/`([^`\n]+)`/g, (_, t) => chalk.bgHex(C.surface).hex(C.cyan)(` ${t} `))
78
+ .replace(/~~([^~]+)~~/g, (_, t) => chalk.strikethrough.hex(C.subtle)(t))
79
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) =>
80
+ chalk.underline.hex(C.blue)(label) + chalk.hex(C.dim)(` (${url})`)
81
+ );
82
+ };
83
+
84
+ const splitTableRow = (row) => {
85
+ const s = String(row || '').trim();
86
+ if (!s.includes('|')) return [s];
87
+ const src = s.startsWith('|') ? s.slice(1) : s;
88
+ const body = src.endsWith('|') ? src.slice(0, -1) : src;
89
+ return body.split('|').map(c => c.trim());
90
+ };
91
+
92
+ const isMarkdownTableLine = (line) => {
93
+ const t = String(line || '').trim();
94
+ if (!t.includes('|')) return false;
95
+ const cells = splitTableRow(t);
96
+ return cells.length >= 2;
97
+ };
98
+
99
+ const isTableSeparatorLine = (line) => {
100
+ const cells = splitTableRow(line).filter(c => c.length > 0);
101
+ if (!cells.length) return false;
102
+ return cells.every(c => /^:?\s*-{3,}\s*:?$/.test(c));
103
+ };
104
+
105
+ const padAnsi = (text, width) => {
106
+ const pad = Math.max(0, width - visibleLen(text));
107
+ return text + ' '.repeat(pad);
108
+ };
109
+
110
+ const renderTableRows = (rows, maxWidth = 96) => {
111
+ if (!rows || !rows.length) return [];
112
+ const parsed = rows.map(r => splitTableRow(r));
113
+ const nonSep = parsed.filter((_, idx) => !isTableSeparatorLine(rows[idx]));
114
+ if (!nonSep.length) return [];
115
+
116
+ const colCount = Math.max(...nonSep.map(r => r.length));
117
+ const normalized = parsed.map(r => {
118
+ const out = r.slice(0, colCount);
119
+ while (out.length < colCount) out.push('');
120
+ return out;
121
+ });
122
+
123
+ const useUnicode = !FORCE_ASCII_TABLES;
124
+ const colSep = useUnicode ? ' │ ' : ' | ';
125
+ const rowSep = useUnicode ? '─┼─' : '-+-';
126
+ const rowFill = useUnicode ? '─' : '-';
127
+ const maxColWidth = Math.max(8, Math.floor((Math.max(60, maxWidth) - (colCount - 1) * colSep.length - 2) / colCount));
128
+ const widths = Array.from({ length: colCount }, () => 3);
129
+ for (const r of normalized) {
130
+ for (let i = 0; i < colCount; i++) {
131
+ const len = visibleLen(stripAnsi(renderInline(r[i] || '')));
132
+ widths[i] = Math.min(maxColWidth, Math.max(widths[i], len));
133
+ }
134
+ }
135
+
136
+ const widthBudget = Math.max(24, maxWidth - 2);
137
+ const totalWidth = () => widths.reduce((a, b) => a + b, 0) + (colCount - 1) * colSep.length + 2;
138
+ const minColWidth = 3;
139
+ while (totalWidth() > widthBudget) {
140
+ let idx = -1;
141
+ let best = -1;
142
+ for (let i = 0; i < widths.length; i++) {
143
+ if (widths[i] > minColWidth && widths[i] > best) { best = widths[i]; idx = i; }
144
+ }
145
+ if (idx < 0) break;
146
+ widths[idx] -= 1;
147
+ }
148
+
149
+ const fitCell = (raw, width) => {
150
+ const plain = stripAnsi(renderInline(raw || ''));
151
+ if (plain.length <= width) return plain;
152
+ if (width <= 1) return plain.slice(0, width);
153
+ return plain.slice(0, width - 1) + ELLIPSIS;
154
+ };
155
+
156
+ const out = [];
157
+ let sawHeader = false;
158
+ for (let rowIdx = 0; rowIdx < normalized.length; rowIdx++) {
159
+ const raw = rows[rowIdx];
160
+ if (isTableSeparatorLine(raw)) {
161
+ const sep = widths.map(w => rowFill.repeat(Math.max(3, w))).join(chalk.hex(C.border)(rowSep));
162
+ out.push(chalk.hex(C.border)(' ' + sep));
163
+ sawHeader = true;
164
+ continue;
165
+ }
166
+
167
+ const r = normalized[rowIdx];
168
+ const cells = r.map((cell, i) => {
169
+ const fitted = fitCell(cell || '', widths[i]);
170
+ const styled = !sawHeader && rowIdx === 0 ? chalk.bold.hex(C.cyan)(fitted) : chalk.hex(C.text)(fitted);
171
+ return padAnsi(styled, widths[i]);
172
+ });
173
+ out.push(' ' + cells.join(chalk.hex(C.border)(colSep)));
174
+ }
175
+
176
+ return out;
177
+ };
178
+
179
+ // ─── Syntax highlighter ───────────────────────────────────────
180
+ const KEYWORDS_JS = /\b(import|export|from|const|let|var|function|async|await|return|if|else|for|while|class|extends|new|try|catch|finally|throw|typeof|instanceof|null|undefined|true|false|void|switch|case|break|continue|default|of|in|do|yield|get|set|static|super)\b/g;
181
+ const KEYWORDS_SH = /\b(echo|sudo|apt|npm|pip|git|cd|ls|mkdir|rm|mv|cp|chmod|chown|systemctl|service|grep|awk|sed|cat|tail|head|curl|wget|ssh|scp|export|source|alias|if|then|fi|else|for|do|done|while|case|esac)\b/g;
182
+ const KEYWORDS_PY = /\b(def|class|import|from|as|return|if|elif|else|for|while|try|except|finally|with|lambda|yield|pass|break|continue|not|and|or|in|is|None|True|False|print|len|range)\b/g;
183
+
184
+ export const highlightCode = (code, lang = '') => {
185
+ const l = lang.toLowerCase();
186
+ return code.split('\n').map(line => {
187
+ let out = line
188
+ // Strings
189
+ .replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g,
190
+ (_, s) => chalk.hex(C.green)(s))
191
+ // Numbers
192
+ .replace(/\b(\d+\.?\d*)\b/g, (_, n) => chalk.hex(C.amber)(n))
193
+ // Comments
194
+ .replace(/(\/\/.*|#.*)$/, (_, c) => chalk.hex(C.subtle)(c))
195
+ .replace(/(\/\*[\s\S]*?\*\/)/, (_, c) => chalk.hex(C.subtle)(c));
196
+
197
+ if (l === 'py' || l === 'python') {
198
+ out = out.replace(KEYWORDS_PY, (_, kw) => chalk.hex(C.blue)(kw));
199
+ } else if (l === 'sh' || l === 'bash' || l === 'shell' || l === 'zsh') {
200
+ out = out.replace(KEYWORDS_SH, (_, kw) => chalk.hex(C.cyan)(kw));
201
+ } else {
202
+ // JS/TS/JSX/TSX/JSON/default
203
+ out = out.replace(KEYWORDS_JS, (_, kw) => chalk.hex(C.blue)(kw));
204
+ out = out.replace(KEYWORDS_SH, (_, kw) => chalk.hex(C.cyan)(kw));
205
+ }
206
+ return out;
207
+ }).join('\n');
208
+ };
209
+
210
+ // ─── Full buffered markdown renderer ─────────────────────────
211
+ export const renderMarkdown = (text) => {
212
+ // Strip raw ANSI sequences the LLM may have embedded in its output
213
+ const deAnsi = (text || '').replace(/\x1b\[[0-9;]*[mA-Za-z]/g, '').replace(/\x1b\][^\x1b]*\x1b\\/g, '');
214
+ const normalizedStructure = deAnsi
215
+ // ---### + optional text → big white dot divider (must precede HR+heading fusion)
216
+ .replace(/(^|\n)\s*-{3,}\s*#{3}\s*([^\n]*)/g, '$1__BIGDOT__$2')
217
+ // Fix fused horizontal-rule + heading, e.g. "---## Title" -> "## Title"
218
+ .replace(/(^|\n)\s*(?:-{3,}|\*{3,}|_{3,})\s*(#{1,4})\s*/g, '$1$2 ')
219
+ // Fix collapsed headings without space, e.g. "###Title" -> "### Title"
220
+ .replace(/(^|\n)(#{1,4})([^#\s\n])/g, '$1$2 $3');
221
+ const clean = stripToolXML(stripToolJSON(normalizedStructure));
222
+ if (!clean) return '';
223
+
224
+ const lines = clean.split('\n');
225
+ const output = [];
226
+ let inCode = false, codeLang = '', codeLines = [];
227
+ let inTable = false, tableRows = [];
228
+ const w = Math.min(process.stdout.columns || 96, 100);
229
+
230
+ const flushTable = () => {
231
+ if (!tableRows.length) return;
232
+ output.push(...renderTableRows(tableRows, w));
233
+ output.push('');
234
+ tableRows = []; inTable = false;
235
+ };
236
+
237
+ for (const rawLine of lines) {
238
+ // Code block toggle
239
+ const codeMatch = rawLine.match(/^```(\w*)/);
240
+ if (codeMatch) {
241
+ if (!inCode) {
242
+ inCode = true; codeLang = codeMatch[1] || ''; codeLines = [];
243
+ const label = codeLang ? chalk.bgHex(C.border).hex(C.cyan)(` ${codeLang} `) : '';
244
+ output.push(chalk.hex(C.border)(' ╭─') + (label || chalk.hex(C.border)('─')));
245
+ } else {
246
+ const highlighted = highlightCode(codeLines.join('\n'), codeLang);
247
+ for (const cl of highlighted.split('\n'))
248
+ output.push(chalk.hex(C.border)(' │ ') + cl);
249
+ output.push(chalk.hex(C.border)(' ╰────'));
250
+ inCode = false; codeLines = []; codeLang = '';
251
+ }
252
+ continue;
253
+ }
254
+ if (inCode) { codeLines.push(rawLine); continue; }
255
+
256
+ // Table
257
+ if (isMarkdownTableLine(rawLine)) {
258
+ inTable = true; tableRows.push(rawLine.trim()); continue;
259
+ } else if (inTable) flushTable();
260
+
261
+ // Big white dot divider: ---### [optional text]
262
+ const bigDot = rawLine.match(/(?:^\s*-{3,}\s*#{3}|^__BIGDOT__)\s*(.*)/);
263
+ if (bigDot) {
264
+ const text = bigDot[1].trim();
265
+ output.push('');
266
+ output.push(chalk.bold.hex(C.bright)(' ●') + (text ? ' ' + chalk.hex(C.bright)(renderInline(text)) : ''));
267
+ continue;
268
+ }
269
+
270
+ // Headings
271
+ const h1 = rawLine.match(/^# ?(.+)/);
272
+ if (h1) { output.push(''); output.push(chalk.bold.hex(C.blue)(' ━━ ') + chalk.bold.hex(C.bright)(renderInline(h1[1]))); output.push(chalk.hex(C.border)(' ' + '─'.repeat(Math.min(w - 4, 58)))); continue; }
273
+ const h2 = rawLine.match(/^## ?(.+)/);
274
+ if (h2) { output.push(''); output.push(chalk.bold.hex(C.cyan)(' ◆ ') + chalk.bold.hex(C.text)(renderInline(h2[1]))); continue; }
275
+ const h3 = rawLine.match(/^### ?(.+)/);
276
+ if (h3) { output.push(''); output.push(chalk.bold.hex(C.teal)(' ● ') + chalk.bold.hex(C.soft)(renderInline(h3[1]))); continue; }
277
+ const h4 = rawLine.match(/^#### ?(.+)/);
278
+ if (h4) { output.push(chalk.hex(C.subtle)(' › ') + chalk.italic.hex(C.soft)(renderInline(h4[1]))); continue; }
279
+
280
+ // HR
281
+ if (rawLine.match(/^[-*_]{3,}\s*$/)) { output.push(chalk.hex(C.border)(' ' + '─'.repeat(Math.min(w - 4, 60)))); continue; }
282
+
283
+ // Blockquote
284
+ const bq = rawLine.match(/^>\s*(.*)/);
285
+ if (bq) { output.push(chalk.hex(C.border)(' ┃ ') + chalk.italic.hex(C.soft)(renderInline(bq[1]))); continue; }
286
+
287
+ // Lists
288
+ const num = rawLine.match(/^(\s*)(\d+)\.\s+(.*)/);
289
+ if (num) { const [, ind, n, c] = num; const lv = Math.floor(ind.length / 2); output.push(' '.repeat(lv + 1) + chalk.hex(C.blue)(n + '.') + ' ' + renderInline(c)); continue; }
290
+ const ul = rawLine.match(/^(\s*)[-*•]\s+(.*)/);
291
+ if (ul) { const [, ind, c] = ul; const lv = Math.floor(ind.length / 2); const b = lv === 0 ? chalk.hex(C.emerald)('•') : lv === 1 ? chalk.hex(C.cyan)('◦') : chalk.hex(C.subtle)('▸'); output.push(' '.repeat(lv + 1) + b + ' ' + renderInline(c)); continue; }
292
+
293
+ // Empty line
294
+ if (!rawLine.trim()) { output.push(''); continue; }
295
+
296
+ // Normal text
297
+ output.push(' ' + renderInline(rawLine));
298
+ }
299
+
300
+ if (inTable) flushTable();
301
+ if (inCode && codeLines.length > 0) {
302
+ for (const cl of codeLines) output.push(chalk.hex(C.border)(' │ ') + chalk.hex(C.text)(cl));
303
+ output.push(chalk.hex(C.border)(' ╰────'));
304
+ }
305
+
306
+ return output.join('\n');
307
+ };
308
+
309
+ // ─── StreamMarkdown — stateful streaming renderer ─────────────
310
+ // Usage:
311
+ // const sm = new StreamMarkdown();
312
+ // sm.push(chunk) → returns string to print immediately
313
+ // sm.flush() → returns remaining buffered content
314
+ // sm.reset() → clears state for next message
315
+ //
316
+ // Handles:
317
+ // - Code blocks accumulate until closing ``` then render fully
318
+ // - Bold/italic rendered when markers are complete (no half-markers)
319
+ // - Headings, lists, blockquotes rendered line-by-line
320
+ // - Tool JSON swallowed silently
321
+
322
+ export class StreamMarkdown {
323
+ constructor() { this.reset(); }
324
+
325
+ reset() {
326
+ this._buf = ''; // incomplete current line buffer
327
+ this._inCode = false;
328
+ this._codeLang = '';
329
+ this._codeLines = [];
330
+ this._codeHeader = ''; // the ╭─ line printed when code starts
331
+ this._jsonBuf = '';
332
+ this._inJson = false;
333
+ this._jsonDepth = 0;
334
+ this._xmlToolBuf = '';
335
+ this._inXmlTool = false;
336
+ this._inTable = false;
337
+ this._tableRows = [];
338
+ }
339
+
340
+ push(chunk) {
341
+ // Swallow tool JSON silently
342
+ let out = '';
343
+ // Normalize "###Title" → "### Title" and "##Title" → "## Title" before buffering
344
+ const normalized = chunk
345
+ // ---### + optional text → big white dot divider (must precede HR+heading fusion)
346
+ .replace(/(^|\n)\s*-{3,}\s*#{3}\s*([^\n]*)/g, '$1__BIGDOT__$2')
347
+ // Fix fused HR + heading in stream chunks: "---## Title" -> "## Title"
348
+ .replace(/(^|\n)\s*(?:-{3,}|\*{3,}|_{3,})\s*(#{1,4})\s*/g, '$1$2 ')
349
+ // Fix collapsed heading spacing
350
+ .replace(/(^|\n)(#{1,4})([^#\s\n])/g, '$1$2 $3');
351
+ this._buf += normalized;
352
+
353
+ // Process complete lines
354
+ const lines = this._buf.split('\n');
355
+ this._buf = lines.pop() ?? '';
356
+
357
+ for (const line of lines) out += this._processLine(line + '\n');
358
+ return out;
359
+ }
360
+
361
+ flush() {
362
+ let out = '';
363
+ if (this._buf.trim() && this._inTable && isMarkdownTableLine(this._buf)) {
364
+ this._tableRows.push(this._buf.trim());
365
+ this._buf = '';
366
+ }
367
+ if (this._inTable && this._tableRows.length) {
368
+ out += this._flushTable();
369
+ }
370
+ if (this._inCode && this._buf) {
371
+ this._codeLines.push(this._buf);
372
+ this._buf = '';
373
+ }
374
+ if (this._inCode && this._codeLines.length) {
375
+ // Close unclosed code block
376
+ const highlighted = highlightCode(this._codeLines.join('\n'), this._codeLang);
377
+ for (const cl of highlighted.split('\n'))
378
+ out += chalk.hex(C.border)(' │ ') + cl + '\n';
379
+ out += chalk.hex(C.border)(' ╰────') + '\n';
380
+ } else if (this._buf.trim()) {
381
+ out += this._renderLine(this._buf);
382
+ }
383
+ this.reset();
384
+ return out;
385
+ }
386
+
387
+ _processLine(line) {
388
+ const raw = line.replace(/\n$/, '');
389
+
390
+ if (this._inTable && !isMarkdownTableLine(raw)) {
391
+ const rendered = this._flushTable();
392
+ if (!raw.trim()) return rendered;
393
+ return rendered + this._renderLine(raw) + '\n';
394
+ }
395
+
396
+ // Tool JSON detection — swallow silently
397
+ if (this._inJson) {
398
+ this._jsonBuf += raw;
399
+ this._jsonDepth += (raw.match(/{/g)||[]).length - (raw.match(/}/g)||[]).length;
400
+ if (this._jsonDepth <= 0) { this._inJson = false; this._jsonBuf = ''; }
401
+ return '';
402
+ }
403
+ if (raw.trim().match(/^\{"tool"\s*:/)) {
404
+ this._inJson = true;
405
+ this._jsonBuf = raw;
406
+ this._jsonDepth = (raw.match(/{/g)||[]).length - (raw.match(/}/g)||[]).length;
407
+ if (this._jsonDepth <= 0) { this._inJson = false; this._jsonBuf = ''; }
408
+ return '';
409
+ }
410
+
411
+ // Tool XML detection — swallow silently
412
+ if (this._inXmlTool) {
413
+ this._xmlToolBuf += raw;
414
+ if (raw.includes('/>') || raw.includes('</tool>')) { this._inXmlTool = false; this._xmlToolBuf = ''; }
415
+ return '';
416
+ }
417
+ if (raw.trim().startsWith('<tool') && (raw.includes('/>') || raw.includes('</tool>'))) {
418
+ return '';
419
+ }
420
+ if (raw.trim().startsWith('<tool')) {
421
+ this._inXmlTool = true;
422
+ this._xmlToolBuf = raw;
423
+ return '';
424
+ }
425
+ // Strip completion signals
426
+ if (/^\s*\[(DONE|INCOMPLETE|BLOCKED|TOOL_CALL|TOOL_RESULT)\]/.test(raw)) return '';
427
+
428
+ // Code block
429
+ const codeMatch = raw.match(/^```(\w*)/);
430
+ if (codeMatch) {
431
+ if (!this._inCode) {
432
+ this._inCode = true; this._codeLang = codeMatch[1] || ''; this._codeLines = [];
433
+ const label = this._codeLang ? chalk.bgHex(C.border).hex(C.cyan)(` ${this._codeLang} `) : '';
434
+ return chalk.hex(C.border)(' ╭─') + (label || chalk.hex(C.border)('─')) + '\n';
435
+ } else {
436
+ const highlighted = highlightCode(this._codeLines.join('\n'), this._codeLang);
437
+ let out = '';
438
+ for (const cl of highlighted.split('\n')) out += chalk.hex(C.border)(' │ ') + cl + '\n';
439
+ out += chalk.hex(C.border)(' ╰────') + '\n';
440
+ this._inCode = false; this._codeLines = []; this._codeLang = '';
441
+ return out;
442
+ }
443
+ }
444
+ if (this._inCode) { this._codeLines.push(raw); return ''; }
445
+
446
+ if (isMarkdownTableLine(raw)) {
447
+ this._inTable = true;
448
+ this._tableRows.push(raw.trim());
449
+ return '';
450
+ }
451
+
452
+ return this._renderLine(raw) + '\n';
453
+ }
454
+
455
+ _flushTable() {
456
+ if (!this._tableRows.length) {
457
+ this._inTable = false;
458
+ return '';
459
+ }
460
+ const w = Math.min(process.stdout.columns || 96, 100);
461
+ const lines = renderTableRows(this._tableRows, w);
462
+ this._tableRows = [];
463
+ this._inTable = false;
464
+ return lines.length ? lines.join('\n') + '\n' : '';
465
+ }
466
+
467
+ _renderLine(line) {
468
+ if (!line.trim()) return '';
469
+
470
+ // Big white dot divider: ---### [optional text]
471
+ const bigDot = line.match(/(?:^\s*-{3,}\s*#{3}|^__BIGDOT__)\s*(.*)/);
472
+ if (bigDot) {
473
+ const text = bigDot[1].trim();
474
+ return '\n' + chalk.bold.hex(C.bright)(' ●') + (text ? ' ' + chalk.hex(C.bright)(renderInline(text)) : '') + '\n';
475
+ }
476
+
477
+ // Headings — support both "## Title" and "##Title" (no space)
478
+ const h1 = line.match(/^# ?(.+)/); if (h1) return '\n' + chalk.bold.hex(C.blue)(' ━━ ') + chalk.bold.hex(C.bright)(renderInline(h1[1])) + '\n' + chalk.hex(C.border)(' ' + '─'.repeat(56));
479
+ const h2 = line.match(/^## ?(.+)/); if (h2) return '\n' + chalk.bold.hex(C.cyan)(' ◆ ') + chalk.bold(renderInline(h2[1]));
480
+ const h3 = line.match(/^### ?(.+)/); if (h3) return '\n' + chalk.bold.hex(C.teal)(' ● ') + chalk.hex(C.soft)(renderInline(h3[1]));
481
+ const h4 = line.match(/^#### ?(.+)/); if (h4) return chalk.hex(C.subtle)(' › ') + chalk.italic.hex(C.soft)(renderInline(h4[1]));
482
+
483
+ // HR
484
+ if (line.match(/^[-*_]{3,}\s*$/)) return chalk.hex(C.border)(' ' + '─'.repeat(56));
485
+
486
+ // Blockquote
487
+ const bq = line.match(/^>\s*(.*)/); if (bq) return chalk.hex(C.border)(' ┃ ') + chalk.italic.hex(C.soft)(renderInline(bq[1]));
488
+
489
+ // Lists
490
+ const num = line.match(/^(\s*)(\d+)\.\s+(.*)/);
491
+ if (num) { const [, ind, n, c] = num; const lv = Math.floor(ind.length / 2); return ' '.repeat(lv + 1) + chalk.hex(C.blue)(n + '.') + ' ' + renderInline(c); }
492
+ const ul = line.match(/^(\s*)[-*•]\s+(.*)/);
493
+ if (ul) { const [, ind, c] = ul; const lv = Math.floor(ind.length / 2); const b = lv === 0 ? chalk.hex(C.emerald)('•') : lv === 1 ? chalk.hex(C.cyan)('◦') : chalk.hex(C.subtle)('▸'); return ' '.repeat(lv + 1) + b + ' ' + renderInline(c); }
494
+
495
+ return ' ' + renderInline(line);
496
+ }
497
+ }
498
+
499
+ // ─── Legacy export (kept for compatibility) ───────────────────
500
+ export const renderMarkdownChunk = (chunk) => {
501
+ try {
502
+ const sm = new StreamMarkdown();
503
+ return sm.push(chunk) + sm.flush();
504
+ } catch { return chunk; }
505
+ };
@@ -0,0 +1,96 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ /**
4
+ * StreamParser — parses the raw SSE text stream from the server.
5
+ *
6
+ * Emits:
7
+ * 'tool_call' — a complete JSON tool call object
8
+ * 'markdown_chunk' — a string of markdown/text to render
9
+ */
10
+ export class StreamParser extends EventEmitter {
11
+ constructor() {
12
+ super();
13
+ this.buffer = '';
14
+ this.jsonBuffer = '';
15
+ this.inJsonBlock = false;
16
+ this.braceDepth = 0;
17
+ }
18
+
19
+ process(chunk) {
20
+ this.buffer += chunk;
21
+ this._parseBuffer();
22
+ }
23
+
24
+ _parseBuffer() {
25
+ const lines = this.buffer.split('\n');
26
+ this.buffer = lines.pop() || '';
27
+ for (const line of lines) this._processLine(line);
28
+ }
29
+
30
+ _processLine(line) {
31
+ const trimmed = line.trim();
32
+
33
+ // Detect start of a JSON tool-call block
34
+ if (!this.inJsonBlock && trimmed.startsWith('{"tool"')) {
35
+ this.inJsonBlock = true;
36
+ this.jsonBuffer = trimmed;
37
+ this.braceDepth =
38
+ (trimmed.match(/{/g) || []).length - (trimmed.match(/}/g) || []).length;
39
+ if (this.braceDepth <= 0) {
40
+ this._tryFlushJson();
41
+ }
42
+ return;
43
+ }
44
+
45
+ if (this.inJsonBlock) {
46
+ this.jsonBuffer += ' ' + trimmed;
47
+ this.braceDepth +=
48
+ (trimmed.match(/{/g) || []).length - (trimmed.match(/}/g) || []).length;
49
+ if (this.braceDepth <= 0) {
50
+ this._tryFlushJson();
51
+ }
52
+ return;
53
+ }
54
+
55
+ // Regular markdown text
56
+ if (line) this.emit('markdown_chunk', line + '\n');
57
+ }
58
+
59
+ _tryFlushJson() {
60
+ try {
61
+ const toolCall = JSON.parse(this.jsonBuffer);
62
+ // Accept ANY object with a "tool" key — no hardcoded list
63
+ if (toolCall.tool) {
64
+ this.emit('tool_call', toolCall);
65
+ } else {
66
+ this.emit('markdown_chunk', this.jsonBuffer + '\n');
67
+ }
68
+ } catch {
69
+ this.emit('markdown_chunk', this.jsonBuffer + '\n');
70
+ } finally {
71
+ this.inJsonBlock = false;
72
+ this.jsonBuffer = '';
73
+ this.braceDepth = 0;
74
+ }
75
+ }
76
+
77
+ flush() {
78
+ if (this.inJsonBlock && this.jsonBuffer) {
79
+ this.emit('markdown_chunk', this.jsonBuffer);
80
+ this.jsonBuffer = '';
81
+ this.inJsonBlock = false;
82
+ this.braceDepth = 0;
83
+ }
84
+ if (this.buffer) {
85
+ this.emit('markdown_chunk', this.buffer);
86
+ this.buffer = '';
87
+ }
88
+ }
89
+
90
+ reset() {
91
+ this.buffer = '';
92
+ this.jsonBuffer = '';
93
+ this.inJsonBlock = false;
94
+ this.braceDepth = 0;
95
+ }
96
+ }