browzy 1.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 (104) hide show
  1. package/README.md +324 -0
  2. package/dist/cli/app.d.ts +16 -0
  3. package/dist/cli/app.js +615 -0
  4. package/dist/cli/banner.d.ts +1 -0
  5. package/dist/cli/banner.js +60 -0
  6. package/dist/cli/commands/compile.d.ts +2 -0
  7. package/dist/cli/commands/compile.js +42 -0
  8. package/dist/cli/commands/ingest.d.ts +2 -0
  9. package/dist/cli/commands/ingest.js +32 -0
  10. package/dist/cli/commands/init.d.ts +2 -0
  11. package/dist/cli/commands/init.js +48 -0
  12. package/dist/cli/commands/lint.d.ts +2 -0
  13. package/dist/cli/commands/lint.js +40 -0
  14. package/dist/cli/commands/query.d.ts +2 -0
  15. package/dist/cli/commands/query.js +36 -0
  16. package/dist/cli/commands/search.d.ts +2 -0
  17. package/dist/cli/commands/search.js +34 -0
  18. package/dist/cli/commands/status.d.ts +2 -0
  19. package/dist/cli/commands/status.js +27 -0
  20. package/dist/cli/components/Banner.d.ts +13 -0
  21. package/dist/cli/components/Banner.js +20 -0
  22. package/dist/cli/components/Markdown.d.ts +14 -0
  23. package/dist/cli/components/Markdown.js +324 -0
  24. package/dist/cli/components/Message.d.ts +14 -0
  25. package/dist/cli/components/Message.js +17 -0
  26. package/dist/cli/components/Spinner.d.ts +7 -0
  27. package/dist/cli/components/Spinner.js +19 -0
  28. package/dist/cli/components/StatusBar.d.ts +14 -0
  29. package/dist/cli/components/StatusBar.js +19 -0
  30. package/dist/cli/components/Suggestions.d.ts +13 -0
  31. package/dist/cli/components/Suggestions.js +14 -0
  32. package/dist/cli/entry.d.ts +2 -0
  33. package/dist/cli/entry.js +61 -0
  34. package/dist/cli/helpers.d.ts +14 -0
  35. package/dist/cli/helpers.js +32 -0
  36. package/dist/cli/hooks/useAutocomplete.d.ts +11 -0
  37. package/dist/cli/hooks/useAutocomplete.js +71 -0
  38. package/dist/cli/hooks/useHistory.d.ts +13 -0
  39. package/dist/cli/hooks/useHistory.js +106 -0
  40. package/dist/cli/hooks/useSession.d.ts +16 -0
  41. package/dist/cli/hooks/useSession.js +133 -0
  42. package/dist/cli/index.d.ts +2 -0
  43. package/dist/cli/index.js +41 -0
  44. package/dist/cli/keystore.d.ts +28 -0
  45. package/dist/cli/keystore.js +59 -0
  46. package/dist/cli/onboarding.d.ts +18 -0
  47. package/dist/cli/onboarding.js +306 -0
  48. package/dist/cli/personality.d.ts +34 -0
  49. package/dist/cli/personality.js +196 -0
  50. package/dist/cli/repl.d.ts +20 -0
  51. package/dist/cli/repl.js +338 -0
  52. package/dist/cli/theme.d.ts +25 -0
  53. package/dist/cli/theme.js +64 -0
  54. package/dist/core/compile/compiler.d.ts +25 -0
  55. package/dist/core/compile/compiler.js +229 -0
  56. package/dist/core/compile/index.d.ts +2 -0
  57. package/dist/core/compile/index.js +1 -0
  58. package/dist/core/config.d.ts +10 -0
  59. package/dist/core/config.js +92 -0
  60. package/dist/core/index.d.ts +12 -0
  61. package/dist/core/index.js +11 -0
  62. package/dist/core/ingest/image.d.ts +3 -0
  63. package/dist/core/ingest/image.js +61 -0
  64. package/dist/core/ingest/index.d.ts +18 -0
  65. package/dist/core/ingest/index.js +79 -0
  66. package/dist/core/ingest/pdf.d.ts +2 -0
  67. package/dist/core/ingest/pdf.js +36 -0
  68. package/dist/core/ingest/text.d.ts +2 -0
  69. package/dist/core/ingest/text.js +38 -0
  70. package/dist/core/ingest/web.d.ts +2 -0
  71. package/dist/core/ingest/web.js +202 -0
  72. package/dist/core/lint/index.d.ts +1 -0
  73. package/dist/core/lint/index.js +1 -0
  74. package/dist/core/lint/linter.d.ts +27 -0
  75. package/dist/core/lint/linter.js +147 -0
  76. package/dist/core/llm/index.d.ts +2 -0
  77. package/dist/core/llm/index.js +1 -0
  78. package/dist/core/llm/provider.d.ts +15 -0
  79. package/dist/core/llm/provider.js +241 -0
  80. package/dist/core/prompts.d.ts +28 -0
  81. package/dist/core/prompts.js +374 -0
  82. package/dist/core/query/engine.d.ts +29 -0
  83. package/dist/core/query/engine.js +131 -0
  84. package/dist/core/query/index.d.ts +2 -0
  85. package/dist/core/query/index.js +1 -0
  86. package/dist/core/sanitization.d.ts +11 -0
  87. package/dist/core/sanitization.js +50 -0
  88. package/dist/core/storage/filesystem.d.ts +23 -0
  89. package/dist/core/storage/filesystem.js +106 -0
  90. package/dist/core/storage/index.d.ts +2 -0
  91. package/dist/core/storage/index.js +2 -0
  92. package/dist/core/storage/sqlite.d.ts +30 -0
  93. package/dist/core/storage/sqlite.js +104 -0
  94. package/dist/core/types.d.ts +95 -0
  95. package/dist/core/types.js +4 -0
  96. package/dist/core/utils.d.ts +8 -0
  97. package/dist/core/utils.js +94 -0
  98. package/dist/core/wiki/index.d.ts +1 -0
  99. package/dist/core/wiki/index.js +1 -0
  100. package/dist/core/wiki/wiki.d.ts +19 -0
  101. package/dist/core/wiki/wiki.js +37 -0
  102. package/dist/index.d.ts +2 -0
  103. package/dist/index.js +3 -0
  104. package/package.json +54 -0
