|
1 | 1 | import { execFileSync } from 'child_process' |
| 2 | +import os from 'os' |
2 | 3 |
|
3 | 4 | import { CODEBUFF_BINARY } from './constants' |
4 | | -import { processStream } from './process-stream' |
| 5 | +import { changeFile } from './tools/change-file' |
| 6 | +import { WebSocketHandler } from './websocket-client' |
5 | 7 | import { API_KEY_ENV_VAR } from '../../common/src/constants' |
| 8 | +import type { ServerAction } from '../../common/src/actions' |
| 9 | +import { getInitialSessionState } from '../../common/src/types/session-state' |
| 10 | +import { getFiles } from '../../npm-app/src/project-files' |
| 11 | + |
| 12 | +export type ClientToolName = |
| 13 | + | 'read_files' |
| 14 | + | 'write_file' |
| 15 | + | 'str_replace' |
| 16 | + | 'run_terminal_command' |
| 17 | + |
| 18 | +export type CodebuffClientOptions = { |
| 19 | + cwd: string |
| 20 | + onError: (error: { message: string }) => void |
| 21 | + overrideTools: Record< |
| 22 | + ClientToolName, |
| 23 | + ( |
| 24 | + args: Extract<ServerAction, { type: 'tool-call-request' }>['args'], |
| 25 | + ) => Promise<{ toolResultMessage: string }> |
| 26 | + > & { |
| 27 | + readFiles: ( |
| 28 | + filePath: string[], |
| 29 | + ) => Promise<{ files: Record<string, string | null> }> |
| 30 | + } |
| 31 | +} |
6 | 32 |
|
7 | | -/** @deprecated Migrate to WebSocketHandler */ |
8 | 33 | export class CodebuffClient { |
| 34 | + private readonly websocketHandler: WebSocketHandler |
| 35 | + private readonly overrideTools: CodebuffClientOptions['overrideTools'] |
| 36 | + private readonly fingerprintId = `codebuff-sdk-${Math.random().toString(36).substring(2, 15)}` |
9 | 37 | public cwd: string |
10 | 38 |
|
11 | | - constructor({ cwd }: { cwd: string }) { |
| 39 | + constructor({ cwd, onError, overrideTools }: CodebuffClientOptions) { |
12 | 40 | // TODO: download binary automatically |
13 | 41 | if (execFileSync('which', [CODEBUFF_BINARY]).toString().trim() === '') { |
14 | 42 | throw new Error( |
15 | | - 'Codebuff binary not found. Please run "npm i -g codebuff"', |
| 43 | + `Could not find ${CODEBUFF_BINARY} in PATH. Please run "npm i -g codebuff" to install the codebuff.`, |
16 | 44 | ) |
17 | 45 | } |
18 | 46 | if (!process.env[API_KEY_ENV_VAR]) { |
19 | 47 | throw new Error( |
20 | 48 | `Codebuff API key not found. Please set the ${API_KEY_ENV_VAR} environment variable.`, |
21 | 49 | ) |
22 | 50 | } |
| 51 | + const apiKey = process.env[API_KEY_ENV_VAR] |
23 | 52 |
|
24 | 53 | this.cwd = cwd |
| 54 | + this.overrideTools = overrideTools |
| 55 | + this.websocketHandler = new WebSocketHandler({ |
| 56 | + apiKey, |
| 57 | + onWebsocketError: () => {}, |
| 58 | + onWebsocketReconnect: () => {}, |
| 59 | + onRequestReconnect: async () => {}, |
| 60 | + onResponseError: async (error) => { |
| 61 | + onError({ message: error.message }) |
| 62 | + }, |
| 63 | + readFiles: this.readFiles.bind(this), |
| 64 | + handleToolCall: this.handleToolCall.bind(this), |
| 65 | + onCostResponse: async () => {}, |
| 66 | + onUsageResponse: async () => {}, |
| 67 | + |
| 68 | + onResponseChunk: async () => {}, |
| 69 | + onSubagentResponseChunk: async () => {}, |
| 70 | + |
| 71 | + onPromptResponse: async () => {}, |
| 72 | + }) |
25 | 73 | } |
26 | 74 |
|
27 | 75 | public async runNewChat({ |
28 | 76 | agent, |
29 | 77 | prompt, |
30 | 78 | params, |
31 | 79 | handleEvent, |
| 80 | + allFiles, |
| 81 | + knowledgeFiles, |
| 82 | + agentTemplates, |
32 | 83 | }: { |
33 | 84 | agent: string |
34 | 85 | prompt: string |
35 | 86 | params?: Record<string, any> |
36 | 87 | handleEvent: (event: any) => void |
| 88 | + allFiles?: Record<string, string> |
| 89 | + knowledgeFiles?: Record<string, string> |
| 90 | + agentTemplates?: Record<string, any> |
37 | 91 | }): Promise<{ |
38 | 92 | agentId: string |
39 | 93 | }> { |
40 | | - const args = [prompt, '-p', '--agent', agent] |
41 | | - if (prompt) { |
42 | | - args.push(prompt) |
43 | | - } |
44 | | - if (params) { |
45 | | - args.push('--params', JSON.stringify(params)) |
46 | | - } |
47 | | - if (this.cwd) { |
48 | | - args.push('--cwd', this.cwd) |
49 | | - } |
50 | | - |
51 | | - await processStream({ |
52 | | - codebuffArgs: args, |
53 | | - handleEvent, |
| 94 | + this.websocketHandler.sendInput({ |
| 95 | + promptId: Math.random().toString(36).substring(2, 15), |
| 96 | + prompt, |
| 97 | + promptParams: params, |
| 98 | + fingerprintId: this.fingerprintId, |
| 99 | + costMode: 'normal', |
| 100 | + sessionState: initialSessionState(this.cwd, { |
| 101 | + knowledgeFiles, |
| 102 | + agentTemplates, |
| 103 | + allFiles, |
| 104 | + }), |
| 105 | + toolResults: [], |
| 106 | + agentId: agent, |
54 | 107 | }) |
55 | 108 |
|
| 109 | + return new Promise((resolve) => {}) |
| 110 | + } |
| 111 | + |
| 112 | + private async readFiles(filePath: string[]) { |
| 113 | + const override = this.overrideTools.readFiles |
| 114 | + if (override) { |
| 115 | + const overrideResult = await override(filePath) |
| 116 | + return overrideResult.files |
| 117 | + } |
| 118 | + return getFiles(filePath) |
| 119 | + } |
| 120 | + |
| 121 | + private async handleToolCall( |
| 122 | + action: Extract<ServerAction, { type: 'tool-call-request' }>, |
| 123 | + ) { |
| 124 | + const toolName = action.toolName |
| 125 | + const args = action.args |
| 126 | + let result: string |
| 127 | + try { |
| 128 | + const override = this.overrideTools[toolName as ClientToolName] |
| 129 | + if (override) { |
| 130 | + const overrideResult = await override(args) |
| 131 | + result = overrideResult.toolResultMessage |
| 132 | + } else if (toolName === 'end_turn') { |
| 133 | + result = '' |
| 134 | + } else if (toolName === 'write_file' || toolName === 'str_replace') { |
| 135 | + const r = changeFile(args, this.cwd) |
| 136 | + result = r.toolResultMessage |
| 137 | + } else if (toolName === 'run_terminal_command') { |
| 138 | + throw new Error( |
| 139 | + 'run_terminal_command not implemented in SDK yet; please provide an override.', |
| 140 | + ) |
| 141 | + } else { |
| 142 | + throw new Error( |
| 143 | + `Tool not implemented in sdk. Please provide an override or modify your agent to not use this tool: ${toolName}`, |
| 144 | + ) |
| 145 | + } |
| 146 | + } catch (error) { |
| 147 | + return { |
| 148 | + type: 'tool-call-response', |
| 149 | + requestId: action.requestId, |
| 150 | + success: false, |
| 151 | + result: |
| 152 | + error && typeof error === 'object' && 'message' in error |
| 153 | + ? error.message |
| 154 | + : typeof error === 'string' |
| 155 | + ? error |
| 156 | + : 'Unknown error', |
| 157 | + } |
| 158 | + } |
56 | 159 | return { |
57 | | - agentId: agent, |
| 160 | + type: 'tool-call-response', |
| 161 | + requestId: action.requestId, |
| 162 | + success: true, |
| 163 | + result, |
58 | 164 | } |
59 | 165 | } |
60 | 166 | } |
| 167 | +function initialSessionState( |
| 168 | + cwd: string, |
| 169 | + options: { |
| 170 | + allFiles?: Record<string, string> |
| 171 | + knowledgeFiles?: Record<string, string> |
| 172 | + agentTemplates?: Record<string, any> |
| 173 | + }, |
| 174 | +) { |
| 175 | + const { knowledgeFiles = {}, agentTemplates = {} } = options |
| 176 | + |
| 177 | + return getInitialSessionState({ |
| 178 | + projectRoot: cwd, |
| 179 | + cwd, |
| 180 | + fileTree: [], |
| 181 | + fileTokenScores: {}, |
| 182 | + tokenCallers: {}, |
| 183 | + knowledgeFiles, |
| 184 | + userKnowledgeFiles: {}, |
| 185 | + agentTemplates, |
| 186 | + gitChanges: { |
| 187 | + status: '', |
| 188 | + diff: '', |
| 189 | + diffCached: '', |
| 190 | + lastCommitMessages: '', |
| 191 | + }, |
| 192 | + changesSinceLastChat: {}, |
| 193 | + shellConfigFiles: {}, |
| 194 | + systemInfo: { |
| 195 | + platform: process.platform, |
| 196 | + shell: 'bash', |
| 197 | + nodeVersion: process.version, |
| 198 | + arch: process.arch, |
| 199 | + homedir: os.homedir(), |
| 200 | + cpus: 16, |
| 201 | + }, |
| 202 | + }) |
| 203 | +} |
0 commit comments