@stacksjs/ai 0.70.87 → 0.70.90

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.
@@ -0,0 +1,197 @@
1
+ let promptCount = 0;
2
+ function formatToolUsage(toolName, input) {
3
+ switch (toolName) {
4
+ case "Read": {
5
+ const filePath = input.file_path || "";
6
+ return `
7
+ \uD83D\uDCD6 Reading: ${filePath.split("/").pop() || filePath}
8
+ `;
9
+ }
10
+ case "Glob":
11
+ return `
12
+ \uD83D\uDD0D Searching files: ${input.pattern || ""}
13
+ `;
14
+ case "Grep":
15
+ return `
16
+ \uD83D\uDD0E Searching for: "${input.pattern || ""}"
17
+ `;
18
+ case "Edit": {
19
+ const filePath = input.file_path || "";
20
+ return `
21
+ \u270F\uFE0F Editing: ${filePath.split("/").pop() || filePath}
22
+ `;
23
+ }
24
+ case "Write": {
25
+ const filePath = input.file_path || "";
26
+ return `
27
+ \uD83D\uDCDD Writing: ${filePath.split("/").pop() || filePath}
28
+ `;
29
+ }
30
+ case "Bash": {
31
+ const cmd = (input.command || "").substring(0, 50);
32
+ return `
33
+ \uD83D\uDCBB Running: ${cmd}${cmd.length >= 50 ? "..." : ""}
34
+ `;
35
+ }
36
+ case "Task":
37
+ return `
38
+ \uD83D\uDE80 Launching agent: ${input.description || "task"}
39
+ `;
40
+ default:
41
+ return `
42
+ \uD83D\uDD27 Using: ${toolName}
43
+ `;
44
+ }
45
+ }
46
+ export function createClaudeLocalAgent(config = {}) {
47
+ return {
48
+ name: "Claude CLI (Local)",
49
+ async process(command, context, _history) {
50
+ const { $: _$ } = await import("bun");
51
+ if ((await _$`which claude`.quiet().nothrow()).exitCode !== 0)
52
+ throw Error("Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code");
53
+ const fullPrompt = context ? `Context:
54
+ ${context}
55
+
56
+ User request: ${command}` : command, cwd = config.cwd || process.cwd();
57
+ try {
58
+ return (await _$`cd ${cwd} && claude --print --dangerously-skip-permissions ${fullPrompt}`.quiet()).text().trim();
59
+ } catch {
60
+ try {
61
+ return (await _$`cd ${cwd} && claude --print --allowedTools "Write,Edit,Bash" ${fullPrompt}`.quiet()).text().trim();
62
+ } catch (innerError) {
63
+ throw Error(`Claude CLI error: ${innerError.message}`);
64
+ }
65
+ }
66
+ }
67
+ };
68
+ }
69
+ export function createClaudeEC2Agent(config) {
70
+ const {
71
+ ec2Host = process.env.BUDDY_EC2_HOST,
72
+ ec2User = process.env.BUDDY_EC2_USER || "ubuntu",
73
+ ec2Key = process.env.BUDDY_EC2_KEY
74
+ } = config;
75
+ return {
76
+ name: "Claude CLI (EC2)",
77
+ async process(command, context, _history) {
78
+ const { $: _$ } = await import("bun");
79
+ if (!ec2Host)
80
+ throw Error("BUDDY_EC2_HOST environment variable not set. Set it to your EC2 instance IP/hostname.");
81
+ const escapedPrompt = (context ? `Context:
82
+ ${context}
83
+
84
+ User request: ${command}` : command).replace(/'/g, "'\\''"), sshArgs = ec2Key ? `-i ${ec2Key}` : "", sshTarget = `${ec2User}@${ec2Host}`;
85
+ try {
86
+ return (await _$`ssh ${sshArgs} ${sshTarget} "claude --print '${escapedPrompt}'"`.quiet()).text().trim();
87
+ } catch (error) {
88
+ throw Error(`EC2 Claude CLI error: ${error.message}. Make sure SSH is configured and claude CLI is installed on EC2.`);
89
+ }
90
+ }
91
+ };
92
+ }
93
+ export async function processCommandStreaming(command, cwd) {
94
+ const { spawn } = await import("bun"), proc = spawn([
95
+ "claude",
96
+ "--print",
97
+ "--verbose",
98
+ "--output-format",
99
+ "stream-json",
100
+ "--dangerously-skip-permissions",
101
+ command
102
+ ], {
103
+ cwd,
104
+ stdout: "pipe",
105
+ stderr: "pipe"
106
+ });
107
+ let fullResponse = "";
108
+ const decoder = new TextDecoder, encoder = new TextEncoder, stream = new ReadableStream({
109
+ async start(controller) {
110
+ try {
111
+ const reader = proc.stdout.getReader();
112
+ let buffer = "", lastSentText = "", result = await reader.read();
113
+ while (!result.done) {
114
+ buffer += decoder.decode(result.value, { stream: !0 });
115
+ const lines = buffer.split(`
116
+ `);
117
+ buffer = lines.pop() || "";
118
+ for (const line of lines) {
119
+ if (!line.trim())
120
+ continue;
121
+ try {
122
+ const event = JSON.parse(line);
123
+ let textContent = "";
124
+ if (event.type === "system" && event.subtype === "init")
125
+ promptCount++;
126
+ else if (event.type === "content_block_start" && event.content_block?.text)
127
+ textContent = event.content_block.text;
128
+ else if (event.type === "content_block_delta" && event.delta?.text)
129
+ textContent = event.delta.text;
130
+ else if (event.type === "assistant" && event.message?.content) {
131
+ for (const block of event.message.content)
132
+ if (block.type === "text" && block.text)
133
+ textContent += block.text;
134
+ else if (block.type === "tool_use" && block.name)
135
+ textContent += formatToolUsage(block.name, block.input || {});
136
+ } else if (event.type === "user" && event.message?.content) {
137
+ for (const block of event.message.content)
138
+ if (block.type === "tool_result")
139
+ textContent += ` \u2713 done
140
+ `;
141
+ } else if (event.type === "result" && event.result) {
142
+ fullResponse = event.result;
143
+ const resultPrefix = event.result.substring(0, Math.min(50, event.result.length));
144
+ if (!lastSentText.includes(resultPrefix))
145
+ textContent = event.result;
146
+ }
147
+ if (textContent) {
148
+ if (event.type === "content_block_delta") {
149
+ lastSentText += textContent;
150
+ controller.enqueue(encoder.encode(textContent));
151
+ } else if (!lastSentText.endsWith(textContent)) {
152
+ lastSentText += textContent;
153
+ controller.enqueue(encoder.encode(textContent));
154
+ }
155
+ }
156
+ } catch {
157
+ if (!lastSentText.includes(line)) {
158
+ lastSentText += line;
159
+ fullResponse += line;
160
+ controller.enqueue(encoder.encode(line));
161
+ }
162
+ }
163
+ }
164
+ result = await reader.read();
165
+ }
166
+ if (buffer.trim())
167
+ try {
168
+ const event = JSON.parse(buffer);
169
+ if (event.result && !fullResponse)
170
+ fullResponse = event.result;
171
+ } catch {
172
+ if (!lastSentText.includes(buffer)) {
173
+ fullResponse += buffer;
174
+ controller.enqueue(encoder.encode(buffer));
175
+ }
176
+ }
177
+ await proc.exited;
178
+ controller.close();
179
+ } catch (error) {
180
+ controller.error(error);
181
+ }
182
+ }
183
+ }), fullResponsePromise = (async () => {
184
+ await proc.exited;
185
+ return fullResponse.trim();
186
+ })();
187
+ return { stream, fullResponse: fullResponsePromise };
188
+ }
189
+ export function resetPromptCount() {
190
+ promptCount = 0;
191
+ }
192
+ export const claudeAgent = {
193
+ createLocal: createClaudeLocalAgent,
194
+ createEC2: createClaudeEC2Agent,
195
+ processStreaming: processCommandStreaming,
196
+ resetPromptCount
197
+ };
@@ -0,0 +1 @@
1
+ export * from "./claude";
package/dist/buddy.js ADDED
@@ -0,0 +1,393 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { claudeAgent } from "./agents";
5
+ import { createAnthropicDriver, createClaudeAgentSDKDriver, createOllamaDriver, createOpenAIDriver } from "./drivers";
6
+ export const CONFIG = {
7
+ workDir: join(homedir(), "Code", ".buddy-repos"),
8
+ commitMessage: "chore: wip",
9
+ ollamaHost: process.env.OLLAMA_HOST || "http://localhost:11434",
10
+ ollamaModel: process.env.OLLAMA_MODEL || "llama3.2"
11
+ }, apiKeys = {
12
+ anthropic: process.env.ANTHROPIC_API_KEY,
13
+ openai: process.env.OPENAI_API_KEY,
14
+ claudeCliHost: process.env.BUDDY_EC2_HOST
15
+ };
16
+ const state = {
17
+ repo: null,
18
+ conversationHistory: [],
19
+ currentDriver: "claude-cli-local",
20
+ github: null
21
+ };
22
+ if (!existsSync(CONFIG.workDir))
23
+ mkdirSync(CONFIG.workDir, { recursive: !0 });
24
+ export const buddyState = {
25
+ getState: () => state,
26
+ setRepo: (repo) => {
27
+ state.repo = repo;
28
+ },
29
+ setCurrentDriver: (driver) => {
30
+ state.currentDriver = driver;
31
+ },
32
+ setGitHub: (github) => {
33
+ state.github = github;
34
+ },
35
+ addToHistory: (message) => {
36
+ state.conversationHistory.push(message);
37
+ },
38
+ clearHistory: () => {
39
+ state.conversationHistory = [];
40
+ }
41
+ };
42
+ export function buildSystemPrompt(context) {
43
+ return `You are Buddy, an AI code assistant that helps users modify codebases through voice commands.
44
+
45
+ ${state.repo ? `You are working on the repository: ${state.repo.name}
46
+ Branch: ${state.repo.branch}
47
+ Path: ${state.repo.path}
48
+
49
+ ${context}` : "No repository is currently open."}
50
+
51
+ When the user gives you a command:
52
+ 1. Analyze what they want to do
53
+ 2. Identify the files that need to be modified
54
+ 3. Generate the exact code changes needed
55
+ 4. Respond with a structured format showing:
56
+ - Summary of changes
57
+ - Files to modify/create with full content
58
+ - Any additional notes
59
+
60
+ Format file changes as:
61
+ FILE: path/to/file.ts
62
+ \`\`\`typescript
63
+ // Full file content
64
+ \`\`\`
65
+
66
+ Be concise but thorough. The user will review and commit your changes.`;
67
+ }
68
+ function createMockDriver() {
69
+ return {
70
+ name: "Mock",
71
+ async process(command) {
72
+ await new Promise((resolve) => setTimeout(resolve, 1000));
73
+ const lowerCommand = command.toLowerCase();
74
+ if (lowerCommand.includes("readme") || lowerCommand.includes("documentation"))
75
+ return `I'll update the README.md file for you.
76
+
77
+ Analyzing the repository structure...
78
+
79
+ FILE: README.md
80
+ \`\`\`markdown
81
+ # Project Name
82
+
83
+ ## Installation
84
+
85
+ \`\`\`bash
86
+ npm install
87
+ # or
88
+ bun install
89
+ \`\`\`
90
+
91
+ ## Usage
92
+
93
+ \`\`\`bash
94
+ npm run start
95
+ \`\`\`
96
+ \`\`\`
97
+
98
+ File modified: README.md
99
+ Lines added: 12`;
100
+ if (lowerCommand.includes("fix") || lowerCommand.includes("bug"))
101
+ return `I'll analyze and fix the issue.
102
+
103
+ Scanning for potential bugs...
104
+
105
+ FILE: src/utils.ts
106
+ \`\`\`typescript
107
+ export function getData(data: { value?: string }) {
108
+ return data?.value ?? 'default';
109
+ }
110
+ \`\`\`
111
+
112
+ Files modified: src/utils.ts
113
+ Lines changed: 4`;
114
+ return `I understand you want to: "${command}"
115
+
116
+ I'll analyze the repository and implement this change.
117
+
118
+ FILE: src/main.ts
119
+ \`\`\`typescript
120
+ // Updated based on your request
121
+ export function main() {
122
+ console.log('Changes applied');
123
+ }
124
+ \`\`\`
125
+
126
+ Files modified: 1`;
127
+ }
128
+ };
129
+ }
130
+ export function getDriver(driverName) {
131
+ const currentState = buddyState.getState();
132
+ switch (driverName) {
133
+ case "claude-cli-local":
134
+ return claudeAgent.createLocal({ cwd: currentState.repo?.path });
135
+ case "claude-cli-ec2":
136
+ return claudeAgent.createEC2({
137
+ cwd: currentState.repo?.path,
138
+ ec2Host: apiKeys.claudeCliHost
139
+ });
140
+ case "claude":
141
+ case "anthropic":
142
+ if (!apiKeys.anthropic)
143
+ throw Error("Anthropic API key not set. Configure your API key in settings.");
144
+ return createAnthropicDriver({ apiKey: apiKeys.anthropic });
145
+ case "openai":
146
+ if (!apiKeys.openai)
147
+ throw Error("OpenAI API key not set. Configure your API key in settings.");
148
+ return createOpenAIDriver({ apiKey: apiKeys.openai });
149
+ case "ollama":
150
+ return createOllamaDriver({
151
+ host: CONFIG.ollamaHost,
152
+ model: CONFIG.ollamaModel
153
+ });
154
+ case "claude-sdk":
155
+ case "claude-agent-sdk":
156
+ return createClaudeAgentSDKDriver({
157
+ cwd: currentState.repo?.path,
158
+ maxTurns: 25,
159
+ permissionMode: "bypassPermissions"
160
+ });
161
+ case "mock":
162
+ return createMockDriver();
163
+ default:
164
+ throw Error(`Unknown driver: ${driverName}. Available: claude-cli-local, claude-cli-ec2, claude, claude-sdk, openai, ollama, mock`);
165
+ }
166
+ }
167
+ export function getAvailableDrivers() {
168
+ return ["claude-cli-local", "claude-cli-ec2", "claude", "claude-sdk", "openai", "ollama", "mock"];
169
+ }
170
+ export async function getRepoContext(repoPath) {
171
+ const { $: _$ } = await import("bun"), files = (await _$`cd ${repoPath} && find . -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -name "*.lock" | head -50`.quiet()).text().trim();
172
+ let readme = "";
173
+ const readmePath = join(repoPath, "README.md");
174
+ if (existsSync(readmePath))
175
+ readme = readFileSync(readmePath, "utf-8").slice(0, 2000);
176
+ let packageJson = "";
177
+ const packagePath = join(repoPath, "package.json");
178
+ if (existsSync(packagePath))
179
+ packageJson = readFileSync(packagePath, "utf-8");
180
+ return `
181
+ Repository Structure:
182
+ ${files}
183
+
184
+ ${readme ? `README.md (excerpt):
185
+ ${readme}
186
+ ` : ""}
187
+ ${packageJson ? `package.json:
188
+ ${packageJson}
189
+ ` : ""}
190
+ `.trim();
191
+ }
192
+ export async function openRepository(input) {
193
+ const { $: _$ } = await import("bun");
194
+ let repoPath, repoName;
195
+ if (input.includes("github.com") || input.startsWith("git@")) {
196
+ repoName = input.split("/").pop()?.replace(".git", "") || "repo";
197
+ repoPath = join(CONFIG.workDir, repoName);
198
+ if (existsSync(repoPath))
199
+ await _$`cd ${repoPath} && git pull --rebase`.quiet();
200
+ else
201
+ await _$`git clone ${input} ${repoPath}`.quiet();
202
+ } else {
203
+ repoPath = input.startsWith("~") ? input.replace("~", homedir()) : input;
204
+ if (!existsSync(repoPath))
205
+ throw Error(`Local path does not exist: ${repoPath}`);
206
+ if (!existsSync(join(repoPath, ".git")))
207
+ throw Error(`Not a git repository: ${repoPath}`);
208
+ repoName = repoPath.split("/").pop() || "repo";
209
+ }
210
+ const branch = (await _$`cd ${repoPath} && git branch --show-current`.quiet()).text().trim(), hasChanges = (await _$`cd ${repoPath} && git status --porcelain`.quiet()).text().trim().length > 0, lastCommit = (await _$`cd ${repoPath} && git log -1 --format="%h %s"`.quiet()).text().trim(), repoState = {
211
+ path: repoPath,
212
+ name: repoName,
213
+ branch,
214
+ hasChanges,
215
+ lastCommit
216
+ };
217
+ buddyState.setRepo(repoState);
218
+ buddyState.clearHistory();
219
+ return repoState;
220
+ }
221
+ export async function applyChanges(aiResponse) {
222
+ const currentState = buddyState.getState();
223
+ if (!currentState.repo)
224
+ throw Error("No repository opened");
225
+ const modifiedFiles = [], filePattern = /FILE:\s*([^\n]+)\n```\w*\n([\s\S]*?)```/g;
226
+ for (const match of aiResponse.matchAll(filePattern)) {
227
+ const filePath = match[1].trim(), content = match[2], fullPath = join(currentState.repo.path, filePath), dir = dirname(fullPath);
228
+ if (!existsSync(dir))
229
+ mkdirSync(dir, { recursive: !0 });
230
+ writeFileSync(fullPath, content);
231
+ modifiedFiles.push(filePath);
232
+ }
233
+ if (modifiedFiles.length > 0 && currentState.repo)
234
+ currentState.repo.hasChanges = !0;
235
+ return modifiedFiles;
236
+ }
237
+ export async function configureGitUser() {
238
+ const currentState = buddyState.getState();
239
+ if (!currentState.repo || !currentState.github)
240
+ return;
241
+ const { $: _$ } = await import("bun"), { name, email } = currentState.github;
242
+ await _$`cd ${currentState.repo.path} && git config user.name ${name}`.quiet();
243
+ await _$`cd ${currentState.repo.path} && git config user.email ${email}`.quiet();
244
+ }
245
+ export async function commitChanges() {
246
+ const currentState = buddyState.getState();
247
+ if (!currentState.repo)
248
+ throw Error("No repository opened");
249
+ const { $: _$ } = await import("bun");
250
+ if (currentState.github)
251
+ await configureGitUser();
252
+ await _$`cd ${currentState.repo.path} && git add -A`.quiet();
253
+ await _$`cd ${currentState.repo.path} && git commit -m ${CONFIG.commitMessage}`.quiet();
254
+ const commitHash = (await _$`cd ${currentState.repo.path} && git rev-parse --short HEAD`.quiet()).text().trim();
255
+ currentState.repo.hasChanges = !1;
256
+ currentState.repo.lastCommit = commitHash;
257
+ return commitHash;
258
+ }
259
+ export async function pushChanges() {
260
+ const currentState = buddyState.getState();
261
+ if (!currentState.repo)
262
+ throw Error("No repository opened");
263
+ const { $: _$ } = await import("bun");
264
+ await _$`cd ${currentState.repo.path} && git push`.quiet();
265
+ }
266
+ export async function processCommand(command, driverName) {
267
+ const currentState = buddyState.getState();
268
+ if (!currentState.repo)
269
+ throw Error("No repository opened");
270
+ const normalizedDriver = driverName || currentState.currentDriver, driver = getDriver(normalizedDriver);
271
+ if (driverName)
272
+ buddyState.setCurrentDriver(driverName);
273
+ const context = await getRepoContext(currentState.repo.path), systemPrompt = buildSystemPrompt(context), response = await driver.process(command, systemPrompt, currentState.conversationHistory);
274
+ buddyState.addToHistory({ role: "user", content: command });
275
+ buddyState.addToHistory({ role: "assistant", content: response });
276
+ return response;
277
+ }
278
+ export async function buddyProcessStreaming(command, driverName, history) {
279
+ const currentState = buddyState.getState();
280
+ if (!currentState.repo)
281
+ throw Error("No repository opened");
282
+ const normalizedDriver = driverName || currentState.currentDriver, streamingDrivers = ["claude-cli-local", "claude-sdk"];
283
+ if (!streamingDrivers.includes(normalizedDriver))
284
+ throw Error(`Streaming only supported for ${streamingDrivers.join(", ")} drivers. Current: ${normalizedDriver}`);
285
+ if (driverName)
286
+ buddyState.setCurrentDriver(driverName);
287
+ let contextualCommand = command;
288
+ if (history && history.length > 0) {
289
+ let conversationContext = `## Previous Conversation
290
+ Here is our conversation so far:
291
+
292
+ `;
293
+ for (const msg of history) {
294
+ const role = msg.role === "user" ? "User" : "Assistant";
295
+ conversationContext += `**${role}:** ${msg.content}
296
+
297
+ `;
298
+ }
299
+ conversationContext += `---
300
+
301
+ ## Current Request
302
+ `;
303
+ contextualCommand = conversationContext + command;
304
+ }
305
+ const result = await claudeAgent.processStreaming(contextualCommand, currentState.repo.path);
306
+ result.fullResponse.then((response) => {
307
+ buddyState.addToHistory({ role: "user", content: command });
308
+ buddyState.addToHistory({ role: "assistant", content: response });
309
+ });
310
+ return result;
311
+ }
312
+ export async function buddyStreamSimple(command, history) {
313
+ if (!apiKeys.anthropic)
314
+ throw Error("Anthropic API key not set. Configure your API key in settings.");
315
+ const messages = [];
316
+ if (history && history.length > 0)
317
+ for (const msg of history)
318
+ messages.push({
319
+ role: msg.role,
320
+ content: msg.content
321
+ });
322
+ messages.push({ role: "user", content: command });
323
+ const systemPrompt = `You are a helpful AI assistant. Answer questions naturally and conversationally.
324
+ You can discuss any topic - technology, science, philosophy, everyday questions, or anything else the user asks about.
325
+ Be concise but thorough. If the user asks about coding or their project specifically, help with that too.`, encoder = new TextEncoder;
326
+ let fullResponse = "", resolveFullResponse;
327
+ const fullResponsePromise = new Promise((resolve) => {
328
+ resolveFullResponse = resolve;
329
+ });
330
+ return {
331
+ stream: new ReadableStream({
332
+ async start(controller) {
333
+ try {
334
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
335
+ method: "POST",
336
+ headers: {
337
+ "Content-Type": "application/json",
338
+ "x-api-key": apiKeys.anthropic,
339
+ "anthropic-version": "2023-06-01"
340
+ },
341
+ body: JSON.stringify({
342
+ model: "claude-sonnet-4-20250514",
343
+ max_tokens: 4096,
344
+ system: systemPrompt,
345
+ stream: !0,
346
+ messages
347
+ })
348
+ });
349
+ if (!response.ok) {
350
+ const error = await response.text();
351
+ throw Error(`Claude API error: ${error}`);
352
+ }
353
+ const reader = response.body?.getReader();
354
+ if (!reader)
355
+ throw Error("No response body");
356
+ const decoder = new TextDecoder;
357
+ let buffer = "";
358
+ while (!0) {
359
+ const { done, value } = await reader.read();
360
+ if (done)
361
+ break;
362
+ buffer += decoder.decode(value, { stream: !0 });
363
+ const lines = buffer.split(`
364
+ `);
365
+ buffer = lines.pop() || "";
366
+ for (const line of lines)
367
+ if (line.startsWith("data: ")) {
368
+ const data = line.slice(6);
369
+ if (data === "[DONE]")
370
+ continue;
371
+ try {
372
+ const event = JSON.parse(data);
373
+ if (event.type === "content_block_delta" && event.delta?.text) {
374
+ const text = event.delta.text;
375
+ fullResponse += text;
376
+ controller.enqueue(encoder.encode(text));
377
+ }
378
+ } catch {}
379
+ }
380
+ }
381
+ buddyState.addToHistory({ role: "user", content: command });
382
+ buddyState.addToHistory({ role: "assistant", content: fullResponse });
383
+ resolveFullResponse(fullResponse);
384
+ controller.close();
385
+ } catch (error) {
386
+ resolveFullResponse(fullResponse);
387
+ controller.error(error);
388
+ }
389
+ }
390
+ }),
391
+ fullResponse: fullResponsePromise
392
+ };
393
+ }