@@ -0,0 +1,324 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Text } from 'ink';
4
+ import chalk from 'chalk';
5
+ import { getTheme } from '../theme.js';
6
+ const tokenCache = new Map();
7
+ const MAX_CACHE = 500;
8
+ // Check a larger sample for markdown syntax
9
+ const MD_SYNTAX_RE = /[#*`|[\]>\-_]|\n\n|^\d+\. /;
10
+ /**
11
+ * Render markdown as styled terminal text.
12
+ * Handles: headers, bold, italic, code blocks, blockquotes,
13
+ * lists, wiki links, markdown links, horizontal rules, tables.
14
+ * Strikethrough disabled (~ used for "approximately" too often).
15
+ */
16
+ export function renderMarkdown(input) {
17
+ // Fast path: skip parsing for plain text (check first 1000 chars)
18
+ if (!MD_SYNTAX_RE.test(input.slice(0, 1000))) {
19
+ return input;
20
+ }
21
+ const cached = tokenCache.get(input);
22
+ if (cached)
23
+ return cached;
24
+ const theme = getTheme();
25
+ const lines = input.split('\n');
26
+ const result = [];
27
+ let inCodeBlock = false;
28
+ let codeBlockLang = '';
29
+ let codeLines = [];
30
+ let inTable = false;
31
+ let tableRows = [];
32
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
33
+ const line = lines[lineIdx];
34
+ // Display math blocks $$...$$ (standalone lines)
35
+ if (line.trim().startsWith('$$') && !inCodeBlock) {
36
+ // Collect until closing $$
37
+ let mathContent = line.trim().slice(2);
38
+ if (mathContent.endsWith('$$')) {
39
+ // Single-line display math
40
+ mathContent = mathContent.slice(0, -2);
41
+ result.push(' ' + chalk.hex(theme.accent)(latexToUnicode(mathContent.trim())));
42
+ continue;
43
+ }
44
+ // Multi-line: collect until $$
45
+ const mathLines = [mathContent];
46
+ while (++lineIdx < lines.length) {
47
+ const ml = lines[lineIdx];
48
+ if (ml.trim().endsWith('$$')) {
49
+ mathLines.push(ml.trim().slice(0, -2));
50
+ break;
51
+ }
52
+ mathLines.push(ml);
53
+ }
54
+ const fullMath = mathLines.join(' ').trim();
55
+ result.push(' ' + chalk.hex(theme.accent)(latexToUnicode(fullMath)));
56
+ continue;
57
+ }
58
+ // Code blocks
59
+ if (line.startsWith('```')) {
60
+ if (inCodeBlock) {
61
+ result.push(chalk.dim(' ┌─' + (codeBlockLang ? ` ${codeBlockLang} ` : '') + '─'));
62
+ for (const cl of codeLines) {
63
+ result.push(chalk.dim(' │ ') + chalk.hex(theme.text)(cl));
64
+ }
65
+ result.push(chalk.dim(' └─'));
66
+ codeLines = [];
67
+ codeBlockLang = '';
68
+ inCodeBlock = false;
69
+ }
70
+ else {
71
+ inCodeBlock = true;
72
+ codeBlockLang = line.slice(3).trim();
73
+ }
74
+ continue;
75
+ }
76
+ if (inCodeBlock) {
77
+ codeLines.push(line);
78
+ continue;
79
+ }
80
+ // Tables: detect pipe-delimited lines
81
+ if (line.includes('|') && line.trim().startsWith('|')) {
82
+ const cells = line.split('|').slice(1, -1).map(c => c.trim());
83
+ // Skip separator rows (---|---|---)
84
+ if (cells.every(c => /^[-:]+$/.test(c)))
85
+ continue;
86
+ if (!inTable) {
87
+ inTable = true;
88
+ tableRows = [];
89
+ }
90
+ tableRows.push(cells);
91
+ continue;
92
+ }
93
+ else if (inTable) {
94
+ // Flush table
95
+ result.push(...renderTable(tableRows, theme));
96
+ tableRows = [];
97
+ inTable = false;
98
+ }
99
+ // Headers
100
+ const headerMatch = line.match(/^(#{1,3})\s+(.+)/);
101
+ if (headerMatch) {
102
+ const level = headerMatch[1].length;
103
+ const text = headerMatch[2];
104
+ const color = level === 1 ? theme.brand : level === 2 ? theme.brandLight : theme.accent;
105
+ result.push(chalk.hex(color).bold(text));
106
+ continue;
107
+ }
108
+ // Blockquotes
109
+ if (line.startsWith('> ')) {
110
+ result.push(chalk.hex(theme.textMuted)(' │ ') + chalk.italic(formatInline(line.slice(2), theme)));
111
+ continue;
112
+ }
113
+ // Horizontal rules
114
+ if (/^[-*_]{3,}\s*$/.test(line.trim())) {
115
+ result.push(chalk.hex(theme.separator)('─'.repeat(40)));
116
+ continue;
117
+ }
118
+ // List items — numbered and bulleted
119
+ const bulletMatch = line.match(/^(\s*)([-*+])\s+(.*)/);
120
+ if (bulletMatch) {
121
+ const indent = bulletMatch[1];
122
+ const content = bulletMatch[3];
123
+ result.push(`${indent}${chalk.hex(theme.accent)('•')} ${formatInline(content, theme)}`);
124
+ continue;
125
+ }
126
+ const numberedMatch = line.match(/^(\s*)(\d+)\.\s+(.*)/);
127
+ if (numberedMatch) {
128
+ const indent = numberedMatch[1];
129
+ const num = numberedMatch[2];
130
+ const content = numberedMatch[3];
131
+ result.push(`${indent}${chalk.hex(theme.accent)(num + '.')} ${formatInline(content, theme)}`);
132
+ continue;
133
+ }
134
+ // Regular text with inline formatting
135
+ result.push(formatInline(line, theme));
136
+ }
137
+ // Flush any remaining table
138
+ if (inTable && tableRows.length > 0) {
139
+ result.push(...renderTable(tableRows, theme));
140
+ }
141
+ const output = result.join('\n');
142
+ // LRU-ish cache
143
+ if (tokenCache.size >= MAX_CACHE) {
144
+ const firstKey = tokenCache.keys().next().value;
145
+ if (firstKey !== undefined)
146
+ tokenCache.delete(firstKey);
147
+ }
148
+ tokenCache.set(input, output);
149
+ return output;
150
+ }
151
+ function formatInline(text, theme) {
152
+ let formatted = text;
153
+ // LaTeX math: inline $...$ and display $$...$$
154
+ formatted = formatted.replace(/\$\$([^$]+)\$\$/g, (_m, tex) => chalk.hex(theme.accent)(latexToUnicode(tex.trim())));
155
+ formatted = formatted.replace(/\$([^$]+)\$/g, (_m, tex) => chalk.hex(theme.accent)(latexToUnicode(tex.trim())));
156
+ // Bold **text**
157
+ formatted = formatted.replace(/\*\*([^*]+)\*\*/g, (_m, t) => chalk.bold(t));
158
+ // Italic *text* (not bold)
159
+ formatted = formatted.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_m, t) => chalk.italic(t));
160
+ // Inline code `text`
161
+ formatted = formatted.replace(/`([^`]+)`/g, (_m, t) => chalk.hex(theme.accent)(t));
162
+ // Wiki links [[slug]] — internal refs, styled but not clickable
163
+ formatted = formatted.replace(/\[\[([^\]]+)\]\]/g, (_m, t) => chalk.hex(theme.link)(`[${t}]`));
164
+ // Markdown links [text](url) — show URL explicitly since OSC 8 is unreliable through Ink
165
+ formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, url) => {
166
+ if (text === url) {
167
+ // URL-only link: just show it
168
+ return chalk.hex(theme.link).underline(url);
169
+ }
170
+ // Named link: show text + URL
171
+ return chalk.hex(theme.link).underline(text) + chalk.hex(theme.textMuted)(` (${url})`);
172
+ });
173
+ // Escaped characters
174
+ formatted = formatted.replace(/\\([()\\`*_{}[\]#+-.])/g, '$1');
175
+ return formatted;
176
+ }
177
+ // ── LaTeX to Unicode ────────────────────────────────────────────
178
+ const LATEX_SYMBOLS = {
179
+ // Greek
180
+ '\\alpha': 'α', '\\beta': 'β', '\\gamma': 'γ', '\\delta': 'δ',
181
+ '\\epsilon': 'ε', '\\zeta': 'ζ', '\\eta': 'η', '\\theta': 'θ',
182
+ '\\iota': 'ι', '\\kappa': 'κ', '\\lambda': 'λ', '\\mu': 'μ',
183
+ '\\nu': 'ν', '\\xi': 'ξ', '\\pi': 'π', '\\rho': 'ρ',
184
+ '\\sigma': 'σ', '\\tau': 'τ', '\\upsilon': 'υ', '\\phi': 'φ',
185
+ '\\chi': 'χ', '\\psi': 'ψ', '\\omega': 'ω',
186
+ '\\Gamma': 'Γ', '\\Delta': 'Δ', '\\Theta': 'Θ', '\\Lambda': 'Λ',
187
+ '\\Xi': 'Ξ', '\\Pi': 'Π', '\\Sigma': 'Σ', '\\Phi': 'Φ',
188
+ '\\Psi': 'Ψ', '\\Omega': 'Ω',
189
+ // Operators
190
+ '\\times': '×', '\\div': '÷', '\\cdot': '·', '\\pm': '±',
191
+ '\\mp': '∓', '\\leq': '≤', '\\geq': '≥', '\\neq': '≠',
192
+ '\\approx': '≈', '\\equiv': '≡', '\\sim': '∼', '\\propto': '∝',
193
+ '\\infty': '∞', '\\partial': '∂', '\\nabla': '∇',
194
+ // Set theory
195
+ '\\in': '∈', '\\notin': '∉', '\\subset': '⊂', '\\supset': '⊃',
196
+ '\\subseteq': '⊆', '\\supseteq': '⊇', '\\cup': '∪', '\\cap': '∩',
197
+ '\\emptyset': '∅', '\\varnothing': '∅',
198
+ // Logic
199
+ '\\forall': '∀', '\\exists': '∃', '\\neg': '¬', '\\land': '∧',
200
+ '\\lor': '∨', '\\implies': '⟹', '\\iff': '⟺',
201
+ '\\therefore': '∴', '\\because': '∵',
202
+ // Arrows
203
+ '\\to': '→', '\\rightarrow': '→', '\\leftarrow': '←',
204
+ '\\leftrightarrow': '↔', '\\Rightarrow': '⇒', '\\Leftarrow': '⇐',
205
+ '\\mapsto': '↦',
206
+ // Big operators
207
+ '\\sum': '∑', '\\prod': '∏', '\\int': '∫', '\\oint': '∮',
208
+ '\\bigcup': '⋃', '\\bigcap': '⋂', '\\bigoplus': '⊕',
209
+ // Misc
210
+ '\\star': '⋆', '\\circ': '∘', '\\bullet': '•',
211
+ '\\ldots': '…', '\\cdots': '⋯', '\\vdots': '⋮', '\\ddots': '⋱',
212
+ '\\langle': '⟨', '\\rangle': '⟩',
213
+ '\\lceil': '⌈', '\\rceil': '⌉', '\\lfloor': '⌊', '\\rfloor': '⌋',
214
+ '\\triangle': '△', '\\square': '□', '\\diamond': '◇',
215
+ '\\perp': '⊥', '\\parallel': '∥', '\\angle': '∠',
216
+ '\\checkmark': '✓', '\\qed': '∎',
217
+ };
218
+ const SUPERSCRIPTS = {
219
+ '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴',
220
+ '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹',
221
+ '+': '⁺', '-': '⁻', '=': '⁼', '(': '⁽', ')': '⁾',
222
+ 'n': 'ⁿ', 'i': 'ⁱ', 'd': 'ᵈ', 'k': 'ᵏ', 'a': 'ᵃ',
223
+ 'b': 'ᵇ', 'c': 'ᶜ', 'e': 'ᵉ', 'f': 'ᶠ', 'g': 'ᵍ',
224
+ 'h': 'ʰ', 'j': 'ʲ', 'l': 'ˡ', 'm': 'ᵐ', 'o': 'ᵒ',
225
+ 'p': 'ᵖ', 'r': 'ʳ', 's': 'ˢ', 't': 'ᵗ', 'u': 'ᵘ',
226
+ 'v': 'ᵛ', 'w': 'ʷ', 'x': 'ˣ', 'y': 'ʸ', 'z': 'ᶻ',
227
+ 'T': 'ᵀ',
228
+ };
229
+ const SUBSCRIPTS = {
230
+ '0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄',
231
+ '5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉',
232
+ '+': '₊', '-': '₋', '=': '₌', '(': '₍', ')': '₎',
233
+ 'a': 'ₐ', 'e': 'ₑ', 'i': 'ᵢ', 'j': 'ⱼ', 'k': 'ₖ',
234
+ 'n': 'ₙ', 'o': 'ₒ', 'p': 'ₚ', 'r': 'ᵣ', 's': 'ₛ',
235
+ 't': 'ₜ', 'u': 'ᵤ', 'v': 'ᵥ', 'x': 'ₓ',
236
+ };
237
+ const MATHBB = {
238
+ 'A': '𝔸', 'B': '𝔹', 'C': 'ℂ', 'D': '𝔻', 'E': '𝔼',
239
+ 'F': '𝔽', 'G': '𝔾', 'H': 'ℍ', 'I': '𝕀', 'J': '𝕁',
240
+ 'K': '𝕂', 'L': '𝕃', 'M': '𝕄', 'N': 'ℕ', 'O': '𝕆',
241
+ 'P': 'ℙ', 'Q': 'ℚ', 'R': 'ℝ', 'S': '𝕊', 'T': '𝕋',
242
+ 'U': '𝕌', 'V': '𝕍', 'W': '𝕎', 'X': '𝕏', 'Y': '𝕐', 'Z': 'ℤ',
243
+ };
244
+ const MATHCAL = {
245
+ 'A': '𝒜', 'B': 'ℬ', 'C': '𝒞', 'D': '𝒟', 'E': 'ℰ',
246
+ 'F': 'ℱ', 'G': '𝒢', 'H': 'ℋ', 'I': 'ℐ', 'J': '𝒥',
247
+ 'K': '𝒦', 'L': 'ℒ', 'M': 'ℳ', 'N': '𝒩', 'O': '𝒪',
248
+ 'P': '𝒫', 'Q': '𝒬', 'R': 'ℛ', 'S': '𝒮', 'T': '𝒯',
249
+ 'U': '𝒰', 'V': '𝒱', 'W': '𝒲', 'X': '𝒳', 'Y': '𝒴', 'Z': '𝒵',
250
+ };
251
+ function latexToUnicode(tex) {
252
+ let result = tex;
253
+ // \mathbb{X} → double-struck
254
+ result = result.replace(/\\mathbb\{([A-Z])\}/g, (_m, c) => MATHBB[c] || c);
255
+ // \mathcal{X} → calligraphic
256
+ result = result.replace(/\\mathcal\{([A-Z])\}/g, (_m, c) => MATHCAL[c] || c);
257
+ // \text{...} → just the text
258
+ result = result.replace(/\\text\{([^}]*)\}/g, '$1');
259
+ result = result.replace(/\\textbf\{([^}]*)\}/g, '$1');
260
+ result = result.replace(/\\mathrm\{([^}]*)\}/g, '$1');
261
+ // Fractions \frac{a}{b} → a/b
262
+ result = result.replace(/\\frac\{([^}]*)\}\{([^}]*)\}/g, '($1/$2)');
263
+ // Square root \sqrt{x} → √x
264
+ result = result.replace(/\\sqrt\{([^}]*)\}/g, '√($1)');
265
+ result = result.replace(/\\sqrt\[(\d+)\]\{([^}]*)\}/g, '$1√($2)');
266
+ // Superscripts ^{...} → Unicode superscript
267
+ result = result.replace(/\^{([^}]*)}/g, (_m, inner) => {
268
+ return inner.split('').map((c) => SUPERSCRIPTS[c] || c).join('');
269
+ });
270
+ result = result.replace(/\^([a-zA-Z0-9])/g, (_m, c) => SUPERSCRIPTS[c] || `^${c}`);
271
+ // Subscripts _{...} → Unicode subscript
272
+ result = result.replace(/_{([^}]*)}/g, (_m, inner) => {
273
+ return inner.split('').map((c) => SUBSCRIPTS[c] || c).join('');
274
+ });
275
+ result = result.replace(/_([a-zA-Z0-9])/g, (_m, c) => SUBSCRIPTS[c] || `_${c}`);
276
+ // Named symbols
277
+ for (const [cmd, sym] of Object.entries(LATEX_SYMBOLS)) {
278
+ // Use word boundary to avoid partial matches
279
+ result = result.split(cmd).join(sym);
280
+ }
281
+ // \left and \right delimiters
282
+ result = result.replace(/\\left\s*/g, '');
283
+ result = result.replace(/\\right\s*/g, '');
284
+ result = result.replace(/\\big\s*/g, '');
285
+ result = result.replace(/\\Big\s*/g, '');
286
+ // Clean up remaining backslash commands we don't handle
287
+ result = result.replace(/\\[a-zA-Z]+/g, (match) => match.slice(1));
288
+ // Clean up extra braces
289
+ result = result.replace(/\{([^{}]*)\}/g, '$1');
290
+ result = result.replace(/\{([^{}]*)\}/g, '$1'); // Second pass for nested
291
+ // Clean whitespace
292
+ result = result.replace(/\s+/g, ' ').trim();
293
+ return result;
294
+ }
295
+ function renderTable(rows, theme) {
296
+ if (rows.length === 0)
297
+ return [];
298
+ // Calculate column widths
299
+ const colCount = Math.max(...rows.map(r => r.length));
300
+ const colWidths = [];
301
+ for (let c = 0; c < colCount; c++) {
302
+ colWidths.push(Math.max(...rows.map(r => (r[c] || '').length), 3));
303
+ }
304
+ const result = [];
305
+ const separator = chalk.dim(' ' + colWidths.map(w => '─'.repeat(w + 2)).join('┼'));
306
+ for (let r = 0; r < rows.length; r++) {
307
+ const cells = rows[r];
308
+ const line = cells.map((cell, c) => {
309
+ const padded = (cell || '').padEnd(colWidths[c] || 3);
310
+ return r === 0 ? chalk.bold(padded) : padded;
311
+ }).join(chalk.dim(' │ '));
312
+ result.push(' ' + line);
313
+ if (r === 0)
314
+ result.push(separator);
315
+ }
316
+ return result;
317
+ }
318
+ /**
319
+ * Ink component for rendering markdown.
320
+ */
321
+ export const MarkdownText = React.memo(({ children }) => {
322
+ const rendered = renderMarkdown(children);
323
+ return _jsx(Text, { children: rendered });
324
+ });
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ export type MessageRole = 'user' | 'assistant' | 'system';
3
+ export interface MessageData {
4
+ id: string;
5
+ role: MessageRole;
6
+ content: string;
7
+ sources?: string[];
8
+ timestamp: number;
9
+ }
10
+ interface MessageProps {
11
+ message: MessageData;
12
+ }
13
+ export declare const Message: React.FC<MessageProps>;
14
+ export {};
@@ -0,0 +1,17 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Text, Box } from 'ink';
4
+ import { getTheme } from '../theme.js';
5
+ import { renderMarkdown } from './Markdown.js';
6
+ export const Message = React.memo(({ message }) => {
7
+ const theme = getTheme();
8
+ if (message.role === 'user') {
9
+ return (_jsx(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: _jsxs(Text, { bold: true, color: theme.userMessage, children: ['› ', message.content] }) }));
10
+ }
11
+ if (message.role === 'system') {
12
+ return (_jsx(Box, { marginBottom: 1, paddingLeft: 2, children: _jsx(Text, { color: theme.systemMessage, children: message.content }) }));
13
+ }
14
+ // Assistant message — render with markdown
15
+ const rendered = renderMarkdown(message.content);
16
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 2, children: [_jsx(Text, { children: rendered }), message.sources && message.sources.length > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.textMuted, children: ["sources: ", message.sources.join(', ')] }) }))] }));
17
+ });
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface SpinnerProps {
3
+ label?: string;
4
+ elapsed?: number;
5
+ }
6
+ export declare const BrowzySpinner: React.FC<SpinnerProps>;
7
+ export {};
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Text } from 'ink';
4
+ import { getTheme } from '../theme.js';
5
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+ export const BrowzySpinner = ({ label, elapsed }) => {
7
+ const [frame, setFrame] = useState(0);
8
+ const theme = getTheme();
9
+ useEffect(() => {
10
+ const timer = setInterval(() => {
11
+ setFrame(f => (f + 1) % FRAMES.length);
12
+ }, 80);
13
+ return () => clearInterval(timer);
14
+ }, []);
15
+ const elapsedStr = elapsed !== undefined && elapsed > 2
16
+ ? ` ${elapsed.toFixed(1)}s`
17
+ : '';
18
+ return (_jsxs(Text, { children: [_jsx(Text, { color: theme.accent, children: FRAMES[frame] }), label && _jsxs(Text, { color: theme.textDim, children: [" ", label] }), elapsedStr && _jsx(Text, { color: theme.textMuted, children: elapsedStr })] }));
19
+ };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ interface StatusBarProps {
3
+ model: string;
4
+ sources: number;
5
+ articles: number;
6
+ tokenUsage?: {
7
+ input: number;
8
+ output: number;
9
+ };
10
+ hint?: string;
11
+ temporaryStatus?: string;
12
+ }
13
+ export declare const StatusBar: React.FC<StatusBarProps>;
14
+ export {};
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Text, Box, useStdout } from 'ink';
4
+ import { getTheme } from '../theme.js';
5
+ export const StatusBar = ({ model, sources, articles, tokenUsage, hint, temporaryStatus, }) => {
6
+ const theme = getTheme();
7
+ const { stdout } = useStdout();
8
+ const cols = stdout.columns || 80;
9
+ const [showTemp, setShowTemp] = useState(false);
10
+ useEffect(() => {
11
+ if (temporaryStatus) {
12
+ setShowTemp(true);
13
+ const timer = setTimeout(() => setShowTemp(false), 4000);
14
+ return () => clearTimeout(timer);
15
+ }
16
+ }, [temporaryStatus]);
17
+ const left = `${sources} sources · ${articles} articles${articles > 0 ? ' · growing' : ''}`;
18
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsx(Text, { color: theme.separator, children: '─'.repeat(cols) }) }), _jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { children: showTemp && temporaryStatus ? (_jsxs(Text, { color: theme.accent, children: [" ", temporaryStatus] })) : (_jsxs(Text, { color: theme.textMuted, children: [" ", left] })) }), _jsx(Box, { children: _jsxs(Text, { color: theme.textMuted, children: [model, " "] }) })] }), hint && (_jsx(Box, { children: _jsxs(Text, { color: theme.textMuted, children: [" ", hint] }) }))] }));
19
+ };
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ export interface Suggestion {
3
+ name: string;
4
+ description: string;
5
+ usage?: string;
6
+ }
7
+ interface SuggestionsProps {
8
+ items: Suggestion[];
9
+ selectedIndex: number;
10
+ visible: boolean;
11
+ }
12
+ export declare const SuggestionList: React.FC<SuggestionsProps>;
13
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text, Box } from 'ink';
3
+ import { getTheme } from '../theme.js';
4
+ const MAX_VISIBLE = 5;
5
+ export const SuggestionList = ({ items, selectedIndex, visible }) => {
6
+ const theme = getTheme();
7
+ if (!visible || items.length === 0)
8
+ return null;
9
+ const visibleItems = items.slice(0, MAX_VISIBLE);
10
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [visibleItems.map((item, i) => {
11
+ const isSelected = i === selectedIndex;
12
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? theme.accent : theme.textDim, bold: isSelected, children: [isSelected ? '› ' : ' ', (item.usage || item.name).padEnd(28)] }), _jsx(Text, { color: theme.textMuted, children: item.description })] }, item.name));
13
+ }), items.length > MAX_VISIBLE && (_jsxs(Text, { color: theme.textMuted, children: [" ... ", items.length - MAX_VISIBLE, " more"] }))] }));
14
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import React from 'react';
4
+ import { render } from 'ink';
5
+ import { Command } from 'commander';
6
+ import { BrowzyApp, BrowzyErrorBoundary } from './app.js';
7
+ import { needsOnboarding, runOnboarding } from './onboarding.js';
8
+ import { initCommand } from './commands/init.js';
9
+ import { ingestCommand } from './commands/ingest.js';
10
+ import { compileCommand } from './commands/compile.js';
11
+ import { queryCommand } from './commands/query.js';
12
+ import { lintCommand } from './commands/lint.js';
13
+ import { statusCommand } from './commands/status.js';
14
+ import { searchCommand } from './commands/search.js';
15
+ // Prevent corepack issues
16
+ process.env.COREPACK_ENABLE_AUTO_PIN = '0';
17
+ // Signal handlers for graceful shutdown
18
+ process.on('SIGTERM', () => process.exit(143));
19
+ process.on('SIGHUP', () => process.exit(129));
20
+ process.on('unhandledRejection', (reason) => {
21
+ console.error('Unhandled rejection:', reason instanceof Error ? reason.message : reason);
22
+ process.exit(1);
23
+ });
24
+ async function main() {
25
+ if (process.argv.length <= 2) {
26
+ if (needsOnboarding()) {
27
+ const success = await runOnboarding();
28
+ if (!success)
29
+ process.exit(0);
30
+ }
31
+ try {
32
+ const { waitUntilExit } = render(React.createElement(BrowzyErrorBoundary, null, React.createElement(BrowzyApp)), { exitOnCtrlC: false });
33
+ await waitUntilExit();
34
+ }
35
+ catch (err) {
36
+ // Ensure terminal is restored even on crash
37
+ process.stdout.write('\x1B[?25h'); // Show cursor
38
+ console.error('browzy error:', err);
39
+ process.exit(1);
40
+ }
41
+ return;
42
+ }
43
+ const program = new Command();
44
+ program
45
+ .name('browzy')
46
+ .description('LLM-powered personal knowledge base engine')
47
+ .version('1.0.0');
48
+ program.addCommand(initCommand);
49
+ program.addCommand(ingestCommand);
50
+ program.addCommand(compileCommand);
51
+ program.addCommand(queryCommand);
52
+ program.addCommand(lintCommand);
53
+ program.addCommand(statusCommand);
54
+ program.addCommand(searchCommand);
55
+ program.parse();
56
+ }
57
+ main().catch(err => {
58
+ process.stdout.write('\x1B[?25h'); // Show cursor
59
+ console.error(err);
60
+ process.exit(1);
61
+ });
@@ -0,0 +1,14 @@
1
+ import type { BrowzyConfig } from '../core/types.js';
2
+ import type { LLMProvider } from '../core/llm/provider.js';
3
+ export declare function getConfig(): BrowzyConfig;
4
+ export declare function getConfigAndLLM(): {
5
+ config: BrowzyConfig;
6
+ llm: LLMProvider;
7
+ };
8
+ export declare function ensureDirs(config: BrowzyConfig): void;
9
+ export declare function spinner(text: string): import("ora").Ora;
10
+ export declare function success(msg: string): void;
11
+ export declare function warn(msg: string): void;
12
+ export declare function error(msg: string): void;
13
+ export declare function info(msg: string): void;
14
+ export declare function table(data: Record<string, unknown>[]): void;
@@ -0,0 +1,32 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { loadConfig, ensureDataDirs, createProvider } from '../core/index.js';
4
+ export function getConfig() {
5
+ return loadConfig();
6
+ }
7
+ export function getConfigAndLLM() {
8
+ const config = loadConfig();
9
+ const llm = createProvider(config.llm);
10
+ return { config, llm };
11
+ }
12
+ export function ensureDirs(config) {
13
+ ensureDataDirs(config);
14
+ }
15
+ export function spinner(text) {
16
+ return ora({ text, color: 'cyan' });
17
+ }
18
+ export function success(msg) {
19
+ console.log(chalk.green('✓'), msg);
20
+ }
21
+ export function warn(msg) {
22
+ console.log(chalk.yellow('⚠'), msg);
23
+ }
24
+ export function error(msg) {
25
+ console.log(chalk.red('✗'), msg);
26
+ }
27
+ export function info(msg) {
28
+ console.log(chalk.blue('ℹ'), msg);
29
+ }
30
+ export function table(data) {
31
+ console.table(data);
32
+ }
@@ -0,0 +1,11 @@
1
+ import type { Suggestion } from '../components/Suggestions.js';
2
+ export declare function useAutocomplete(): {
3
+ getMatches: (input: string) => Suggestion[];
4
+ selectedIndex: number;
5
+ visible: boolean;
6
+ updateForInput: (input: string) => void;
7
+ moveSelection: (direction: "up" | "down", input: string) => void;
8
+ acceptSuggestion: (input: string) => string | null;
9
+ getGhostText: (input: string) => string;
10
+ setVisible: import("react").Dispatch<import("react").SetStateAction<boolean>>;
11
+ };
@@ -0,0 +1,71 @@
1
+ import { useState, useCallback } from 'react';
2
+ const COMMANDS = [
3
+ { name: '/add', description: 'Feed your browzy new knowledge', usage: '/add <urls or paths...>' },
4
+ { name: '/health', description: 'How is your browzy doing?' },
5
+ { name: '/model', description: 'Switch models', usage: '/model [model-id]' },
6
+ { name: '/rebuild', description: 'Recompile from scratch' },
7
+ { name: '/export', description: 'Save this session as markdown' },
8
+ { name: '/help', description: 'All commands' },
9
+ { name: '/quit', description: 'Exit' },
10
+ ];
11
+ export function useAutocomplete() {
12
+ const [selectedIndex, setSelectedIndex] = useState(0);
13
+ const [visible, setVisible] = useState(false);
14
+ const getMatches = useCallback((input) => {
15
+ if (!input.startsWith('/'))
16
+ return [];
17
+ const query = input.toLowerCase();
18
+ return COMMANDS.filter(c => c.name.startsWith(query));
19
+ }, []);
20
+ const updateForInput = useCallback((input) => {
21
+ if (input.startsWith('/') && !input.includes(' ')) {
22
+ const matches = getMatches(input);
23
+ setVisible(matches.length > 0 && input !== matches[0]?.name);
24
+ setSelectedIndex(0);
25
+ }
26
+ else {
27
+ setVisible(false);
28
+ }
29
+ }, [getMatches]);
30
+ const moveSelection = useCallback((direction, input) => {
31
+ const matches = getMatches(input);
32
+ if (matches.length === 0)
33
+ return;
34
+ setSelectedIndex(prev => {
35
+ if (direction === 'up')
36
+ return Math.max(0, prev - 1);
37
+ return Math.min(matches.length - 1, prev + 1);
38
+ });
39
+ }, [getMatches]);
40
+ const acceptSuggestion = useCallback((input) => {
41
+ const matches = getMatches(input);
42
+ if (matches.length === 0 || !visible)
43
+ return null;
44
+ const selected = matches[selectedIndex];
45
+ if (!selected)
46
+ return null;
47
+ setVisible(false);
48
+ return selected.name + ' ';
49
+ }, [getMatches, selectedIndex, visible]);
50
+ const getGhostText = useCallback((input) => {
51
+ if (!visible)
52
+ return '';
53
+ const matches = getMatches(input);
54
+ if (matches.length === 0)
55
+ return '';
56
+ const match = matches[selectedIndex] || matches[0];
57
+ if (!match || !match.name.startsWith(input))
58
+ return '';
59
+ return match.name.slice(input.length);
60
+ }, [getMatches, selectedIndex, visible]);
61
+ return {
62
+ getMatches,
63
+ selectedIndex,
64
+ visible,
65
+ updateForInput,
66
+ moveSelection,
67
+ acceptSuggestion,
68
+ getGhostText,
69
+ setVisible,
70
+ };
71
+ }