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/index.ts
ADDED
|
@@ -0,0 +1,1431 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { parseArgs, printHelp, getVersion } from './cli'
|
|
3
|
+
import { loadConfig, saveConfig, getConfigPath } from './config'
|
|
4
|
+
import { resolveAuth, refreshAuth, authLabel, type AuthResult } from './auth'
|
|
5
|
+
import { ClaudeProvider } from './claude'
|
|
6
|
+
import { SessionManager } from './session'
|
|
7
|
+
import { loadSkills, buildSystemPrompt, formatSkillList } from './skills'
|
|
8
|
+
import { TUI } from './tui'
|
|
9
|
+
import { TokenTracker } from './tokens'
|
|
10
|
+
import { exportToMarkdown } from './export'
|
|
11
|
+
import { resolveModel, formatModelList, modelDisplayName } from './models'
|
|
12
|
+
import { parseModelString, formatProviderList } from './providers'
|
|
13
|
+
import { OpenAICompatProvider } from './openai-provider'
|
|
14
|
+
import { gitDiff, gitStatus, gitStageAll, gitCommit, isGitRepo } from './git'
|
|
15
|
+
import { getPersona, formatPersonaList, type Persona } from './personas'
|
|
16
|
+
import { copyToClipboard } from './clipboard'
|
|
17
|
+
import { undoStack, registerPlugins, registerWindowsTools, TOOLS } from './tools'
|
|
18
|
+
import { loadPlugins, pluginsToTools, formatPluginList, getPluginDir } from './plugins'
|
|
19
|
+
import { formatApprovalPrompt, formatEditDiff } from './approval'
|
|
20
|
+
import { extractImages } from './images'
|
|
21
|
+
import { openApp, openFile, openUrl, getRunningApps, getSystemInfo, getDateTimeInfo, getOutlookEvents, getKnownApps } from './windows'
|
|
22
|
+
import { fetchNews, getNewsCategories, type NewsCategory } from './news'
|
|
23
|
+
import { generateBriefing } from './briefing'
|
|
24
|
+
import { initTasks, stopTasks, addTask, completeTask, removeTask, listTasks, formatTaskList, parseTime, type Task } from './tasks'
|
|
25
|
+
import { initPeople, addPerson, findPerson, listPeople, logInteraction, delegateTask, getDelegations, getPendingFollowUps, markFollowUpDone, formatPeopleList, formatPersonDetail, formatDelegationList, formatFollowUps, generatePeopleDashboard, type PersonGroup, type InteractionType } from './people'
|
|
26
|
+
import { initMemos, saveMemo, searchMemos, listMemos, deleteMemo, formatMemoList, formatMemoDetail, formatMemoTags } from './memos'
|
|
27
|
+
import { isFirstRunToday, markMorningDone, generateMorningBriefing } from './morning'
|
|
28
|
+
import { openEmailDraft, formatDraftPreview } from './email'
|
|
29
|
+
import { initPomodoro, startPomodoro, stopPomodoro, pomodoroStatus, stopPomodoroTimer } from './pomodoro'
|
|
30
|
+
import { initFinance, addTransaction, getMonthSummary, getRecentTransactions, removeTransaction } from './finance'
|
|
31
|
+
import { initDecisions, logDecision, searchDecisions, listDecisions, formatDecisionList, formatDecisionDetail } from './decisions'
|
|
32
|
+
import { initWorkflows, runWorkflow, listWorkflows, createWorkflow, deleteWorkflow, formatWorkflowList, type WorkflowStep } from './workflows'
|
|
33
|
+
import { initMonitor, startMonitor, stopMonitor, listMonitors, stopAllMonitors } from './monitor'
|
|
34
|
+
import { initInvestigations } from './investigate'
|
|
35
|
+
import { writeFileSync } from 'node:fs'
|
|
36
|
+
import { join } from 'node:path'
|
|
37
|
+
import type { Message, ToolCall } from './types'
|
|
38
|
+
|
|
39
|
+
async function main(): Promise<void> {
|
|
40
|
+
const cliArgs = parseArgs(process.argv.slice(2))
|
|
41
|
+
|
|
42
|
+
// ── Help / Version ───────────────────────────────────────
|
|
43
|
+
if (cliArgs.help) {
|
|
44
|
+
printHelp()
|
|
45
|
+
process.exit(0)
|
|
46
|
+
}
|
|
47
|
+
if (cliArgs.version) {
|
|
48
|
+
console.log(`smolerclaw v${getVersion()}`)
|
|
49
|
+
process.exit(0)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Load config and auth ─────────────────────────────────
|
|
53
|
+
const config = loadConfig()
|
|
54
|
+
if (cliArgs.model) config.model = resolveModel(cliArgs.model)
|
|
55
|
+
if (cliArgs.maxTokens) config.maxTokens = cliArgs.maxTokens
|
|
56
|
+
|
|
57
|
+
let auth: AuthResult
|
|
58
|
+
try {
|
|
59
|
+
auth = resolveAuth(config.apiKey, config.authMode)
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('smolerclaw:', err instanceof Error ? err.message : err)
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Initialize provider based on model string
|
|
66
|
+
const { provider: providerType, model: providerModel } = parseModelString(config.model)
|
|
67
|
+
let claude: ClaudeProvider | OpenAICompatProvider
|
|
68
|
+
|
|
69
|
+
if (providerType === 'openai' || providerType === 'ollama') {
|
|
70
|
+
claude = new OpenAICompatProvider(providerType, providerModel, config.maxTokens)
|
|
71
|
+
} else {
|
|
72
|
+
const claudeProvider = new ClaudeProvider(auth.apiKey, config.model, config.maxTokens, config.toolApproval)
|
|
73
|
+
|
|
74
|
+
// Auto-refresh credentials on 401 so the session survives token expiration
|
|
75
|
+
claudeProvider.setAuthRefresh(() => {
|
|
76
|
+
const freshAuth = refreshAuth(config.apiKey, config.authMode)
|
|
77
|
+
if (freshAuth && freshAuth.apiKey !== auth.apiKey) {
|
|
78
|
+
auth = freshAuth
|
|
79
|
+
claudeProvider.updateApiKey(freshAuth.apiKey)
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
claude = claudeProvider
|
|
86
|
+
}
|
|
87
|
+
const sessionName = cliArgs.session || 'default'
|
|
88
|
+
const sessions = new SessionManager(config.dataDir)
|
|
89
|
+
if (cliArgs.session) sessions.switchTo(cliArgs.session)
|
|
90
|
+
const skills = loadSkills(config.skillsDir)
|
|
91
|
+
const systemPrompt = buildSystemPrompt(config.systemPrompt, skills, config.language)
|
|
92
|
+
const enableTools = !cliArgs.noTools
|
|
93
|
+
|
|
94
|
+
// Register Windows/business tools
|
|
95
|
+
registerWindowsTools()
|
|
96
|
+
|
|
97
|
+
// Load plugins
|
|
98
|
+
const pluginDir = getPluginDir(join(config.dataDir, '..'))
|
|
99
|
+
const plugins = loadPlugins(pluginDir)
|
|
100
|
+
if (plugins.length > 0) {
|
|
101
|
+
registerPlugins(plugins)
|
|
102
|
+
TOOLS.push(...pluginsToTools(plugins))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Pipe mode: stdin is not a TTY ────────────────────────
|
|
106
|
+
const isPiped = !process.stdin.isTTY
|
|
107
|
+
|
|
108
|
+
if (cliArgs.print || isPiped) {
|
|
109
|
+
await runPrintMode(claude, sessions, systemPrompt, enableTools, cliArgs.prompt, isPiped)
|
|
110
|
+
process.exit(0)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Interactive TUI mode ─────────────────────────────────
|
|
114
|
+
await runInteractive(claude, sessions, config, auth, skills, systemPrompt, enableTools, plugins, cliArgs.prompt)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Print Mode ───────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
// Common provider interface for both Claude and OpenAI-compatible
|
|
120
|
+
type AnyProvider = { chat: ClaudeProvider['chat']; setModel: (m: string) => void; setApprovalCallback?: ClaudeProvider['setApprovalCallback']; setAutoApproveAll?: ClaudeProvider['setAutoApproveAll'] }
|
|
121
|
+
|
|
122
|
+
async function runPrintMode(
|
|
123
|
+
claude: AnyProvider,
|
|
124
|
+
sessions: SessionManager,
|
|
125
|
+
systemPrompt: string,
|
|
126
|
+
enableTools: boolean,
|
|
127
|
+
prompt?: string,
|
|
128
|
+
isPiped?: boolean,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
let input = prompt || ''
|
|
131
|
+
|
|
132
|
+
// Read stdin if piped
|
|
133
|
+
if (isPiped) {
|
|
134
|
+
const stdinText = await readStdin()
|
|
135
|
+
input = input ? `${input}\n\n${stdinText}` : stdinText
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!input.trim()) {
|
|
139
|
+
console.error('smolerclaw: no input provided')
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const userMsg: Message = { role: 'user', content: input.trim(), timestamp: Date.now() }
|
|
144
|
+
sessions.addMessage(userMsg)
|
|
145
|
+
|
|
146
|
+
let fullText = ''
|
|
147
|
+
for await (const event of claude.chat(sessions.messages, systemPrompt, enableTools)) {
|
|
148
|
+
if (event.type === 'text') {
|
|
149
|
+
process.stdout.write(event.text)
|
|
150
|
+
fullText += event.text
|
|
151
|
+
} else if (event.type === 'error') {
|
|
152
|
+
console.error(`\nsmolerclaw error: ${event.error}`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Ensure trailing newline
|
|
157
|
+
if (fullText && !fullText.endsWith('\n')) {
|
|
158
|
+
process.stdout.write('\n')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const assistantMsg: Message = { role: 'assistant', content: fullText, timestamp: Date.now() }
|
|
162
|
+
sessions.addMessage(assistantMsg)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function readStdin(): Promise<string> {
|
|
166
|
+
const chunks: Uint8Array[] = []
|
|
167
|
+
for await (const chunk of process.stdin) {
|
|
168
|
+
chunks.push(chunk)
|
|
169
|
+
}
|
|
170
|
+
return Buffer.concat(chunks).toString('utf-8')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Interactive Mode ─────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
async function runInteractive(
|
|
176
|
+
claude: AnyProvider,
|
|
177
|
+
sessions: SessionManager,
|
|
178
|
+
config: ReturnType<typeof loadConfig>,
|
|
179
|
+
auth: AuthResult,
|
|
180
|
+
skills: ReturnType<typeof loadSkills>,
|
|
181
|
+
systemPrompt: string,
|
|
182
|
+
enableTools: boolean,
|
|
183
|
+
plugins: ReturnType<typeof loadPlugins>,
|
|
184
|
+
initialPrompt?: string,
|
|
185
|
+
): Promise<void> {
|
|
186
|
+
const tracker = new TokenTracker(config.model)
|
|
187
|
+
const tui = new TUI(config.model, sessions.session.name, authLabel(auth), config.dataDir)
|
|
188
|
+
let currentPersona = 'default'
|
|
189
|
+
let activeSystemPrompt = systemPrompt
|
|
190
|
+
|
|
191
|
+
// Initialize people, task, and memo systems
|
|
192
|
+
initPeople(config.dataDir)
|
|
193
|
+
initMemos(config.dataDir)
|
|
194
|
+
initFinance(config.dataDir)
|
|
195
|
+
initDecisions(config.dataDir)
|
|
196
|
+
initPomodoro((msg) => tui.showSystem(`\n*** ${msg} ***\n`))
|
|
197
|
+
initWorkflows(config.dataDir)
|
|
198
|
+
initInvestigations(config.dataDir)
|
|
199
|
+
initMonitor((msg) => tui.showSystem(`\n*** ${msg} ***\n`))
|
|
200
|
+
initTasks(config.dataDir, (task: Task) => {
|
|
201
|
+
tui.showSystem(`\n*** LEMBRETE: ${task.title} ***\n`)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Wire tool approval callback
|
|
205
|
+
if (config.toolApproval !== 'auto' && claude.setApprovalCallback) {
|
|
206
|
+
claude.setApprovalCallback(async (toolName, input, riskLevel) => {
|
|
207
|
+
// Show diff preview for edit_file
|
|
208
|
+
if (toolName === 'edit_file' && input.old_text && input.new_text) {
|
|
209
|
+
const diffLines = formatEditDiff(String(input.old_text), String(input.new_text))
|
|
210
|
+
for (const line of diffLines) {
|
|
211
|
+
tui.showSystem(line)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const desc = formatApprovalPrompt(toolName, input)
|
|
215
|
+
const approved = await tui.promptApproval(desc)
|
|
216
|
+
if (tui._approveAllRequested) {
|
|
217
|
+
claude.setAutoApproveAll?.(true)
|
|
218
|
+
tui._approveAllRequested = false
|
|
219
|
+
}
|
|
220
|
+
return approved
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Restore existing messages
|
|
225
|
+
for (const msg of sessions.messages) {
|
|
226
|
+
if (msg.role === 'user') tui.addUserMessage(msg.content)
|
|
227
|
+
else tui.addAssistantMessage(msg.content)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let activeAbort: AbortController | null = null
|
|
231
|
+
|
|
232
|
+
async function handleSubmit(input: string): Promise<void> {
|
|
233
|
+
if (input.startsWith('/')) {
|
|
234
|
+
await handleCommand(input)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Cost budget check
|
|
239
|
+
if (config.maxSessionCost > 0) {
|
|
240
|
+
const spent = tracker.totals.costCents
|
|
241
|
+
if (spent >= config.maxSessionCost) {
|
|
242
|
+
tui.showError(`Budget exceeded (~$${(spent / 100).toFixed(4)} / $${(config.maxSessionCost / 100).toFixed(4)}). Use /budget <cents> to increase or /clear to reset.`)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
if (spent >= config.maxSessionCost * 0.8) {
|
|
246
|
+
tui.showSystem(`Budget: ${Math.round((spent / config.maxSessionCost) * 100)}% used`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Extract image attachments from input
|
|
251
|
+
const { text: cleanedInput, images } = extractImages(input)
|
|
252
|
+
const userMsg: Message = {
|
|
253
|
+
role: 'user',
|
|
254
|
+
content: cleanedInput,
|
|
255
|
+
images: images.length > 0 ? images.map((i) => ({ mediaType: i.mediaType, base64: i.base64 })) : undefined,
|
|
256
|
+
timestamp: Date.now(),
|
|
257
|
+
}
|
|
258
|
+
sessions.addMessage(userMsg)
|
|
259
|
+
tui.addUserMessage(images.length > 0 ? `${cleanedInput} (${images.length} image${images.length > 1 ? 's' : ''})` : cleanedInput)
|
|
260
|
+
tui.disableInput()
|
|
261
|
+
|
|
262
|
+
tui.startStream()
|
|
263
|
+
let fullText = ''
|
|
264
|
+
const toolCalls: ToolCall[] = []
|
|
265
|
+
let pendingToolInput: Record<string, unknown> = {}
|
|
266
|
+
let totalInput = 0
|
|
267
|
+
let totalOutput = 0
|
|
268
|
+
activeAbort = new AbortController()
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
for await (const event of claude.chat(sessions.messages, activeSystemPrompt, enableTools)) {
|
|
272
|
+
if (activeAbort.signal.aborted) break
|
|
273
|
+
|
|
274
|
+
switch (event.type) {
|
|
275
|
+
case 'text':
|
|
276
|
+
tui.appendStream(event.text)
|
|
277
|
+
fullText += event.text
|
|
278
|
+
break
|
|
279
|
+
|
|
280
|
+
case 'tool_call':
|
|
281
|
+
tui.flushStream()
|
|
282
|
+
tui.showToolCall(event.name, event.input)
|
|
283
|
+
pendingToolInput = event.input as Record<string, unknown>
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
case 'tool_result':
|
|
287
|
+
tui.showToolResult(event.name, event.result)
|
|
288
|
+
toolCalls.push({
|
|
289
|
+
id: event.id,
|
|
290
|
+
name: event.name,
|
|
291
|
+
input: pendingToolInput,
|
|
292
|
+
result: event.result,
|
|
293
|
+
})
|
|
294
|
+
pendingToolInput = {}
|
|
295
|
+
tui.resetStreamBuffer()
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
case 'tool_blocked':
|
|
299
|
+
tui.showError(event.reason)
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
case 'usage':
|
|
303
|
+
totalInput += event.inputTokens
|
|
304
|
+
totalOutput += event.outputTokens
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
case 'error':
|
|
308
|
+
tui.showError(event.error)
|
|
309
|
+
break
|
|
310
|
+
|
|
311
|
+
case 'done':
|
|
312
|
+
break
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
if (!activeAbort.signal.aborted) {
|
|
317
|
+
tui.showError(err instanceof Error ? err.message : String(err))
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
activeAbort = null
|
|
322
|
+
tui.endStream()
|
|
323
|
+
|
|
324
|
+
// Track and display token usage
|
|
325
|
+
const usage = { inputTokens: totalInput, outputTokens: totalOutput }
|
|
326
|
+
const cost = tracker.add(usage)
|
|
327
|
+
|
|
328
|
+
if (totalInput > 0 || totalOutput > 0) {
|
|
329
|
+
tui.showUsage(tracker.formatUsage(usage))
|
|
330
|
+
tui.updateSessionCost(`~$${(tracker.totals.costCents / 100).toFixed(4)}`)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const assistantMsg: Message = {
|
|
334
|
+
role: 'assistant',
|
|
335
|
+
content: fullText,
|
|
336
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
337
|
+
usage: totalInput > 0 ? { inputTokens: totalInput, outputTokens: totalOutput, costCents: cost.totalCostCents } : undefined,
|
|
338
|
+
timestamp: Date.now(),
|
|
339
|
+
}
|
|
340
|
+
sessions.addMessage(assistantMsg)
|
|
341
|
+
sessions.trimHistory(config.maxHistory)
|
|
342
|
+
tui.enableInput()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function handleCommand(input: string): Promise<void> {
|
|
346
|
+
const parts = input.slice(1).split(' ')
|
|
347
|
+
const cmd = parts[0].toLowerCase()
|
|
348
|
+
const args = parts.slice(1)
|
|
349
|
+
|
|
350
|
+
switch (cmd) {
|
|
351
|
+
case 'exit':
|
|
352
|
+
case 'quit':
|
|
353
|
+
case 'sair':
|
|
354
|
+
case 'q':
|
|
355
|
+
cleanup()
|
|
356
|
+
break
|
|
357
|
+
|
|
358
|
+
case 'clear':
|
|
359
|
+
case 'limpar':
|
|
360
|
+
sessions.clear()
|
|
361
|
+
tui.clearMessages()
|
|
362
|
+
tui.showSystem('Conversation cleared.')
|
|
363
|
+
break
|
|
364
|
+
|
|
365
|
+
case 'new':
|
|
366
|
+
case 'novo':
|
|
367
|
+
case 'nova': {
|
|
368
|
+
const name = args[0] || `s-${Date.now()}`
|
|
369
|
+
sessions.switchTo(name)
|
|
370
|
+
tui.clearMessages()
|
|
371
|
+
tui.updateSession(name)
|
|
372
|
+
tui.showSystem(`New session: ${name}`)
|
|
373
|
+
break
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
case 'load':
|
|
377
|
+
case 'carregar': {
|
|
378
|
+
const name = args[0]
|
|
379
|
+
if (!name) {
|
|
380
|
+
tui.showError('Usage: /load <name>')
|
|
381
|
+
break
|
|
382
|
+
}
|
|
383
|
+
sessions.switchTo(name)
|
|
384
|
+
tui.clearMessages()
|
|
385
|
+
for (const msg of sessions.messages) {
|
|
386
|
+
if (msg.role === 'user') tui.addUserMessage(msg.content)
|
|
387
|
+
else tui.addAssistantMessage(msg.content)
|
|
388
|
+
}
|
|
389
|
+
tui.updateSession(name)
|
|
390
|
+
tui.showSystem(`Loaded: ${name}`)
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
case 'sessions':
|
|
395
|
+
case 'sessoes':
|
|
396
|
+
case 'ls': {
|
|
397
|
+
const list = sessions.list()
|
|
398
|
+
if (list.length === 0) {
|
|
399
|
+
tui.showSystem('No saved sessions.')
|
|
400
|
+
break
|
|
401
|
+
}
|
|
402
|
+
const details = list.map((name) => {
|
|
403
|
+
const info = sessions.getInfo(name)
|
|
404
|
+
const marker = name === sessions.session.name ? ' *' : ' '
|
|
405
|
+
const age = info ? formatAge(info.updated) : ''
|
|
406
|
+
const msgs = info ? `${info.messageCount} msgs` : ''
|
|
407
|
+
return `${marker} ${name.padEnd(20)} ${msgs.padEnd(10)} ${age}`
|
|
408
|
+
})
|
|
409
|
+
tui.showSystem('Sessions:\n' + details.join('\n'))
|
|
410
|
+
break
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
case 'delete':
|
|
414
|
+
case 'deletar':
|
|
415
|
+
case 'rm': {
|
|
416
|
+
const name = args[0]
|
|
417
|
+
if (!name) {
|
|
418
|
+
tui.showError('Usage: /delete <name>')
|
|
419
|
+
break
|
|
420
|
+
}
|
|
421
|
+
if (sessions.delete(name)) {
|
|
422
|
+
tui.showSystem(`Deleted: ${name}`)
|
|
423
|
+
} else {
|
|
424
|
+
tui.showError(`Session not found: ${name}`)
|
|
425
|
+
}
|
|
426
|
+
break
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
case 'model':
|
|
430
|
+
case 'modelo': {
|
|
431
|
+
const m = args[0]
|
|
432
|
+
if (!m) {
|
|
433
|
+
tui.showSystem(formatModelList(config.model) + '\n\n' + formatProviderList())
|
|
434
|
+
break
|
|
435
|
+
}
|
|
436
|
+
const { provider, model: modelName } = parseModelString(m)
|
|
437
|
+
const resolved = provider === 'anthropic' ? resolveModel(modelName) : modelName
|
|
438
|
+
config.model = provider === 'anthropic' ? resolved : `${provider}:${resolved}`
|
|
439
|
+
saveConfig(config)
|
|
440
|
+
if (provider === 'anthropic') {
|
|
441
|
+
claude.setModel(resolved)
|
|
442
|
+
} else {
|
|
443
|
+
// For non-anthropic providers, show info but keep using claude for now
|
|
444
|
+
// Full provider switch requires restarting the provider instance
|
|
445
|
+
tui.showSystem(`Note: ${provider} provider selected. Restart smolerclaw for full provider switch.`)
|
|
446
|
+
}
|
|
447
|
+
tracker.setModel(resolved)
|
|
448
|
+
tui.updateModel(config.model)
|
|
449
|
+
tui.showSystem(`Model -> ${config.model}`)
|
|
450
|
+
break
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
case 'skills':
|
|
454
|
+
case 'habilidades': {
|
|
455
|
+
tui.showSystem(formatSkillList(skills))
|
|
456
|
+
break
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
case 'auth':
|
|
460
|
+
tui.showSystem(
|
|
461
|
+
`Auth: ${auth.source}` +
|
|
462
|
+
(auth.subscriptionType ? ` (${auth.subscriptionType})` : '') +
|
|
463
|
+
(auth.expiresAt
|
|
464
|
+
? `\nExpires: ${new Date(auth.expiresAt).toLocaleString()}`
|
|
465
|
+
: ''),
|
|
466
|
+
)
|
|
467
|
+
break
|
|
468
|
+
|
|
469
|
+
case 'config':
|
|
470
|
+
tui.showSystem(`Config: ${getConfigPath()}`)
|
|
471
|
+
break
|
|
472
|
+
|
|
473
|
+
case 'export':
|
|
474
|
+
case 'exportar': {
|
|
475
|
+
const datePart = new Date().toISOString().split('T')[0]
|
|
476
|
+
const exportPath = args[0] || `smolerclaw-${sessions.session.name}-${datePart}.md`
|
|
477
|
+
try {
|
|
478
|
+
const md = exportToMarkdown(sessions.session)
|
|
479
|
+
writeFileSync(exportPath, md)
|
|
480
|
+
tui.showSystem(`Exported to: ${exportPath}`)
|
|
481
|
+
} catch (err) {
|
|
482
|
+
tui.showError(`Export failed: ${err instanceof Error ? err.message : err}`)
|
|
483
|
+
}
|
|
484
|
+
break
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
case 'cost':
|
|
488
|
+
case 'custo':
|
|
489
|
+
tui.showSystem(`Session: ${tracker.formatSession()}`)
|
|
490
|
+
break
|
|
491
|
+
|
|
492
|
+
case 'retry':
|
|
493
|
+
case 'repetir': {
|
|
494
|
+
const lastUserMsg = [...sessions.messages].reverse().find((m) => m.role === 'user')
|
|
495
|
+
if (!lastUserMsg) {
|
|
496
|
+
tui.showError('No previous message to retry.')
|
|
497
|
+
break
|
|
498
|
+
}
|
|
499
|
+
// Remove last exchange (assistant + user) via safe method that persists
|
|
500
|
+
const msgs = sessions.messages
|
|
501
|
+
let toPop = 0
|
|
502
|
+
if (msgs.length > 0 && msgs[msgs.length - 1].role === 'assistant') toPop++
|
|
503
|
+
if (msgs.length > toPop && msgs[msgs.length - 1 - toPop].role === 'user') toPop++
|
|
504
|
+
if (toPop > 0) sessions.popMessages(toPop)
|
|
505
|
+
|
|
506
|
+
tui.showSystem('Retrying...')
|
|
507
|
+
// Reconstruct input with image references if present
|
|
508
|
+
const retryInput = lastUserMsg.images?.length
|
|
509
|
+
? lastUserMsg.content // images are stored in session, will be picked up again
|
|
510
|
+
: lastUserMsg.content
|
|
511
|
+
await handleSubmit(retryInput)
|
|
512
|
+
break
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
case 'help':
|
|
516
|
+
case 'ajuda':
|
|
517
|
+
case '?':
|
|
518
|
+
tui.showSystem(
|
|
519
|
+
[
|
|
520
|
+
'Comandos / Commands (en | pt):',
|
|
521
|
+
' /help /ajuda Mostrar ajuda',
|
|
522
|
+
' /clear /limpar Limpar conversa',
|
|
523
|
+
' /new /novo Nova sessao',
|
|
524
|
+
' /load /carregar Carregar sessao',
|
|
525
|
+
' /sessions /sessoes Listar sessoes',
|
|
526
|
+
' /delete /deletar Deletar sessao',
|
|
527
|
+
' /model /modelo Ver/trocar modelo',
|
|
528
|
+
' /persona /modo Trocar modo (business, coder...)',
|
|
529
|
+
' /export /exportar Exportar para markdown',
|
|
530
|
+
' /copy /copiar Copiar ultima resposta',
|
|
531
|
+
' /cost /custo Ver uso de tokens',
|
|
532
|
+
' /retry /repetir Repetir ultima msg',
|
|
533
|
+
' /undo /desfazer Desfazer alteracao',
|
|
534
|
+
' /search /buscar Buscar na conversa',
|
|
535
|
+
' /lang /idioma Definir idioma',
|
|
536
|
+
' /commit /commitar Git commit com IA',
|
|
537
|
+
' /exit /sair Sair',
|
|
538
|
+
'',
|
|
539
|
+
'Negocios / Business:',
|
|
540
|
+
' /briefing /resumo Briefing diario',
|
|
541
|
+
' /news /noticias Radar de noticias',
|
|
542
|
+
' /open /abrir Abrir app Windows',
|
|
543
|
+
' /apps /programas Apps em execucao',
|
|
544
|
+
' /sysinfo /sistema Recursos do sistema',
|
|
545
|
+
' /calendar /agenda Calendario Outlook',
|
|
546
|
+
'',
|
|
547
|
+
'Pessoas / People:',
|
|
548
|
+
' /addperson /novapessoa Cadastrar pessoa',
|
|
549
|
+
' /people /pessoas Listar todas',
|
|
550
|
+
' /team /equipe Listar equipe',
|
|
551
|
+
' /family /familia Listar familia',
|
|
552
|
+
' /person /pessoa Detalhes de alguem',
|
|
553
|
+
' /delegate /delegar Delegar tarefa',
|
|
554
|
+
' /delegations /delegacoes Listar delegacoes',
|
|
555
|
+
' /followups Follow-ups pendentes',
|
|
556
|
+
' /dashboard /painel Painel geral',
|
|
557
|
+
'',
|
|
558
|
+
'Monitor:',
|
|
559
|
+
' /monitor /vigiar Monitorar processo (ex: /monitor nginx)',
|
|
560
|
+
' /monitor stop <nome> Parar monitoramento',
|
|
561
|
+
'',
|
|
562
|
+
'Workflows:',
|
|
563
|
+
' /workflow /fluxo Listar workflows',
|
|
564
|
+
' /workflow run <nome> Executar (ex: /workflow iniciar-dia)',
|
|
565
|
+
'',
|
|
566
|
+
'Pomodoro:',
|
|
567
|
+
' /pomodoro /foco Iniciar (ex: /foco revisar codigo)',
|
|
568
|
+
' /pomodoro status Ver tempo restante',
|
|
569
|
+
' /pomodoro stop Parar',
|
|
570
|
+
'',
|
|
571
|
+
'Financas / Finance:',
|
|
572
|
+
' /entrada <$> <cat> Registrar entrada',
|
|
573
|
+
' /saida <$> <cat> Registrar saida',
|
|
574
|
+
' /finance /balanco Resumo mensal',
|
|
575
|
+
'',
|
|
576
|
+
'Decisoes / Decisions:',
|
|
577
|
+
' /decisoes [busca] Listar/buscar decisoes',
|
|
578
|
+
'',
|
|
579
|
+
'Email:',
|
|
580
|
+
' /email /rascunho Rascunho (ex: /email joao@x.com oi | texto)',
|
|
581
|
+
'',
|
|
582
|
+
'Memos / Notes:',
|
|
583
|
+
' /memo /anotar Salvar memo (ex: /memo senha wifi #casa)',
|
|
584
|
+
' /memos /notas Buscar memos (ex: /memos docker)',
|
|
585
|
+
' /tags /memotags Listar tags',
|
|
586
|
+
' /rmmemo /rmnota Remover memo',
|
|
587
|
+
'',
|
|
588
|
+
'Investigacao / Investigation:',
|
|
589
|
+
' /investigar /investigate Listar investigacoes',
|
|
590
|
+
' /investigar <busca> Buscar por palavra-chave',
|
|
591
|
+
'',
|
|
592
|
+
'Tarefas / Tasks:',
|
|
593
|
+
' /task /tarefa Criar tarefa (ex: /tarefa 18h buscar pao)',
|
|
594
|
+
' /tasks /tarefas Listar pendentes',
|
|
595
|
+
' /done /feito Marcar como concluida',
|
|
596
|
+
' /rmtask /rmtarefa Remover tarefa',
|
|
597
|
+
'',
|
|
598
|
+
'Tab completes commands. Use \\ at end of line for multi-line.',
|
|
599
|
+
'',
|
|
600
|
+
'Keys:',
|
|
601
|
+
' Ctrl+C Cancel stream / exit',
|
|
602
|
+
' Ctrl+D Exit',
|
|
603
|
+
' Ctrl+L Redraw screen',
|
|
604
|
+
' Up/Down Input history',
|
|
605
|
+
' PgUp/PgDown Scroll messages',
|
|
606
|
+
].join('\n'),
|
|
607
|
+
)
|
|
608
|
+
break
|
|
609
|
+
|
|
610
|
+
case 'commit':
|
|
611
|
+
case 'commitar': {
|
|
612
|
+
if (!await isGitRepo()) {
|
|
613
|
+
tui.showError('Not a git repository.')
|
|
614
|
+
break
|
|
615
|
+
}
|
|
616
|
+
const status = await gitStatus()
|
|
617
|
+
if (status === '(clean)') {
|
|
618
|
+
tui.showSystem('Nothing to commit — working tree clean.')
|
|
619
|
+
break
|
|
620
|
+
}
|
|
621
|
+
tui.showSystem('Changes:\n' + status)
|
|
622
|
+
tui.disableInput()
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
// Get diff and generate commit message via AI
|
|
626
|
+
const diff = await gitDiff()
|
|
627
|
+
const commitPrompt = `Generate a concise git commit message for these changes. Use conventional commits format (feat:, fix:, refactor:, docs:, chore:, etc.). One line, max 72 chars. No quotes. Just the message.\n\nDiff:\n${diff.slice(0, 8000)}`
|
|
628
|
+
|
|
629
|
+
tui.startStream()
|
|
630
|
+
let commitMsg = ''
|
|
631
|
+
for await (const event of claude.chat(
|
|
632
|
+
[{ role: 'user', content: commitPrompt, timestamp: Date.now() }],
|
|
633
|
+
'You generate git commit messages. Output ONLY the commit message, nothing else.',
|
|
634
|
+
false,
|
|
635
|
+
)) {
|
|
636
|
+
if (event.type === 'text') {
|
|
637
|
+
commitMsg += event.text
|
|
638
|
+
tui.appendStream(event.text)
|
|
639
|
+
} else if (event.type === 'error') {
|
|
640
|
+
tui.showError(event.error)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
tui.endStream()
|
|
644
|
+
|
|
645
|
+
commitMsg = commitMsg.trim().replace(/^["']|["']$/g, '')
|
|
646
|
+
|
|
647
|
+
// Guard: don't stage/commit with empty message
|
|
648
|
+
if (!commitMsg) {
|
|
649
|
+
tui.showError('Failed to generate commit message. Aborting.')
|
|
650
|
+
break
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Stage and commit
|
|
654
|
+
await gitStageAll()
|
|
655
|
+
const result = await gitCommit(commitMsg)
|
|
656
|
+
if (result.ok) {
|
|
657
|
+
tui.showSystem(`Committed: ${commitMsg}`)
|
|
658
|
+
} else {
|
|
659
|
+
tui.showError(`Commit failed: ${result.output}`)
|
|
660
|
+
}
|
|
661
|
+
} catch (err) {
|
|
662
|
+
tui.showError(`Commit error: ${err instanceof Error ? err.message : String(err)}`)
|
|
663
|
+
}
|
|
664
|
+
tui.enableInput()
|
|
665
|
+
break
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
case 'persona':
|
|
669
|
+
case 'modo': {
|
|
670
|
+
const name = args[0]
|
|
671
|
+
if (!name) {
|
|
672
|
+
tui.showSystem(formatPersonaList(currentPersona))
|
|
673
|
+
break
|
|
674
|
+
}
|
|
675
|
+
const persona = getPersona(name)
|
|
676
|
+
if (!persona) {
|
|
677
|
+
tui.showError(`Unknown persona: ${name}. Try /persona to see options.`)
|
|
678
|
+
break
|
|
679
|
+
}
|
|
680
|
+
currentPersona = persona.name
|
|
681
|
+
if (persona.systemPrompt) {
|
|
682
|
+
activeSystemPrompt = buildSystemPrompt(persona.systemPrompt, skills, config.language)
|
|
683
|
+
} else {
|
|
684
|
+
activeSystemPrompt = systemPrompt
|
|
685
|
+
}
|
|
686
|
+
tui.showSystem(`Persona -> ${persona.name}: ${persona.description}`)
|
|
687
|
+
break
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
case 'copy':
|
|
691
|
+
case 'copiar': {
|
|
692
|
+
// Copy last assistant message to clipboard
|
|
693
|
+
const lastAssistant = [...sessions.messages].reverse().find((m) => m.role === 'assistant')
|
|
694
|
+
if (!lastAssistant) {
|
|
695
|
+
tui.showError('No assistant message to copy.')
|
|
696
|
+
break
|
|
697
|
+
}
|
|
698
|
+
const ok = await copyToClipboard(lastAssistant.content)
|
|
699
|
+
if (ok) {
|
|
700
|
+
tui.showSystem('Copied last response to clipboard.')
|
|
701
|
+
} else {
|
|
702
|
+
tui.showError('Failed to copy. Is xclip/pbcopy available?')
|
|
703
|
+
}
|
|
704
|
+
break
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
case 'ask':
|
|
708
|
+
case 'perguntar': {
|
|
709
|
+
const question = args.join(' ')
|
|
710
|
+
if (!question) {
|
|
711
|
+
tui.showError('Usage: /ask <question>')
|
|
712
|
+
break
|
|
713
|
+
}
|
|
714
|
+
tui.addUserMessage(`(ephemeral) ${question}`)
|
|
715
|
+
tui.disableInput()
|
|
716
|
+
tui.startStream()
|
|
717
|
+
let askText = ''
|
|
718
|
+
// Send as isolated message — not saved to session, no tools
|
|
719
|
+
for await (const event of claude.chat(
|
|
720
|
+
[{ role: 'user', content: question, timestamp: Date.now() }],
|
|
721
|
+
activeSystemPrompt,
|
|
722
|
+
false,
|
|
723
|
+
)) {
|
|
724
|
+
if (event.type === 'text') {
|
|
725
|
+
askText += event.text
|
|
726
|
+
tui.appendStream(event.text)
|
|
727
|
+
} else if (event.type === 'error') {
|
|
728
|
+
tui.showError(event.error)
|
|
729
|
+
} else if (event.type === 'usage') {
|
|
730
|
+
// Show usage inline but don't track in session
|
|
731
|
+
tui.showUsage(`${event.inputTokens} in / ${event.outputTokens} out (ephemeral)`)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
tui.endStream()
|
|
735
|
+
tui.enableInput()
|
|
736
|
+
break
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
case 'fork': {
|
|
740
|
+
const forkName = args[0] || `fork-${Date.now()}`
|
|
741
|
+
sessions.fork(forkName)
|
|
742
|
+
tui.updateSession(forkName)
|
|
743
|
+
tui.showSystem(`Forked session -> ${forkName} (${sessions.messages.length} messages copied)`)
|
|
744
|
+
break
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
case 'plugins': {
|
|
748
|
+
tui.showSystem(formatPluginList(plugins))
|
|
749
|
+
break
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
case 'budget':
|
|
753
|
+
case 'orcamento': {
|
|
754
|
+
const val = args[0]
|
|
755
|
+
if (!val) {
|
|
756
|
+
const max = config.maxSessionCost
|
|
757
|
+
const spent = tracker.totals.costCents
|
|
758
|
+
if (max === 0) {
|
|
759
|
+
tui.showSystem(`Budget: unlimited (spent ~$${(spent / 100).toFixed(4)})`)
|
|
760
|
+
} else {
|
|
761
|
+
const pct = Math.round((spent / max) * 100)
|
|
762
|
+
tui.showSystem(`Budget: ~$${(spent / 100).toFixed(4)} / $${(max / 100).toFixed(4)} (${pct}%)`)
|
|
763
|
+
}
|
|
764
|
+
break
|
|
765
|
+
}
|
|
766
|
+
const cents = Number(val)
|
|
767
|
+
if (isNaN(cents) || cents < 0) {
|
|
768
|
+
tui.showError('Usage: /budget <cents> (e.g., /budget 50 for $0.50)')
|
|
769
|
+
break
|
|
770
|
+
}
|
|
771
|
+
config.maxSessionCost = cents
|
|
772
|
+
saveConfig(config)
|
|
773
|
+
tui.showSystem(cents === 0 ? 'Budget: unlimited' : `Budget set: $${(cents / 100).toFixed(2)}`)
|
|
774
|
+
break
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
case 'undo':
|
|
778
|
+
case 'desfazer': {
|
|
779
|
+
const peek = undoStack.peek()
|
|
780
|
+
if (!peek) {
|
|
781
|
+
tui.showError('Nothing to undo.')
|
|
782
|
+
break
|
|
783
|
+
}
|
|
784
|
+
const result = undoStack.undo()
|
|
785
|
+
if (result) {
|
|
786
|
+
tui.showSystem(result)
|
|
787
|
+
}
|
|
788
|
+
break
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
case 'search':
|
|
792
|
+
case 'buscar': {
|
|
793
|
+
const query = args.join(' ').toLowerCase()
|
|
794
|
+
if (!query) {
|
|
795
|
+
tui.showError('Usage: /search <text>')
|
|
796
|
+
break
|
|
797
|
+
}
|
|
798
|
+
const matches: string[] = []
|
|
799
|
+
for (const msg of sessions.messages) {
|
|
800
|
+
if (msg.content.toLowerCase().includes(query)) {
|
|
801
|
+
const preview = msg.content.slice(0, 100).replace(/\n/g, ' ')
|
|
802
|
+
const ts = new Date(msg.timestamp).toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit' })
|
|
803
|
+
matches.push(` [${msg.role} ${ts}] ${preview}${msg.content.length > 100 ? '...' : ''}`)
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
tui.showSystem(
|
|
807
|
+
matches.length > 0
|
|
808
|
+
? `Found ${matches.length} match${matches.length > 1 ? 'es' : ''}:\n${matches.join('\n')}`
|
|
809
|
+
: `No matches for "${query}".`,
|
|
810
|
+
)
|
|
811
|
+
break
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
case 'lang':
|
|
815
|
+
case 'language':
|
|
816
|
+
case 'idioma': {
|
|
817
|
+
const lang = args[0]
|
|
818
|
+
if (!lang) {
|
|
819
|
+
tui.showSystem(`Language: ${config.language} (auto = match user's language)`)
|
|
820
|
+
break
|
|
821
|
+
}
|
|
822
|
+
config.language = lang
|
|
823
|
+
saveConfig(config)
|
|
824
|
+
tui.showSystem(`Language -> ${lang}`)
|
|
825
|
+
break
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ── Business assistant commands ──────────────────────
|
|
829
|
+
|
|
830
|
+
case 'briefing':
|
|
831
|
+
case 'resumo': {
|
|
832
|
+
tui.showSystem('Carregando briefing...')
|
|
833
|
+
tui.disableInput()
|
|
834
|
+
try {
|
|
835
|
+
const briefing = await generateBriefing()
|
|
836
|
+
tui.showSystem(briefing)
|
|
837
|
+
} catch (err) {
|
|
838
|
+
tui.showError(`Briefing falhou: ${err instanceof Error ? err.message : String(err)}`)
|
|
839
|
+
}
|
|
840
|
+
tui.enableInput()
|
|
841
|
+
break
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
case 'news':
|
|
845
|
+
case 'noticias': {
|
|
846
|
+
const category = args[0] as NewsCategory | undefined
|
|
847
|
+
const validCats: NewsCategory[] = ['business', 'tech', 'finance', 'brazil', 'world']
|
|
848
|
+
if (category && !validCats.includes(category)) {
|
|
849
|
+
tui.showSystem(getNewsCategories())
|
|
850
|
+
break
|
|
851
|
+
}
|
|
852
|
+
tui.showSystem('Buscando noticias...')
|
|
853
|
+
tui.disableInput()
|
|
854
|
+
try {
|
|
855
|
+
const news = await fetchNews(category ? [category] : undefined)
|
|
856
|
+
tui.showSystem(news)
|
|
857
|
+
} catch (err) {
|
|
858
|
+
tui.showError(`Falha ao buscar noticias: ${err instanceof Error ? err.message : String(err)}`)
|
|
859
|
+
}
|
|
860
|
+
tui.enableInput()
|
|
861
|
+
break
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
case 'open':
|
|
865
|
+
case 'abrir': {
|
|
866
|
+
const appName = args.join(' ')
|
|
867
|
+
if (!appName) {
|
|
868
|
+
tui.showSystem(`Apps disponiveis: ${getKnownApps().join(', ')}\nUso: /open <app> ou /open <app> <arquivo>`)
|
|
869
|
+
break
|
|
870
|
+
}
|
|
871
|
+
// Check if second arg looks like a file path
|
|
872
|
+
const appArg = args.length > 1 ? args.slice(1).join(' ') : undefined
|
|
873
|
+
const result = await openApp(args[0], appArg)
|
|
874
|
+
tui.showSystem(result)
|
|
875
|
+
break
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
case 'openfile':
|
|
879
|
+
case 'abrirarquivo': {
|
|
880
|
+
const filePath = args.join(' ')
|
|
881
|
+
if (!filePath) {
|
|
882
|
+
tui.showError('Uso: /openfile <caminho>')
|
|
883
|
+
break
|
|
884
|
+
}
|
|
885
|
+
const result = await openFile(filePath)
|
|
886
|
+
tui.showSystem(result)
|
|
887
|
+
break
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
case 'openurl': {
|
|
891
|
+
const url = args[0]
|
|
892
|
+
if (!url) {
|
|
893
|
+
tui.showError('Uso: /openurl <url>')
|
|
894
|
+
break
|
|
895
|
+
}
|
|
896
|
+
const result = await openUrl(url)
|
|
897
|
+
tui.showSystem(result)
|
|
898
|
+
break
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
case 'apps':
|
|
902
|
+
case 'programas': {
|
|
903
|
+
tui.disableInput()
|
|
904
|
+
try {
|
|
905
|
+
const result = await getRunningApps()
|
|
906
|
+
tui.showSystem(result)
|
|
907
|
+
} catch (err) {
|
|
908
|
+
tui.showError(`Apps: ${err instanceof Error ? err.message : String(err)}`)
|
|
909
|
+
}
|
|
910
|
+
tui.enableInput()
|
|
911
|
+
break
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
case 'sysinfo':
|
|
915
|
+
case 'sistema': {
|
|
916
|
+
tui.disableInput()
|
|
917
|
+
try {
|
|
918
|
+
const result = await getSystemInfo()
|
|
919
|
+
tui.showSystem(result)
|
|
920
|
+
} catch (err) {
|
|
921
|
+
tui.showError(`Sysinfo: ${err instanceof Error ? err.message : String(err)}`)
|
|
922
|
+
}
|
|
923
|
+
tui.enableInput()
|
|
924
|
+
break
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
case 'calendar':
|
|
928
|
+
case 'calendario':
|
|
929
|
+
case 'agenda':
|
|
930
|
+
case 'cal': {
|
|
931
|
+
tui.disableInput()
|
|
932
|
+
try {
|
|
933
|
+
const dateInfo = await getDateTimeInfo()
|
|
934
|
+
const events = await getOutlookEvents()
|
|
935
|
+
tui.showSystem(`${dateInfo}\n\n--- Agenda ---\n${events}`)
|
|
936
|
+
} catch (err) {
|
|
937
|
+
tui.showError(`Calendar: ${err instanceof Error ? err.message : String(err)}`)
|
|
938
|
+
}
|
|
939
|
+
tui.enableInput()
|
|
940
|
+
break
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// ── Monitor commands ───────────────────────────────────
|
|
944
|
+
|
|
945
|
+
case 'monitor':
|
|
946
|
+
case 'vigiar': {
|
|
947
|
+
const sub = args[0]?.toLowerCase()
|
|
948
|
+
if (!sub || sub === 'list' || sub === 'listar') {
|
|
949
|
+
tui.showSystem(listMonitors())
|
|
950
|
+
} else if (sub === 'stop' || sub === 'parar') {
|
|
951
|
+
const name = args[1]
|
|
952
|
+
if (!name) { tui.showError('Uso: /monitor stop <processo>'); break }
|
|
953
|
+
tui.showSystem(stopMonitor(name))
|
|
954
|
+
} else {
|
|
955
|
+
// Start monitoring
|
|
956
|
+
const intervalSec = parseInt(args[1]) || 60
|
|
957
|
+
tui.showSystem(startMonitor(sub, intervalSec))
|
|
958
|
+
}
|
|
959
|
+
break
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// ── Workflow commands ──────────────────────────────────
|
|
963
|
+
|
|
964
|
+
case 'workflow':
|
|
965
|
+
case 'fluxo': {
|
|
966
|
+
const sub = args[0]?.toLowerCase()
|
|
967
|
+
if (!sub || sub === 'list' || sub === 'listar') {
|
|
968
|
+
tui.showSystem(formatWorkflowList())
|
|
969
|
+
} else if (sub === 'run' || sub === 'rodar') {
|
|
970
|
+
const name = args[1]
|
|
971
|
+
if (!name) {
|
|
972
|
+
tui.showError('Uso: /workflow run <nome>')
|
|
973
|
+
break
|
|
974
|
+
}
|
|
975
|
+
tui.disableInput()
|
|
976
|
+
try {
|
|
977
|
+
const result = await runWorkflow(name, (msg) => tui.showSystem(msg))
|
|
978
|
+
tui.showSystem(result)
|
|
979
|
+
} catch (err) {
|
|
980
|
+
tui.showError(`Workflow: ${err instanceof Error ? err.message : String(err)}`)
|
|
981
|
+
}
|
|
982
|
+
tui.enableInput()
|
|
983
|
+
} else if (sub === 'delete' || sub === 'deletar') {
|
|
984
|
+
const name = args[1]
|
|
985
|
+
if (!name) { tui.showError('Uso: /workflow delete <nome>'); break }
|
|
986
|
+
if (deleteWorkflow(name)) {
|
|
987
|
+
tui.showSystem(`Workflow removido: ${name}`)
|
|
988
|
+
} else {
|
|
989
|
+
tui.showError(`Workflow nao encontrado: ${name}`)
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
// Treat as "run <name>"
|
|
993
|
+
tui.disableInput()
|
|
994
|
+
try {
|
|
995
|
+
const result = await runWorkflow(sub, (msg) => tui.showSystem(msg))
|
|
996
|
+
tui.showSystem(result)
|
|
997
|
+
} catch (err) {
|
|
998
|
+
tui.showError(`Workflow: ${err instanceof Error ? err.message : String(err)}`)
|
|
999
|
+
}
|
|
1000
|
+
tui.enableInput()
|
|
1001
|
+
}
|
|
1002
|
+
break
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// ── Pomodoro commands ─────────────────────────────────
|
|
1006
|
+
|
|
1007
|
+
case 'pomodoro':
|
|
1008
|
+
case 'foco': {
|
|
1009
|
+
const sub = args[0]?.toLowerCase()
|
|
1010
|
+
if (sub === 'stop' || sub === 'parar') {
|
|
1011
|
+
tui.showSystem(stopPomodoro())
|
|
1012
|
+
} else if (sub === 'status') {
|
|
1013
|
+
tui.showSystem(pomodoroStatus())
|
|
1014
|
+
} else if (!sub) {
|
|
1015
|
+
tui.showSystem(pomodoroStatus())
|
|
1016
|
+
} else {
|
|
1017
|
+
// Start with label
|
|
1018
|
+
const label = args.join(' ')
|
|
1019
|
+
const workMin = 25
|
|
1020
|
+
const breakMin = 5
|
|
1021
|
+
tui.showSystem(startPomodoro(label, workMin, breakMin))
|
|
1022
|
+
}
|
|
1023
|
+
break
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// ── Finance commands ────────────────────────────────────
|
|
1027
|
+
|
|
1028
|
+
case 'entrada':
|
|
1029
|
+
case 'income': {
|
|
1030
|
+
// /entrada 500 salario descricao
|
|
1031
|
+
const amount = parseFloat(args[0])
|
|
1032
|
+
if (isNaN(amount) || args.length < 3) {
|
|
1033
|
+
tui.showSystem('Uso: /entrada <valor> <categoria> <descricao>')
|
|
1034
|
+
break
|
|
1035
|
+
}
|
|
1036
|
+
const tx = addTransaction('entrada', amount, args[1], args.slice(2).join(' '))
|
|
1037
|
+
tui.showSystem(`+ R$ ${tx.amount.toFixed(2)} (${tx.category}) — ${tx.description}`)
|
|
1038
|
+
break
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
case 'saida':
|
|
1042
|
+
case 'expense': {
|
|
1043
|
+
const amount = parseFloat(args[0])
|
|
1044
|
+
if (isNaN(amount) || args.length < 3) {
|
|
1045
|
+
tui.showSystem('Uso: /saida <valor> <categoria> <descricao>')
|
|
1046
|
+
break
|
|
1047
|
+
}
|
|
1048
|
+
const tx = addTransaction('saida', amount, args[1], args.slice(2).join(' '))
|
|
1049
|
+
tui.showSystem(`- R$ ${tx.amount.toFixed(2)} (${tx.category}) — ${tx.description}`)
|
|
1050
|
+
break
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
case 'finance':
|
|
1054
|
+
case 'financas':
|
|
1055
|
+
case 'balanco': {
|
|
1056
|
+
const sub = args[0]
|
|
1057
|
+
if (sub === 'recent' || sub === 'recentes') {
|
|
1058
|
+
tui.showSystem(getRecentTransactions())
|
|
1059
|
+
} else {
|
|
1060
|
+
tui.showSystem(getMonthSummary() + '\n\n' + getRecentTransactions(5))
|
|
1061
|
+
}
|
|
1062
|
+
break
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ── Decision commands ───────────────────────────────────
|
|
1066
|
+
|
|
1067
|
+
case 'decisions':
|
|
1068
|
+
case 'decisoes': {
|
|
1069
|
+
const query = args.join(' ')
|
|
1070
|
+
if (query) {
|
|
1071
|
+
const results = searchDecisions(query)
|
|
1072
|
+
tui.showSystem(formatDecisionList(results))
|
|
1073
|
+
} else {
|
|
1074
|
+
tui.showSystem(formatDecisionList(listDecisions()))
|
|
1075
|
+
}
|
|
1076
|
+
break
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// ── Investigation commands ─────────────────────────────
|
|
1080
|
+
|
|
1081
|
+
case 'investigar':
|
|
1082
|
+
case 'investigate':
|
|
1083
|
+
case 'investigacoes': {
|
|
1084
|
+
const query = args.join(' ')
|
|
1085
|
+
if (query) {
|
|
1086
|
+
const { searchInvestigations, formatInvestigationList } = await import('./investigate')
|
|
1087
|
+
tui.showSystem(formatInvestigationList(searchInvestigations(query)))
|
|
1088
|
+
} else {
|
|
1089
|
+
const { listInvestigations, formatInvestigationList } = await import('./investigate')
|
|
1090
|
+
tui.showSystem(formatInvestigationList(listInvestigations()))
|
|
1091
|
+
}
|
|
1092
|
+
break
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// ── Email command ──────────────────────────────────────
|
|
1096
|
+
|
|
1097
|
+
case 'email':
|
|
1098
|
+
case 'rascunho': {
|
|
1099
|
+
// Quick email: /email to@addr.com assunto | corpo
|
|
1100
|
+
const text = args.join(' ')
|
|
1101
|
+
if (!text) {
|
|
1102
|
+
tui.showSystem('Uso: /email <destinatario> <assunto> | <corpo>\nOu peca a IA: "escreve um email para joao@email.com cobrando o relatorio"')
|
|
1103
|
+
break
|
|
1104
|
+
}
|
|
1105
|
+
// Parse: first word is email, rest before | is subject, after | is body
|
|
1106
|
+
const emailAddr = args[0]
|
|
1107
|
+
const restText = args.slice(1).join(' ')
|
|
1108
|
+
const pipeIdx = restText.indexOf('|')
|
|
1109
|
+
if (pipeIdx === -1) {
|
|
1110
|
+
tui.showSystem('Formato: /email <destinatario> <assunto> | <corpo>\nUse | para separar assunto do corpo.')
|
|
1111
|
+
break
|
|
1112
|
+
}
|
|
1113
|
+
const subject = restText.slice(0, pipeIdx).trim()
|
|
1114
|
+
const body = restText.slice(pipeIdx + 1).trim()
|
|
1115
|
+
if (!subject || !body) {
|
|
1116
|
+
tui.showError('Assunto e corpo sao obrigatorios.')
|
|
1117
|
+
break
|
|
1118
|
+
}
|
|
1119
|
+
const draft = { to: emailAddr, subject, body }
|
|
1120
|
+
tui.showSystem(formatDraftPreview(draft))
|
|
1121
|
+
tui.disableInput()
|
|
1122
|
+
try {
|
|
1123
|
+
const result = await openEmailDraft(draft)
|
|
1124
|
+
tui.showSystem(result)
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
tui.showError(`Email: ${err instanceof Error ? err.message : String(err)}`)
|
|
1127
|
+
}
|
|
1128
|
+
tui.enableInput()
|
|
1129
|
+
break
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ── Memo commands ─────────────────────────────────────
|
|
1133
|
+
|
|
1134
|
+
case 'memo':
|
|
1135
|
+
case 'anotar':
|
|
1136
|
+
case 'note': {
|
|
1137
|
+
const text = args.join(' ')
|
|
1138
|
+
if (!text) {
|
|
1139
|
+
// Show recent memos
|
|
1140
|
+
const memos = listMemos()
|
|
1141
|
+
tui.showSystem(formatMemoList(memos))
|
|
1142
|
+
break
|
|
1143
|
+
}
|
|
1144
|
+
const memo = saveMemo(text)
|
|
1145
|
+
const tagStr = memo.tags.length > 0 ? ` [${memo.tags.map((t: string) => '#' + t).join(' ')}]` : ''
|
|
1146
|
+
tui.showSystem(`Memo salvo${tagStr} {${memo.id}}`)
|
|
1147
|
+
break
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
case 'memos':
|
|
1151
|
+
case 'notas': {
|
|
1152
|
+
const query = args.join(' ')
|
|
1153
|
+
if (query) {
|
|
1154
|
+
const results = searchMemos(query)
|
|
1155
|
+
tui.showSystem(formatMemoList(results))
|
|
1156
|
+
} else {
|
|
1157
|
+
const memos = listMemos()
|
|
1158
|
+
tui.showSystem(formatMemoList(memos))
|
|
1159
|
+
}
|
|
1160
|
+
break
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
case 'memotags':
|
|
1164
|
+
case 'tags': {
|
|
1165
|
+
tui.showSystem(formatMemoTags())
|
|
1166
|
+
break
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
case 'rmmemo':
|
|
1170
|
+
case 'rmnota': {
|
|
1171
|
+
const id = args[0]
|
|
1172
|
+
if (!id) {
|
|
1173
|
+
tui.showError('Uso: /rmmemo <id>')
|
|
1174
|
+
break
|
|
1175
|
+
}
|
|
1176
|
+
if (deleteMemo(id)) {
|
|
1177
|
+
tui.showSystem('Memo removido.')
|
|
1178
|
+
} else {
|
|
1179
|
+
tui.showError(`Memo nao encontrado: ${id}`)
|
|
1180
|
+
}
|
|
1181
|
+
break
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// ── People management commands ────────────────────────
|
|
1185
|
+
|
|
1186
|
+
case 'people':
|
|
1187
|
+
case 'pessoas':
|
|
1188
|
+
case 'equipe':
|
|
1189
|
+
case 'team':
|
|
1190
|
+
case 'familia':
|
|
1191
|
+
case 'family':
|
|
1192
|
+
case 'contato':
|
|
1193
|
+
case 'contatos':
|
|
1194
|
+
case 'contacts': {
|
|
1195
|
+
const groupMap: Record<string, PersonGroup> = {
|
|
1196
|
+
equipe: 'equipe', team: 'equipe',
|
|
1197
|
+
familia: 'familia', family: 'familia',
|
|
1198
|
+
contato: 'contato', contatos: 'contato', contacts: 'contato',
|
|
1199
|
+
}
|
|
1200
|
+
const groupFilter = groupMap[cmd] || args[0] as PersonGroup | undefined
|
|
1201
|
+
const people = listPeople(groupFilter)
|
|
1202
|
+
tui.showSystem(formatPeopleList(people))
|
|
1203
|
+
break
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
case 'person':
|
|
1207
|
+
case 'pessoa': {
|
|
1208
|
+
const ref = args.join(' ')
|
|
1209
|
+
if (!ref) {
|
|
1210
|
+
tui.showError('Uso: /person <nome>')
|
|
1211
|
+
break
|
|
1212
|
+
}
|
|
1213
|
+
const person = findPerson(ref)
|
|
1214
|
+
if (!person) {
|
|
1215
|
+
tui.showError(`Pessoa nao encontrada: "${ref}"`)
|
|
1216
|
+
break
|
|
1217
|
+
}
|
|
1218
|
+
tui.showSystem(formatPersonDetail(person))
|
|
1219
|
+
break
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
case 'addperson':
|
|
1223
|
+
case 'addpessoa':
|
|
1224
|
+
case 'novapessoa': {
|
|
1225
|
+
// /addperson <group> <name> [role]
|
|
1226
|
+
const group = args[0] as PersonGroup
|
|
1227
|
+
const validGroups: PersonGroup[] = ['equipe', 'familia', 'contato']
|
|
1228
|
+
if (!group || !validGroups.includes(group)) {
|
|
1229
|
+
tui.showSystem('Uso: /addperson <equipe|familia|contato> <nome> [papel]\nEx: /addperson equipe Joao dev frontend')
|
|
1230
|
+
break
|
|
1231
|
+
}
|
|
1232
|
+
const nameAndRole = args.slice(1).join(' ')
|
|
1233
|
+
if (!nameAndRole) {
|
|
1234
|
+
tui.showError('Nome obrigatorio. Ex: /addperson equipe Joao dev frontend')
|
|
1235
|
+
break
|
|
1236
|
+
}
|
|
1237
|
+
// Split name from role at first comma if present
|
|
1238
|
+
const [pName, ...roleParts] = nameAndRole.split(',')
|
|
1239
|
+
const pRole = roleParts.join(',').trim() || undefined
|
|
1240
|
+
const newPerson = addPerson(pName.trim(), group, pRole)
|
|
1241
|
+
tui.showSystem(`Adicionado: ${newPerson.name} (${group}) [${newPerson.id}]`)
|
|
1242
|
+
break
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
case 'delegate':
|
|
1246
|
+
case 'delegar': {
|
|
1247
|
+
// /delegate <person> <task>
|
|
1248
|
+
const personName = args[0]
|
|
1249
|
+
if (!personName || args.length < 2) {
|
|
1250
|
+
tui.showSystem('Uso: /delegate <pessoa> <tarefa>\nEx: /delegate Joao revisar relatorio')
|
|
1251
|
+
break
|
|
1252
|
+
}
|
|
1253
|
+
const taskText = args.slice(1).join(' ')
|
|
1254
|
+
const delegation = delegateTask(personName, taskText)
|
|
1255
|
+
if (!delegation) {
|
|
1256
|
+
tui.showError(`Pessoa nao encontrada: "${personName}"`)
|
|
1257
|
+
break
|
|
1258
|
+
}
|
|
1259
|
+
tui.showSystem(`Delegado para ${personName}: "${taskText}" [${delegation.id}]`)
|
|
1260
|
+
break
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
case 'delegations':
|
|
1264
|
+
case 'delegacoes':
|
|
1265
|
+
case 'delegados': {
|
|
1266
|
+
const personRef = args[0]
|
|
1267
|
+
const delegations = getDelegations(personRef)
|
|
1268
|
+
tui.showSystem(formatDelegationList(delegations))
|
|
1269
|
+
break
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
case 'followups': {
|
|
1273
|
+
const followUps = getPendingFollowUps()
|
|
1274
|
+
tui.showSystem(formatFollowUps(followUps))
|
|
1275
|
+
break
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
case 'dashboard':
|
|
1279
|
+
case 'painel': {
|
|
1280
|
+
tui.showSystem(generatePeopleDashboard())
|
|
1281
|
+
break
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// ── Task/reminder commands ────────────────────────────
|
|
1285
|
+
|
|
1286
|
+
case 'task':
|
|
1287
|
+
case 'tarefa': {
|
|
1288
|
+
const text = args.join(' ')
|
|
1289
|
+
if (!text) {
|
|
1290
|
+
// Show pending tasks
|
|
1291
|
+
const tasks = listTasks()
|
|
1292
|
+
tui.showSystem(formatTaskList(tasks))
|
|
1293
|
+
break
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// Parse time from the text (look for time patterns)
|
|
1297
|
+
const dueTime = parseTime(text)
|
|
1298
|
+
|
|
1299
|
+
// Remove time-related parts from the title
|
|
1300
|
+
let title = text
|
|
1301
|
+
.replace(/\b(para\s+(as\s+)?)?\d{1,2}\s*[h:]\s*\d{0,2}\b/gi, '')
|
|
1302
|
+
.replace(/\b(em\s+\d+\s*(min|minutos?|h|horas?))\b/gi, '')
|
|
1303
|
+
.replace(/\b(amanha|amanhã)\b/gi, '')
|
|
1304
|
+
.replace(/\s{2,}/g, ' ')
|
|
1305
|
+
.trim()
|
|
1306
|
+
|
|
1307
|
+
// If title became empty, use the original text
|
|
1308
|
+
if (!title) title = text
|
|
1309
|
+
|
|
1310
|
+
const task = addTask(title, dueTime || undefined)
|
|
1311
|
+
const dueStr = dueTime
|
|
1312
|
+
? ` — lembrete: ${dueTime.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}`
|
|
1313
|
+
: ''
|
|
1314
|
+
tui.showSystem(`Tarefa criada: "${task.title}"${dueStr} [${task.id}]`)
|
|
1315
|
+
break
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
case 'tasks':
|
|
1319
|
+
case 'tarefas': {
|
|
1320
|
+
const showAll = args[0] === 'all' || args[0] === 'todas'
|
|
1321
|
+
const tasks = listTasks(showAll)
|
|
1322
|
+
tui.showSystem(formatTaskList(tasks))
|
|
1323
|
+
break
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
case 'done':
|
|
1327
|
+
case 'feito':
|
|
1328
|
+
case 'concluido': {
|
|
1329
|
+
const ref = args.join(' ')
|
|
1330
|
+
if (!ref) {
|
|
1331
|
+
tui.showError('Uso: /done <id ou parte do titulo>')
|
|
1332
|
+
break
|
|
1333
|
+
}
|
|
1334
|
+
const task = completeTask(ref)
|
|
1335
|
+
if (task) {
|
|
1336
|
+
tui.showSystem(`Concluida: "${task.title}"`)
|
|
1337
|
+
} else {
|
|
1338
|
+
tui.showError(`Tarefa nao encontrada: "${ref}"`)
|
|
1339
|
+
}
|
|
1340
|
+
break
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
case 'rmtask':
|
|
1344
|
+
case 'rmtarefa': {
|
|
1345
|
+
const ref = args.join(' ')
|
|
1346
|
+
if (!ref) {
|
|
1347
|
+
tui.showError('Uso: /rmtask <id ou parte do titulo>')
|
|
1348
|
+
break
|
|
1349
|
+
}
|
|
1350
|
+
const removed = removeTask(ref)
|
|
1351
|
+
if (removed) {
|
|
1352
|
+
tui.showSystem('Tarefa removida.')
|
|
1353
|
+
} else {
|
|
1354
|
+
tui.showError(`Tarefa nao encontrada: "${ref}"`)
|
|
1355
|
+
}
|
|
1356
|
+
break
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
default:
|
|
1360
|
+
tui.showError(`Unknown command: /${cmd}. Try /help`)
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function cleanup(): void {
|
|
1365
|
+
stopTasks()
|
|
1366
|
+
stopPomodoroTimer()
|
|
1367
|
+
stopAllMonitors()
|
|
1368
|
+
tui.stop()
|
|
1369
|
+
process.exit(0)
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
process.on('SIGINT', cleanup)
|
|
1373
|
+
process.on('SIGTERM', cleanup)
|
|
1374
|
+
|
|
1375
|
+
tui.start({
|
|
1376
|
+
onSubmit: handleSubmit,
|
|
1377
|
+
onCancel: () => {
|
|
1378
|
+
activeAbort?.abort()
|
|
1379
|
+
tui.endStream()
|
|
1380
|
+
tui.showSystem('Cancelled.')
|
|
1381
|
+
tui.enableInput()
|
|
1382
|
+
},
|
|
1383
|
+
onExit: cleanup,
|
|
1384
|
+
})
|
|
1385
|
+
|
|
1386
|
+
const authInfo = auth.source === 'subscription'
|
|
1387
|
+
? `Authenticated via Claude ${auth.subscriptionType} subscription.`
|
|
1388
|
+
: 'Authenticated via API key.'
|
|
1389
|
+
tui.showSystem(`smolerclaw v${getVersion()} — the micro AI assistant.\n${authInfo}\nType /ajuda for commands.`)
|
|
1390
|
+
|
|
1391
|
+
// Morning briefing — first run of the day
|
|
1392
|
+
if (isFirstRunToday(config.dataDir)) {
|
|
1393
|
+
try {
|
|
1394
|
+
const briefing = await generateMorningBriefing()
|
|
1395
|
+
tui.showSystem(briefing)
|
|
1396
|
+
markMorningDone()
|
|
1397
|
+
} catch {
|
|
1398
|
+
// Don't block startup if briefing fails
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Auto-submit initial prompt if provided
|
|
1403
|
+
if (initialPrompt) {
|
|
1404
|
+
await handleSubmit(initialPrompt)
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function formatAge(timestamp: number): string {
|
|
1409
|
+
if (!timestamp || timestamp <= 0) return ''
|
|
1410
|
+
const diff = Date.now() - timestamp
|
|
1411
|
+
if (diff < 0) return ''
|
|
1412
|
+
const mins = Math.floor(diff / 60_000)
|
|
1413
|
+
if (mins < 1) return 'just now'
|
|
1414
|
+
if (mins < 60) return `${mins}m ago`
|
|
1415
|
+
const hours = Math.floor(mins / 60)
|
|
1416
|
+
if (hours < 24) return `${hours}h ago`
|
|
1417
|
+
const days = Math.floor(hours / 24)
|
|
1418
|
+
if (days > 365) return `${Math.floor(days / 365)}y ago`
|
|
1419
|
+
return `${days}d ago`
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
main().catch((err) => {
|
|
1423
|
+
// Ensure terminal is restored on crash
|
|
1424
|
+
try {
|
|
1425
|
+
process.stdin.setRawMode?.(false)
|
|
1426
|
+
process.stdout.write('\x1b[?1049l') // exit alt screen
|
|
1427
|
+
process.stdout.write('\x1b[?25h') // show cursor
|
|
1428
|
+
} catch { /* best effort */ }
|
|
1429
|
+
console.error('Fatal:', err)
|
|
1430
|
+
process.exit(1)
|
|
1431
|
+
})
|