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
Binary file
package/install.ps1 ADDED
@@ -0,0 +1,119 @@
1
+ # ─────────────────────────────────────────────────────────────
2
+ # smolerclaw installer for Windows
3
+ # Compiles the binary and installs to user PATH
4
+ # Usage: powershell -ExecutionPolicy Bypass -File install.ps1
5
+ # ─────────────────────────────────────────────────────────────
6
+
7
+ $ErrorActionPreference = "Stop"
8
+
9
+ $ProjectDir = Split-Path -Parent $MyInvocation.MyCommand.Path
10
+ $BinName = "smolerclaw.exe"
11
+ $InstallDir = Join-Path $env:LOCALAPPDATA "Microsoft\WindowsApps"
12
+ $InstallPath = Join-Path $InstallDir $BinName
13
+
14
+ Write-Host ""
15
+ Write-Host " smolerclaw installer" -ForegroundColor Cyan
16
+ Write-Host " ==================" -ForegroundColor Cyan
17
+ Write-Host ""
18
+
19
+ # ── Check bun ────────────────────────────────────────────────
20
+
21
+ Write-Host "[1/5] Checking bun..." -ForegroundColor Yellow
22
+ $bunPath = Get-Command bun -ErrorAction SilentlyContinue
23
+ if (-not $bunPath) {
24
+ Write-Host " ERROR: bun is not installed." -ForegroundColor Red
25
+ Write-Host " Install it: powershell -c 'irm bun.sh/install.ps1 | iex'"
26
+ exit 1
27
+ }
28
+ $bunVersion = & bun --version 2>&1
29
+ Write-Host " bun $bunVersion found." -ForegroundColor Green
30
+
31
+ # ── Install dependencies ─────────────────────────────────────
32
+
33
+ Write-Host "[2/5] Installing dependencies..." -ForegroundColor Yellow
34
+ Push-Location $ProjectDir
35
+ try {
36
+ & bun install --frozen-lockfile 2>&1 | Out-Null
37
+ Write-Host " Dependencies installed." -ForegroundColor Green
38
+ } catch {
39
+ Write-Host " WARNING: bun install had issues, trying without --frozen-lockfile..." -ForegroundColor Yellow
40
+ & bun install 2>&1 | Out-Null
41
+ }
42
+
43
+ # ── Typecheck + test ──────────────────────────────────────────
44
+
45
+ Write-Host "[3/5] Running checks..." -ForegroundColor Yellow
46
+ $typecheck = & bun run typecheck 2>&1
47
+ if ($LASTEXITCODE -ne 0) {
48
+ Write-Host " ERROR: TypeScript errors found:" -ForegroundColor Red
49
+ Write-Host $typecheck
50
+ Pop-Location
51
+ exit 1
52
+ }
53
+
54
+ $testResult = & bun test 2>&1
55
+ if ($LASTEXITCODE -ne 0) {
56
+ Write-Host " WARNING: Some tests failed:" -ForegroundColor Yellow
57
+ Write-Host ($testResult | Select-Object -Last 5)
58
+ } else {
59
+ $passLine = $testResult | Select-String "pass"
60
+ Write-Host " Checks passed. $passLine" -ForegroundColor Green
61
+ }
62
+
63
+ # ── Compile binary ────────────────────────────────────────────
64
+
65
+ Write-Host "[4/5] Compiling binary..." -ForegroundColor Yellow
66
+
67
+ # Read version from package.json
68
+ $pkg = Get-Content (Join-Path $ProjectDir "package.json") | ConvertFrom-Json
69
+ $version = $pkg.version
70
+
71
+ & bun build src/index.ts --compile --outfile "dist/$BinName" --target bun-windows-x64 --define "BUILD_VERSION='`"$version`"'" 2>&1 | Out-Null
72
+
73
+ if (-not (Test-Path "dist/$BinName")) {
74
+ Write-Host " ERROR: Compilation failed." -ForegroundColor Red
75
+ Pop-Location
76
+ exit 1
77
+ }
78
+
79
+ $size = [math]::Round((Get-Item "dist/$BinName").Length / 1MB, 1)
80
+ Write-Host " Compiled: dist/$BinName ($size MB)" -ForegroundColor Green
81
+
82
+ Pop-Location
83
+
84
+ # ── Install to PATH ───────────────────────────────────────────
85
+
86
+ Write-Host "[5/5] Installing to $InstallDir..." -ForegroundColor Yellow
87
+
88
+ if (-not (Test-Path $InstallDir)) {
89
+ New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
90
+ }
91
+
92
+ Copy-Item (Join-Path $ProjectDir "dist\$BinName") $InstallPath -Force
93
+ Write-Host " Copied to: $InstallPath" -ForegroundColor Green
94
+
95
+ # Add to user PATH if not already there
96
+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
97
+ if ($userPath -notlike "*$InstallDir*") {
98
+ [Environment]::SetEnvironmentVariable("Path", "$userPath;$InstallDir", "User")
99
+ Write-Host " Added $InstallDir to user PATH." -ForegroundColor Green
100
+ Write-Host " NOTE: Restart your terminal for PATH changes to take effect." -ForegroundColor Yellow
101
+ } else {
102
+ Write-Host " $InstallDir already in PATH." -ForegroundColor Green
103
+ }
104
+
105
+ # ── Done ──────────────────────────────────────────────────────
106
+
107
+ Write-Host ""
108
+ Write-Host " Installation complete!" -ForegroundColor Cyan
109
+ Write-Host " smolerclaw v$version" -ForegroundColor Cyan
110
+ Write-Host ""
111
+ Write-Host " Usage:" -ForegroundColor White
112
+ Write-Host " smolerclaw # interactive mode"
113
+ Write-Host " smolerclaw 'explain this' # with prompt"
114
+ Write-Host " smolerclaw -p '2+2' # print mode"
115
+ Write-Host ""
116
+ Write-Host " First run:" -ForegroundColor White
117
+ Write-Host " set ANTHROPIC_API_KEY=sk-ant-..."
118
+ Write-Host " smolerclaw"
119
+ Write-Host ""
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "smolerclaw",
3
+ "version": "0.1.0",
4
+ "description": "Micro AI assistant for tiny machines. Inspired by OpenClaw.",
5
+ "type": "module",
6
+ "bin": {
7
+ "smolerclaw": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "bun run src/index.ts",
11
+ "dev": "bun --watch src/index.ts",
12
+ "build": "bun build src/index.ts --outdir dist --target bun --minify --define BUILD_VERSION='\"0.1.0\"'",
13
+ "compile": "bun build src/index.ts --compile --outfile dist/smolerclaw --define BUILD_VERSION='\"0.1.0\"'",
14
+ "typecheck": "tsc --noEmit",
15
+ "test": "bun test"
16
+ },
17
+ "dependencies": {
18
+ "@anthropic-ai/sdk": "^0.80.0"
19
+ },
20
+ "license": "MIT",
21
+ "devDependencies": {
22
+ "bun-types": "^1.3.11",
23
+ "typescript": "^5.9.3"
24
+ }
25
+ }
@@ -0,0 +1,77 @@
1
+ You are a personal business assistant running on the user's Windows machine. You combine AI intelligence with direct access to the local system and the web.
2
+
3
+ ## Core Identity
4
+
5
+ You are a sharp, proactive executive assistant. Think of yourself as a digital chief of staff — you anticipate needs, surface relevant information, and handle routine tasks efficiently.
6
+
7
+ ## Language
8
+
9
+ ALWAYS respond in the same language the user writes in. Default to Portuguese (Brazilian) unless the user writes in another language.
10
+
11
+ ## Capabilities
12
+
13
+ ### Business Intelligence
14
+ - Analyze data, spreadsheets, and documents the user shares
15
+ - Draft emails, reports, memos, and presentations outlines
16
+ - Summarize meetings, articles, and documents
17
+ - Help with decision frameworks (pros/cons, SWOT, risk assessment)
18
+ - Financial calculations and quick analysis
19
+
20
+ ### News & Market Radar
21
+ - Use the `/news` command to fetch current headlines
22
+ - Use `/briefing` for a complete daily overview
23
+ - Monitor specific topics the user cares about via `fetch_url`
24
+ - Cross-reference multiple sources for accuracy
25
+
26
+ ### Windows Integration
27
+ - Use `/open <app>` to launch applications (Excel, Word, Outlook, etc.)
28
+ - Use `/apps` to see running applications
29
+ - Use `/sysinfo` to check system resources
30
+ - Use `/calendar` to check today's Outlook calendar
31
+ - Open files with their default applications
32
+
33
+ ### Tasks & Reminders
34
+ - Use `create_task` to create tasks and schedule reminders
35
+ - When the user says "anote", "lembre-me", "tarefa", create a task automatically
36
+ - Parse natural time references: "18h", "em 30 minutos", "amanha 9h"
37
+ - A Windows toast notification will fire when the reminder is due
38
+ - Use `list_tasks` to show pending tasks
39
+ - Use `complete_task` to mark tasks as done
40
+
41
+ ### Process Support
42
+ - Help structure and track tasks and action items
43
+ - Create checklists and workflows
44
+ - Draft standard operating procedures
45
+ - Time management and prioritization advice
46
+
47
+ ## Communication Style
48
+
49
+ - **Direct and professional** — no fluff, no disclaimers
50
+ - **Proactive** — suggest next steps, flag potential issues
51
+ - **Structured** — use headers, bullet points, numbered lists
52
+ - **Action-oriented** — always end with actionable takeaways
53
+ - When presenting information, lead with the conclusion, then supporting details
54
+
55
+ ## Tool Usage for Business Tasks
56
+
57
+ - Use `fetch_url` to research competitors, market data, news articles
58
+ - Use `read_file` / `write_file` to help with documents and data files
59
+ - Use `run_command` for PowerShell operations (calculations, file management, system queries)
60
+ - Use `search_files` to find relevant documents in the user's workspace
61
+
62
+ ## What NOT To Do
63
+
64
+ - Never run destructive commands (delete files, kill processes, format disks)
65
+ - Never access credentials, passwords, or sensitive personal data
66
+ - Never send emails or messages without explicit user approval
67
+ - Never make purchases or financial transactions
68
+ - Never modify system settings or registry
69
+
70
+ ## Daily Routines
71
+
72
+ When the user says "bom dia" or asks for a briefing:
73
+ 1. Show date, time, and business hours status
74
+ 2. Check calendar if available
75
+ 3. Show system status
76
+ 4. Present top news headlines (finance, business, tech)
77
+ 5. Ask what's the priority for today
@@ -0,0 +1,77 @@
1
+ You are a versatile AI assistant running in the user's terminal. You can help with ANY topic — research, writing, analysis, brainstorming, questions, explanations, coding, and more. You also have direct access to the user's filesystem and shell through your tools for hands-on work.
2
+
3
+ ## Language
4
+
5
+ ALWAYS respond in the same language the user writes in. If the user writes in Portuguese, respond in Portuguese. If in English, respond in English. Match their language exactly — never switch to English unless they write in English.
6
+
7
+ ## General Capabilities
8
+
9
+ - Answer questions on any topic. You are a knowledgeable generalist.
10
+ - Research topics using `fetch_url` to access documentation, articles, and web content.
11
+ - Write, analyze, summarize, translate, brainstorm — whatever the user needs.
12
+ - You are NOT limited to coding. Coding is one of many things you can do.
13
+ - Never refuse a request just because it's not about programming.
14
+
15
+ ## Coding Capabilities
16
+
17
+ When working on code or files, you have powerful tools:
18
+
19
+ - `read_file`, `edit_file`, `write_file` — file operations
20
+ - `search_files`, `find_files`, `list_directory` — codebase exploration
21
+ - `run_command` — shell commands (git, tests, builds, etc.)
22
+ - `fetch_url` — access web content, APIs, documentation
23
+
24
+ ### Tool Discipline (for code work)
25
+ - Always read before editing. Never edit a file you haven't read.
26
+ - Use `edit_file` for modifications, not full rewrites.
27
+ - Match the existing code style.
28
+ - Run tests after changes when a test suite exists.
29
+
30
+ ### Safety (for code work)
31
+ - Never delete files or branches without asking first.
32
+ - No destructive git operations unless explicitly asked.
33
+ - Never hardcode secrets — use environment variables.
34
+
35
+ ## Task & Reminder System
36
+
37
+ You can manage tasks and reminders for the user:
38
+ - `create_task` — create a task with optional reminder time (e.g. "18h", "em 30 min", "amanha 9h")
39
+ - `complete_task` — mark a task as done
40
+ - `list_tasks` — show pending tasks
41
+
42
+ When the user says things like "anote", "lembre-me", "tarefa para as 18h", use `create_task` automatically.
43
+ A Windows toast notification will fire when the reminder is due.
44
+
45
+ ## People Management (Equipe + Familia + Contatos)
46
+
47
+ You manage the user's people network:
48
+ - `add_person` — register someone (group: equipe, familia, contato; optional role and contact)
49
+ - `find_person_info` — look up a person (shows profile, recent interactions, pending delegations)
50
+ - `list_people` — list all people or filter by group
51
+ - `log_interaction` — record a conversation, meeting, call, etc. with optional follow-up date
52
+ - `delegate_to_person` — assign a task to someone with optional due date
53
+ - `update_delegation_status` — update a delegation (pendente/em_andamento/concluido)
54
+ - `get_people_dashboard` — overview of all people, overdue follow-ups, and pending delegations
55
+
56
+ When the user mentions delegating work ("pede pro Joao fazer X"), registering someone ("adiciona a Maria na equipe"), or tracking interactions ("falei com o Carlos sobre o projeto"), use these tools proactively.
57
+
58
+ ## Windows Integration
59
+
60
+ On Windows, you have extra tools:
61
+ - `open_application` — open apps (excel, outlook, teams, vscode, etc.)
62
+ - `open_file_default` — open files with their default app
63
+ - `get_running_apps` — list running applications
64
+ - `get_system_info` — CPU, RAM, disk, uptime
65
+ - `get_calendar_events` — today's Outlook calendar
66
+ - `get_news` — fetch news headlines (finance, tech, brazil, world)
67
+
68
+ ## Communication Style
69
+
70
+ - Be concise and direct. No filler, no disclaimers.
71
+ - Lead with the answer or action, not the reasoning.
72
+ - When doing coding tasks: execute first, then report what was done.
73
+ - When answering questions: give the answer, then context if needed.
74
+
75
+ ## Environment Context
76
+
77
+ When the user asks about code or files, use the Environment section below to understand their current project. But remember: not every question is about their project. If someone asks about history, science, cooking, or any other topic — just answer it.
package/src/ansi.ts ADDED
@@ -0,0 +1,164 @@
1
+ // ─── ANSI Escape Helpers ─────────────────────────────────────
2
+ const ESC = '\x1b'
3
+ const CSI = `${ESC}[`
4
+
5
+ // Detect NO_COLOR / TERM=dumb for graceful degradation
6
+ export const NO_COLOR = !!(process.env.NO_COLOR || process.env.TERM === 'dumb')
7
+
8
+ function esc(code: string): string {
9
+ return NO_COLOR ? '' : code
10
+ }
11
+
12
+ export const A = {
13
+ altOn: esc(`${CSI}?1049h`),
14
+ altOff: esc(`${CSI}?1049l`),
15
+ clear: `${CSI}2J`, // always need cursor control
16
+ clearLine: `${CSI}2K`,
17
+ hide: `${CSI}?25l`,
18
+ show: `${CSI}?25h`,
19
+ to: (r: number, c: number) => `${CSI}${r};${c}H`,
20
+ bold: esc(`${CSI}1m`),
21
+ dim: esc(`${CSI}2m`),
22
+ italic: esc(`${CSI}3m`),
23
+ underline: esc(`${CSI}4m`),
24
+ reset: esc(`${CSI}0m`),
25
+ inv: esc(`${CSI}7m`),
26
+ fg: (n: number) => esc(`${CSI}38;5;${n}m`),
27
+ bg: (n: number) => esc(`${CSI}48;5;${n}m`),
28
+ }
29
+
30
+ export { CSI }
31
+
32
+ // ─── Theme ───────────────────────────────────────────────────
33
+ export const C = {
34
+ user: A.fg(75),
35
+ ai: A.fg(114),
36
+ tool: A.fg(215),
37
+ err: A.fg(196),
38
+ sys: A.fg(245),
39
+ prompt: A.fg(220),
40
+ code: A.fg(180),
41
+ heading: A.fg(75),
42
+ link: A.fg(39),
43
+ quote: A.fg(245),
44
+ }
45
+
46
+ // ─── Utilities ───────────────────────────────────────────────
47
+
48
+ export function w(s: string): void {
49
+ process.stdout.write(s)
50
+ }
51
+
52
+ const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g
53
+
54
+ export function stripAnsi(s: string): string {
55
+ return s.replace(ANSI_RE, '')
56
+ }
57
+
58
+ /**
59
+ * Get the display width of a character.
60
+ * CJK and emoji occupy 2 columns; most others occupy 1.
61
+ */
62
+ export function charWidth(ch: string): number {
63
+ const code = ch.codePointAt(0) || 0
64
+ // CJK Unified Ideographs, CJK Compatibility, Hangul, Fullwidth forms
65
+ if (
66
+ (code >= 0x1100 && code <= 0x115f) || // Hangul Jamo
67
+ (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) || // CJK
68
+ (code >= 0xac00 && code <= 0xd7a3) || // Hangul Syllables
69
+ (code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
70
+ (code >= 0xfe10 && code <= 0xfe6f) || // CJK Compatibility Forms
71
+ (code >= 0xff01 && code <= 0xff60) || // Fullwidth
72
+ (code >= 0xffe0 && code <= 0xffe6) || // Fullwidth
73
+ (code >= 0x1f300 && code <= 0x1fbff) || // Emoji & misc symbols
74
+ (code >= 0x20000 && code <= 0x2ffff) // CJK Extension B+
75
+ ) {
76
+ return 2
77
+ }
78
+ return 1
79
+ }
80
+
81
+ /**
82
+ * Get the visible display width of a string (non-ANSI, width-aware).
83
+ */
84
+ export function visibleLength(s: string): number {
85
+ const plain = stripAnsi(s)
86
+ let width = 0
87
+ for (const ch of plain) {
88
+ width += charWidth(ch)
89
+ }
90
+ return width
91
+ }
92
+
93
+ /**
94
+ * Get the display width of a string's first N characters.
95
+ */
96
+ export function displayWidth(s: string, charCount: number): number {
97
+ let width = 0
98
+ let i = 0
99
+ for (const ch of s) {
100
+ if (i >= charCount) break
101
+ width += charWidth(ch)
102
+ i++
103
+ }
104
+ return width
105
+ }
106
+
107
+ /**
108
+ * Word-wrap text to maxWidth, preserving ANSI escape codes.
109
+ * Lines are split on word boundaries. Continuation lines get 2-space indent.
110
+ */
111
+ export function wrapText(text: string, maxWidth: number): string[] {
112
+ if (maxWidth < 10) maxWidth = 10
113
+ const results: string[] = []
114
+
115
+ for (const rawLine of text.split('\n')) {
116
+ const plainLen = visibleLength(rawLine)
117
+ if (plainLen <= maxWidth) {
118
+ results.push(rawLine)
119
+ continue
120
+ }
121
+
122
+ // For ANSI-containing lines, we need to track visible position
123
+ // Simple approach: work with plain text for wrapping decisions,
124
+ // but rebuild output by walking the original with ANSI codes
125
+ const plain = stripAnsi(rawLine)
126
+ const words = plain.split(' ')
127
+ const lines: string[] = []
128
+ let current = ''
129
+
130
+ for (const word of words) {
131
+ const testLen = current.length + (current ? 1 : 0) + word.length
132
+ if (testLen > maxWidth && current) {
133
+ lines.push(current)
134
+ current = ' ' + word
135
+ } else {
136
+ current += (current ? ' ' : '') + word
137
+ }
138
+ }
139
+ if (current) lines.push(current)
140
+
141
+ // If the original had ANSI codes and we're wrapping to plain text,
142
+ // re-apply the dominant ANSI prefix from the original line
143
+ const ansiPrefix = extractAnsiPrefix(rawLine)
144
+ for (let i = 0; i < lines.length; i++) {
145
+ if (ansiPrefix && i === 0) {
146
+ lines[i] = ansiPrefix + lines[i] + A.reset
147
+ } else if (ansiPrefix) {
148
+ lines[i] = ansiPrefix + lines[i] + A.reset
149
+ }
150
+ }
151
+
152
+ results.push(...lines)
153
+ }
154
+
155
+ return results
156
+ }
157
+
158
+ /**
159
+ * Extract leading ANSI escape sequences from a string.
160
+ */
161
+ function extractAnsiPrefix(s: string): string {
162
+ const match = s.match(/^(\x1b\[[0-9;]*[a-zA-Z])+/)
163
+ return match ? match[0] : ''
164
+ }
@@ -0,0 +1,74 @@
1
+ import type { ToolApprovalMode } from './types'
2
+
3
+ export type ApprovalCallback = (toolName: string, input: Record<string, unknown>, riskLevel: string) => Promise<boolean>
4
+
5
+ /**
6
+ * Determines whether a tool call needs user approval.
7
+ */
8
+ export function needsApproval(
9
+ mode: ToolApprovalMode,
10
+ toolName: string,
11
+ riskLevel: string,
12
+ ): boolean {
13
+ if (mode === 'auto') return false
14
+ if (riskLevel === 'safe') return false
15
+
16
+ if (mode === 'confirm-writes') {
17
+ // Only confirm write operations and commands
18
+ return ['write_file', 'edit_file', 'run_command'].includes(toolName)
19
+ }
20
+
21
+ if (mode === 'confirm-all') {
22
+ return riskLevel !== 'safe'
23
+ }
24
+
25
+ return false
26
+ }
27
+
28
+ /**
29
+ * Format a tool call for approval display.
30
+ */
31
+ export function formatApprovalPrompt(toolName: string, input: Record<string, unknown>): string {
32
+ switch (toolName) {
33
+ case 'write_file':
34
+ return `Write file: ${input.path}`
35
+ case 'edit_file':
36
+ return `Edit file: ${input.path}`
37
+ case 'run_command': {
38
+ const cmd = String(input.command || '')
39
+ return `Run: ${cmd.length > 60 ? cmd.slice(0, 57) + '...' : cmd}`
40
+ }
41
+ default:
42
+ return `${toolName}: ${JSON.stringify(input).slice(0, 60)}`
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Generate a colored diff preview for edit_file operations.
48
+ * Returns lines ready for TUI display.
49
+ */
50
+ export function formatEditDiff(oldText: string, newText: string, maxLines: number = 20): string[] {
51
+ const oldLines = oldText.split('\n')
52
+ const newLines = newText.split('\n')
53
+ const lines: string[] = []
54
+
55
+ // Show removed lines (red)
56
+ const shownOld = oldLines.slice(0, maxLines)
57
+ for (const line of shownOld) {
58
+ lines.push(` \x1b[31m- ${line}\x1b[0m`)
59
+ }
60
+ if (oldLines.length > maxLines) {
61
+ lines.push(` \x1b[2m ... (${oldLines.length - maxLines} more removed)\x1b[0m`)
62
+ }
63
+
64
+ // Show added lines (green)
65
+ const shownNew = newLines.slice(0, maxLines)
66
+ for (const line of shownNew) {
67
+ lines.push(` \x1b[32m+ ${line}\x1b[0m`)
68
+ }
69
+ if (newLines.length > maxLines) {
70
+ lines.push(` \x1b[2m ... (${newLines.length - maxLines} more added)\x1b[0m`)
71
+ }
72
+
73
+ return lines
74
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,125 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ const CRED_PATH = join(homedir(), '.claude', '.credentials.json')
6
+
7
+ interface ClaudeOAuth {
8
+ accessToken: string
9
+ refreshToken: string
10
+ expiresAt: number
11
+ subscriptionType: string
12
+ rateLimitTier: string
13
+ }
14
+
15
+ interface ClaudeCredentials {
16
+ claudeAiOauth?: ClaudeOAuth
17
+ }
18
+
19
+ export interface AuthResult {
20
+ apiKey: string
21
+ source: 'api-key' | 'subscription'
22
+ subscriptionType?: string
23
+ expiresAt?: number
24
+ }
25
+
26
+ /**
27
+ * Resolve authentication in priority order:
28
+ * 1. ANTHROPIC_API_KEY env var
29
+ * 2. Claude Code subscription (OAuth token from ~/.claude/.credentials.json)
30
+ * 3. apiKey from smolerclaw config file
31
+ *
32
+ * authMode overrides: "api-key" skips subscription, "subscription" skips api-key.
33
+ */
34
+ export function resolveAuth(
35
+ configApiKey: string,
36
+ authMode: 'auto' | 'api-key' | 'subscription' = 'auto',
37
+ ): AuthResult {
38
+ if (authMode === 'subscription') {
39
+ const sub = trySubscription()
40
+ if (sub) return sub
41
+ throw new Error(
42
+ 'Claude Code credentials not found or expired.\n' +
43
+ 'Run `claude` to refresh, then restart smolerclaw.',
44
+ )
45
+ }
46
+
47
+ if (authMode === 'api-key') {
48
+ const key = process.env.ANTHROPIC_API_KEY || configApiKey
49
+ if (key) return { apiKey: key, source: 'api-key' }
50
+ throw new Error(
51
+ 'No API key found.\n' +
52
+ 'Set ANTHROPIC_API_KEY env var or add apiKey to config.',
53
+ )
54
+ }
55
+
56
+ // auto mode: try all sources in order
57
+
58
+ // 1. Explicit env var always wins
59
+ if (process.env.ANTHROPIC_API_KEY) {
60
+ return { apiKey: process.env.ANTHROPIC_API_KEY, source: 'api-key' }
61
+ }
62
+
63
+ // 2. Claude Code subscription
64
+ const sub = trySubscription()
65
+ if (sub) return sub
66
+
67
+ // 3. Config file API key
68
+ if (configApiKey) {
69
+ return { apiKey: configApiKey, source: 'api-key' }
70
+ }
71
+
72
+ throw new Error(
73
+ 'No authentication found.\n' +
74
+ 'Options:\n' +
75
+ ' 1. Install Claude Code with a Pro/Max subscription (auto-detected)\n' +
76
+ ' 2. Set ANTHROPIC_API_KEY env var\n' +
77
+ ' 3. Add apiKey to ~/.config/smolerclaw/config.json',
78
+ )
79
+ }
80
+
81
+ function trySubscription(): AuthResult | null {
82
+ if (!existsSync(CRED_PATH)) return null
83
+
84
+ try {
85
+ const raw: ClaudeCredentials = JSON.parse(readFileSync(CRED_PATH, 'utf-8'))
86
+ const oauth = raw.claudeAiOauth
87
+ if (!oauth?.accessToken) return null
88
+
89
+ // Check expiration with 60s buffer
90
+ if (Date.now() > oauth.expiresAt - 60_000) return null
91
+
92
+ return {
93
+ apiKey: oauth.accessToken,
94
+ source: 'subscription',
95
+ subscriptionType: oauth.subscriptionType,
96
+ expiresAt: oauth.expiresAt,
97
+ }
98
+ } catch {
99
+ return null
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Re-read credentials from disk. Useful when the OAuth token expires
105
+ * mid-session — Claude Code auto-refreshes it, so a re-read often works.
106
+ * Returns null if no valid credentials are found.
107
+ */
108
+ export function refreshAuth(
109
+ configApiKey: string,
110
+ authMode: 'auto' | 'api-key' | 'subscription' = 'auto',
111
+ ): AuthResult | null {
112
+ try {
113
+ return resolveAuth(configApiKey, authMode)
114
+ } catch {
115
+ return null
116
+ }
117
+ }
118
+
119
+ /** Human-readable label for the TUI header */
120
+ export function authLabel(auth: AuthResult): string {
121
+ if (auth.source === 'subscription') {
122
+ return `sub:${auth.subscriptionType || 'pro'}`
123
+ }
124
+ return 'api-key'
125
+ }