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
package/src/memos.ts ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Persistent memo/note system — a personal knowledge base.
3
+ * Memos are tagged, searchable, and auto-consulted by the AI.
4
+ */
5
+
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
7
+ import { join } from 'node:path'
8
+
9
+ // ─── Types ──────────────────────────────────────────────────
10
+
11
+ export interface Memo {
12
+ id: string
13
+ content: string
14
+ tags: string[]
15
+ createdAt: string
16
+ updatedAt: string
17
+ }
18
+
19
+ // ─── Storage ────────────────────────────────────────────────
20
+
21
+ let _dataDir = ''
22
+ let _memos: Memo[] = []
23
+
24
+ const DATA_FILE = () => join(_dataDir, 'memos.json')
25
+
26
+ function save(): void {
27
+ writeFileSync(DATA_FILE(), JSON.stringify(_memos, null, 2))
28
+ }
29
+
30
+ function load(): void {
31
+ const file = DATA_FILE()
32
+ if (!existsSync(file)) {
33
+ _memos = []
34
+ return
35
+ }
36
+ try {
37
+ _memos = JSON.parse(readFileSync(file, 'utf-8'))
38
+ } catch {
39
+ _memos = []
40
+ }
41
+ }
42
+
43
+ // ─── Init ───────────────────────────────────────────────────
44
+
45
+ export function initMemos(dataDir: string): void {
46
+ _dataDir = dataDir
47
+ if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true })
48
+ load()
49
+ }
50
+
51
+ // ─── CRUD ───────────────────────────────────────────────────
52
+
53
+ export function saveMemo(content: string, tags: string[] = []): Memo {
54
+ const now = new Date().toISOString()
55
+
56
+ // Auto-extract tags from #hashtags in content
57
+ const hashTags = content.match(/#(\w+)/g)?.map((t) => t.slice(1).toLowerCase()) || []
58
+ const allTags = [...new Set([...tags.map((t) => t.toLowerCase()), ...hashTags])]
59
+
60
+ const memo: Memo = {
61
+ id: genId(),
62
+ content: content.trim(),
63
+ tags: allTags,
64
+ createdAt: now,
65
+ updatedAt: now,
66
+ }
67
+ _memos = [..._memos, memo]
68
+ save()
69
+ return memo
70
+ }
71
+
72
+ export function updateMemo(id: string, content: string): Memo | null {
73
+ const found = _memos.find((m) => m.id === id)
74
+ if (!found) return null
75
+
76
+ const hashTags = content.match(/#(\w+)/g)?.map((t) => t.slice(1).toLowerCase()) || []
77
+ const allTags = [...new Set([...found.tags, ...hashTags])]
78
+
79
+ _memos = _memos.map((m) =>
80
+ m.id === id
81
+ ? { ...m, content: content.trim(), tags: allTags, updatedAt: new Date().toISOString() }
82
+ : m,
83
+ )
84
+ save()
85
+ return _memos.find((m) => m.id === id) || null
86
+ }
87
+
88
+ export function deleteMemo(id: string): boolean {
89
+ const idx = _memos.findIndex((m) => m.id === id)
90
+ if (idx === -1) return false
91
+ _memos = [..._memos.slice(0, idx), ..._memos.slice(idx + 1)]
92
+ save()
93
+ return true
94
+ }
95
+
96
+ // ─── Search ─────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Search memos by keyword or tag.
100
+ * Matches against content and tags (case-insensitive).
101
+ */
102
+ export function searchMemos(query: string): Memo[] {
103
+ const lower = query.toLowerCase().trim()
104
+ if (!lower) return [..._memos]
105
+
106
+ // Check if query is a tag search (starts with #)
107
+ const isTagSearch = lower.startsWith('#')
108
+ const searchTerm = isTagSearch ? lower.slice(1) : lower
109
+
110
+ return _memos.filter((m) => {
111
+ if (isTagSearch) {
112
+ return m.tags.some((t) => t.includes(searchTerm))
113
+ }
114
+ return (
115
+ m.content.toLowerCase().includes(searchTerm) ||
116
+ m.tags.some((t) => t.includes(searchTerm))
117
+ )
118
+ }).sort((a, b) =>
119
+ new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
120
+ )
121
+ }
122
+
123
+ /**
124
+ * Get all memos, most recent first.
125
+ */
126
+ export function listMemos(limit = 20): Memo[] {
127
+ return [..._memos]
128
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
129
+ .slice(0, limit)
130
+ }
131
+
132
+ /**
133
+ * Get all unique tags with count.
134
+ */
135
+ export function getMemoTags(): Array<{ tag: string; count: number }> {
136
+ const tagMap = new Map<string, number>()
137
+ for (const memo of _memos) {
138
+ for (const tag of memo.tags) {
139
+ tagMap.set(tag, (tagMap.get(tag) || 0) + 1)
140
+ }
141
+ }
142
+ return [...tagMap.entries()]
143
+ .map(([tag, count]) => ({ tag, count }))
144
+ .sort((a, b) => b.count - a.count)
145
+ }
146
+
147
+ // ─── Formatting ─────────────────────────────────────────────
148
+
149
+ export function formatMemoList(memos: Memo[]): string {
150
+ if (memos.length === 0) return 'Nenhum memo encontrado.'
151
+
152
+ const lines = memos.map((m) => {
153
+ const date = new Date(m.updatedAt).toLocaleDateString('pt-BR', {
154
+ day: '2-digit', month: '2-digit',
155
+ })
156
+ const tags = m.tags.length > 0 ? ` [${m.tags.map((t) => `#${t}`).join(' ')}]` : ''
157
+ const preview = m.content.length > 80
158
+ ? m.content.slice(0, 80).replace(/\n/g, ' ') + '...'
159
+ : m.content.replace(/\n/g, ' ')
160
+ return ` [${date}] ${preview}${tags} {${m.id}}`
161
+ })
162
+
163
+ return `Memos (${memos.length}):\n${lines.join('\n')}`
164
+ }
165
+
166
+ export function formatMemoDetail(memo: Memo): string {
167
+ const created = new Date(memo.createdAt).toLocaleDateString('pt-BR')
168
+ const updated = new Date(memo.updatedAt).toLocaleDateString('pt-BR')
169
+ const tags = memo.tags.length > 0 ? `Tags: ${memo.tags.map((t) => `#${t}`).join(' ')}` : ''
170
+ const dates = created === updated ? `Criado: ${created}` : `Criado: ${created} | Atualizado: ${updated}`
171
+
172
+ return `--- Memo {${memo.id}} ---\n${memo.content}\n\n${tags}\n${dates}`
173
+ }
174
+
175
+ export function formatMemoTags(): string {
176
+ const tags = getMemoTags()
177
+ if (tags.length === 0) return 'Nenhuma tag.'
178
+ const lines = tags.map((t) => ` #${t.tag} (${t.count})`)
179
+ return `Tags:\n${lines.join('\n')}`
180
+ }
181
+
182
+ // ─── Helpers ────────────────────────────────────────────────
183
+
184
+ function genId(): string {
185
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
186
+ let id = ''
187
+ for (let i = 0; i < 6; i++) {
188
+ id += chars[Math.floor(Math.random() * chars.length)]
189
+ }
190
+ return id
191
+ }
package/src/models.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Model registry with aliases and metadata.
3
+ */
4
+
5
+ export interface ModelInfo {
6
+ id: string
7
+ alias: string
8
+ name: string
9
+ contextWindow: number
10
+ tier: 'fast' | 'balanced' | 'powerful'
11
+ }
12
+
13
+ export const MODELS: ModelInfo[] = [
14
+ {
15
+ id: 'claude-haiku-4-5-20251001',
16
+ alias: 'haiku',
17
+ name: 'Claude Haiku 4.5',
18
+ contextWindow: 200_000,
19
+ tier: 'fast',
20
+ },
21
+ {
22
+ id: 'claude-sonnet-4-20250514',
23
+ alias: 'sonnet',
24
+ name: 'Claude Sonnet 4',
25
+ contextWindow: 200_000,
26
+ tier: 'balanced',
27
+ },
28
+ {
29
+ id: 'claude-sonnet-4-6-20250627',
30
+ alias: 'sonnet-4.6',
31
+ name: 'Claude Sonnet 4.6',
32
+ contextWindow: 200_000,
33
+ tier: 'balanced',
34
+ },
35
+ {
36
+ id: 'claude-opus-4-20250514',
37
+ alias: 'opus',
38
+ name: 'Claude Opus 4',
39
+ contextWindow: 200_000,
40
+ tier: 'powerful',
41
+ },
42
+ {
43
+ id: 'claude-opus-4-6-20250318',
44
+ alias: 'opus-4.6',
45
+ name: 'Claude Opus 4.6',
46
+ contextWindow: 200_000,
47
+ tier: 'powerful',
48
+ },
49
+ ]
50
+
51
+ /**
52
+ * Resolve a model name or alias to a full model ID.
53
+ * Accepts: full ID, alias, or partial match.
54
+ */
55
+ export function resolveModel(input: string): string {
56
+ // Exact match on ID
57
+ const exact = MODELS.find((m) => m.id === input)
58
+ if (exact) return exact.id
59
+
60
+ // Alias match
61
+ const lower = input.toLowerCase()
62
+ const byAlias = MODELS.find((m) => m.alias === lower)
63
+ if (byAlias) return byAlias.id
64
+
65
+ // Partial match (e.g., "haiku" matches "claude-haiku-4-5-*")
66
+ const partial = MODELS.find((m) => m.id.includes(lower) || m.name.toLowerCase().includes(lower))
67
+ if (partial) return partial.id
68
+
69
+ // Unknown model — pass through as-is (custom/fine-tuned models)
70
+ return input
71
+ }
72
+
73
+ /**
74
+ * Get display name for a model ID.
75
+ */
76
+ export function modelDisplayName(id: string): string {
77
+ const info = MODELS.find((m) => m.id === id)
78
+ return info ? `${info.name} (${info.alias})` : id
79
+ }
80
+
81
+ /**
82
+ * Format model list for display.
83
+ */
84
+ export function formatModelList(currentModel: string): string {
85
+ const lines = ['Available models:']
86
+ for (const m of MODELS) {
87
+ const marker = m.id === currentModel ? ' *' : ' '
88
+ const tier = m.tier === 'fast' ? '⚡' : m.tier === 'balanced' ? '⚖️' : '🧠'
89
+ lines.push(`${marker} ${m.alias.padEnd(12)} ${tier} ${m.name}`)
90
+ }
91
+ lines.push('')
92
+ lines.push('Use: /model <alias> (e.g., /model sonnet)')
93
+ return lines.join('\n')
94
+ }
package/src/monitor.ts ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Process monitor — watch Windows processes and notify if they stop.
3
+ * Non-destructive: only checks process existence, never kills anything.
4
+ */
5
+
6
+ import { IS_WINDOWS } from './platform'
7
+
8
+ // ─── Types ──────────────────────────────────────────────────
9
+
10
+ interface MonitoredProcess {
11
+ name: string
12
+ interval: ReturnType<typeof setInterval>
13
+ lastSeen: boolean
14
+ }
15
+
16
+ type MonitorCallback = (message: string) => void
17
+
18
+ // ─── State ──────────────────────────────────────────────────
19
+
20
+ const _monitors = new Map<string, MonitoredProcess>()
21
+ let _onNotify: MonitorCallback | null = null
22
+
23
+ // ─── Init ───────────────────────────────────────────────────
24
+
25
+ export function initMonitor(onNotify: MonitorCallback): void {
26
+ _onNotify = onNotify
27
+ }
28
+
29
+ // ─── Public API ─────────────────────────────────────────────
30
+
31
+ /**
32
+ * Start monitoring a process by name. Checks every intervalSec seconds.
33
+ */
34
+ export function startMonitor(processName: string, intervalSec = 60): string {
35
+ if (!IS_WINDOWS) return 'Error: monitor is only available on Windows.'
36
+
37
+ const key = processName.toLowerCase()
38
+ if (_monitors.has(key)) {
39
+ return `"${processName}" ja esta sendo monitorado.`
40
+ }
41
+
42
+ const interval = setInterval(() => checkProcess(key), intervalSec * 1000)
43
+
44
+ _monitors.set(key, {
45
+ name: processName,
46
+ interval,
47
+ lastSeen: true, // assume running at start
48
+ })
49
+
50
+ // Do an initial check
51
+ checkProcess(key)
52
+
53
+ return `Monitorando "${processName}" a cada ${intervalSec}s.`
54
+ }
55
+
56
+ /**
57
+ * Stop monitoring a process.
58
+ */
59
+ export function stopMonitor(processName: string): string {
60
+ const key = processName.toLowerCase()
61
+ const monitor = _monitors.get(key)
62
+ if (!monitor) {
63
+ return `"${processName}" nao esta sendo monitorado.`
64
+ }
65
+
66
+ clearInterval(monitor.interval)
67
+ _monitors.delete(key)
68
+ return `Monitor parado: "${processName}"`
69
+ }
70
+
71
+ /**
72
+ * List all monitored processes.
73
+ */
74
+ export function listMonitors(): string {
75
+ if (_monitors.size === 0) return 'Nenhum processo monitorado.'
76
+
77
+ const lines = [..._monitors.values()].map((m) => {
78
+ const status = m.lastSeen ? 'rodando' : 'PARADO'
79
+ return ` ${m.name.padEnd(20)} [${status}]`
80
+ })
81
+
82
+ return `Processos monitorados (${_monitors.size}):\n${lines.join('\n')}`
83
+ }
84
+
85
+ /**
86
+ * Stop all monitors (call on exit).
87
+ */
88
+ export function stopAllMonitors(): void {
89
+ for (const monitor of _monitors.values()) {
90
+ clearInterval(monitor.interval)
91
+ }
92
+ _monitors.clear()
93
+ }
94
+
95
+ // ─── Internal ───────────────────────────────────────────────
96
+
97
+ async function checkProcess(key: string): Promise<void> {
98
+ const monitor = _monitors.get(key)
99
+ if (!monitor) return
100
+
101
+ const isRunning = await isProcessRunning(monitor.name)
102
+
103
+ if (monitor.lastSeen && !isRunning) {
104
+ // Process just stopped
105
+ const msg = `ALERTA: "${monitor.name}" parou de rodar!`
106
+ fireToast('Processo parou!', `"${monitor.name}" nao esta mais rodando.`)
107
+ _onNotify?.(msg)
108
+ } else if (!monitor.lastSeen && isRunning) {
109
+ // Process came back
110
+ const msg = `"${monitor.name}" voltou a rodar.`
111
+ _onNotify?.(msg)
112
+ }
113
+
114
+ // Update state immutably
115
+ _monitors.set(key, { ...monitor, lastSeen: isRunning })
116
+ }
117
+
118
+ async function isProcessRunning(name: string): Promise<boolean> {
119
+ if (!IS_WINDOWS) return false
120
+
121
+ try {
122
+ const cmd = `(Get-Process -Name '${name}' -ErrorAction SilentlyContinue) -ne $null`
123
+ const proc = Bun.spawn(
124
+ ['powershell', '-NoProfile', '-NonInteractive', '-Command', cmd],
125
+ { stdout: 'pipe', stderr: 'pipe' },
126
+ )
127
+ const timer = setTimeout(() => proc.kill(), 10_000)
128
+ const [stdout] = await Promise.all([
129
+ new Response(proc.stdout).text(),
130
+ new Response(proc.stderr).text(),
131
+ ])
132
+ await proc.exited
133
+ clearTimeout(timer)
134
+ return stdout.trim().toLowerCase() === 'true'
135
+ } catch {
136
+ return false
137
+ }
138
+ }
139
+
140
+ async function fireToast(title: string, body: string): Promise<void> {
141
+ if (!IS_WINDOWS) return
142
+
143
+ const safeTitle = title.replace(/'/g, "''")
144
+ const safeBody = body.replace(/'/g, "''")
145
+
146
+ const cmd = [
147
+ '[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null',
148
+ '[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null',
149
+ `$template = '<toast><visual><binding template="ToastText02"><text id="1">${safeTitle}</text><text id="2">${safeBody}</text></binding></visual><audio src="ms-winsoundevent:Notification.Default"/></toast>'`,
150
+ '$xml = New-Object Windows.Data.Xml.Dom.XmlDocument',
151
+ '$xml.LoadXml($template)',
152
+ '$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)',
153
+ '[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("smolerclaw").Show($toast)',
154
+ ].join('; ')
155
+
156
+ try {
157
+ const proc = Bun.spawn(
158
+ ['powershell', '-NoProfile', '-NonInteractive', '-Command', cmd],
159
+ { stdout: 'pipe', stderr: 'pipe' },
160
+ )
161
+ const timer = setTimeout(() => proc.kill(), 10_000)
162
+ await Promise.all([
163
+ new Response(proc.stdout).text(),
164
+ new Response(proc.stderr).text(),
165
+ ])
166
+ await proc.exited
167
+ clearTimeout(timer)
168
+ } catch { /* best effort */ }
169
+ }
package/src/morning.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Morning routine — auto-detect first use of the day and show briefing.
3
+ * Stores last-run date to avoid repeating.
4
+ */
5
+
6
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
7
+ import { join } from 'node:path'
8
+ import { getDateTimeInfo, getOutlookEvents, getSystemInfo } from './windows'
9
+ import { fetchNews } from './news'
10
+ import { listTasks, formatTaskList } from './tasks'
11
+ import { getPendingFollowUps, getDelegations, formatFollowUps, formatDelegationList } from './people'
12
+ import { IS_WINDOWS } from './platform'
13
+
14
+ let _dataDir = ''
15
+ const LAST_RUN_FILE = () => join(_dataDir, 'last-morning.txt')
16
+
17
+ /**
18
+ * Check if this is the first run of the day.
19
+ */
20
+ export function isFirstRunToday(dataDir: string): boolean {
21
+ _dataDir = dataDir
22
+ const file = LAST_RUN_FILE()
23
+ const today = new Date().toISOString().split('T')[0]
24
+
25
+ if (!existsSync(file)) return true
26
+
27
+ try {
28
+ const lastDate = readFileSync(file, 'utf-8').trim()
29
+ return lastDate !== today
30
+ } catch {
31
+ return true
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Mark today as "briefing shown".
37
+ */
38
+ export function markMorningDone(): void {
39
+ const today = new Date().toISOString().split('T')[0]
40
+ writeFileSync(LAST_RUN_FILE(), today)
41
+ }
42
+
43
+ /**
44
+ * Generate a complete morning briefing.
45
+ */
46
+ export async function generateMorningBriefing(): Promise<string> {
47
+ const sections: string[] = []
48
+
49
+ sections.push('==============================')
50
+ sections.push(' BOM DIA! Briefing do dia')
51
+ sections.push('==============================\n')
52
+
53
+ // Date & time
54
+ const dateInfo = await getDateTimeInfo()
55
+ sections.push(dateInfo)
56
+
57
+ // Today's tasks
58
+ const tasks = listTasks()
59
+ const todayTasks = tasks.filter((t) => {
60
+ if (!t.dueAt) return false
61
+ const due = new Date(t.dueAt)
62
+ const today = new Date()
63
+ return due.toDateString() === today.toDateString()
64
+ })
65
+ if (todayTasks.length > 0) {
66
+ sections.push('\n--- Tarefas do dia ---')
67
+ sections.push(formatTaskList(todayTasks))
68
+ }
69
+
70
+ // Pending follow-ups
71
+ const followUps = getPendingFollowUps()
72
+ if (followUps.length > 0) {
73
+ sections.push('\n--- Follow-ups pendentes ---')
74
+ sections.push(formatFollowUps(followUps))
75
+ }
76
+
77
+ // Overdue delegations
78
+ const delegations = getDelegations()
79
+ const overdue = delegations.filter((d) => d.status === 'atrasado')
80
+ if (overdue.length > 0) {
81
+ sections.push('\n--- Delegacoes atrasadas ---')
82
+ sections.push(formatDelegationList(overdue))
83
+ }
84
+
85
+ // Calendar (Windows only)
86
+ if (IS_WINDOWS) {
87
+ try {
88
+ const events = await getOutlookEvents()
89
+ sections.push('\n--- Agenda ---')
90
+ sections.push(events)
91
+ } catch { /* skip */ }
92
+ }
93
+
94
+ // Top news (limited)
95
+ try {
96
+ const news = await fetchNews(['finance', 'business', 'tech'], 2)
97
+ sections.push('\n' + news)
98
+ } catch { /* skip */ }
99
+
100
+ // Pending tasks count
101
+ const allPending = listTasks()
102
+ if (allPending.length > 0 && todayTasks.length !== allPending.length) {
103
+ sections.push(`\n${allPending.length} tarefa(s) pendente(s) no total. Use /tarefas para ver todas.`)
104
+ }
105
+
106
+ sections.push('\n==============================')
107
+ return sections.join('\n')
108
+ }