Skip to content

Commit 0e35cad

Browse files
committed
sdk, in progress client: runNewChat, handle tools
1 parent 1d9a80c commit 0e35cad

File tree

3 files changed

+248
-20
lines changed

3 files changed

+248
-20
lines changed

sdk/src/client.ts

Lines changed: 162 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,203 @@
11
import { execFileSync } from 'child_process'
2+
import os from 'os'
23

34
import { CODEBUFF_BINARY } from './constants'
4-
import { processStream } from './process-stream'
5+
import { changeFile } from './tools/change-file'
6+
import { WebSocketHandler } from './websocket-client'
57
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+
}
632

7-
/** @deprecated Migrate to WebSocketHandler */
833
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)}`
937
public cwd: string
1038

11-
constructor({ cwd }: { cwd: string }) {
39+
constructor({ cwd, onError, overrideTools }: CodebuffClientOptions) {
1240
// TODO: download binary automatically
1341
if (execFileSync('which', [CODEBUFF_BINARY]).toString().trim() === '') {
1442
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.`,
1644
)
1745
}
1846
if (!process.env[API_KEY_ENV_VAR]) {
1947
throw new Error(
2048
`Codebuff API key not found. Please set the ${API_KEY_ENV_VAR} environment variable.`,
2149
)
2250
}
51+
const apiKey = process.env[API_KEY_ENV_VAR]
2352

2453
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+
})
2573
}
2674

2775
public async runNewChat({
2876
agent,
2977
prompt,
3078
params,
3179
handleEvent,
80+
allFiles,
81+
knowledgeFiles,
82+
agentTemplates,
3283
}: {
3384
agent: string
3485
prompt: string
3586
params?: Record<string, any>
3687
handleEvent: (event: any) => void
88+
allFiles?: Record<string, string>
89+
knowledgeFiles?: Record<string, string>
90+
agentTemplates?: Record<string, any>
3791
}): Promise<{
3892
agentId: string
3993
}> {
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,
54107
})
55108

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+
}
56159
return {
57-
agentId: agent,
160+
type: 'tool-call-response',
161+
requestId: action.requestId,
162+
success: true,
163+
result,
58164
}
59165
}
60166
}
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+
}

sdk/src/tools/change-file.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import z from 'zod'
2+
import fs from 'fs'
3+
import path from 'path'
4+
import { applyPatch } from '@codebuff/common/util/patch'
5+
6+
const FileChangeSchema = z.object({
7+
type: z.enum(['patch', 'file']),
8+
path: z.string(),
9+
content: z.string(),
10+
})
11+
12+
export function changeFile(
13+
parameters: unknown,
14+
cwd: string,
15+
): { toolResultMessage: string } {
16+
const fileChange = FileChangeSchema.parse(parameters)
17+
const lines = fileChange.content.split('\n')
18+
19+
const { created, modified, invalid } = applyChanges(cwd, [fileChange])
20+
21+
const results: string[] = []
22+
23+
for (const file of created) {
24+
results.push(
25+
`Created ${file} successfully. Changes made:\n${lines.join('\n')}`,
26+
)
27+
}
28+
29+
for (const file of modified) {
30+
results.push(
31+
`Wrote to ${file} successfully. Changes made:\n${lines.join('\n')}`,
32+
)
33+
}
34+
35+
for (const file of invalid) {
36+
results.push(
37+
`Failed to write to ${file}; file path caused an error or file could not be written`,
38+
)
39+
}
40+
41+
return { toolResultMessage: results.join('\n') }
42+
}
43+
44+
function applyChanges(
45+
projectRoot: string,
46+
changes: {
47+
type: 'patch' | 'file'
48+
path: string
49+
content: string
50+
}[],
51+
) {
52+
const created: string[] = []
53+
const modified: string[] = []
54+
const invalid: string[] = []
55+
56+
for (const change of changes) {
57+
const { path: filePath, content, type } = change
58+
try {
59+
const fullPath = path.join(projectRoot, filePath)
60+
const fileExists = fs.existsSync(fullPath)
61+
if (!fileExists) {
62+
const dirPath = path.dirname(fullPath)
63+
fs.mkdirSync(dirPath, { recursive: true })
64+
}
65+
66+
if (type === 'file') {
67+
fs.writeFileSync(fullPath, content)
68+
} else {
69+
const oldContent = fs.readFileSync(fullPath, 'utf-8')
70+
const newContent = applyPatch(oldContent, content)
71+
fs.writeFileSync(fullPath, newContent)
72+
}
73+
74+
if (fileExists) {
75+
modified.push(filePath)
76+
} else {
77+
created.push(filePath)
78+
}
79+
} catch (error) {
80+
console.error(`Failed to apply patch to ${filePath}:`, error, content)
81+
invalid.push(filePath)
82+
}
83+
}
84+
85+
return { created, modified, invalid }
86+
}

sdk/src/websocket-client.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ export class WebSocketHandler {
157157
return {
158158
...({
159159
type: 'prompt',
160-
fingerprintId: 'codebuff-sdk',
161160
} as const),
162161
authToken: this.apiKey,
163162
}

0 commit comments

Comments
 (0)