smolerclaw 1.0.0 → 1.0.2
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/dist/README.md +159 -0
- package/package.json +11 -3
- package/.github/workflows/ci.yml +0 -30
- package/.github/workflows/release.yml +0 -67
- package/bun.lock +0 -33
- package/install.ps1 +0 -119
- package/skills/business.md +0 -77
- package/skills/default.md +0 -77
- package/src/ansi.ts +0 -164
- package/src/approval.ts +0 -74
- package/src/auth.ts +0 -125
- package/src/briefing.ts +0 -52
- package/src/claude.ts +0 -267
- package/src/cli.ts +0 -137
- package/src/clipboard.ts +0 -27
- package/src/config.ts +0 -87
- package/src/context-window.ts +0 -190
- package/src/context.ts +0 -125
- package/src/decisions.ts +0 -122
- package/src/email.ts +0 -123
- package/src/errors.ts +0 -78
- package/src/export.ts +0 -82
- package/src/finance.ts +0 -148
- package/src/git.ts +0 -62
- package/src/history.ts +0 -100
- package/src/images.ts +0 -68
- package/src/index.ts +0 -1431
- package/src/investigate.ts +0 -415
- package/src/markdown.ts +0 -125
- package/src/memos.ts +0 -191
- package/src/models.ts +0 -94
- package/src/monitor.ts +0 -169
- package/src/morning.ts +0 -108
- package/src/news.ts +0 -329
- package/src/openai-provider.ts +0 -127
- package/src/people.ts +0 -472
- package/src/personas.ts +0 -99
- package/src/platform.ts +0 -84
- package/src/plugins.ts +0 -125
- package/src/pomodoro.ts +0 -169
- package/src/providers.ts +0 -70
- package/src/retry.ts +0 -108
- package/src/session.ts +0 -128
- package/src/skills.ts +0 -102
- package/src/tasks.ts +0 -418
- package/src/tokens.ts +0 -102
- package/src/tool-safety.ts +0 -100
- package/src/tools.ts +0 -1479
- package/src/tui.ts +0 -693
- package/src/types.ts +0 -55
- package/src/undo.ts +0 -83
- package/src/windows.ts +0 -299
- package/src/workflows.ts +0 -197
- package/tests/ansi.test.ts +0 -58
- package/tests/approval.test.ts +0 -43
- package/tests/briefing.test.ts +0 -10
- package/tests/cli.test.ts +0 -53
- package/tests/context-window.test.ts +0 -83
- package/tests/images.test.ts +0 -28
- package/tests/memos.test.ts +0 -116
- package/tests/models.test.ts +0 -34
- package/tests/news.test.ts +0 -13
- package/tests/path-guard.test.ts +0 -37
- package/tests/people.test.ts +0 -204
- package/tests/skills.test.ts +0 -35
- package/tests/ssrf.test.ts +0 -80
- package/tests/tasks.test.ts +0 -152
- package/tests/tokens.test.ts +0 -44
- package/tests/tool-safety.test.ts +0 -55
- package/tests/windows-security.test.ts +0 -59
- package/tests/windows.test.ts +0 -20
- package/tsconfig.json +0 -19
package/src/ansi.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
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
|
-
}
|
package/src/approval.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
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
|
-
}
|
package/src/briefing.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Daily briefing — morning summary combining calendar, system, and news.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { getDateTimeInfo, getOutlookEvents, getSystemInfo } from './windows'
|
|
6
|
-
import { fetchNews } from './news'
|
|
7
|
-
import { IS_WINDOWS } from './platform'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Generate a daily briefing with date, calendar, system, and top news.
|
|
11
|
-
*/
|
|
12
|
-
export async function generateBriefing(): Promise<string> {
|
|
13
|
-
const sections: string[] = []
|
|
14
|
-
|
|
15
|
-
// Header
|
|
16
|
-
sections.push('=== BRIEFING DIARIO ===')
|
|
17
|
-
|
|
18
|
-
// Date & time
|
|
19
|
-
const dateInfo = await getDateTimeInfo()
|
|
20
|
-
sections.push(dateInfo)
|
|
21
|
-
|
|
22
|
-
// Calendar (Windows only, non-blocking)
|
|
23
|
-
if (IS_WINDOWS) {
|
|
24
|
-
try {
|
|
25
|
-
const events = await getOutlookEvents()
|
|
26
|
-
sections.push(`\n--- Agenda ---\n${events}`)
|
|
27
|
-
} catch {
|
|
28
|
-
sections.push('\n--- Agenda ---\nOutlook nao disponivel.')
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// System status
|
|
33
|
-
if (IS_WINDOWS) {
|
|
34
|
-
try {
|
|
35
|
-
const sys = await getSystemInfo()
|
|
36
|
-
sections.push(`\n--- Sistema ---\n${sys}`)
|
|
37
|
-
} catch {
|
|
38
|
-
// Skip system info on error
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Top news (limited to 3 per source for briefing)
|
|
43
|
-
try {
|
|
44
|
-
const news = await fetchNews(['finance', 'business', 'tech'], 3)
|
|
45
|
-
sections.push(`\n${news}`)
|
|
46
|
-
} catch {
|
|
47
|
-
sections.push('\n--- Noticias ---\nFalha ao carregar noticias.')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
sections.push('\n======================')
|
|
51
|
-
return sections.join('\n')
|
|
52
|
-
}
|
package/src/claude.ts
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import Anthropic from '@anthropic-ai/sdk'
|
|
2
|
-
import type { Message, ChatEvent, ToolApprovalMode } from './types'
|
|
3
|
-
import { TOOLS, executeTool } from './tools'
|
|
4
|
-
import { withRetry } from './retry'
|
|
5
|
-
import { trimToContextWindow, compressToolResults, estimateTokens, needsSummary, buildSummaryRequest, summarizationPrompt } from './context-window'
|
|
6
|
-
import { assessToolRisk } from './tool-safety'
|
|
7
|
-
import { humanizeError } from './errors'
|
|
8
|
-
import { needsApproval, type ApprovalCallback } from './approval'
|
|
9
|
-
|
|
10
|
-
export class ClaudeProvider {
|
|
11
|
-
private client: Anthropic
|
|
12
|
-
private approvalMode: ToolApprovalMode
|
|
13
|
-
private approvalCallback: ApprovalCallback | null = null
|
|
14
|
-
private autoApproveAll = false
|
|
15
|
-
private onAuthExpired: (() => boolean) | null = null
|
|
16
|
-
|
|
17
|
-
constructor(
|
|
18
|
-
apiKey: string,
|
|
19
|
-
private model: string,
|
|
20
|
-
private maxTokens: number,
|
|
21
|
-
approvalMode: ToolApprovalMode = 'auto',
|
|
22
|
-
) {
|
|
23
|
-
this.client = new Anthropic({ apiKey })
|
|
24
|
-
this.approvalMode = approvalMode
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Replace the API key and recreate the client (used after auth refresh) */
|
|
28
|
-
updateApiKey(newKey: string): void {
|
|
29
|
-
this.client = new Anthropic({ apiKey: newKey })
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Register a callback that fires on 401 to attempt credential refresh */
|
|
33
|
-
setAuthRefresh(cb: () => boolean): void {
|
|
34
|
-
this.onAuthExpired = cb
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
setModel(model: string): void {
|
|
38
|
-
this.model = model
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
setApprovalMode(mode: ToolApprovalMode): void {
|
|
42
|
-
this.approvalMode = mode
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
setApprovalCallback(cb: ApprovalCallback): void {
|
|
46
|
-
this.approvalCallback = cb
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
setAutoApproveAll(value: boolean): void {
|
|
50
|
-
this.autoApproveAll = value
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async *chat(
|
|
54
|
-
messages: Message[],
|
|
55
|
-
systemPrompt: string,
|
|
56
|
-
enableTools = true,
|
|
57
|
-
): AsyncGenerator<ChatEvent> {
|
|
58
|
-
let processed = compressToolResults(messages)
|
|
59
|
-
const systemTokens = estimateTokens(systemPrompt)
|
|
60
|
-
|
|
61
|
-
// Auto-summary when context is getting large
|
|
62
|
-
if (needsSummary(processed, this.model, systemTokens)) {
|
|
63
|
-
const req = buildSummaryRequest(processed, this.model, systemTokens)
|
|
64
|
-
if (req) {
|
|
65
|
-
try {
|
|
66
|
-
const summaryText = await this.generateSummary(req.toSummarize)
|
|
67
|
-
const summaryMsg: Message = {
|
|
68
|
-
role: 'assistant',
|
|
69
|
-
content: `[Conversation summary]\n${summaryText}`,
|
|
70
|
-
timestamp: Date.now(),
|
|
71
|
-
}
|
|
72
|
-
processed = [
|
|
73
|
-
{ role: 'user', content: 'Continue from this summary of our earlier conversation.', timestamp: Date.now() },
|
|
74
|
-
summaryMsg,
|
|
75
|
-
...req.toKeep,
|
|
76
|
-
]
|
|
77
|
-
} catch {
|
|
78
|
-
// Fallback to simple trim if summary fails
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const trimmed = trimToContextWindow(processed, this.model, systemTokens)
|
|
84
|
-
const apiMessages = toApiMessages(trimmed)
|
|
85
|
-
const tools = enableTools ? TOOLS : undefined
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
yield* this.streamLoop(apiMessages, systemPrompt, tools)
|
|
89
|
-
} catch (err) {
|
|
90
|
-
yield { type: 'error', error: humanizeError(err) }
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private async generateSummary(messages: Message[]): Promise<string> {
|
|
95
|
-
const prompt = summarizationPrompt(messages)
|
|
96
|
-
const resp = await this.client.messages.create({
|
|
97
|
-
model: this.model,
|
|
98
|
-
max_tokens: 1024,
|
|
99
|
-
messages: [{ role: 'user', content: prompt }],
|
|
100
|
-
})
|
|
101
|
-
const textBlock = resp.content.find((b) => b.type === 'text')
|
|
102
|
-
return textBlock?.type === 'text' ? textBlock.text : 'Summary unavailable.'
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private async *streamLoop(
|
|
106
|
-
messages: Anthropic.MessageParam[],
|
|
107
|
-
system: string,
|
|
108
|
-
tools?: Anthropic.Tool[],
|
|
109
|
-
): AsyncGenerator<ChatEvent> {
|
|
110
|
-
const MAX_TOOL_ROUNDS = 25
|
|
111
|
-
const convo = [...messages]
|
|
112
|
-
let round = 0
|
|
113
|
-
|
|
114
|
-
while (round++ < MAX_TOOL_ROUNDS) {
|
|
115
|
-
let stream: ReturnType<typeof this.client.messages.stream>
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
stream = await withRetry(
|
|
119
|
-
async () => {
|
|
120
|
-
return this.client.messages.stream({
|
|
121
|
-
model: this.model,
|
|
122
|
-
max_tokens: this.maxTokens,
|
|
123
|
-
system,
|
|
124
|
-
messages: convo,
|
|
125
|
-
...(tools?.length ? { tools } : {}),
|
|
126
|
-
})
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
onAuthExpired: this.onAuthExpired ?? undefined,
|
|
130
|
-
},
|
|
131
|
-
)
|
|
132
|
-
} catch (err) {
|
|
133
|
-
yield { type: 'error', error: humanizeError(err) }
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
for await (const event of stream) {
|
|
138
|
-
if (
|
|
139
|
-
event.type === 'content_block_delta' &&
|
|
140
|
-
event.delta.type === 'text_delta'
|
|
141
|
-
) {
|
|
142
|
-
yield { type: 'text', text: event.delta.text }
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const final = await stream.finalMessage()
|
|
147
|
-
|
|
148
|
-
if (final.usage) {
|
|
149
|
-
yield {
|
|
150
|
-
type: 'usage',
|
|
151
|
-
inputTokens: final.usage.input_tokens,
|
|
152
|
-
outputTokens: final.usage.output_tokens,
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (final.stop_reason !== 'tool_use') {
|
|
157
|
-
yield { type: 'done' }
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const toolBlocks = final.content.filter(
|
|
162
|
-
(b: Anthropic.ContentBlock): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
convo.push({ role: 'assistant', content: final.content })
|
|
166
|
-
|
|
167
|
-
const toolResults: Anthropic.ToolResultBlockParam[] = []
|
|
168
|
-
for (const tc of toolBlocks) {
|
|
169
|
-
const input = tc.input as Record<string, unknown>
|
|
170
|
-
const risk = assessToolRisk(tc.name, input)
|
|
171
|
-
|
|
172
|
-
// Block dangerous operations always
|
|
173
|
-
if (risk.level === 'dangerous') {
|
|
174
|
-
yield { type: 'tool_blocked', id: tc.id, name: tc.name, reason: `Blocked dangerous operation: ${risk.reason}` }
|
|
175
|
-
toolResults.push({
|
|
176
|
-
type: 'tool_result',
|
|
177
|
-
tool_use_id: tc.id,
|
|
178
|
-
content: `Error: Operation blocked for safety. Reason: ${risk.reason}. This command appears dangerous and was not executed.`,
|
|
179
|
-
})
|
|
180
|
-
continue
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Check if approval is needed
|
|
184
|
-
if (!this.autoApproveAll && needsApproval(this.approvalMode, tc.name, risk.level) && this.approvalCallback) {
|
|
185
|
-
yield { type: 'tool_call', id: tc.id, name: tc.name, input: tc.input }
|
|
186
|
-
const approved = await this.approvalCallback(tc.name, input, risk.level)
|
|
187
|
-
if (!approved) {
|
|
188
|
-
yield { type: 'tool_blocked', id: tc.id, name: tc.name, reason: 'Rejected by user' }
|
|
189
|
-
toolResults.push({
|
|
190
|
-
type: 'tool_result',
|
|
191
|
-
tool_use_id: tc.id,
|
|
192
|
-
content: 'Error: User rejected this operation.',
|
|
193
|
-
})
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
// Approved — execute (tool_call already yielded above)
|
|
197
|
-
const result = await executeTool(tc.name, input)
|
|
198
|
-
yield { type: 'tool_result', id: tc.id, name: tc.name, result }
|
|
199
|
-
toolResults.push({ type: 'tool_result', tool_use_id: tc.id, content: result })
|
|
200
|
-
continue
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Auto-approved — execute normally
|
|
204
|
-
yield { type: 'tool_call', id: tc.id, name: tc.name, input: tc.input }
|
|
205
|
-
const result = await executeTool(tc.name, input)
|
|
206
|
-
yield { type: 'tool_result', id: tc.id, name: tc.name, result }
|
|
207
|
-
toolResults.push({ type: 'tool_result', tool_use_id: tc.id, content: result })
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
convo.push({ role: 'user', content: toolResults })
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
yield { type: 'error', error: `Stopped after ${MAX_TOOL_ROUNDS} tool rounds to prevent runaway execution.` }
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function toApiMessages(messages: Message[]): Anthropic.MessageParam[] {
|
|
218
|
-
const result: Anthropic.MessageParam[] = []
|
|
219
|
-
|
|
220
|
-
for (const msg of messages) {
|
|
221
|
-
if (msg.role === 'user') {
|
|
222
|
-
if (msg.images?.length) {
|
|
223
|
-
// Build multi-modal content with images + text
|
|
224
|
-
const content: Anthropic.ContentBlockParam[] = msg.images.map((img) => ({
|
|
225
|
-
type: 'image' as const,
|
|
226
|
-
source: {
|
|
227
|
-
type: 'base64' as const,
|
|
228
|
-
media_type: img.mediaType,
|
|
229
|
-
data: img.base64,
|
|
230
|
-
},
|
|
231
|
-
}))
|
|
232
|
-
content.push({ type: 'text', text: msg.content })
|
|
233
|
-
result.push({ role: 'user', content })
|
|
234
|
-
} else {
|
|
235
|
-
result.push({ role: 'user', content: msg.content })
|
|
236
|
-
}
|
|
237
|
-
} else if (msg.role === 'assistant') {
|
|
238
|
-
if (msg.toolCalls?.length) {
|
|
239
|
-
const content: Anthropic.ContentBlockParam[] = []
|
|
240
|
-
if (msg.content) {
|
|
241
|
-
content.push({ type: 'text', text: msg.content })
|
|
242
|
-
}
|
|
243
|
-
for (const tc of msg.toolCalls) {
|
|
244
|
-
content.push({
|
|
245
|
-
type: 'tool_use',
|
|
246
|
-
id: tc.id,
|
|
247
|
-
name: tc.name,
|
|
248
|
-
input: tc.input,
|
|
249
|
-
})
|
|
250
|
-
}
|
|
251
|
-
result.push({ role: 'assistant', content })
|
|
252
|
-
result.push({
|
|
253
|
-
role: 'user',
|
|
254
|
-
content: msg.toolCalls.map((tc) => ({
|
|
255
|
-
type: 'tool_result' as const,
|
|
256
|
-
tool_use_id: tc.id,
|
|
257
|
-
content: tc.result,
|
|
258
|
-
})),
|
|
259
|
-
})
|
|
260
|
-
} else {
|
|
261
|
-
result.push({ role: 'assistant', content: msg.content })
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return result
|
|
267
|
-
}
|