smolerclaw 0.1.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 (73) hide show
  1. package/.github/workflows/ci.yml +30 -0
  2. package/.github/workflows/release.yml +67 -0
  3. package/bun.lock +33 -0
  4. package/dist/index.js +321 -0
  5. package/dist/tinyclaw.exe +0 -0
  6. package/install.ps1 +119 -0
  7. package/package.json +25 -0
  8. package/skills/business.md +77 -0
  9. package/skills/default.md +77 -0
  10. package/src/ansi.ts +164 -0
  11. package/src/approval.ts +74 -0
  12. package/src/auth.ts +125 -0
  13. package/src/briefing.ts +52 -0
  14. package/src/claude.ts +267 -0
  15. package/src/cli.ts +137 -0
  16. package/src/clipboard.ts +27 -0
  17. package/src/config.ts +87 -0
  18. package/src/context-window.ts +190 -0
  19. package/src/context.ts +125 -0
  20. package/src/decisions.ts +122 -0
  21. package/src/email.ts +123 -0
  22. package/src/errors.ts +78 -0
  23. package/src/export.ts +82 -0
  24. package/src/finance.ts +148 -0
  25. package/src/git.ts +62 -0
  26. package/src/history.ts +100 -0
  27. package/src/images.ts +68 -0
  28. package/src/index.ts +1431 -0
  29. package/src/investigate.ts +415 -0
  30. package/src/markdown.ts +125 -0
  31. package/src/memos.ts +191 -0
  32. package/src/models.ts +94 -0
  33. package/src/monitor.ts +169 -0
  34. package/src/morning.ts +108 -0
  35. package/src/news.ts +329 -0
  36. package/src/openai-provider.ts +127 -0
  37. package/src/people.ts +472 -0
  38. package/src/personas.ts +99 -0
  39. package/src/platform.ts +84 -0
  40. package/src/plugins.ts +125 -0
  41. package/src/pomodoro.ts +169 -0
  42. package/src/providers.ts +70 -0
  43. package/src/retry.ts +108 -0
  44. package/src/session.ts +128 -0
  45. package/src/skills.ts +102 -0
  46. package/src/tasks.ts +418 -0
  47. package/src/tokens.ts +102 -0
  48. package/src/tool-safety.ts +100 -0
  49. package/src/tools.ts +1479 -0
  50. package/src/tui.ts +693 -0
  51. package/src/types.ts +55 -0
  52. package/src/undo.ts +83 -0
  53. package/src/windows.ts +299 -0
  54. package/src/workflows.ts +197 -0
  55. package/tests/ansi.test.ts +58 -0
  56. package/tests/approval.test.ts +43 -0
  57. package/tests/briefing.test.ts +10 -0
  58. package/tests/cli.test.ts +53 -0
  59. package/tests/context-window.test.ts +83 -0
  60. package/tests/images.test.ts +28 -0
  61. package/tests/memos.test.ts +116 -0
  62. package/tests/models.test.ts +34 -0
  63. package/tests/news.test.ts +13 -0
  64. package/tests/path-guard.test.ts +37 -0
  65. package/tests/people.test.ts +204 -0
  66. package/tests/skills.test.ts +35 -0
  67. package/tests/ssrf.test.ts +80 -0
  68. package/tests/tasks.test.ts +152 -0
  69. package/tests/tokens.test.ts +44 -0
  70. package/tests/tool-safety.test.ts +55 -0
  71. package/tests/windows-security.test.ts +59 -0
  72. package/tests/windows.test.ts +20 -0
  73. package/tsconfig.json +19 -0
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Investigation system — collect evidence, analyze, and produce structured reports.
3
+ *
4
+ * Types of investigation:
5
+ * bug — malfunction diagnosis
6
+ * feature — material gathering for feature construction
7
+ * test — collecting scenarios and test material
8
+ * audit — code/system audit
9
+ * incident — runtime/production incident
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
13
+ import { join, resolve, relative } from 'node:path'
14
+
15
+ // ─── Types ──────────────────────────────────────────────────
16
+
17
+ export type InvestigationType = 'bug' | 'feature' | 'test' | 'audit' | 'incident'
18
+ export type InvestigationStatus = 'aberta' | 'em_andamento' | 'concluida' | 'arquivada'
19
+ export type EvidenceSource = 'file' | 'command' | 'log' | 'diff' | 'url' | 'observation'
20
+
21
+ export interface Evidence {
22
+ id: string
23
+ source: EvidenceSource
24
+ label: string // short description
25
+ content: string // the actual evidence data
26
+ path?: string // file path or URL (when applicable)
27
+ timestamp: string
28
+ }
29
+
30
+ export interface Finding {
31
+ id: string
32
+ severity: 'critical' | 'high' | 'medium' | 'low' | 'info'
33
+ title: string
34
+ description: string
35
+ evidence_ids: string[] // references to evidence that supports this finding
36
+ timestamp: string
37
+ }
38
+
39
+ export interface Investigation {
40
+ id: string
41
+ title: string
42
+ type: InvestigationType
43
+ status: InvestigationStatus
44
+ hypothesis?: string // initial theory to test
45
+ tags: string[]
46
+ evidence: Evidence[]
47
+ findings: Finding[]
48
+ summary?: string // final summary when closed
49
+ recommendations?: string // action items when closed
50
+ created: string
51
+ updated: string
52
+ }
53
+
54
+ // ─── Storage ────────────────────────────────────────────────
55
+
56
+ let _dataDir = ''
57
+ let _investigations: Investigation[] = []
58
+
59
+ const DATA_FILE = () => join(_dataDir, 'investigations.json')
60
+
61
+ function save(): void {
62
+ writeFileSync(DATA_FILE(), JSON.stringify(_investigations, null, 2))
63
+ }
64
+
65
+ function load(): void {
66
+ const file = DATA_FILE()
67
+ if (!existsSync(file)) { _investigations = []; return }
68
+ try { _investigations = JSON.parse(readFileSync(file, 'utf-8')) }
69
+ catch { _investigations = [] }
70
+ }
71
+
72
+ // ─── Init ───────────────────────────────────────────────────
73
+
74
+ export function initInvestigations(dataDir: string): void {
75
+ _dataDir = dataDir
76
+ if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true })
77
+ load()
78
+ }
79
+
80
+ // ─── Operations ─────────────────────────────────────────────
81
+
82
+ export function openInvestigation(
83
+ title: string,
84
+ type: InvestigationType,
85
+ hypothesis?: string,
86
+ tags: string[] = [],
87
+ ): Investigation {
88
+ const now = new Date().toISOString()
89
+ const inv: Investigation = {
90
+ id: genId(),
91
+ title: title.trim(),
92
+ type,
93
+ status: 'aberta',
94
+ hypothesis: hypothesis?.trim(),
95
+ tags: tags.map((t) => t.toLowerCase()),
96
+ evidence: [],
97
+ findings: [],
98
+ created: now,
99
+ updated: now,
100
+ }
101
+ _investigations = [..._investigations, inv]
102
+ save()
103
+ return inv
104
+ }
105
+
106
+ export function collectEvidence(
107
+ investigationRef: string,
108
+ source: EvidenceSource,
109
+ label: string,
110
+ content: string,
111
+ path?: string,
112
+ ): Evidence | null {
113
+ const inv = findInvestigation(investigationRef)
114
+ if (!inv) return null
115
+
116
+ const ev: Evidence = {
117
+ id: genId(),
118
+ source,
119
+ label: label.trim(),
120
+ content: content.trim(),
121
+ path: path?.trim(),
122
+ timestamp: new Date().toISOString(),
123
+ }
124
+
125
+ const updated: Investigation = {
126
+ ...inv,
127
+ evidence: [...inv.evidence, ev],
128
+ status: inv.status === 'aberta' ? 'em_andamento' : inv.status,
129
+ updated: new Date().toISOString(),
130
+ }
131
+ _investigations = _investigations.map((i) => i.id === inv.id ? updated : i)
132
+ save()
133
+ return ev
134
+ }
135
+
136
+ export function addFinding(
137
+ investigationRef: string,
138
+ severity: Finding['severity'],
139
+ title: string,
140
+ description: string,
141
+ evidenceIds: string[] = [],
142
+ ): Finding | null {
143
+ const inv = findInvestigation(investigationRef)
144
+ if (!inv) return null
145
+
146
+ // Validate evidence IDs exist
147
+ const validIds = evidenceIds.filter((eid) =>
148
+ inv.evidence.some((e) => e.id === eid),
149
+ )
150
+
151
+ const finding: Finding = {
152
+ id: genId(),
153
+ severity,
154
+ title: title.trim(),
155
+ description: description.trim(),
156
+ evidence_ids: validIds,
157
+ timestamp: new Date().toISOString(),
158
+ }
159
+
160
+ const updated: Investigation = {
161
+ ...inv,
162
+ findings: [...inv.findings, finding],
163
+ updated: new Date().toISOString(),
164
+ }
165
+ _investigations = _investigations.map((i) => i.id === inv.id ? updated : i)
166
+ save()
167
+ return finding
168
+ }
169
+
170
+ export function closeInvestigation(
171
+ investigationRef: string,
172
+ summary: string,
173
+ recommendations?: string,
174
+ ): Investigation | null {
175
+ const inv = findInvestigation(investigationRef)
176
+ if (!inv) return null
177
+
178
+ const updated: Investigation = {
179
+ ...inv,
180
+ status: 'concluida',
181
+ summary: summary.trim(),
182
+ recommendations: recommendations?.trim(),
183
+ updated: new Date().toISOString(),
184
+ }
185
+ _investigations = _investigations.map((i) => i.id === inv.id ? updated : i)
186
+ save()
187
+ return updated
188
+ }
189
+
190
+ export function getInvestigation(ref: string): Investigation | null {
191
+ return findInvestigation(ref)
192
+ }
193
+
194
+ export function listInvestigations(
195
+ status?: InvestigationStatus,
196
+ type?: InvestigationType,
197
+ limit = 20,
198
+ ): Investigation[] {
199
+ let filtered = [..._investigations]
200
+ if (status) filtered = filtered.filter((i) => i.status === status)
201
+ if (type) filtered = filtered.filter((i) => i.type === type)
202
+ return filtered
203
+ .sort((a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime())
204
+ .slice(0, limit)
205
+ }
206
+
207
+ export function searchInvestigations(query: string): Investigation[] {
208
+ const lower = query.toLowerCase()
209
+ return _investigations.filter((inv) =>
210
+ inv.title.toLowerCase().includes(lower) ||
211
+ inv.hypothesis?.toLowerCase().includes(lower) ||
212
+ inv.tags.some((t) => t.includes(lower)) ||
213
+ inv.findings.some((f) => f.title.toLowerCase().includes(lower)) ||
214
+ inv.summary?.toLowerCase().includes(lower),
215
+ ).sort((a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime())
216
+ }
217
+
218
+ // ─── Report Generation ──────────────────────────────────────
219
+
220
+ export function generateReport(investigationRef: string): string | null {
221
+ const inv = findInvestigation(investigationRef)
222
+ if (!inv) return null
223
+
224
+ const lines: string[] = []
225
+ const date = (iso: string) => new Date(iso).toLocaleDateString('pt-BR', {
226
+ day: '2-digit', month: '2-digit', year: 'numeric',
227
+ hour: '2-digit', minute: '2-digit',
228
+ })
229
+ const typeLabels: Record<InvestigationType, string> = {
230
+ bug: 'Bug / Mal funcionamento',
231
+ feature: 'Construcao de funcionalidade',
232
+ test: 'Material para testes',
233
+ audit: 'Auditoria',
234
+ incident: 'Incidente',
235
+ }
236
+ const severityOrder: Record<string, number> = {
237
+ critical: 0, high: 1, medium: 2, low: 3, info: 4,
238
+ }
239
+
240
+ lines.push(`# Investigacao: ${inv.title}`)
241
+ lines.push('')
242
+ lines.push(`**Tipo:** ${typeLabels[inv.type]}`)
243
+ lines.push(`**Status:** ${inv.status}`)
244
+ lines.push(`**Abertura:** ${date(inv.created)}`)
245
+ lines.push(`**Ultima atualizacao:** ${date(inv.updated)}`)
246
+ if (inv.tags.length) lines.push(`**Tags:** ${inv.tags.join(', ')}`)
247
+ lines.push(`**ID:** ${inv.id}`)
248
+
249
+ if (inv.hypothesis) {
250
+ lines.push('')
251
+ lines.push(`## Hipotese`)
252
+ lines.push(inv.hypothesis)
253
+ }
254
+
255
+ // Evidence
256
+ if (inv.evidence.length > 0) {
257
+ lines.push('')
258
+ lines.push(`## Evidencias (${inv.evidence.length})`)
259
+ for (const ev of inv.evidence) {
260
+ const ts = date(ev.timestamp)
261
+ lines.push('')
262
+ lines.push(`### [${ev.id}] ${ev.label}`)
263
+ lines.push(`- Fonte: ${ev.source}${ev.path ? ` (${ev.path})` : ''}`)
264
+ lines.push(`- Coletada: ${ts}`)
265
+ // Show content, truncated if very large
266
+ const content = ev.content.length > 2000
267
+ ? ev.content.slice(0, 2000) + '\n... (truncado)'
268
+ : ev.content
269
+ lines.push('```')
270
+ lines.push(content)
271
+ lines.push('```')
272
+ }
273
+ }
274
+
275
+ // Findings
276
+ if (inv.findings.length > 0) {
277
+ const sorted = [...inv.findings].sort((a, b) =>
278
+ (severityOrder[a.severity] ?? 4) - (severityOrder[b.severity] ?? 4),
279
+ )
280
+ lines.push('')
281
+ lines.push(`## Conclusoes (${inv.findings.length})`)
282
+ for (const f of sorted) {
283
+ const badge = severityBadge(f.severity)
284
+ lines.push('')
285
+ lines.push(`### ${badge} ${f.title}`)
286
+ lines.push(f.description)
287
+ if (f.evidence_ids.length > 0) {
288
+ lines.push(`- Evidencias: ${f.evidence_ids.join(', ')}`)
289
+ }
290
+ }
291
+ }
292
+
293
+ // Summary & recommendations
294
+ if (inv.summary) {
295
+ lines.push('')
296
+ lines.push(`## Resumo`)
297
+ lines.push(inv.summary)
298
+ }
299
+ if (inv.recommendations) {
300
+ lines.push('')
301
+ lines.push(`## Recomendacoes`)
302
+ lines.push(inv.recommendations)
303
+ }
304
+
305
+ return lines.join('\n')
306
+ }
307
+
308
+ // ─── Formatting (for TUI display) ──────────────────────────
309
+
310
+ export function formatInvestigationList(investigations: Investigation[]): string {
311
+ if (investigations.length === 0) return 'Nenhuma investigacao encontrada.'
312
+
313
+ const lines = investigations.map((inv) => {
314
+ const date = new Date(inv.updated).toLocaleDateString('pt-BR', {
315
+ day: '2-digit', month: '2-digit',
316
+ })
317
+ const status = statusBadge(inv.status)
318
+ const evCount = inv.evidence.length
319
+ const fCount = inv.findings.length
320
+ const tags = inv.tags.length > 0 ? ` [${inv.tags.join(', ')}]` : ''
321
+ return ` ${status} [${date}] ${inv.title} (${inv.type}) — ${evCount} ev, ${fCount} concl${tags} {${inv.id}}`
322
+ })
323
+
324
+ return `Investigacoes (${investigations.length}):\n${lines.join('\n')}`
325
+ }
326
+
327
+ export function formatInvestigationDetail(inv: Investigation): string {
328
+ const date = (iso: string) => new Date(iso).toLocaleDateString('pt-BR')
329
+ const status = statusBadge(inv.status)
330
+ const lines = [
331
+ `--- Investigacao {${inv.id}} ---`,
332
+ `Titulo: ${inv.title}`,
333
+ `Tipo: ${inv.type} | Status: ${status}`,
334
+ `Criada: ${date(inv.created)} | Atualizada: ${date(inv.updated)}`,
335
+ ]
336
+
337
+ if (inv.hypothesis) lines.push(`Hipotese: ${inv.hypothesis}`)
338
+ if (inv.tags.length) lines.push(`Tags: ${inv.tags.join(', ')}`)
339
+
340
+ lines.push(`\nEvidencias: ${inv.evidence.length}`)
341
+ for (const ev of inv.evidence.slice(-5)) {
342
+ const preview = ev.content.slice(0, 80).replace(/\n/g, ' ')
343
+ lines.push(` [${ev.id}] ${ev.source}: ${ev.label} — "${preview}..."`)
344
+ }
345
+ if (inv.evidence.length > 5) {
346
+ lines.push(` ... (${inv.evidence.length - 5} mais)`)
347
+ }
348
+
349
+ lines.push(`\nConclusoes: ${inv.findings.length}`)
350
+ for (const f of inv.findings) {
351
+ lines.push(` ${severityBadge(f.severity)} ${f.title}`)
352
+ }
353
+
354
+ if (inv.summary) lines.push(`\nResumo: ${inv.summary}`)
355
+ if (inv.recommendations) lines.push(`Recomendacoes: ${inv.recommendations}`)
356
+
357
+ return lines.join('\n')
358
+ }
359
+
360
+ export function formatEvidenceDetail(ev: Evidence): string {
361
+ const ts = new Date(ev.timestamp).toLocaleDateString('pt-BR', {
362
+ day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
363
+ })
364
+ const lines = [
365
+ `--- Evidencia {${ev.id}} ---`,
366
+ `Label: ${ev.label}`,
367
+ `Fonte: ${ev.source}${ev.path ? ` (${ev.path})` : ''}`,
368
+ `Coletada: ${ts}`,
369
+ '',
370
+ ev.content.length > 3000
371
+ ? ev.content.slice(0, 3000) + '\n... (truncado)'
372
+ : ev.content,
373
+ ]
374
+ return lines.join('\n')
375
+ }
376
+
377
+ // ─── Helpers ────────────────────────────────────────────────
378
+
379
+ function findInvestigation(ref: string): Investigation | null {
380
+ const lower = ref.toLowerCase().trim()
381
+ // Exact ID match
382
+ const byId = _investigations.find((i) => i.id === lower)
383
+ if (byId) return byId
384
+ // Partial title match (most recent)
385
+ const byTitle = _investigations
386
+ .filter((i) => i.title.toLowerCase().includes(lower))
387
+ .sort((a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime())
388
+ return byTitle[0] || null
389
+ }
390
+
391
+ function statusBadge(status: InvestigationStatus): string {
392
+ switch (status) {
393
+ case 'aberta': return '○'
394
+ case 'em_andamento': return '◉'
395
+ case 'concluida': return '●'
396
+ case 'arquivada': return '◌'
397
+ }
398
+ }
399
+
400
+ function severityBadge(severity: Finding['severity']): string {
401
+ switch (severity) {
402
+ case 'critical': return '[CRITICO]'
403
+ case 'high': return '[ALTO]'
404
+ case 'medium': return '[MEDIO]'
405
+ case 'low': return '[BAIXO]'
406
+ case 'info': return '[INFO]'
407
+ }
408
+ }
409
+
410
+ function genId(): string {
411
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
412
+ let id = ''
413
+ for (let i = 0; i < 6; i++) id += chars[Math.floor(Math.random() * chars.length)]
414
+ return id
415
+ }
@@ -0,0 +1,125 @@
1
+ import { A, C } from './ansi'
2
+
3
+ /**
4
+ * Render markdown text to ANSI-formatted terminal lines.
5
+ * Handles: headers, bold, italic, inline code, code blocks,
6
+ * bullet/numbered lists, blockquotes, and links.
7
+ */
8
+ export function renderMarkdown(text: string): string[] {
9
+ const lines = text.split('\n')
10
+ const output: string[] = []
11
+ let inCodeBlock = false
12
+ let codeLang = ''
13
+
14
+ for (let i = 0; i < lines.length; i++) {
15
+ const line = lines[i]
16
+
17
+ // ── Code block toggle ──
18
+ if (line.trimStart().startsWith('```')) {
19
+ if (!inCodeBlock) {
20
+ inCodeBlock = true
21
+ codeLang = line.trimStart().slice(3).trim()
22
+ const label = codeLang ? ` ${codeLang}` : ''
23
+ output.push(` ${A.dim}┌──${label}${'─'.repeat(Math.max(1, 40 - label.length))}${A.reset}`)
24
+ } else {
25
+ inCodeBlock = false
26
+ codeLang = ''
27
+ output.push(` ${A.dim}└${'─'.repeat(42)}${A.reset}`)
28
+ }
29
+ continue
30
+ }
31
+
32
+ // ── Inside code block ──
33
+ if (inCodeBlock) {
34
+ output.push(` ${A.dim}│${A.reset} ${C.code}${line}${A.reset}`)
35
+ continue
36
+ }
37
+
38
+ // ── Blank line ──
39
+ if (!line.trim()) {
40
+ output.push('')
41
+ continue
42
+ }
43
+
44
+ // ── Headers ──
45
+ const headerMatch = line.match(/^(#{1,3})\s+(.+)/)
46
+ if (headerMatch) {
47
+ const level = headerMatch[1].length
48
+ const text = headerMatch[2]
49
+ const prefix = level === 1 ? '━' : level === 2 ? '─' : '·'
50
+ output.push(` ${C.heading}${A.bold}${prefix} ${renderInline(text)}${A.reset}`)
51
+ continue
52
+ }
53
+
54
+ // ── Blockquote ──
55
+ if (line.trimStart().startsWith('>')) {
56
+ const content = line.replace(/^\s*>\s?/, '')
57
+ output.push(` ${C.quote}│ ${renderInline(content)}${A.reset}`)
58
+ continue
59
+ }
60
+
61
+ // ── Bullet list ──
62
+ const bulletMatch = line.match(/^(\s*)([-*+])\s+(.+)/)
63
+ if (bulletMatch) {
64
+ const indent = Math.floor(bulletMatch[1].length / 2)
65
+ const content = bulletMatch[3]
66
+ const pad = ' '.repeat(indent)
67
+ output.push(` ${pad}${A.dim}•${A.reset} ${renderInline(content)}`)
68
+ continue
69
+ }
70
+
71
+ // ── Numbered list ──
72
+ const numMatch = line.match(/^(\s*)(\d+)[.)]\s+(.+)/)
73
+ if (numMatch) {
74
+ const indent = Math.floor(numMatch[1].length / 2)
75
+ const num = numMatch[2]
76
+ const content = numMatch[3]
77
+ const pad = ' '.repeat(indent)
78
+ output.push(` ${pad}${A.dim}${num}.${A.reset} ${renderInline(content)}`)
79
+ continue
80
+ }
81
+
82
+ // ── Horizontal rule ──
83
+ if (/^[-*_]{3,}\s*$/.test(line.trim())) {
84
+ output.push(` ${A.dim}${'─'.repeat(40)}${A.reset}`)
85
+ continue
86
+ }
87
+
88
+ // ── Regular paragraph line ──
89
+ output.push(` ${renderInline(line)}`)
90
+ }
91
+
92
+ // Close unclosed code block
93
+ if (inCodeBlock) {
94
+ output.push(` ${A.dim}└${'─'.repeat(42)}${A.reset}`)
95
+ }
96
+
97
+ return output
98
+ }
99
+
100
+ /**
101
+ * Apply inline markdown formatting (bold, italic, code, links).
102
+ */
103
+ function renderInline(text: string): string {
104
+ let result = text
105
+
106
+ // Inline code (must come before bold/italic to avoid conflicts)
107
+ result = result.replace(/`([^`]+)`/g, `${A.inv} $1 ${A.reset}`)
108
+
109
+ // Bold + italic
110
+ result = result.replace(/\*\*\*(.+?)\*\*\*/g, `${A.bold}${A.italic}$1${A.reset}`)
111
+
112
+ // Bold
113
+ result = result.replace(/\*\*(.+?)\*\*/g, `${A.bold}$1${A.reset}`)
114
+ result = result.replace(/__(.+?)__/g, `${A.bold}$1${A.reset}`)
115
+
116
+ // Italic
117
+ result = result.replace(/\*(.+?)\*/g, `${A.italic}$1${A.reset}`)
118
+ result = result.replace(/_(.+?)_/g, `${A.italic}$1${A.reset}`)
119
+
120
+ // Links [text](url)
121
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
122
+ `${A.underline}$1${A.reset} ${C.link}($2)${A.reset}`)
123
+
124
+ return result
125
+ }