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.
- package/.github/workflows/ci.yml +30 -0
- package/.github/workflows/release.yml +67 -0
- package/bun.lock +33 -0
- package/dist/index.js +321 -0
- package/dist/tinyclaw.exe +0 -0
- package/install.ps1 +119 -0
- package/package.json +25 -0
- package/skills/business.md +77 -0
- package/skills/default.md +77 -0
- package/src/ansi.ts +164 -0
- package/src/approval.ts +74 -0
- package/src/auth.ts +125 -0
- package/src/briefing.ts +52 -0
- package/src/claude.ts +267 -0
- package/src/cli.ts +137 -0
- package/src/clipboard.ts +27 -0
- package/src/config.ts +87 -0
- package/src/context-window.ts +190 -0
- package/src/context.ts +125 -0
- package/src/decisions.ts +122 -0
- package/src/email.ts +123 -0
- package/src/errors.ts +78 -0
- package/src/export.ts +82 -0
- package/src/finance.ts +148 -0
- package/src/git.ts +62 -0
- package/src/history.ts +100 -0
- package/src/images.ts +68 -0
- package/src/index.ts +1431 -0
- package/src/investigate.ts +415 -0
- package/src/markdown.ts +125 -0
- package/src/memos.ts +191 -0
- package/src/models.ts +94 -0
- package/src/monitor.ts +169 -0
- package/src/morning.ts +108 -0
- package/src/news.ts +329 -0
- package/src/openai-provider.ts +127 -0
- package/src/people.ts +472 -0
- package/src/personas.ts +99 -0
- package/src/platform.ts +84 -0
- package/src/plugins.ts +125 -0
- package/src/pomodoro.ts +169 -0
- package/src/providers.ts +70 -0
- package/src/retry.ts +108 -0
- package/src/session.ts +128 -0
- package/src/skills.ts +102 -0
- package/src/tasks.ts +418 -0
- package/src/tokens.ts +102 -0
- package/src/tool-safety.ts +100 -0
- package/src/tools.ts +1479 -0
- package/src/tui.ts +693 -0
- package/src/types.ts +55 -0
- package/src/undo.ts +83 -0
- package/src/windows.ts +299 -0
- package/src/workflows.ts +197 -0
- package/tests/ansi.test.ts +58 -0
- package/tests/approval.test.ts +43 -0
- package/tests/briefing.test.ts +10 -0
- package/tests/cli.test.ts +53 -0
- package/tests/context-window.test.ts +83 -0
- package/tests/images.test.ts +28 -0
- package/tests/memos.test.ts +116 -0
- package/tests/models.test.ts +34 -0
- package/tests/news.test.ts +13 -0
- package/tests/path-guard.test.ts +37 -0
- package/tests/people.test.ts +204 -0
- package/tests/skills.test.ts +35 -0
- package/tests/ssrf.test.ts +80 -0
- package/tests/tasks.test.ts +152 -0
- package/tests/tokens.test.ts +44 -0
- package/tests/tool-safety.test.ts +55 -0
- package/tests/windows-security.test.ts +59 -0
- package/tests/windows.test.ts +20 -0
- 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
|
+
}
|
package/src/markdown.ts
ADDED
|
@@ -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
|
+
}
|