codebakers 1.0.45
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/.vscodeignore +18 -0
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/codebakers-1.0.0.vsix +0 -0
- package/codebakers-1.0.10.vsix +0 -0
- package/codebakers-1.0.11.vsix +0 -0
- package/codebakers-1.0.12.vsix +0 -0
- package/codebakers-1.0.13.vsix +0 -0
- package/codebakers-1.0.14.vsix +0 -0
- package/codebakers-1.0.15.vsix +0 -0
- package/codebakers-1.0.16.vsix +0 -0
- package/codebakers-1.0.17.vsix +0 -0
- package/codebakers-1.0.18.vsix +0 -0
- package/codebakers-1.0.19.vsix +0 -0
- package/codebakers-1.0.20.vsix +0 -0
- package/codebakers-1.0.21.vsix +0 -0
- package/codebakers-1.0.22.vsix +0 -0
- package/codebakers-1.0.23.vsix +0 -0
- package/codebakers-1.0.24.vsix +0 -0
- package/codebakers-1.0.25.vsix +0 -0
- package/codebakers-1.0.26.vsix +0 -0
- package/codebakers-1.0.27.vsix +0 -0
- package/codebakers-1.0.28.vsix +0 -0
- package/codebakers-1.0.29.vsix +0 -0
- package/codebakers-1.0.30.vsix +0 -0
- package/codebakers-1.0.31.vsix +0 -0
- package/codebakers-1.0.32.vsix +0 -0
- package/codebakers-1.0.35.vsix +0 -0
- package/codebakers-1.0.36.vsix +0 -0
- package/codebakers-1.0.37.vsix +0 -0
- package/codebakers-1.0.38.vsix +0 -0
- package/codebakers-1.0.39.vsix +0 -0
- package/codebakers-1.0.40.vsix +0 -0
- package/codebakers-1.0.41.vsix +0 -0
- package/codebakers-1.0.42.vsix +0 -0
- package/codebakers-1.0.43.vsix +0 -0
- package/codebakers-1.0.44.vsix +0 -0
- package/codebakers-1.0.45.vsix +0 -0
- package/dist/extension.js +1394 -0
- package/esbuild.js +63 -0
- package/media/icon.png +0 -0
- package/media/icon.svg +7 -0
- package/nul +1 -0
- package/package.json +127 -0
- package/preview.html +547 -0
- package/src/ChatPanelProvider.ts +1815 -0
- package/src/ChatViewProvider.ts +749 -0
- package/src/CodeBakersClient.ts +1146 -0
- package/src/CodeValidator.ts +645 -0
- package/src/FileOperations.ts +410 -0
- package/src/ProjectContext.ts +526 -0
- package/src/extension.ts +332 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,1146 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
3
|
+
import { CodeValidator, ValidationResult, DependencyCheck, TypeScriptCheckResult } from './CodeValidator';
|
|
4
|
+
|
|
5
|
+
export interface FileOperation {
|
|
6
|
+
action: 'create' | 'edit' | 'delete';
|
|
7
|
+
path: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
content?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CommandToRun {
|
|
13
|
+
command: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ChatResponse {
|
|
18
|
+
content: string;
|
|
19
|
+
thinking?: string;
|
|
20
|
+
fileOperations?: FileOperation[];
|
|
21
|
+
commands?: CommandToRun[];
|
|
22
|
+
projectUpdates?: {
|
|
23
|
+
patterns?: string[];
|
|
24
|
+
tasks?: string[];
|
|
25
|
+
decisions?: Record<string, string>;
|
|
26
|
+
};
|
|
27
|
+
validation?: ValidationResult;
|
|
28
|
+
dependencyCheck?: DependencyCheck;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface StreamCallbacks {
|
|
32
|
+
onThinking?: (text: string) => void;
|
|
33
|
+
onContent?: (text: string) => void;
|
|
34
|
+
onDone?: () => void;
|
|
35
|
+
onError?: (error: Error) => void;
|
|
36
|
+
abortSignal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ChatOptions {
|
|
40
|
+
maxRetries?: number;
|
|
41
|
+
runTypeScriptCheck?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Pattern {
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
content: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class CodeBakersClient {
|
|
51
|
+
private anthropic: Anthropic | null = null;
|
|
52
|
+
private sessionToken: string | null = null;
|
|
53
|
+
private patterns: Map<string, Pattern> = new Map();
|
|
54
|
+
private readonly DEFAULT_TIMEOUT = 10000; // 10 seconds
|
|
55
|
+
private validator: CodeValidator;
|
|
56
|
+
private validatorInitialized: boolean = false;
|
|
57
|
+
|
|
58
|
+
constructor(private readonly context: vscode.ExtensionContext) {
|
|
59
|
+
// Initialize code validator
|
|
60
|
+
this.validator = new CodeValidator();
|
|
61
|
+
// Load cached session token
|
|
62
|
+
this.sessionToken = context.globalState.get('codebakers.sessionToken') || null;
|
|
63
|
+
|
|
64
|
+
// Clean up corrupted tokens (URL-encoded or invalid)
|
|
65
|
+
if (this.sessionToken && this.sessionToken.includes('%')) {
|
|
66
|
+
console.log('CodeBakers: Clearing corrupted URL-encoded token');
|
|
67
|
+
this.sessionToken = null;
|
|
68
|
+
context.globalState.update('codebakers.sessionToken', undefined);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Logout and clear session
|
|
74
|
+
*/
|
|
75
|
+
async logout(): Promise<void> {
|
|
76
|
+
this.sessionToken = null;
|
|
77
|
+
this.anthropic = null;
|
|
78
|
+
await this.context.globalState.update('codebakers.sessionToken', undefined);
|
|
79
|
+
await this.context.globalState.update('codebakers.user', undefined);
|
|
80
|
+
vscode.window.showInformationMessage('Logged out of CodeBakers');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fetch with timeout to prevent hanging
|
|
85
|
+
*/
|
|
86
|
+
private async _fetchWithTimeout(url: string, options: RequestInit = {}, timeout = this.DEFAULT_TIMEOUT): Promise<Response> {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(url, {
|
|
92
|
+
...options,
|
|
93
|
+
signal: controller.signal,
|
|
94
|
+
});
|
|
95
|
+
return response;
|
|
96
|
+
} finally {
|
|
97
|
+
clearTimeout(timeoutId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if user has a valid session token (doesn't validate with server)
|
|
103
|
+
*/
|
|
104
|
+
hasSessionToken(): boolean {
|
|
105
|
+
return !!this.sessionToken;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle OAuth callback from VS Code URI handler
|
|
110
|
+
* Called when vscode://codebakers.codebakers/callback?token=xxx is received
|
|
111
|
+
*/
|
|
112
|
+
async handleOAuthCallback(encodedToken: string): Promise<boolean> {
|
|
113
|
+
try {
|
|
114
|
+
console.log('handleOAuthCallback: token length:', encodedToken?.length);
|
|
115
|
+
console.log('handleOAuthCallback: token preview:', encodedToken?.substring(0, 50));
|
|
116
|
+
|
|
117
|
+
if (!encodedToken) {
|
|
118
|
+
vscode.window.showErrorMessage('Login failed: No token received');
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Try to decode the base64url token payload
|
|
123
|
+
// The token might be URL-encoded, so try decoding that first
|
|
124
|
+
let tokenToDecode = encodedToken;
|
|
125
|
+
if (encodedToken.includes('%')) {
|
|
126
|
+
tokenToDecode = decodeURIComponent(encodedToken);
|
|
127
|
+
console.log('handleOAuthCallback: URL-decoded token');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Decode base64url (supports both with and without padding)
|
|
131
|
+
let decoded: string;
|
|
132
|
+
try {
|
|
133
|
+
decoded = Buffer.from(tokenToDecode, 'base64url').toString('utf-8');
|
|
134
|
+
} catch {
|
|
135
|
+
// Try standard base64 as fallback
|
|
136
|
+
const base64 = tokenToDecode.replace(/-/g, '+').replace(/_/g, '/');
|
|
137
|
+
decoded = Buffer.from(base64, 'base64').toString('utf-8');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log('handleOAuthCallback: decoded preview:', decoded?.substring(0, 100));
|
|
141
|
+
|
|
142
|
+
const tokenPayload = JSON.parse(decoded) as {
|
|
143
|
+
token: string;
|
|
144
|
+
teamId: string;
|
|
145
|
+
profileId: string;
|
|
146
|
+
githubId: string;
|
|
147
|
+
githubUsername: string;
|
|
148
|
+
email: string;
|
|
149
|
+
plan: string;
|
|
150
|
+
trial: { endsAt: string; daysRemaining: number } | null;
|
|
151
|
+
createdAt: string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Store session token (the decoded base64url payload, not URL-encoded)
|
|
155
|
+
// If the token was URL-encoded, we need to store the decoded version
|
|
156
|
+
const cleanToken = encodedToken.includes('%') ? decodeURIComponent(encodedToken) : encodedToken;
|
|
157
|
+
this.sessionToken = cleanToken;
|
|
158
|
+
await this.context.globalState.update('codebakers.sessionToken', cleanToken);
|
|
159
|
+
|
|
160
|
+
// Store auth info for display
|
|
161
|
+
this.currentPlan = tokenPayload.plan;
|
|
162
|
+
this.trialInfo = tokenPayload.trial;
|
|
163
|
+
this.isUnlimited = tokenPayload.plan === 'pro';
|
|
164
|
+
|
|
165
|
+
// Store additional user info
|
|
166
|
+
await this.context.globalState.update('codebakers.user', {
|
|
167
|
+
teamId: tokenPayload.teamId,
|
|
168
|
+
profileId: tokenPayload.profileId,
|
|
169
|
+
githubUsername: tokenPayload.githubUsername,
|
|
170
|
+
email: tokenPayload.email,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Initialize Anthropic client with our API key
|
|
174
|
+
await this._initializeAnthropic();
|
|
175
|
+
|
|
176
|
+
vscode.window.showInformationMessage(
|
|
177
|
+
`Welcome to CodeBakers, @${tokenPayload.githubUsername}! ${tokenPayload.trial ? `Trial: ${tokenPayload.trial.daysRemaining} days remaining` : ''}`
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return true;
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error('Failed to handle OAuth callback:', e);
|
|
183
|
+
console.error('Token was:', encodedToken?.substring(0, 100));
|
|
184
|
+
vscode.window.showErrorMessage(`Login failed: ${e instanceof Error ? e.message : 'Invalid token'}`);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async checkAuth(): Promise<boolean> {
|
|
190
|
+
if (!this.sessionToken) return false;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Include token in query param as fallback (VS Code may strip headers)
|
|
194
|
+
const url = `${this._getApiEndpoint()}/api/auth/check?token=${encodeURIComponent(this.sessionToken)}`;
|
|
195
|
+
const response = await this._fetchWithTimeout(url, {
|
|
196
|
+
headers: {
|
|
197
|
+
'Authorization': `Bearer ${this.sessionToken}`
|
|
198
|
+
}
|
|
199
|
+
}, 5000); // 5 second timeout for auth check
|
|
200
|
+
return response.ok;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async login(): Promise<boolean> {
|
|
207
|
+
try {
|
|
208
|
+
console.log('CodeBakers: login() called');
|
|
209
|
+
|
|
210
|
+
// Open browser to CodeBakers login
|
|
211
|
+
// The callback will be handled by the global URI handler in extension.ts
|
|
212
|
+
const uriScheme = vscode.env.uriScheme;
|
|
213
|
+
console.log('CodeBakers: uriScheme =', uriScheme);
|
|
214
|
+
|
|
215
|
+
const rawCallbackUri = vscode.Uri.parse(`${uriScheme}://codebakers.codebakers/callback`);
|
|
216
|
+
console.log('CodeBakers: rawCallbackUri =', rawCallbackUri.toString());
|
|
217
|
+
|
|
218
|
+
const callbackUri = await vscode.env.asExternalUri(rawCallbackUri);
|
|
219
|
+
console.log('CodeBakers: callbackUri =', callbackUri.toString());
|
|
220
|
+
|
|
221
|
+
const apiEndpoint = this._getApiEndpoint();
|
|
222
|
+
console.log('CodeBakers: apiEndpoint =', apiEndpoint);
|
|
223
|
+
|
|
224
|
+
const loginUrl = `${apiEndpoint}/vscode-login?callback=${encodeURIComponent(callbackUri.toString())}`;
|
|
225
|
+
console.log('CodeBakers: loginUrl =', loginUrl);
|
|
226
|
+
|
|
227
|
+
await vscode.env.openExternal(vscode.Uri.parse(loginUrl));
|
|
228
|
+
console.log('CodeBakers: openExternal called successfully');
|
|
229
|
+
|
|
230
|
+
// Return true to indicate login was initiated
|
|
231
|
+
// The actual login completion is handled by handleOAuthCallback
|
|
232
|
+
return true;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('CodeBakers: login() error:', error);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private currentPlan: string = 'trial';
|
|
240
|
+
private isUnlimited: boolean = false;
|
|
241
|
+
private trialInfo: { endsAt: string; daysRemaining: number } | null = null;
|
|
242
|
+
|
|
243
|
+
private async _initializeAnthropic(): Promise<void> {
|
|
244
|
+
// Get API key from our server (user's CodeBakers subscription includes Claude access)
|
|
245
|
+
try {
|
|
246
|
+
console.log('_initializeAnthropic: sessionToken exists:', !!this.sessionToken);
|
|
247
|
+
console.log('_initializeAnthropic: sessionToken length:', this.sessionToken?.length);
|
|
248
|
+
console.log('_initializeAnthropic: sessionToken preview:', this.sessionToken?.substring(0, 50));
|
|
249
|
+
console.log('_initializeAnthropic: contains %:', this.sessionToken?.includes('%'));
|
|
250
|
+
|
|
251
|
+
// Verify token can be decoded
|
|
252
|
+
if (this.sessionToken) {
|
|
253
|
+
try {
|
|
254
|
+
const decoded = Buffer.from(this.sessionToken, 'base64url').toString('utf-8');
|
|
255
|
+
const parsed = JSON.parse(decoded);
|
|
256
|
+
console.log('_initializeAnthropic: token decoded successfully, teamId:', parsed.teamId);
|
|
257
|
+
} catch (e) {
|
|
258
|
+
console.error('_initializeAnthropic: FAILED to decode token locally:', e);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Use plain object for headers (more compatible)
|
|
263
|
+
const authHeader = `Bearer ${this.sessionToken}`;
|
|
264
|
+
console.log('_initializeAnthropic: authHeader length:', authHeader.length);
|
|
265
|
+
console.log('_initializeAnthropic: authHeader preview:', authHeader.substring(0, 60));
|
|
266
|
+
|
|
267
|
+
const fetchOptions: RequestInit = {
|
|
268
|
+
method: 'GET',
|
|
269
|
+
headers: {
|
|
270
|
+
'Authorization': authHeader,
|
|
271
|
+
'Content-Type': 'application/json',
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
console.log('_initializeAnthropic: fetchOptions.headers:', JSON.stringify(fetchOptions.headers));
|
|
276
|
+
|
|
277
|
+
// Include token in query param as fallback (VS Code may strip headers)
|
|
278
|
+
const url = `${this._getApiEndpoint()}/api/claude/key?token=${encodeURIComponent(this.sessionToken || '')}`;
|
|
279
|
+
console.log('_initializeAnthropic: fetching:', url.substring(0, 80) + '...');
|
|
280
|
+
|
|
281
|
+
const response = await this._fetchWithTimeout(url, fetchOptions);
|
|
282
|
+
|
|
283
|
+
interface ClaudeKeyData {
|
|
284
|
+
apiKey?: string;
|
|
285
|
+
plan?: string;
|
|
286
|
+
unlimited?: boolean;
|
|
287
|
+
trial?: { endsAt: string; daysRemaining: number };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface ClaudeKeyResponse {
|
|
291
|
+
data?: ClaudeKeyData;
|
|
292
|
+
error?: string;
|
|
293
|
+
message?: string;
|
|
294
|
+
upgradeUrl?: string;
|
|
295
|
+
trialUrl?: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const rawResponse = await response.json() as ClaudeKeyResponse;
|
|
299
|
+
// Server wraps successful responses in {data: ...}
|
|
300
|
+
const data = rawResponse.data || rawResponse as unknown as ClaudeKeyData;
|
|
301
|
+
|
|
302
|
+
console.log('Claude key response:', response.status, JSON.stringify(data));
|
|
303
|
+
|
|
304
|
+
// Handle subscription required error
|
|
305
|
+
if (response.status === 402) {
|
|
306
|
+
const selection = await vscode.window.showWarningMessage(
|
|
307
|
+
rawResponse.message || 'Subscribe to CodeBakers Pro ($99/month) for unlimited access',
|
|
308
|
+
'Subscribe Now',
|
|
309
|
+
'Start Free Trial'
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
if (selection === 'Subscribe Now') {
|
|
313
|
+
vscode.env.openExternal(vscode.Uri.parse(rawResponse.upgradeUrl || 'https://www.codebakers.ai/dashboard/billing'));
|
|
314
|
+
} else if (selection === 'Start Free Trial') {
|
|
315
|
+
vscode.env.openExternal(vscode.Uri.parse(rawResponse.trialUrl || 'https://www.codebakers.ai/dashboard/billing'));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
throw new Error('SUBSCRIPTION_REQUIRED');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
console.error('API error response:', JSON.stringify(rawResponse));
|
|
323
|
+
// Include token info in error for debugging
|
|
324
|
+
const tokenInfo = this.sessionToken
|
|
325
|
+
? `token len=${this.sessionToken.length}, starts=${this.sessionToken.substring(0,10)}`
|
|
326
|
+
: 'NO TOKEN';
|
|
327
|
+
throw new Error(`API ${response.status}: ${rawResponse.error || rawResponse.message || 'Unknown'} [${tokenInfo}]`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!data.apiKey) {
|
|
331
|
+
throw new Error(`No API key in response: ${JSON.stringify(data)}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const { apiKey, plan, unlimited, trial } = data;
|
|
335
|
+
|
|
336
|
+
// Store plan info
|
|
337
|
+
this.currentPlan = plan || 'trial';
|
|
338
|
+
this.isUnlimited = unlimited || false;
|
|
339
|
+
this.trialInfo = trial || null;
|
|
340
|
+
|
|
341
|
+
// Show trial warning if applicable
|
|
342
|
+
if (trial && trial.daysRemaining <= 3) {
|
|
343
|
+
vscode.window.showWarningMessage(
|
|
344
|
+
`Your CodeBakers trial expires in ${trial.daysRemaining} day${trial.daysRemaining === 1 ? '' : 's'}. Subscribe to keep using the extension.`,
|
|
345
|
+
'Subscribe Now'
|
|
346
|
+
).then(selection => {
|
|
347
|
+
if (selection === 'Subscribe Now') {
|
|
348
|
+
vscode.env.openExternal(vscode.Uri.parse('https://www.codebakers.ai/dashboard/billing'));
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.anthropic = new Anthropic({ apiKey });
|
|
354
|
+
|
|
355
|
+
// Also fetch patterns
|
|
356
|
+
await this._loadPatterns();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('Failed to initialize Anthropic client:', error);
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get current plan info for display
|
|
365
|
+
*/
|
|
366
|
+
getPlanInfo(): { plan: string; unlimited: boolean; trial: { endsAt: string; daysRemaining: number } | null } {
|
|
367
|
+
return {
|
|
368
|
+
plan: this.currentPlan,
|
|
369
|
+
unlimited: this.isUnlimited,
|
|
370
|
+
trial: this.trialInfo,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private async _loadPatterns(): Promise<void> {
|
|
375
|
+
try {
|
|
376
|
+
// Include token in query param as fallback (VS Code may strip headers)
|
|
377
|
+
const url = `${this._getApiEndpoint()}/api/patterns/list?token=${encodeURIComponent(this.sessionToken || '')}`;
|
|
378
|
+
const response = await this._fetchWithTimeout(url, {
|
|
379
|
+
headers: {
|
|
380
|
+
'Authorization': `Bearer ${this.sessionToken}`
|
|
381
|
+
}
|
|
382
|
+
}, 15000); // 15 seconds for pattern loading
|
|
383
|
+
|
|
384
|
+
if (response.ok) {
|
|
385
|
+
const data = await response.json() as { patterns?: Pattern[] };
|
|
386
|
+
if (data.patterns) {
|
|
387
|
+
data.patterns.forEach((p: Pattern) => this.patterns.set(p.name, p));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error('Failed to load patterns:', error);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async chat(
|
|
396
|
+
messages: any[],
|
|
397
|
+
projectState: any,
|
|
398
|
+
callbacks?: StreamCallbacks,
|
|
399
|
+
options?: ChatOptions
|
|
400
|
+
): Promise<ChatResponse> {
|
|
401
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
402
|
+
const runTscCheck = options?.runTypeScriptCheck ?? true;
|
|
403
|
+
|
|
404
|
+
if (!this.anthropic) {
|
|
405
|
+
await this._initializeAnthropic();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (!this.anthropic) {
|
|
409
|
+
throw new Error('Not authenticated. Please login first.');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Build the system prompt with CodeBakers enforcement
|
|
413
|
+
const systemPrompt = this._buildSystemPrompt(projectState);
|
|
414
|
+
|
|
415
|
+
// Detect which patterns might be relevant based on the conversation
|
|
416
|
+
const relevantPatterns = await this._detectRelevantPatterns(messages);
|
|
417
|
+
|
|
418
|
+
// Include pattern content in system prompt
|
|
419
|
+
const patternsContent = relevantPatterns
|
|
420
|
+
.map(p => `## Pattern: ${p.name}\n${p.content}`)
|
|
421
|
+
.join('\n\n');
|
|
422
|
+
|
|
423
|
+
// Extract system messages and add to system prompt (Claude API doesn't accept role: "system" in messages)
|
|
424
|
+
const systemMessages = messages.filter(m => m.role === 'system');
|
|
425
|
+
const chatMessages = messages.filter(m => m.role !== 'system');
|
|
426
|
+
|
|
427
|
+
// Build full system prompt including any context from "system" role messages
|
|
428
|
+
const contextFromMessages = systemMessages.length > 0
|
|
429
|
+
? '\n\n# CONTEXT\n' + systemMessages.map(m => m.content).join('\n\n')
|
|
430
|
+
: '';
|
|
431
|
+
|
|
432
|
+
const fullSystemPrompt = `${systemPrompt}\n\n# LOADED PATTERNS\n${patternsContent}${contextFromMessages}`;
|
|
433
|
+
|
|
434
|
+
// Retry logic with exponential backoff
|
|
435
|
+
let lastError: Error | null = null;
|
|
436
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
437
|
+
try {
|
|
438
|
+
// Check if aborted before starting
|
|
439
|
+
if (callbacks?.abortSignal?.aborted) {
|
|
440
|
+
throw new Error('Request was cancelled');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Use streaming to show response in real-time
|
|
444
|
+
let fullText = '';
|
|
445
|
+
|
|
446
|
+
const stream = this.anthropic.messages.stream({
|
|
447
|
+
model: 'claude-sonnet-4-20250514',
|
|
448
|
+
max_tokens: 16000,
|
|
449
|
+
system: fullSystemPrompt,
|
|
450
|
+
messages: chatMessages.map(m => ({
|
|
451
|
+
role: m.role as 'user' | 'assistant',
|
|
452
|
+
content: m.content
|
|
453
|
+
}))
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Set up abort handling
|
|
457
|
+
if (callbacks?.abortSignal) {
|
|
458
|
+
callbacks.abortSignal.addEventListener('abort', () => {
|
|
459
|
+
stream.abort();
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Handle streaming text events
|
|
464
|
+
stream.on('text', (text) => {
|
|
465
|
+
// Check if aborted during streaming
|
|
466
|
+
if (callbacks?.abortSignal?.aborted) {
|
|
467
|
+
stream.abort();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
fullText += text;
|
|
472
|
+
|
|
473
|
+
// Parse thinking and content in real-time
|
|
474
|
+
const { thinking, content } = this._parseThinkingAndContent(fullText);
|
|
475
|
+
|
|
476
|
+
if (thinking) {
|
|
477
|
+
callbacks?.onThinking?.(thinking);
|
|
478
|
+
}
|
|
479
|
+
callbacks?.onContent?.(content);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Wait for completion
|
|
483
|
+
await stream.finalMessage();
|
|
484
|
+
|
|
485
|
+
// Check if aborted
|
|
486
|
+
if (callbacks?.abortSignal?.aborted) {
|
|
487
|
+
throw new Error('Request was cancelled');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
callbacks?.onDone?.();
|
|
491
|
+
|
|
492
|
+
// Parse final thinking and content
|
|
493
|
+
const { thinking, content } = this._parseThinkingAndContent(fullText);
|
|
494
|
+
|
|
495
|
+
// Parse file operations and commands from response
|
|
496
|
+
const fileOperations = this.parseFileOperations(content);
|
|
497
|
+
const commands = this.parseCommands(content);
|
|
498
|
+
|
|
499
|
+
// Clean content for display (remove XML tags)
|
|
500
|
+
const cleanContent = this.cleanContentForDisplay(content);
|
|
501
|
+
|
|
502
|
+
// COMPLIANCE CHECK - Verify AI followed rules
|
|
503
|
+
const compliance = this._checkCompliance(fullText, thinking, fileOperations, cleanContent);
|
|
504
|
+
|
|
505
|
+
// VALIDATION - Deep code quality checks
|
|
506
|
+
let validation: ValidationResult | undefined;
|
|
507
|
+
let dependencyCheck: DependencyCheck | undefined;
|
|
508
|
+
let tscResult: TypeScriptCheckResult | undefined;
|
|
509
|
+
|
|
510
|
+
if (fileOperations.length > 0) {
|
|
511
|
+
// Initialize validator if needed
|
|
512
|
+
if (!this.validatorInitialized) {
|
|
513
|
+
await this.validator.initialize();
|
|
514
|
+
this.validatorInitialized = true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Check dependencies before allowing apply
|
|
518
|
+
dependencyCheck = this.validator.checkDependencies(fileOperations);
|
|
519
|
+
|
|
520
|
+
// Validate generated code
|
|
521
|
+
validation = await this.validator.validateFileOperations(fileOperations);
|
|
522
|
+
|
|
523
|
+
// Run TypeScript check if enabled
|
|
524
|
+
if (runTscCheck) {
|
|
525
|
+
tscResult = await this.validator.runTypeScriptCheck();
|
|
526
|
+
if (validation) {
|
|
527
|
+
validation.tscResult = tscResult;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Parse any project updates from the response
|
|
533
|
+
const projectUpdates = this._extractProjectUpdates(cleanContent);
|
|
534
|
+
|
|
535
|
+
// Build footer with counts and validation status
|
|
536
|
+
const fileCount = fileOperations.length;
|
|
537
|
+
const cmdCount = commands.length;
|
|
538
|
+
const patternNames = relevantPatterns.map(p => p.name).join(', ') || 'core';
|
|
539
|
+
|
|
540
|
+
// Determine overall status
|
|
541
|
+
const hasComplianceIssues = !compliance.passed;
|
|
542
|
+
const hasValidationErrors = validation && !validation.passed;
|
|
543
|
+
const hasMissingDeps = dependencyCheck && dependencyCheck.missing.length > 0;
|
|
544
|
+
const hasTscErrors = tscResult && !tscResult.passed;
|
|
545
|
+
|
|
546
|
+
let statusIcon = '✅';
|
|
547
|
+
if (hasValidationErrors || hasTscErrors) statusIcon = '❌';
|
|
548
|
+
else if (hasComplianceIssues || hasMissingDeps) statusIcon = '⚠️';
|
|
549
|
+
|
|
550
|
+
let contentWithFooter = cleanContent;
|
|
551
|
+
|
|
552
|
+
// Add dependency warning if packages missing
|
|
553
|
+
if (hasMissingDeps && dependencyCheck) {
|
|
554
|
+
contentWithFooter += '\n\n---\n📦 **Missing Packages:**\n```bash\nnpm install ' +
|
|
555
|
+
dependencyCheck.missing.join(' ') + '\n```';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Add TypeScript errors if any
|
|
559
|
+
if (hasTscErrors && tscResult) {
|
|
560
|
+
contentWithFooter += '\n\n---\n🔴 **TypeScript Errors (' + tscResult.errorCount + '):**\n' +
|
|
561
|
+
tscResult.errors.slice(0, 5).map(e => `- ${e.file}:${e.line} - ${e.message}`).join('\n');
|
|
562
|
+
if (tscResult.errorCount > 5) {
|
|
563
|
+
contentWithFooter += `\n ...and ${tscResult.errorCount - 5} more`;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Add validation errors/warnings
|
|
568
|
+
if (validation) {
|
|
569
|
+
if (validation.errors.length > 0) {
|
|
570
|
+
contentWithFooter += '\n\n---\n❌ **Validation Errors:**\n' +
|
|
571
|
+
validation.errors.map(e => `- ${e.file}: ${e.message}`).join('\n');
|
|
572
|
+
}
|
|
573
|
+
if (validation.warnings.length > 0) {
|
|
574
|
+
contentWithFooter += '\n\n---\n⚠️ **Warnings:**\n' +
|
|
575
|
+
validation.warnings.map(w => `- ${w.file}: ${w.message}`).join('\n');
|
|
576
|
+
}
|
|
577
|
+
if (validation.suggestions.length > 0) {
|
|
578
|
+
contentWithFooter += '\n\n---\n💡 **Suggestions:**\n' +
|
|
579
|
+
validation.suggestions.map(s => `- ${s}`).join('\n');
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Add compliance warning if rules were violated
|
|
584
|
+
if (hasComplianceIssues) {
|
|
585
|
+
contentWithFooter += '\n\n---\n⚠️ **Quality Warning:** ' + compliance.issues.join(', ');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Build TSC status for footer
|
|
589
|
+
const tscStatus = tscResult ? (tscResult.passed ? '✅' : '❌') : '⏭️';
|
|
590
|
+
|
|
591
|
+
contentWithFooter += '\n\n---\n🍪 **CodeBakers** ' + statusIcon + ' | Files: ' +
|
|
592
|
+
fileCount + ' | Commands: ' + cmdCount + ' | TSC: ' + tscStatus + ' | Patterns: ' + patternNames + ' | v1.0.41';
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
content: contentWithFooter,
|
|
596
|
+
thinking: thinking || undefined,
|
|
597
|
+
fileOperations: fileOperations.length > 0 ? fileOperations : undefined,
|
|
598
|
+
commands: commands.length > 0 ? commands : undefined,
|
|
599
|
+
projectUpdates,
|
|
600
|
+
validation,
|
|
601
|
+
dependencyCheck
|
|
602
|
+
};
|
|
603
|
+
} catch (error: any) {
|
|
604
|
+
lastError = error;
|
|
605
|
+
console.error(`Claude API error (attempt ${attempt}/${maxRetries}):`, error);
|
|
606
|
+
|
|
607
|
+
// Don't retry if cancelled
|
|
608
|
+
if (error.message === 'Request was cancelled') {
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Don't retry auth errors
|
|
613
|
+
if (error.message?.includes('authenticated') || error.message?.includes('SUBSCRIPTION')) {
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Retry with exponential backoff for network/API errors
|
|
618
|
+
if (attempt < maxRetries) {
|
|
619
|
+
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); // Max 10 seconds
|
|
620
|
+
callbacks?.onError?.(new Error(`Request failed, retrying in ${delay / 1000}s...`));
|
|
621
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// All retries exhausted
|
|
627
|
+
const finalError = lastError || new Error('Request failed after multiple retries');
|
|
628
|
+
callbacks?.onError?.(finalError);
|
|
629
|
+
throw finalError;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Parse <thinking> tags from response to separate reasoning from content
|
|
634
|
+
*/
|
|
635
|
+
private _parseThinkingAndContent(text: string): { thinking: string | null; content: string } {
|
|
636
|
+
const thinkingMatch = text.match(/<thinking>([\s\S]*?)<\/thinking>/);
|
|
637
|
+
|
|
638
|
+
if (thinkingMatch) {
|
|
639
|
+
const thinking = thinkingMatch[1].trim();
|
|
640
|
+
const content = text.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim();
|
|
641
|
+
return { thinking, content };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return { thinking: null, content: text };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Parse <file_operation> tags from response
|
|
649
|
+
*/
|
|
650
|
+
parseFileOperations(text: string): FileOperation[] {
|
|
651
|
+
const operations: FileOperation[] = [];
|
|
652
|
+
const regex = /<file_operation>([\s\S]*?)<\/file_operation>/g;
|
|
653
|
+
let match;
|
|
654
|
+
|
|
655
|
+
while ((match = regex.exec(text)) !== null) {
|
|
656
|
+
const block = match[1];
|
|
657
|
+
|
|
658
|
+
// Extract action
|
|
659
|
+
const actionMatch = block.match(/<action>(create|edit|delete)<\/action>/);
|
|
660
|
+
if (!actionMatch) continue;
|
|
661
|
+
|
|
662
|
+
// Extract path
|
|
663
|
+
const pathMatch = block.match(/<path>([^<]+)<\/path>/);
|
|
664
|
+
if (!pathMatch) continue;
|
|
665
|
+
|
|
666
|
+
// Extract optional description
|
|
667
|
+
const descMatch = block.match(/<description>([^<]+)<\/description>/);
|
|
668
|
+
|
|
669
|
+
// Extract content (only for create/edit)
|
|
670
|
+
const contentMatch = block.match(/<content>([\s\S]*?)<\/content>/);
|
|
671
|
+
|
|
672
|
+
operations.push({
|
|
673
|
+
action: actionMatch[1] as 'create' | 'edit' | 'delete',
|
|
674
|
+
path: pathMatch[1].trim(),
|
|
675
|
+
description: descMatch ? descMatch[1].trim() : undefined,
|
|
676
|
+
content: contentMatch ? contentMatch[1].trim() : undefined,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return operations;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Parse <run_command> tags from response
|
|
685
|
+
*/
|
|
686
|
+
parseCommands(text: string): CommandToRun[] {
|
|
687
|
+
const commands: CommandToRun[] = [];
|
|
688
|
+
const regex = /<run_command>([\s\S]*?)<\/run_command>/g;
|
|
689
|
+
let match;
|
|
690
|
+
|
|
691
|
+
while ((match = regex.exec(text)) !== null) {
|
|
692
|
+
const block = match[1];
|
|
693
|
+
|
|
694
|
+
// Extract command
|
|
695
|
+
const cmdMatch = block.match(/<command>([^<]+)<\/command>/);
|
|
696
|
+
if (!cmdMatch) continue;
|
|
697
|
+
|
|
698
|
+
// Extract optional description
|
|
699
|
+
const descMatch = block.match(/<description>([^<]+)<\/description>/);
|
|
700
|
+
|
|
701
|
+
commands.push({
|
|
702
|
+
command: cmdMatch[1].trim(),
|
|
703
|
+
description: descMatch ? descMatch[1].trim() : undefined,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return commands;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Remove file operation and command tags from content for display
|
|
712
|
+
*/
|
|
713
|
+
cleanContentForDisplay(text: string): string {
|
|
714
|
+
return text
|
|
715
|
+
.replace(/<file_operation>[\s\S]*?<\/file_operation>/g, '')
|
|
716
|
+
.replace(/<run_command>[\s\S]*?<\/run_command>/g, '')
|
|
717
|
+
.trim();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* COMPLIANCE CHECK - Verify the AI followed CodeBakers rules
|
|
722
|
+
* This is programmatic enforcement - catches when AI ignores instructions
|
|
723
|
+
*/
|
|
724
|
+
private _checkCompliance(
|
|
725
|
+
fullText: string,
|
|
726
|
+
thinking: string | null,
|
|
727
|
+
fileOperations: FileOperation[],
|
|
728
|
+
cleanContent: string
|
|
729
|
+
): { passed: boolean; issues: string[] } {
|
|
730
|
+
const issues: string[] = [];
|
|
731
|
+
|
|
732
|
+
// Check 1: Did AI include thinking block?
|
|
733
|
+
if (!thinking && cleanContent.length > 200) {
|
|
734
|
+
// Only flag for substantial responses
|
|
735
|
+
issues.push('Missing reasoning (thinking block)');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Check 2: If response mentions code but no file_operation tags
|
|
739
|
+
const hasCodeBlocks = /```[\s\S]*?```/.test(fullText);
|
|
740
|
+
const mentionsFiles = /\.(tsx?|jsx?|css|json|md)\b/.test(cleanContent);
|
|
741
|
+
if (hasCodeBlocks && mentionsFiles && fileOperations.length === 0) {
|
|
742
|
+
issues.push('Code in markdown instead of file_operation tags');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Check 3: Check for 'any' type usage in file operations
|
|
746
|
+
for (const op of fileOperations) {
|
|
747
|
+
if (op.content) {
|
|
748
|
+
// Check for `: any` type annotations
|
|
749
|
+
const anyCount = (op.content.match(/:\s*any\b/g) || []).length;
|
|
750
|
+
if (anyCount > 2) {
|
|
751
|
+
issues.push(`Excessive 'any' types in ${op.path} (${anyCount} found)`);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Check for missing error handling in async functions
|
|
755
|
+
if (op.content.includes('async ') && !op.content.includes('try') && !op.content.includes('catch')) {
|
|
756
|
+
if (op.content.includes('await ')) {
|
|
757
|
+
issues.push(`Missing try/catch in ${op.path}`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Check 4: API routes should have error handling
|
|
764
|
+
for (const op of fileOperations) {
|
|
765
|
+
if (op.path.includes('/api/') && op.content) {
|
|
766
|
+
if (!op.content.includes('catch') && !op.content.includes('NextResponse.json')) {
|
|
767
|
+
issues.push(`API route ${op.path} may lack error handling`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
passed: issues.length === 0,
|
|
774
|
+
issues
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async summarize(text: string): Promise<string> {
|
|
779
|
+
if (!this.anthropic) {
|
|
780
|
+
throw new Error('Not authenticated');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const response = await this.anthropic.messages.create({
|
|
784
|
+
model: 'claude-sonnet-4-20250514',
|
|
785
|
+
max_tokens: 1024,
|
|
786
|
+
system: 'You are a conversation summarizer. Create concise summaries that preserve key decisions, code changes, and context. Be specific about file names and technical decisions.',
|
|
787
|
+
messages: [{
|
|
788
|
+
role: 'user',
|
|
789
|
+
content: text
|
|
790
|
+
}]
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
return response.content[0].type === 'text' ? response.content[0].text : '';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async getAvailablePatterns(): Promise<Pattern[]> {
|
|
797
|
+
return Array.from(this.patterns.values());
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ==========================================
|
|
801
|
+
// TOOL EXECUTION (All MCP tools available)
|
|
802
|
+
// ==========================================
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Execute any CodeBakers tool
|
|
806
|
+
*/
|
|
807
|
+
async executeTool(toolName: string, args: Record<string, any> = {}): Promise<any> {
|
|
808
|
+
if (!this.sessionToken) {
|
|
809
|
+
throw new Error('Not authenticated. Please login first.');
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const projectPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
813
|
+
|
|
814
|
+
// Include token in query param as fallback (VS Code may strip headers)
|
|
815
|
+
const url = `${this._getApiEndpoint()}/api/tools?token=${encodeURIComponent(this.sessionToken)}`;
|
|
816
|
+
const response = await this._fetchWithTimeout(url, {
|
|
817
|
+
method: 'POST',
|
|
818
|
+
headers: {
|
|
819
|
+
'Authorization': `Bearer ${this.sessionToken}`,
|
|
820
|
+
'Content-Type': 'application/json',
|
|
821
|
+
},
|
|
822
|
+
body: JSON.stringify({
|
|
823
|
+
tool: toolName,
|
|
824
|
+
args,
|
|
825
|
+
projectPath,
|
|
826
|
+
}),
|
|
827
|
+
}, 30000); // 30 seconds for tool execution
|
|
828
|
+
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
const errorData = await response.json() as { message?: string };
|
|
831
|
+
throw new Error(errorData.message || `Tool execution failed: ${toolName}`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return response.json();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* List all available tools
|
|
839
|
+
*/
|
|
840
|
+
async listTools(): Promise<Array<{ name: string; category: string }>> {
|
|
841
|
+
if (!this.sessionToken) {
|
|
842
|
+
return [];
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Include token in query param as fallback (VS Code may strip headers)
|
|
846
|
+
const url = `${this._getApiEndpoint()}/api/tools?token=${encodeURIComponent(this.sessionToken)}`;
|
|
847
|
+
const response = await this._fetchWithTimeout(url, {
|
|
848
|
+
headers: {
|
|
849
|
+
'Authorization': `Bearer ${this.sessionToken}`,
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
if (!response.ok) {
|
|
854
|
+
throw new Error('Failed to fetch tools');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const data = await response.json() as { data: { tools: Array<{ name: string; category: string }> } };
|
|
858
|
+
return data.data?.tools || [];
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Convenience methods for common tools
|
|
862
|
+
|
|
863
|
+
async discoverPatterns(task: string, keywords: string[] = []): Promise<any> {
|
|
864
|
+
return this.executeTool('discover_patterns', { task, keywords });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async validateComplete(feature: string, files: string[]): Promise<any> {
|
|
868
|
+
return this.executeTool('validate_complete', { feature, files });
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async guardianAnalyze(files: string[]): Promise<any> {
|
|
872
|
+
return this.executeTool('guardian_analyze', { files });
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async guardianHeal(issues: any[]): Promise<any> {
|
|
876
|
+
return this.executeTool('guardian_heal', { issues });
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async guardianVerify(): Promise<any> {
|
|
880
|
+
return this.executeTool('guardian_verify', {});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async guardianStatus(): Promise<any> {
|
|
884
|
+
return this.executeTool('guardian_status', {});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async rippleCheck(entityName: string, changeType?: string): Promise<any> {
|
|
888
|
+
return this.executeTool('ripple_check', { entityName, changeType });
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async runAudit(): Promise<any> {
|
|
892
|
+
return this.executeTool('run_audit', {});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async runTests(): Promise<any> {
|
|
896
|
+
return this.executeTool('run_tests', {});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async detectIntent(message: string): Promise<any> {
|
|
900
|
+
return this.executeTool('detect_intent', { message });
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private _buildSystemPrompt(projectState: any): string {
|
|
904
|
+
return `# CodeBakers AI Assistant
|
|
905
|
+
Version: 1.0.37
|
|
906
|
+
|
|
907
|
+
You are CodeBakers, an advanced AI coding assistant that can READ files, WRITE files, and RUN commands - just like Claude Code or Cursor.
|
|
908
|
+
|
|
909
|
+
## YOUR CAPABILITIES
|
|
910
|
+
|
|
911
|
+
You can:
|
|
912
|
+
1. **Read files** - Access any file in the workspace
|
|
913
|
+
2. **Write files** - Create new files or edit existing ones
|
|
914
|
+
3. **Run commands** - Execute terminal commands (npm, git, etc.)
|
|
915
|
+
4. **Apply patterns** - Use production-ready code patterns
|
|
916
|
+
|
|
917
|
+
## THINKING PROCESS (REQUIRED)
|
|
918
|
+
|
|
919
|
+
Before EVERY response, show your reasoning in <thinking> tags:
|
|
920
|
+
|
|
921
|
+
<thinking>
|
|
922
|
+
- Understanding: What is the user asking for?
|
|
923
|
+
- Analysis: What files exist? What needs to change?
|
|
924
|
+
- Plan: Step-by-step implementation approach
|
|
925
|
+
- Patterns: Which CodeBakers patterns apply?
|
|
926
|
+
- Risks: What could go wrong? Edge cases?
|
|
927
|
+
</thinking>
|
|
928
|
+
|
|
929
|
+
## FILE OPERATIONS FORMAT
|
|
930
|
+
|
|
931
|
+
When you need to create or edit files, use this EXACT format:
|
|
932
|
+
|
|
933
|
+
<file_operation>
|
|
934
|
+
<action>create</action>
|
|
935
|
+
<path>src/components/LoginForm.tsx</path>
|
|
936
|
+
<description>Create login form component with validation</description>
|
|
937
|
+
<content>
|
|
938
|
+
// File content here
|
|
939
|
+
import React from 'react';
|
|
940
|
+
// ... rest of code
|
|
941
|
+
</content>
|
|
942
|
+
</file_operation>
|
|
943
|
+
|
|
944
|
+
For editing existing files:
|
|
945
|
+
<file_operation>
|
|
946
|
+
<action>edit</action>
|
|
947
|
+
<path>src/app/page.tsx</path>
|
|
948
|
+
<description>Add login button to homepage</description>
|
|
949
|
+
<content>
|
|
950
|
+
// FULL new file content (not a diff)
|
|
951
|
+
</content>
|
|
952
|
+
</file_operation>
|
|
953
|
+
|
|
954
|
+
For deleting files:
|
|
955
|
+
<file_operation>
|
|
956
|
+
<action>delete</action>
|
|
957
|
+
<path>src/old-file.ts</path>
|
|
958
|
+
<description>Remove deprecated file</description>
|
|
959
|
+
</file_operation>
|
|
960
|
+
|
|
961
|
+
## COMMAND EXECUTION FORMAT
|
|
962
|
+
|
|
963
|
+
When you need to run terminal commands:
|
|
964
|
+
|
|
965
|
+
<run_command>
|
|
966
|
+
<command>npm install zod react-hook-form</command>
|
|
967
|
+
<description>Install form validation dependencies</description>
|
|
968
|
+
</run_command>
|
|
969
|
+
|
|
970
|
+
<run_command>
|
|
971
|
+
<command>npx prisma db push</command>
|
|
972
|
+
<description>Push schema changes to database</description>
|
|
973
|
+
</run_command>
|
|
974
|
+
|
|
975
|
+
## CODING STANDARDS
|
|
976
|
+
|
|
977
|
+
1. **TypeScript** - Always use TypeScript with proper types
|
|
978
|
+
2. **Error Handling** - Always wrap async operations in try/catch
|
|
979
|
+
3. **Validation** - Use Zod for input validation
|
|
980
|
+
4. **Loading States** - Always handle loading and error states
|
|
981
|
+
5. **Accessibility** - Include ARIA labels, keyboard navigation
|
|
982
|
+
6. **Security** - Never expose secrets, validate all inputs
|
|
983
|
+
|
|
984
|
+
## CODE QUALITY REQUIREMENTS
|
|
985
|
+
|
|
986
|
+
Every file you create/edit MUST have:
|
|
987
|
+
- Proper imports (no unused imports)
|
|
988
|
+
- Type annotations (no 'any' unless absolutely necessary)
|
|
989
|
+
- Error boundaries for React components
|
|
990
|
+
- Loading and error states for async operations
|
|
991
|
+
- Comments for complex logic only
|
|
992
|
+
|
|
993
|
+
## RESPONSE STRUCTURE
|
|
994
|
+
|
|
995
|
+
1. <thinking>...</thinking> - Your reasoning (REQUIRED)
|
|
996
|
+
2. Brief explanation of what you'll do
|
|
997
|
+
3. <file_operation>...</file_operation> blocks for each file
|
|
998
|
+
4. <run_command>...</run_command> blocks for commands
|
|
999
|
+
5. Summary of changes made
|
|
1000
|
+
6. Footer with patterns used
|
|
1001
|
+
|
|
1002
|
+
## CURRENT PROJECT STATE
|
|
1003
|
+
${projectState ? JSON.stringify(projectState, null, 2) : 'No workspace open - ask user to open a project folder'}
|
|
1004
|
+
|
|
1005
|
+
## PROJECT FILE STRUCTURE
|
|
1006
|
+
${projectState?.fileTree ? `
|
|
1007
|
+
IMPORTANT: Use this structure to know WHERE to create files:
|
|
1008
|
+
\`\`\`
|
|
1009
|
+
${projectState.fileTree}
|
|
1010
|
+
\`\`\`
|
|
1011
|
+
- Create API routes in the existing api/ folder
|
|
1012
|
+
- Create components in the existing components/ folder
|
|
1013
|
+
- Follow the existing project structure - DO NOT create new top-level folders unless necessary
|
|
1014
|
+
` : 'No file tree available - ask user to open a project folder'}
|
|
1015
|
+
|
|
1016
|
+
## EXISTING TYPES (Reuse these - do NOT recreate)
|
|
1017
|
+
${projectState?.existingTypes || 'No existing types found - you may create new types as needed'}
|
|
1018
|
+
|
|
1019
|
+
## INSTALLED PACKAGES
|
|
1020
|
+
${projectState?.installedPackages?.length > 0 ? `
|
|
1021
|
+
Available packages (already installed):
|
|
1022
|
+
${projectState.installedPackages.slice(0, 30).join(', ')}
|
|
1023
|
+
|
|
1024
|
+
IMPORTANT: Only import from packages listed above or Node.js built-ins.
|
|
1025
|
+
If you need a package not listed, include a <run_command> to install it.
|
|
1026
|
+
` : 'No package.json found'}
|
|
1027
|
+
|
|
1028
|
+
## FOOTER (Required on every response with code)
|
|
1029
|
+
|
|
1030
|
+
---
|
|
1031
|
+
🍪 **CodeBakers** | Files: [count] | Commands: [count] | Patterns: [list] | v1.0.40
|
|
1032
|
+
|
|
1033
|
+
## CRITICAL RULES (ENFORCED - NOT OPTIONAL)
|
|
1034
|
+
|
|
1035
|
+
These rules are STRUCTURALLY ENFORCED. The user PAID for this quality guarantee.
|
|
1036
|
+
|
|
1037
|
+
### MANDATORY THINKING BLOCK
|
|
1038
|
+
You MUST start every response with <thinking>...</thinking> containing:
|
|
1039
|
+
1. What patterns from LOADED PATTERNS section apply?
|
|
1040
|
+
2. What existing code patterns should I match?
|
|
1041
|
+
3. What could go wrong? (error cases, edge cases)
|
|
1042
|
+
|
|
1043
|
+
If your response lacks <thinking> tags, it is INVALID and will be rejected.
|
|
1044
|
+
|
|
1045
|
+
### MANDATORY PATTERN USAGE
|
|
1046
|
+
Look at the "# LOADED PATTERNS" section below. You MUST:
|
|
1047
|
+
1. Use code structures shown in the patterns
|
|
1048
|
+
2. Use the same libraries (Zod, React Hook Form, etc.)
|
|
1049
|
+
3. Match the error handling style
|
|
1050
|
+
4. Include all required elements (loading states, validation, etc.)
|
|
1051
|
+
|
|
1052
|
+
If you write code that ignores the loaded patterns, you are FAILING your job.
|
|
1053
|
+
|
|
1054
|
+
### MANDATORY FILE OPERATION FORMAT
|
|
1055
|
+
For ANY file change, you MUST use:
|
|
1056
|
+
<file_operation>
|
|
1057
|
+
<action>create|edit|delete</action>
|
|
1058
|
+
<path>relative/path/to/file.ts</path>
|
|
1059
|
+
<description>What this change does</description>
|
|
1060
|
+
<content>COMPLETE file content - never partial</content>
|
|
1061
|
+
</file_operation>
|
|
1062
|
+
|
|
1063
|
+
Code in regular markdown blocks will NOT be applied. Only <file_operation> blocks work.
|
|
1064
|
+
|
|
1065
|
+
### MANDATORY TEST REQUIREMENT
|
|
1066
|
+
Every feature MUST include at least one test file. Do not ask "want me to add tests?" - just add them.
|
|
1067
|
+
|
|
1068
|
+
### MANDATORY FOOTER
|
|
1069
|
+
End every code response with:
|
|
1070
|
+
---
|
|
1071
|
+
🍪 **CodeBakers** | Files: [count] | Commands: [count] | Patterns: [list] | v1.0.40
|
|
1072
|
+
|
|
1073
|
+
### NEVER DO THESE (Pattern Violations)
|
|
1074
|
+
- ❌ Skip error handling (wrap async in try/catch)
|
|
1075
|
+
- ❌ Use 'any' type (use proper types from patterns)
|
|
1076
|
+
- ❌ Ignore loaded patterns (they exist for a reason)
|
|
1077
|
+
- ❌ Create files without validation (use Zod)
|
|
1078
|
+
- ❌ Skip loading states (always handle pending/error/success)
|
|
1079
|
+
- ❌ Write code from memory when patterns exist
|
|
1080
|
+
|
|
1081
|
+
### SELF-CHECK BEFORE RESPONDING
|
|
1082
|
+
Before sending, verify:
|
|
1083
|
+
[ ] <thinking> block present?
|
|
1084
|
+
[ ] Patterns from LOADED PATTERNS section used?
|
|
1085
|
+
[ ] <file_operation> tags for all file changes?
|
|
1086
|
+
[ ] Error handling included?
|
|
1087
|
+
[ ] Loading states handled?
|
|
1088
|
+
[ ] Footer included?
|
|
1089
|
+
|
|
1090
|
+
If any checkbox is NO, fix it before responding.
|
|
1091
|
+
`;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
private async _detectRelevantPatterns(messages: any[]): Promise<Pattern[]> {
|
|
1095
|
+
const lastMessage = messages[messages.length - 1]?.content?.toLowerCase() || '';
|
|
1096
|
+
const relevant: Pattern[] = [];
|
|
1097
|
+
|
|
1098
|
+
// Keyword-based pattern detection
|
|
1099
|
+
const keywordMap: Record<string, string[]> = {
|
|
1100
|
+
'00-core': ['any', 'code', 'build', 'create'],
|
|
1101
|
+
'01-database': ['database', 'db', 'schema', 'table', 'query', 'drizzle', 'postgres'],
|
|
1102
|
+
'02-auth': ['auth', 'login', 'signup', 'session', 'password', 'oauth'],
|
|
1103
|
+
'03-api': ['api', 'route', 'endpoint', 'rest', 'fetch'],
|
|
1104
|
+
'04-frontend': ['component', 'react', 'form', 'ui', 'page'],
|
|
1105
|
+
'05-payments': ['payment', 'stripe', 'billing', 'subscription', 'checkout'],
|
|
1106
|
+
'08-testing': ['test', 'testing', 'playwright', 'vitest'],
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
// Always include core
|
|
1110
|
+
if (this.patterns.has('00-core')) {
|
|
1111
|
+
relevant.push(this.patterns.get('00-core')!);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Add relevant patterns based on keywords
|
|
1115
|
+
for (const [pattern, keywords] of Object.entries(keywordMap)) {
|
|
1116
|
+
if (pattern === '00-core') continue;
|
|
1117
|
+
|
|
1118
|
+
if (keywords.some(kw => lastMessage.includes(kw))) {
|
|
1119
|
+
const p = this.patterns.get(pattern);
|
|
1120
|
+
if (p) relevant.push(p);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return relevant;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private _extractProjectUpdates(content: string): ChatResponse['projectUpdates'] {
|
|
1128
|
+
// Extract any structured updates from the response
|
|
1129
|
+
// This is a simple implementation - could be enhanced with XML tags in prompt
|
|
1130
|
+
const updates: ChatResponse['projectUpdates'] = {};
|
|
1131
|
+
|
|
1132
|
+
// Look for patterns like "Added pattern: X"
|
|
1133
|
+
const patternMatches = content.match(/(?:using|loaded|applied) pattern[s]?: ([^\n]+)/gi);
|
|
1134
|
+
if (patternMatches) {
|
|
1135
|
+
updates.patterns = patternMatches.flatMap(m =>
|
|
1136
|
+
m.split(':')[1]?.split(',').map(s => s.trim()) || []
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return Object.keys(updates).length > 0 ? updates : undefined;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
private _getApiEndpoint(): string {
|
|
1144
|
+
return vscode.workspace.getConfiguration('codebakers').get('apiEndpoint', 'https://www.codebakers.ai');
|
|
1145
|
+
}
|
|
1146
|
+
}
|