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
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
|
+
}
|