@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.
- package/dist/agents/claude/index.js +197 -0
- package/dist/agents/index.js +1 -0
- package/dist/buddy.js +393 -0
- package/dist/drivers/anthropic/index.js +276 -0
- package/dist/drivers/claude-agent-sdk/index.js +198 -0
- package/dist/drivers/index.js +4 -0
- package/dist/drivers/ollama/index.js +332 -0
- package/dist/drivers/openai/index.js +351 -0
- package/dist/image.js +375 -0
- package/dist/index.js +16 -175
- package/dist/mcp.js +361 -0
- package/dist/personalization.js +244 -0
- package/dist/search.js +316 -0
- package/dist/text.js +51 -0
- package/dist/types.js +0 -0
- package/dist/utils/client-bedrock-runtime.js +17 -0
- package/dist/utils/client-bedrock.js +20 -0
- package/dist/utils/model-access.js +21 -0
- package/dist/utils/retry.js +39 -0
- package/dist/utils/tokens.js +59 -0
- package/dist/utils/usage.js +27 -0
- package/dist/utils/vision.js +54 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|