@stacksjs/ai 0.70.55 → 0.70.57
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/package.json +10 -7
- package/src/agents/claude/index.ts +0 -310
- package/src/agents/index.ts +0 -7
- package/src/buddy.ts +0 -619
- package/src/drivers/anthropic/index.ts +0 -430
- package/src/drivers/claude-agent-sdk/index.ts +0 -370
- package/src/drivers/index.ts +0 -14
- package/src/drivers/ollama/index.ts +0 -514
- package/src/drivers/openai/index.ts +0 -529
- package/src/image.ts +0 -607
- package/src/index.ts +0 -53
- package/src/mcp.ts +0 -658
- package/src/personalization.ts +0 -490
- package/src/search.ts +0 -555
- package/src/text.ts +0 -79
- package/src/types.ts +0 -229
- package/src/utils/client-bedrock-runtime.ts +0 -51
- package/src/utils/client-bedrock.ts +0 -75
- package/src/utils/model-access.ts +0 -32
- package/src/utils/retry.ts +0 -124
- package/src/utils/tokens.ts +0 -159
- package/src/utils/usage.ts +0 -98
- package/src/utils/vision.ts +0 -119
- /package/dist/{src/agents → agents}/claude/index.d.ts +0 -0
- /package/dist/{src/agents → agents}/index.d.ts +0 -0
- /package/dist/{src/buddy.d.ts → buddy.d.ts} +0 -0
- /package/dist/{src/drivers → drivers}/anthropic/index.d.ts +0 -0
- /package/dist/{src/drivers → drivers}/claude-agent-sdk/index.d.ts +0 -0
- /package/dist/{src/drivers → drivers}/index.d.ts +0 -0
- /package/dist/{src/drivers → drivers}/ollama/index.d.ts +0 -0
- /package/dist/{src/drivers → drivers}/openai/index.d.ts +0 -0
- /package/dist/{src/image.d.ts → image.d.ts} +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{src/index.js → index.js} +0 -0
- /package/dist/{src/mcp.d.ts → mcp.d.ts} +0 -0
- /package/dist/{src/personalization.d.ts → personalization.d.ts} +0 -0
- /package/dist/{src/search.d.ts → search.d.ts} +0 -0
- /package/dist/{src/text.d.ts → text.d.ts} +0 -0
- /package/dist/{src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{src/utils → utils}/client-bedrock-runtime.d.ts +0 -0
- /package/dist/{src/utils → utils}/client-bedrock.d.ts +0 -0
- /package/dist/{src/utils → utils}/model-access.d.ts +0 -0
- /package/dist/{src/utils → utils}/retry.d.ts +0 -0
- /package/dist/{src/utils → utils}/tokens.d.ts +0 -0
- /package/dist/{src/utils → utils}/usage.d.ts +0 -0
- /package/dist/{src/utils → utils}/vision.d.ts +0 -0
package/src/buddy.ts
DELETED
|
@@ -1,619 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Buddy - Voice AI Code Assistant
|
|
3
|
-
*
|
|
4
|
-
* This module contains the shared state and utilities for Buddy,
|
|
5
|
-
* an AI-powered code assistant that helps users modify codebases
|
|
6
|
-
* through voice commands.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
AIDriver,
|
|
11
|
-
AIMessage,
|
|
12
|
-
BuddyApiKeys,
|
|
13
|
-
BuddyConfig,
|
|
14
|
-
BuddyState,
|
|
15
|
-
GitHubCredentials,
|
|
16
|
-
RepoState,
|
|
17
|
-
StreamingResult,
|
|
18
|
-
} from './types'
|
|
19
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
20
|
-
import { homedir } from 'node:os'
|
|
21
|
-
import { dirname, join } from 'node:path'
|
|
22
|
-
import { claudeAgent } from './agents'
|
|
23
|
-
import { createAnthropicDriver, createClaudeAgentSDKDriver, createOllamaDriver, createOpenAIDriver } from './drivers'
|
|
24
|
-
|
|
25
|
-
// =============================================================================
|
|
26
|
-
// Configuration
|
|
27
|
-
// =============================================================================
|
|
28
|
-
|
|
29
|
-
export const CONFIG: BuddyConfig = {
|
|
30
|
-
workDir: join(homedir(), 'Code', '.buddy-repos'),
|
|
31
|
-
commitMessage: 'chore: wip',
|
|
32
|
-
ollamaHost: process.env.OLLAMA_HOST || 'http://localhost:11434',
|
|
33
|
-
ollamaModel: process.env.OLLAMA_MODEL || 'llama3.2',
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// API Keys state (can be set at runtime via settings endpoint)
|
|
37
|
-
export const apiKeys: BuddyApiKeys = {
|
|
38
|
-
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
39
|
-
openai: process.env.OPENAI_API_KEY,
|
|
40
|
-
claudeCliHost: process.env.BUDDY_EC2_HOST,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// =============================================================================
|
|
44
|
-
// State Management
|
|
45
|
-
// =============================================================================
|
|
46
|
-
|
|
47
|
-
// State singleton
|
|
48
|
-
const state: BuddyState = {
|
|
49
|
-
repo: null,
|
|
50
|
-
conversationHistory: [],
|
|
51
|
-
currentDriver: 'claude-cli-local',
|
|
52
|
-
github: null,
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Ensure work directory exists
|
|
56
|
-
if (!existsSync(CONFIG.workDir)) {
|
|
57
|
-
mkdirSync(CONFIG.workDir, { recursive: true })
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Buddy State Manager
|
|
61
|
-
export interface BuddyStateManager {
|
|
62
|
-
getState: () => BuddyState
|
|
63
|
-
setRepo: (repo: RepoState | null) => void
|
|
64
|
-
setCurrentDriver: (driver: string) => void
|
|
65
|
-
setGitHub: (github: GitHubCredentials | null) => void
|
|
66
|
-
addToHistory: (message: AIMessage) => void
|
|
67
|
-
clearHistory: () => void
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export const buddyState: BuddyStateManager = {
|
|
71
|
-
getState: (): BuddyState => state,
|
|
72
|
-
setRepo: (repo: RepoState | null): void => { state.repo = repo },
|
|
73
|
-
setCurrentDriver: (driver: string): void => { state.currentDriver = driver },
|
|
74
|
-
setGitHub: (github: GitHubCredentials | null): void => { state.github = github },
|
|
75
|
-
addToHistory: (message: AIMessage): void => { state.conversationHistory.push(message) },
|
|
76
|
-
clearHistory: (): void => { state.conversationHistory = [] },
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// =============================================================================
|
|
80
|
-
// AI Driver Utilities
|
|
81
|
-
// =============================================================================
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Build system prompt for AI with repository context
|
|
85
|
-
*/
|
|
86
|
-
export function buildSystemPrompt(context: string): string {
|
|
87
|
-
return `You are Buddy, an AI code assistant that helps users modify codebases through voice commands.
|
|
88
|
-
|
|
89
|
-
${state.repo
|
|
90
|
-
? `You are working on the repository: ${state.repo.name}
|
|
91
|
-
Branch: ${state.repo.branch}
|
|
92
|
-
Path: ${state.repo.path}
|
|
93
|
-
|
|
94
|
-
${context}`
|
|
95
|
-
: 'No repository is currently open.'}
|
|
96
|
-
|
|
97
|
-
When the user gives you a command:
|
|
98
|
-
1. Analyze what they want to do
|
|
99
|
-
2. Identify the files that need to be modified
|
|
100
|
-
3. Generate the exact code changes needed
|
|
101
|
-
4. Respond with a structured format showing:
|
|
102
|
-
- Summary of changes
|
|
103
|
-
- Files to modify/create with full content
|
|
104
|
-
- Any additional notes
|
|
105
|
-
|
|
106
|
-
Format file changes as:
|
|
107
|
-
FILE: path/to/file.ts
|
|
108
|
-
\`\`\`typescript
|
|
109
|
-
// Full file content
|
|
110
|
-
\`\`\`
|
|
111
|
-
|
|
112
|
-
Be concise but thorough. The user will review and commit your changes.`
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Create a mock driver for testing
|
|
117
|
-
*/
|
|
118
|
-
function createMockDriver(): AIDriver {
|
|
119
|
-
return {
|
|
120
|
-
name: 'Mock',
|
|
121
|
-
async process(command: string): Promise<string> {
|
|
122
|
-
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
123
|
-
|
|
124
|
-
const lowerCommand = command.toLowerCase()
|
|
125
|
-
|
|
126
|
-
if (lowerCommand.includes('readme') || lowerCommand.includes('documentation')) {
|
|
127
|
-
return `I'll update the README.md file for you.
|
|
128
|
-
|
|
129
|
-
Analyzing the repository structure...
|
|
130
|
-
|
|
131
|
-
FILE: README.md
|
|
132
|
-
\`\`\`markdown
|
|
133
|
-
# Project Name
|
|
134
|
-
|
|
135
|
-
## Installation
|
|
136
|
-
|
|
137
|
-
\`\`\`bash
|
|
138
|
-
npm install
|
|
139
|
-
# or
|
|
140
|
-
bun install
|
|
141
|
-
\`\`\`
|
|
142
|
-
|
|
143
|
-
## Usage
|
|
144
|
-
|
|
145
|
-
\`\`\`bash
|
|
146
|
-
npm run start
|
|
147
|
-
\`\`\`
|
|
148
|
-
\`\`\`
|
|
149
|
-
|
|
150
|
-
File modified: README.md
|
|
151
|
-
Lines added: 12`
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (lowerCommand.includes('fix') || lowerCommand.includes('bug')) {
|
|
155
|
-
return `I'll analyze and fix the issue.
|
|
156
|
-
|
|
157
|
-
Scanning for potential bugs...
|
|
158
|
-
|
|
159
|
-
FILE: src/utils.ts
|
|
160
|
-
\`\`\`typescript
|
|
161
|
-
export function getData(data: { value?: string }) {
|
|
162
|
-
return data?.value ?? 'default';
|
|
163
|
-
}
|
|
164
|
-
\`\`\`
|
|
165
|
-
|
|
166
|
-
Files modified: src/utils.ts
|
|
167
|
-
Lines changed: 4`
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return `I understand you want to: "${command}"
|
|
171
|
-
|
|
172
|
-
I'll analyze the repository and implement this change.
|
|
173
|
-
|
|
174
|
-
FILE: src/main.ts
|
|
175
|
-
\`\`\`typescript
|
|
176
|
-
// Updated based on your request
|
|
177
|
-
export function main() {
|
|
178
|
-
console.log('Changes applied');
|
|
179
|
-
}
|
|
180
|
-
\`\`\`
|
|
181
|
-
|
|
182
|
-
Files modified: 1`
|
|
183
|
-
},
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Get driver instance by name
|
|
189
|
-
*/
|
|
190
|
-
export function getDriver(driverName: string): AIDriver {
|
|
191
|
-
const currentState = buddyState.getState()
|
|
192
|
-
|
|
193
|
-
switch (driverName) {
|
|
194
|
-
case 'claude-cli-local':
|
|
195
|
-
return claudeAgent.createLocal({ cwd: currentState.repo?.path })
|
|
196
|
-
|
|
197
|
-
case 'claude-cli-ec2':
|
|
198
|
-
return claudeAgent.createEC2({
|
|
199
|
-
cwd: currentState.repo?.path,
|
|
200
|
-
ec2Host: apiKeys.claudeCliHost,
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
case 'claude':
|
|
204
|
-
case 'anthropic':
|
|
205
|
-
if (!apiKeys.anthropic) {
|
|
206
|
-
throw new Error('Anthropic API key not set. Configure your API key in settings.')
|
|
207
|
-
}
|
|
208
|
-
return createAnthropicDriver({ apiKey: apiKeys.anthropic })
|
|
209
|
-
|
|
210
|
-
case 'openai':
|
|
211
|
-
if (!apiKeys.openai) {
|
|
212
|
-
throw new Error('OpenAI API key not set. Configure your API key in settings.')
|
|
213
|
-
}
|
|
214
|
-
return createOpenAIDriver({ apiKey: apiKeys.openai })
|
|
215
|
-
|
|
216
|
-
case 'ollama':
|
|
217
|
-
return createOllamaDriver({
|
|
218
|
-
host: CONFIG.ollamaHost,
|
|
219
|
-
model: CONFIG.ollamaModel,
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
case 'claude-sdk':
|
|
223
|
-
case 'claude-agent-sdk':
|
|
224
|
-
return createClaudeAgentSDKDriver({
|
|
225
|
-
cwd: currentState.repo?.path,
|
|
226
|
-
maxTurns: 25,
|
|
227
|
-
permissionMode: 'bypassPermissions',
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
case 'mock':
|
|
231
|
-
return createMockDriver()
|
|
232
|
-
|
|
233
|
-
default:
|
|
234
|
-
throw new Error(`Unknown driver: ${driverName}. Available: claude-cli-local, claude-cli-ec2, claude, claude-sdk, openai, ollama, mock`)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Get list of available AI drivers
|
|
240
|
-
*/
|
|
241
|
-
export function getAvailableDrivers(): string[] {
|
|
242
|
-
return ['claude-cli-local', 'claude-cli-ec2', 'claude', 'claude-sdk', 'openai', 'ollama', 'mock']
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// =============================================================================
|
|
246
|
-
// Repository Operations
|
|
247
|
-
// =============================================================================
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Get repository structure for context
|
|
251
|
-
*/
|
|
252
|
-
export async function getRepoContext(repoPath: string): Promise<string> {
|
|
253
|
-
const { $: _$ } = await import('bun')
|
|
254
|
-
const treeResult = await _$`cd ${repoPath} && find . -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -name "*.lock" | head -50`.quiet()
|
|
255
|
-
const files = treeResult.text().trim()
|
|
256
|
-
|
|
257
|
-
let readme = ''
|
|
258
|
-
const readmePath = join(repoPath, 'README.md')
|
|
259
|
-
if (existsSync(readmePath)) {
|
|
260
|
-
readme = readFileSync(readmePath, 'utf-8').slice(0, 2000)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
let packageJson = ''
|
|
264
|
-
const packagePath = join(repoPath, 'package.json')
|
|
265
|
-
if (existsSync(packagePath)) {
|
|
266
|
-
packageJson = readFileSync(packagePath, 'utf-8')
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return `
|
|
270
|
-
Repository Structure:
|
|
271
|
-
${files}
|
|
272
|
-
|
|
273
|
-
${readme ? `README.md (excerpt):\n${readme}\n` : ''}
|
|
274
|
-
${packageJson ? `package.json:\n${packageJson}\n` : ''}
|
|
275
|
-
`.trim()
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Clone or open a repository
|
|
280
|
-
*/
|
|
281
|
-
export async function openRepository(input: string): Promise<RepoState> {
|
|
282
|
-
const { $: _$ } = await import('bun')
|
|
283
|
-
let repoPath: string
|
|
284
|
-
let repoName: string
|
|
285
|
-
|
|
286
|
-
if (input.includes('github.com') || input.startsWith('git@')) {
|
|
287
|
-
repoName = input.split('/').pop()?.replace('.git', '') || 'repo'
|
|
288
|
-
repoPath = join(CONFIG.workDir, repoName)
|
|
289
|
-
|
|
290
|
-
if (existsSync(repoPath)) {
|
|
291
|
-
// Repository already exists, pull latest
|
|
292
|
-
await _$`cd ${repoPath} && git pull --rebase`.quiet()
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
// Clone the repository
|
|
296
|
-
await _$`git clone ${input} ${repoPath}`.quiet()
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
repoPath = input.startsWith('~') ? input.replace('~', homedir()) : input
|
|
301
|
-
|
|
302
|
-
if (!existsSync(repoPath)) {
|
|
303
|
-
throw new Error(`Local path does not exist: ${repoPath}`)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (!existsSync(join(repoPath, '.git'))) {
|
|
307
|
-
throw new Error(`Not a git repository: ${repoPath}`)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
repoName = repoPath.split('/').pop() || 'repo'
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const branchResult = await _$`cd ${repoPath} && git branch --show-current`.quiet()
|
|
314
|
-
const branch = branchResult.text().trim()
|
|
315
|
-
|
|
316
|
-
const statusResult = await _$`cd ${repoPath} && git status --porcelain`.quiet()
|
|
317
|
-
const hasChanges = statusResult.text().trim().length > 0
|
|
318
|
-
|
|
319
|
-
const lastCommitResult = await _$`cd ${repoPath} && git log -1 --format="%h %s"`.quiet()
|
|
320
|
-
const lastCommit = lastCommitResult.text().trim()
|
|
321
|
-
|
|
322
|
-
const repoState: RepoState = {
|
|
323
|
-
path: repoPath,
|
|
324
|
-
name: repoName,
|
|
325
|
-
branch,
|
|
326
|
-
hasChanges,
|
|
327
|
-
lastCommit,
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
buddyState.setRepo(repoState)
|
|
331
|
-
buddyState.clearHistory()
|
|
332
|
-
return repoState
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Apply file changes from AI response
|
|
337
|
-
*/
|
|
338
|
-
export async function applyChanges(aiResponse: string): Promise<string[]> {
|
|
339
|
-
const currentState = buddyState.getState()
|
|
340
|
-
|
|
341
|
-
if (!currentState.repo) {
|
|
342
|
-
throw new Error('No repository opened')
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const modifiedFiles: string[] = []
|
|
346
|
-
const filePattern = /FILE:\s*([^\n]+)\n```\w*\n([\s\S]*?)```/g
|
|
347
|
-
|
|
348
|
-
for (const match of aiResponse.matchAll(filePattern)) {
|
|
349
|
-
const filePath = match[1].trim()
|
|
350
|
-
const content = match[2]
|
|
351
|
-
|
|
352
|
-
const fullPath = join(currentState.repo.path, filePath)
|
|
353
|
-
|
|
354
|
-
const dir = dirname(fullPath)
|
|
355
|
-
if (!existsSync(dir)) {
|
|
356
|
-
mkdirSync(dir, { recursive: true })
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
writeFileSync(fullPath, content)
|
|
360
|
-
modifiedFiles.push(filePath)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (modifiedFiles.length > 0 && currentState.repo) {
|
|
364
|
-
currentState.repo.hasChanges = true
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return modifiedFiles
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Configure git user for commits
|
|
372
|
-
*/
|
|
373
|
-
export async function configureGitUser(): Promise<void> {
|
|
374
|
-
const currentState = buddyState.getState()
|
|
375
|
-
if (!currentState.repo || !currentState.github) return
|
|
376
|
-
|
|
377
|
-
const { $: _$ } = await import('bun')
|
|
378
|
-
const { name, email } = currentState.github
|
|
379
|
-
|
|
380
|
-
await _$`cd ${currentState.repo.path} && git config user.name ${name}`.quiet()
|
|
381
|
-
await _$`cd ${currentState.repo.path} && git config user.email ${email}`.quiet()
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Stage and commit changes
|
|
386
|
-
*/
|
|
387
|
-
export async function commitChanges(): Promise<string> {
|
|
388
|
-
const currentState = buddyState.getState()
|
|
389
|
-
|
|
390
|
-
if (!currentState.repo) {
|
|
391
|
-
throw new Error('No repository opened')
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const { $: _$ } = await import('bun')
|
|
395
|
-
|
|
396
|
-
if (currentState.github) {
|
|
397
|
-
await configureGitUser()
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
await _$`cd ${currentState.repo.path} && git add -A`.quiet()
|
|
401
|
-
await _$`cd ${currentState.repo.path} && git commit -m ${CONFIG.commitMessage}`.quiet()
|
|
402
|
-
|
|
403
|
-
const hashResult = await _$`cd ${currentState.repo.path} && git rev-parse --short HEAD`.quiet()
|
|
404
|
-
const commitHash = hashResult.text().trim()
|
|
405
|
-
|
|
406
|
-
currentState.repo.hasChanges = false
|
|
407
|
-
currentState.repo.lastCommit = commitHash
|
|
408
|
-
|
|
409
|
-
return commitHash
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Push changes to remote
|
|
414
|
-
*/
|
|
415
|
-
export async function pushChanges(): Promise<void> {
|
|
416
|
-
const currentState = buddyState.getState()
|
|
417
|
-
|
|
418
|
-
if (!currentState.repo) {
|
|
419
|
-
throw new Error('No repository opened')
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const { $: _$ } = await import('bun')
|
|
423
|
-
await _$`cd ${currentState.repo.path} && git push`.quiet()
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// =============================================================================
|
|
427
|
-
// Command Processing
|
|
428
|
-
// =============================================================================
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Process command with selected AI driver
|
|
432
|
-
*/
|
|
433
|
-
export async function processCommand(command: string, driverName?: string): Promise<string> {
|
|
434
|
-
const currentState = buddyState.getState()
|
|
435
|
-
|
|
436
|
-
if (!currentState.repo) {
|
|
437
|
-
throw new Error('No repository opened')
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const normalizedDriver = driverName || currentState.currentDriver
|
|
441
|
-
const driver = getDriver(normalizedDriver)
|
|
442
|
-
|
|
443
|
-
if (driverName) {
|
|
444
|
-
buddyState.setCurrentDriver(driverName)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const context = await getRepoContext(currentState.repo.path)
|
|
448
|
-
const systemPrompt = buildSystemPrompt(context)
|
|
449
|
-
const response = await driver.process(command, systemPrompt, currentState.conversationHistory)
|
|
450
|
-
|
|
451
|
-
buddyState.addToHistory({ role: 'user', content: command })
|
|
452
|
-
buddyState.addToHistory({ role: 'assistant', content: response })
|
|
453
|
-
|
|
454
|
-
return response
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Process command with streaming output using Claude CLI
|
|
459
|
-
*/
|
|
460
|
-
export async function buddyProcessStreaming(
|
|
461
|
-
command: string,
|
|
462
|
-
driverName?: string,
|
|
463
|
-
history?: Array<{role: string; content: string}>,
|
|
464
|
-
): Promise<StreamingResult> {
|
|
465
|
-
const currentState = buddyState.getState()
|
|
466
|
-
|
|
467
|
-
if (!currentState.repo) {
|
|
468
|
-
throw new Error('No repository opened')
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const normalizedDriver = driverName || currentState.currentDriver
|
|
472
|
-
const streamingDrivers = ['claude-cli-local', 'claude-sdk']
|
|
473
|
-
if (!streamingDrivers.includes(normalizedDriver)) {
|
|
474
|
-
throw new Error(`Streaming only supported for ${streamingDrivers.join(', ')} drivers. Current: ${normalizedDriver}`)
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (driverName) {
|
|
478
|
-
buddyState.setCurrentDriver(driverName)
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Build command with conversation history for context
|
|
482
|
-
let contextualCommand = command
|
|
483
|
-
if (history && history.length > 0) {
|
|
484
|
-
let conversationContext = '## Previous Conversation\nHere is our conversation so far:\n\n'
|
|
485
|
-
for (const msg of history) {
|
|
486
|
-
const role = msg.role === 'user' ? 'User' : 'Assistant'
|
|
487
|
-
conversationContext += `**${role}:** ${msg.content}\n\n`
|
|
488
|
-
}
|
|
489
|
-
conversationContext += '---\n\n## Current Request\n'
|
|
490
|
-
contextualCommand = conversationContext + command
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const result = await claudeAgent.processStreaming(contextualCommand, currentState.repo.path)
|
|
494
|
-
|
|
495
|
-
// Update history when streaming completes
|
|
496
|
-
result.fullResponse.then((response) => {
|
|
497
|
-
buddyState.addToHistory({ role: 'user', content: command })
|
|
498
|
-
buddyState.addToHistory({ role: 'assistant', content: response })
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
return result
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Stream a simple Q&A response using the Anthropic API directly.
|
|
506
|
-
* This provides true token-by-token streaming like ChatGPT/Claude web.
|
|
507
|
-
* Use this for questions/explanations that don't require agentic tool use.
|
|
508
|
-
*/
|
|
509
|
-
export async function buddyStreamSimple(
|
|
510
|
-
command: string,
|
|
511
|
-
history?: Array<{ role: string; content: string }>,
|
|
512
|
-
): Promise<StreamingResult> {
|
|
513
|
-
|
|
514
|
-
if (!apiKeys.anthropic) {
|
|
515
|
-
throw new Error('Anthropic API key not set. Configure your API key in settings.')
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Build conversation messages
|
|
519
|
-
const messages: AIMessage[] = []
|
|
520
|
-
if (history && history.length > 0) {
|
|
521
|
-
for (const msg of history) {
|
|
522
|
-
messages.push({
|
|
523
|
-
role: msg.role as 'user' | 'assistant',
|
|
524
|
-
content: msg.content,
|
|
525
|
-
})
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
messages.push({ role: 'user', content: command })
|
|
529
|
-
|
|
530
|
-
// For simple Q&A mode, use a generic helpful assistant prompt
|
|
531
|
-
// Don't include repo context - this keeps answers general and not code-focused
|
|
532
|
-
const systemPrompt = `You are a helpful AI assistant. Answer questions naturally and conversationally.
|
|
533
|
-
You can discuss any topic - technology, science, philosophy, everyday questions, or anything else the user asks about.
|
|
534
|
-
Be concise but thorough. If the user asks about coding or their project specifically, help with that too.`
|
|
535
|
-
|
|
536
|
-
const encoder = new TextEncoder()
|
|
537
|
-
let fullResponse = ''
|
|
538
|
-
let resolveFullResponse: (value: string) => void
|
|
539
|
-
const fullResponsePromise = new Promise<string>((resolve) => {
|
|
540
|
-
resolveFullResponse = resolve
|
|
541
|
-
})
|
|
542
|
-
|
|
543
|
-
// Create streaming response using Anthropic API
|
|
544
|
-
const stream = new ReadableStream<Uint8Array>({
|
|
545
|
-
async start(controller) {
|
|
546
|
-
try {
|
|
547
|
-
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
548
|
-
method: 'POST',
|
|
549
|
-
headers: {
|
|
550
|
-
'Content-Type': 'application/json',
|
|
551
|
-
'x-api-key': apiKeys.anthropic!,
|
|
552
|
-
'anthropic-version': '2023-06-01',
|
|
553
|
-
},
|
|
554
|
-
body: JSON.stringify({
|
|
555
|
-
model: 'claude-sonnet-4-20250514',
|
|
556
|
-
max_tokens: 4096,
|
|
557
|
-
system: systemPrompt,
|
|
558
|
-
stream: true,
|
|
559
|
-
messages,
|
|
560
|
-
}),
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
if (!response.ok) {
|
|
564
|
-
const error = await response.text()
|
|
565
|
-
throw new Error(`Claude API error: ${error}`)
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const reader = response.body?.getReader()
|
|
569
|
-
if (!reader) throw new Error('No response body')
|
|
570
|
-
|
|
571
|
-
const decoder = new TextDecoder()
|
|
572
|
-
let buffer = ''
|
|
573
|
-
|
|
574
|
-
while (true) {
|
|
575
|
-
const { done, value } = await reader.read()
|
|
576
|
-
if (done) break
|
|
577
|
-
|
|
578
|
-
buffer += decoder.decode(value, { stream: true })
|
|
579
|
-
const lines = buffer.split('\n')
|
|
580
|
-
buffer = lines.pop() || ''
|
|
581
|
-
|
|
582
|
-
for (const line of lines) {
|
|
583
|
-
if (line.startsWith('data: ')) {
|
|
584
|
-
const data = line.slice(6)
|
|
585
|
-
if (data === '[DONE]') continue
|
|
586
|
-
|
|
587
|
-
try {
|
|
588
|
-
const event = JSON.parse(data) as { type?: string; delta?: { text?: string } }
|
|
589
|
-
if (event.type === 'content_block_delta' && event.delta?.text) {
|
|
590
|
-
const text = event.delta.text
|
|
591
|
-
fullResponse += text
|
|
592
|
-
controller.enqueue(encoder.encode(text))
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
catch {
|
|
596
|
-
// Skip invalid JSON
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Update history and resolve the full response
|
|
603
|
-
buddyState.addToHistory({ role: 'user', content: command })
|
|
604
|
-
buddyState.addToHistory({ role: 'assistant', content: fullResponse })
|
|
605
|
-
resolveFullResponse(fullResponse)
|
|
606
|
-
controller.close()
|
|
607
|
-
}
|
|
608
|
-
catch (error) {
|
|
609
|
-
resolveFullResponse(fullResponse) // Resolve with what we have
|
|
610
|
-
controller.error(error)
|
|
611
|
-
}
|
|
612
|
-
},
|
|
613
|
-
})
|
|
614
|
-
|
|
615
|
-
return {
|
|
616
|
-
stream,
|
|
617
|
-
fullResponse: fullResponsePromise,
|
|
618
|
-
}
|
|
619
|
-
}
|