@kapeta/local-cluster-service 0.52.0 → 0.52.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.52.1](https://github.com/kapetacom/local-cluster-service/compare/v0.52.0...v0.52.1) (2024-06-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * adjust errorclassifier, add errordetails and adjust codefix ([1eb8000](https://github.com/kapetacom/local-cluster-service/commit/1eb80005f59c103d558219d07bed6f01b9ebc562))
7
+ * look for similar files when file asked for does not exist ([abe75c4](https://github.com/kapetacom/local-cluster-service/commit/abe75c4fbcbd812565aa154d6437a288e58403ac))
8
+ * review comments ([542f3dc](https://github.com/kapetacom/local-cluster-service/commit/542f3dca02fc006ee0bd7adf2ad76f28295ba6f2))
9
+
1
10
  # [0.52.0](https://github.com/kapetacom/local-cluster-service/compare/v0.51.4...v0.52.0) (2024-06-10)
2
11
 
3
12
 
@@ -26,6 +26,11 @@ export declare class StormCodegen {
26
26
  */
27
27
  private processBlockCode;
28
28
  private verifyAndFixCode;
29
+ private tryToFixFile;
30
+ private classifyErrors;
31
+ private getErrorDetailsForFile;
32
+ private createFixRequestForFile;
33
+ private getErrorLine;
29
34
  removePrefix(prefix: string, str: string): string;
30
35
  /**
31
36
  * Sends the code to the AI for a fix
@@ -38,11 +38,13 @@ const event_parser_1 = require("./event-parser");
38
38
  const stream_1 = require("./stream");
39
39
  const nodejs_utils_1 = require("@kapeta/nodejs-utils");
40
40
  const promises_1 = require("fs/promises");
41
- const path_1 = __importStar(require("path"));
41
+ const path_1 = __importDefault(require("path"));
42
+ const path_2 = __importStar(require("path"));
42
43
  const node_os_1 = __importDefault(require("node:os"));
43
44
  const fs_1 = require("fs");
44
- const path_2 = __importDefault(require("path"));
45
45
  const yaml_1 = __importDefault(require("yaml"));
46
+ const fs = __importStar(require("node:fs"));
47
+ const uuid_1 = require("uuid");
46
48
  const SIMULATED_DELAY = 1000;
47
49
  class SimulatedFileDelay {
48
50
  file;
@@ -197,7 +199,7 @@ class StormCodegen {
197
199
  ...data,
198
200
  payload: {
199
201
  ...data.payload,
200
- path: (0, path_1.join)(basePath, data.payload.filename),
202
+ path: (0, path_2.join)(basePath, data.payload.filename),
201
203
  blockName,
202
204
  blockRef,
203
205
  instanceId,
@@ -209,7 +211,7 @@ class StormCodegen {
209
211
  ...data,
210
212
  payload: {
211
213
  ...data.payload,
212
- path: (0, path_1.join)(basePath, data.payload.filename),
214
+ path: (0, path_2.join)(basePath, data.payload.filename),
213
215
  blockName,
214
216
  blockRef,
215
217
  instanceId,
@@ -221,7 +223,7 @@ class StormCodegen {
221
223
  ...data,
222
224
  payload: {
223
225
  ...data.payload,
224
- path: (0, path_1.join)(basePath, data.payload.filename),
226
+ path: (0, path_2.join)(basePath, data.payload.filename),
225
227
  blockName,
226
228
  blockRef,
227
229
  instanceId,
@@ -241,7 +243,7 @@ class StormCodegen {
241
243
  created: Date.now(),
242
244
  payload: {
243
245
  filename: data.payload.filename,
244
- path: (0, path_1.join)(this.getBasePath(blockUri.fullName), data.payload.filename),
246
+ path: (0, path_2.join)(this.getBasePath(blockUri.fullName), data.payload.filename),
245
247
  content: data.payload.content,
246
248
  blockRef: ref,
247
249
  instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
@@ -297,7 +299,7 @@ class StormCodegen {
297
299
  }
298
300
  // Gather the context files for implementation. These will be all be passed to the AI
299
301
  const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
300
- // Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
302
+ // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
301
303
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
302
304
  if (serviceFiles.length > 0) {
303
305
  await this.processTemplates((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
@@ -307,17 +309,17 @@ class StormCodegen {
307
309
  return;
308
310
  }
309
311
  for (const serviceFile of serviceFiles) {
310
- const filePath = (0, path_1.join)(basePath, serviceFile.filename);
312
+ const filePath = (0, path_2.join)(basePath, serviceFile.filename);
311
313
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
312
314
  }
313
315
  for (const serviceFile of contextFiles) {
314
- const filePath = (0, path_1.join)(basePath, serviceFile.filename);
316
+ const filePath = (0, path_2.join)(basePath, serviceFile.filename);
315
317
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
316
318
  }
317
- const kapetaYmlPath = (0, path_1.join)(basePath, 'kapeta.yml');
319
+ const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
318
320
  await (0, promises_1.writeFile)(kapetaYmlPath, yaml_1.default.stringify(block.content));
319
321
  for (const screenFile of screenFiles) {
320
- const filePath = (0, path_1.join)(basePath, screenFile.payload.filename);
322
+ const filePath = (0, path_2.join)(basePath, screenFile.payload.filename);
321
323
  await (0, promises_1.writeFile)(filePath, screenFile.payload.content);
322
324
  }
323
325
  const screenFilesConverted = screenFiles.map(screenFile => {
@@ -329,8 +331,8 @@ class StormCodegen {
329
331
  type: codegen_1.AIFileTypes.WEB_SCREEN,
330
332
  };
331
333
  });
332
- const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
333
334
  allFiles.push(...screenFilesConverted);
335
+ const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
334
336
  const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
335
337
  await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
336
338
  const blockRef = block.uri;
@@ -346,7 +348,7 @@ class StormCodegen {
346
348
  },
347
349
  });
348
350
  }
349
- async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, knownFiles) {
351
+ async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles) {
350
352
  let attempts = 0;
351
353
  let validCode = false;
352
354
  for (let i = 0; i <= 3; i++) {
@@ -360,35 +362,10 @@ class StormCodegen {
360
362
  }
361
363
  if (result && !result.valid) {
362
364
  console.debug('Validation error:', result);
363
- const errorStream = await stormClient_1.stormClient.createErrorClassification(result.error, []);
364
- const fixes = new Map();
365
- this.out.on('aborted', () => {
366
- errorStream.abort();
367
- });
368
- errorStream.on('data', (evt) => {
369
- if (evt.type === 'ERROR_CLASSIFIER') {
370
- // find the file that caused the error
371
- // strip base path from event file name, if it exists sometimes the AI sends the full path
372
- const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
373
- const file = filesToBeFixed.find((f) => f.filename === eventFileName);
374
- if (!file) {
375
- console.log(`Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`);
376
- }
377
- // read the content of the file
378
- const content = (0, fs_1.readFileSync)((0, path_1.join)(basePath, eventFileName), 'utf8');
379
- const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
380
- .map((e) => e.filename)
381
- .join('\n')}\n---\n${content}`;
382
- //console.log(`trying to fix the code in ${eventFileName}`);
383
- //console.debug(`with the fix:\n${fix}`);
384
- const code = this.codeFix(fix);
385
- fixes.set((0, path_1.join)(basePath, eventFileName), code);
386
- }
387
- });
388
- await errorStream.waitForDone();
389
- for (const [filename, codePromise] of fixes) {
390
- const code = await codePromise;
391
- (0, fs_1.writeFileSync)(filename, code);
365
+ const errors = await this.classifyErrors(result.error, basePath);
366
+ for (const [filename, fileErrors] of errors.entries()) {
367
+ // todo: only try to fix file if it is part of filesToBeFixed
368
+ await this.tryToFixFile(basePath, filename, fileErrors, allFiles, codeGenerator);
392
369
  }
393
370
  }
394
371
  }
@@ -403,6 +380,127 @@ class StormCodegen {
403
380
  console.error(`Validation failed for ${basePath} after ${attempts} attempts`);
404
381
  }
405
382
  }
383
+ async tryToFixFile(basePath, filename, fileErrors, allFiles, codeGenerator) {
384
+ console.log(`Processing ${filename}`);
385
+ const language = await codeGenerator.language();
386
+ const relevantFiles = allFiles.filter(file => file.type != codegen_1.AIFileTypes.IGNORE);
387
+ for (let attempts = 1; attempts <= 5; attempts++) {
388
+ if (fileErrors.length == 0) {
389
+ console.log(`No more errors for ${filename}`);
390
+ return;
391
+ }
392
+ console.log(`Errors in ${filename} - requesting error details`);
393
+ const filesForContext = await this.getErrorDetailsForFile(basePath, filename, fileErrors[0], relevantFiles, language);
394
+ console.log(`Get error details for ${filename} requesting code fixes`);
395
+ const fix = this.createFixRequestForFile(basePath, filename, fileErrors[0], filesForContext, relevantFiles, language);
396
+ const codeFixFiles = await this.codeFix(fix);
397
+ console.log(`Got fixed code for ${filename}`);
398
+ for (const codeFixFile of codeFixFiles) {
399
+ const filePath = codeFixFile.filename.indexOf(basePath) > -1 ? codeFixFile.filename : (0, path_2.join)(basePath, codeFixFile.filename);
400
+ const existing = (0, fs_1.readFileSync)(filePath);
401
+ if (existing.toString().replace(/(\r\n|\r|\n)+$/, '') == codeFixFile.content.replace(/(\r\n|\r|\n)+$/, '')) {
402
+ console.log(`${filename} not changed by gemini`);
403
+ continue;
404
+ }
405
+ (0, fs_1.writeFileSync)(filePath, codeFixFile.content);
406
+ }
407
+ const result = await codeGenerator.validateForTarget(basePath);
408
+ if (result && result.valid) {
409
+ return;
410
+ }
411
+ const errors = await this.classifyErrors(result.error, basePath);
412
+ fileErrors = errors.get(filename) ?? [];
413
+ }
414
+ }
415
+ async classifyErrors(errors, basePath) {
416
+ const errorStream = await stormClient_1.stormClient.createErrorClassification(errors, []);
417
+ const fixes = new Map();
418
+ this.out.on('aborted', () => {
419
+ errorStream.abort();
420
+ });
421
+ errorStream.on('data', (evt) => {
422
+ if (evt.type === 'ERROR_CLASSIFIER') {
423
+ const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
424
+ const fix = { error: evt.payload.error, lineNumber: evt.payload.lineNumber, column: evt.payload.column };
425
+ let existingFixes = fixes.get(eventFileName);
426
+ if (existingFixes) {
427
+ existingFixes.push(fix);
428
+ }
429
+ else {
430
+ fixes.set(eventFileName, [fix]);
431
+ }
432
+ }
433
+ });
434
+ await errorStream.waitForDone();
435
+ return fixes;
436
+ }
437
+ async getErrorDetailsForFile(basePath, filename, error, allFiles, language) {
438
+ const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename); // to compensate when compiler returns absolute path
439
+ return new Promise(async (resolve, reject) => {
440
+ const request = {
441
+ "language": language,
442
+ "sourceFile": {
443
+ "filename": filename,
444
+ "content": (0, fs_1.readFileSync)(filePath, 'utf8')
445
+ },
446
+ "error": error,
447
+ "projectFiles": allFiles.map(f => f.filename)
448
+ };
449
+ const detailsStream = await stormClient_1.stormClient.createErrorDetails(JSON.stringify(request), []);
450
+ detailsStream.on('data', (evt) => {
451
+ if (evt.type === 'ERROR_DETAILS') {
452
+ resolve(evt.payload.files);
453
+ }
454
+ });
455
+ this.out.on('aborted', () => {
456
+ detailsStream.abort();
457
+ });
458
+ detailsStream.on('error', (err) => {
459
+ console.log("error", err);
460
+ reject(err);
461
+ });
462
+ await detailsStream.waitForDone();
463
+ });
464
+ }
465
+ createFixRequestForFile(basePath, filename, error, filesForContext, allFiles, language) {
466
+ const files = new Set(filesForContext);
467
+ files.add(filename);
468
+ const requestedFiles = Array.from(files).flatMap(file => {
469
+ if (fs.existsSync(file)) {
470
+ return file;
471
+ }
472
+ // file does not exist - look for similar
473
+ const candidateName = file.split('/').pop();
474
+ return allFiles
475
+ .filter(file => file.filename.split('/').pop() === candidateName)
476
+ .map(f => f.filename);
477
+ });
478
+ const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename);
479
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
480
+ const affectedLine = this.getErrorLine(error, content);
481
+ const fixRequest = {
482
+ "language": language,
483
+ "filename": filename,
484
+ "errors": error.error,
485
+ "affectedLine": affectedLine,
486
+ "projectFiles": requestedFiles.map(filename => {
487
+ const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename);
488
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
489
+ return { filename: filename, content: content };
490
+ }),
491
+ "conversationId": this.conversationId,
492
+ "sessionId": (0, uuid_1.v4)()
493
+ };
494
+ return JSON.stringify(fixRequest);
495
+ }
496
+ getErrorLine(errorDetails, sourceCode) {
497
+ const lines = sourceCode.split('\n');
498
+ const errorLine = lines[errorDetails.lineNumber - 1];
499
+ if (!errorLine) {
500
+ return "Error: Line number out of range.";
501
+ }
502
+ return errorLine;
503
+ }
406
504
  removePrefix(prefix, str) {
407
505
  if (str.startsWith(prefix)) {
408
506
  return str.slice(prefix.length);
@@ -412,18 +510,19 @@ class StormCodegen {
412
510
  /**
413
511
  * Sends the code to the AI for a fix
414
512
  */
415
- async codeFix(fix) {
513
+ async codeFix(fix, history) {
416
514
  return new Promise(async (resolve, reject) => {
417
- const fixStream = await stormClient_1.stormClient.createCodeFix(fix, []);
515
+ const fixStream = await stormClient_1.stormClient.createCodeFix(fix, history);
418
516
  fixStream.on('data', (evt) => {
419
517
  if (evt.type === 'CODE_FIX') {
420
- resolve(evt.payload.content);
518
+ resolve(evt.payload.content.files);
421
519
  }
422
520
  });
423
521
  this.out.on('aborted', () => {
424
522
  fixStream.abort();
425
523
  });
426
524
  fixStream.on('error', (err) => {
525
+ console.log("error", err);
427
526
  reject(err);
428
527
  });
429
528
  await fixStream.waitForDone();
@@ -455,7 +554,7 @@ class StormCodegen {
455
554
  created: Date.now(),
456
555
  payload: {
457
556
  filename: file.filename,
458
- path: (0, path_1.join)(basePath, file.filename),
557
+ path: (0, path_2.join)(basePath, file.filename),
459
558
  content: file.content,
460
559
  blockName: aiName,
461
560
  blockRef: ref,
@@ -475,7 +574,7 @@ class StormCodegen {
475
574
  created: Date.now(),
476
575
  payload: {
477
576
  filename: filename,
478
- path: (0, path_1.join)(basePath, filename),
577
+ path: (0, path_2.join)(basePath, filename),
479
578
  content: content,
480
579
  blockName,
481
580
  blockRef: ref,
@@ -111,14 +111,28 @@ export interface StormEventCodeFix {
111
111
  reason: string;
112
112
  created: number;
113
113
  payload: {
114
- filename: string;
115
- content: string;
114
+ content: {
115
+ files: StormEventErrorDetailsFile[];
116
+ };
116
117
  };
117
118
  }
118
119
  export interface StormEventErrorClassifierInfo {
119
120
  error: string;
120
121
  filename: string;
121
- potentialFix: string;
122
+ lineNumber: number;
123
+ column: number;
124
+ }
125
+ export interface StormEventErrorDetailsFile {
126
+ filename: string;
127
+ content: string;
128
+ }
129
+ export interface StormEventErrorDetails {
130
+ type: 'ERROR_DETAILS';
131
+ reason: string;
132
+ created: number;
133
+ payload: {
134
+ files: string[];
135
+ };
122
136
  }
123
137
  export interface ScreenTemplate {
124
138
  name: string;
@@ -218,4 +232,4 @@ export interface StormEventPhases {
218
232
  phaseType: StormEventPhaseType;
219
233
  };
220
234
  }
221
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
235
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases;
@@ -11,6 +11,7 @@ declare class StormClient {
11
11
  createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
12
12
  createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
13
13
  createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
14
+ createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
14
15
  }
15
16
  export declare const stormClient: StormClient;
16
17
  export {};
@@ -100,5 +100,11 @@ class StormClient {
100
100
  prompt,
101
101
  });
102
102
  }
103
+ createErrorDetails(prompt, history, conversationId) {
104
+ return this.send('/v2/code/errordetails', {
105
+ conversationId: conversationId,
106
+ prompt,
107
+ });
108
+ }
103
109
  }
104
110
  exports.stormClient = new StormClient();
@@ -26,6 +26,11 @@ export declare class StormCodegen {
26
26
  */
27
27
  private processBlockCode;
28
28
  private verifyAndFixCode;
29
+ private tryToFixFile;
30
+ private classifyErrors;
31
+ private getErrorDetailsForFile;
32
+ private createFixRequestForFile;
33
+ private getErrorLine;
29
34
  removePrefix(prefix: string, str: string): string;
30
35
  /**
31
36
  * Sends the code to the AI for a fix
@@ -38,11 +38,13 @@ const event_parser_1 = require("./event-parser");
38
38
  const stream_1 = require("./stream");
39
39
  const nodejs_utils_1 = require("@kapeta/nodejs-utils");
40
40
  const promises_1 = require("fs/promises");
41
- const path_1 = __importStar(require("path"));
41
+ const path_1 = __importDefault(require("path"));
42
+ const path_2 = __importStar(require("path"));
42
43
  const node_os_1 = __importDefault(require("node:os"));
43
44
  const fs_1 = require("fs");
44
- const path_2 = __importDefault(require("path"));
45
45
  const yaml_1 = __importDefault(require("yaml"));
46
+ const fs = __importStar(require("node:fs"));
47
+ const uuid_1 = require("uuid");
46
48
  const SIMULATED_DELAY = 1000;
47
49
  class SimulatedFileDelay {
48
50
  file;
@@ -197,7 +199,7 @@ class StormCodegen {
197
199
  ...data,
198
200
  payload: {
199
201
  ...data.payload,
200
- path: (0, path_1.join)(basePath, data.payload.filename),
202
+ path: (0, path_2.join)(basePath, data.payload.filename),
201
203
  blockName,
202
204
  blockRef,
203
205
  instanceId,
@@ -209,7 +211,7 @@ class StormCodegen {
209
211
  ...data,
210
212
  payload: {
211
213
  ...data.payload,
212
- path: (0, path_1.join)(basePath, data.payload.filename),
214
+ path: (0, path_2.join)(basePath, data.payload.filename),
213
215
  blockName,
214
216
  blockRef,
215
217
  instanceId,
@@ -221,7 +223,7 @@ class StormCodegen {
221
223
  ...data,
222
224
  payload: {
223
225
  ...data.payload,
224
- path: (0, path_1.join)(basePath, data.payload.filename),
226
+ path: (0, path_2.join)(basePath, data.payload.filename),
225
227
  blockName,
226
228
  blockRef,
227
229
  instanceId,
@@ -241,7 +243,7 @@ class StormCodegen {
241
243
  created: Date.now(),
242
244
  payload: {
243
245
  filename: data.payload.filename,
244
- path: (0, path_1.join)(this.getBasePath(blockUri.fullName), data.payload.filename),
246
+ path: (0, path_2.join)(this.getBasePath(blockUri.fullName), data.payload.filename),
245
247
  content: data.payload.content,
246
248
  blockRef: ref,
247
249
  instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(ref),
@@ -297,7 +299,7 @@ class StormCodegen {
297
299
  }
298
300
  // Gather the context files for implementation. These will be all be passed to the AI
299
301
  const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
300
- // Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
302
+ // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
301
303
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
302
304
  if (serviceFiles.length > 0) {
303
305
  await this.processTemplates((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
@@ -307,17 +309,17 @@ class StormCodegen {
307
309
  return;
308
310
  }
309
311
  for (const serviceFile of serviceFiles) {
310
- const filePath = (0, path_1.join)(basePath, serviceFile.filename);
312
+ const filePath = (0, path_2.join)(basePath, serviceFile.filename);
311
313
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
312
314
  }
313
315
  for (const serviceFile of contextFiles) {
314
- const filePath = (0, path_1.join)(basePath, serviceFile.filename);
316
+ const filePath = (0, path_2.join)(basePath, serviceFile.filename);
315
317
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
316
318
  }
317
- const kapetaYmlPath = (0, path_1.join)(basePath, 'kapeta.yml');
319
+ const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
318
320
  await (0, promises_1.writeFile)(kapetaYmlPath, yaml_1.default.stringify(block.content));
319
321
  for (const screenFile of screenFiles) {
320
- const filePath = (0, path_1.join)(basePath, screenFile.payload.filename);
322
+ const filePath = (0, path_2.join)(basePath, screenFile.payload.filename);
321
323
  await (0, promises_1.writeFile)(filePath, screenFile.payload.content);
322
324
  }
323
325
  const screenFilesConverted = screenFiles.map(screenFile => {
@@ -329,8 +331,8 @@ class StormCodegen {
329
331
  type: codegen_1.AIFileTypes.WEB_SCREEN,
330
332
  };
331
333
  });
332
- const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
333
334
  allFiles.push(...screenFilesConverted);
335
+ const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
334
336
  const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
335
337
  await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
336
338
  const blockRef = block.uri;
@@ -346,7 +348,7 @@ class StormCodegen {
346
348
  },
347
349
  });
348
350
  }
349
- async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, knownFiles) {
351
+ async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles) {
350
352
  let attempts = 0;
351
353
  let validCode = false;
352
354
  for (let i = 0; i <= 3; i++) {
@@ -360,35 +362,10 @@ class StormCodegen {
360
362
  }
361
363
  if (result && !result.valid) {
362
364
  console.debug('Validation error:', result);
363
- const errorStream = await stormClient_1.stormClient.createErrorClassification(result.error, []);
364
- const fixes = new Map();
365
- this.out.on('aborted', () => {
366
- errorStream.abort();
367
- });
368
- errorStream.on('data', (evt) => {
369
- if (evt.type === 'ERROR_CLASSIFIER') {
370
- // find the file that caused the error
371
- // strip base path from event file name, if it exists sometimes the AI sends the full path
372
- const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
373
- const file = filesToBeFixed.find((f) => f.filename === eventFileName);
374
- if (!file) {
375
- console.log(`Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`);
376
- }
377
- // read the content of the file
378
- const content = (0, fs_1.readFileSync)((0, path_1.join)(basePath, eventFileName), 'utf8');
379
- const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
380
- .map((e) => e.filename)
381
- .join('\n')}\n---\n${content}`;
382
- //console.log(`trying to fix the code in ${eventFileName}`);
383
- //console.debug(`with the fix:\n${fix}`);
384
- const code = this.codeFix(fix);
385
- fixes.set((0, path_1.join)(basePath, eventFileName), code);
386
- }
387
- });
388
- await errorStream.waitForDone();
389
- for (const [filename, codePromise] of fixes) {
390
- const code = await codePromise;
391
- (0, fs_1.writeFileSync)(filename, code);
365
+ const errors = await this.classifyErrors(result.error, basePath);
366
+ for (const [filename, fileErrors] of errors.entries()) {
367
+ // todo: only try to fix file if it is part of filesToBeFixed
368
+ await this.tryToFixFile(basePath, filename, fileErrors, allFiles, codeGenerator);
392
369
  }
393
370
  }
394
371
  }
@@ -403,6 +380,127 @@ class StormCodegen {
403
380
  console.error(`Validation failed for ${basePath} after ${attempts} attempts`);
404
381
  }
405
382
  }
383
+ async tryToFixFile(basePath, filename, fileErrors, allFiles, codeGenerator) {
384
+ console.log(`Processing ${filename}`);
385
+ const language = await codeGenerator.language();
386
+ const relevantFiles = allFiles.filter(file => file.type != codegen_1.AIFileTypes.IGNORE);
387
+ for (let attempts = 1; attempts <= 5; attempts++) {
388
+ if (fileErrors.length == 0) {
389
+ console.log(`No more errors for ${filename}`);
390
+ return;
391
+ }
392
+ console.log(`Errors in ${filename} - requesting error details`);
393
+ const filesForContext = await this.getErrorDetailsForFile(basePath, filename, fileErrors[0], relevantFiles, language);
394
+ console.log(`Get error details for ${filename} requesting code fixes`);
395
+ const fix = this.createFixRequestForFile(basePath, filename, fileErrors[0], filesForContext, relevantFiles, language);
396
+ const codeFixFiles = await this.codeFix(fix);
397
+ console.log(`Got fixed code for ${filename}`);
398
+ for (const codeFixFile of codeFixFiles) {
399
+ const filePath = codeFixFile.filename.indexOf(basePath) > -1 ? codeFixFile.filename : (0, path_2.join)(basePath, codeFixFile.filename);
400
+ const existing = (0, fs_1.readFileSync)(filePath);
401
+ if (existing.toString().replace(/(\r\n|\r|\n)+$/, '') == codeFixFile.content.replace(/(\r\n|\r|\n)+$/, '')) {
402
+ console.log(`${filename} not changed by gemini`);
403
+ continue;
404
+ }
405
+ (0, fs_1.writeFileSync)(filePath, codeFixFile.content);
406
+ }
407
+ const result = await codeGenerator.validateForTarget(basePath);
408
+ if (result && result.valid) {
409
+ return;
410
+ }
411
+ const errors = await this.classifyErrors(result.error, basePath);
412
+ fileErrors = errors.get(filename) ?? [];
413
+ }
414
+ }
415
+ async classifyErrors(errors, basePath) {
416
+ const errorStream = await stormClient_1.stormClient.createErrorClassification(errors, []);
417
+ const fixes = new Map();
418
+ this.out.on('aborted', () => {
419
+ errorStream.abort();
420
+ });
421
+ errorStream.on('data', (evt) => {
422
+ if (evt.type === 'ERROR_CLASSIFIER') {
423
+ const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
424
+ const fix = { error: evt.payload.error, lineNumber: evt.payload.lineNumber, column: evt.payload.column };
425
+ let existingFixes = fixes.get(eventFileName);
426
+ if (existingFixes) {
427
+ existingFixes.push(fix);
428
+ }
429
+ else {
430
+ fixes.set(eventFileName, [fix]);
431
+ }
432
+ }
433
+ });
434
+ await errorStream.waitForDone();
435
+ return fixes;
436
+ }
437
+ async getErrorDetailsForFile(basePath, filename, error, allFiles, language) {
438
+ const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename); // to compensate when compiler returns absolute path
439
+ return new Promise(async (resolve, reject) => {
440
+ const request = {
441
+ "language": language,
442
+ "sourceFile": {
443
+ "filename": filename,
444
+ "content": (0, fs_1.readFileSync)(filePath, 'utf8')
445
+ },
446
+ "error": error,
447
+ "projectFiles": allFiles.map(f => f.filename)
448
+ };
449
+ const detailsStream = await stormClient_1.stormClient.createErrorDetails(JSON.stringify(request), []);
450
+ detailsStream.on('data', (evt) => {
451
+ if (evt.type === 'ERROR_DETAILS') {
452
+ resolve(evt.payload.files);
453
+ }
454
+ });
455
+ this.out.on('aborted', () => {
456
+ detailsStream.abort();
457
+ });
458
+ detailsStream.on('error', (err) => {
459
+ console.log("error", err);
460
+ reject(err);
461
+ });
462
+ await detailsStream.waitForDone();
463
+ });
464
+ }
465
+ createFixRequestForFile(basePath, filename, error, filesForContext, allFiles, language) {
466
+ const files = new Set(filesForContext);
467
+ files.add(filename);
468
+ const requestedFiles = Array.from(files).flatMap(file => {
469
+ if (fs.existsSync(file)) {
470
+ return file;
471
+ }
472
+ // file does not exist - look for similar
473
+ const candidateName = file.split('/').pop();
474
+ return allFiles
475
+ .filter(file => file.filename.split('/').pop() === candidateName)
476
+ .map(f => f.filename);
477
+ });
478
+ const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename);
479
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
480
+ const affectedLine = this.getErrorLine(error, content);
481
+ const fixRequest = {
482
+ "language": language,
483
+ "filename": filename,
484
+ "errors": error.error,
485
+ "affectedLine": affectedLine,
486
+ "projectFiles": requestedFiles.map(filename => {
487
+ const filePath = filename.indexOf(basePath) > -1 ? filename : (0, path_2.join)(basePath, filename);
488
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
489
+ return { filename: filename, content: content };
490
+ }),
491
+ "conversationId": this.conversationId,
492
+ "sessionId": (0, uuid_1.v4)()
493
+ };
494
+ return JSON.stringify(fixRequest);
495
+ }
496
+ getErrorLine(errorDetails, sourceCode) {
497
+ const lines = sourceCode.split('\n');
498
+ const errorLine = lines[errorDetails.lineNumber - 1];
499
+ if (!errorLine) {
500
+ return "Error: Line number out of range.";
501
+ }
502
+ return errorLine;
503
+ }
406
504
  removePrefix(prefix, str) {
407
505
  if (str.startsWith(prefix)) {
408
506
  return str.slice(prefix.length);
@@ -412,18 +510,19 @@ class StormCodegen {
412
510
  /**
413
511
  * Sends the code to the AI for a fix
414
512
  */
415
- async codeFix(fix) {
513
+ async codeFix(fix, history) {
416
514
  return new Promise(async (resolve, reject) => {
417
- const fixStream = await stormClient_1.stormClient.createCodeFix(fix, []);
515
+ const fixStream = await stormClient_1.stormClient.createCodeFix(fix, history);
418
516
  fixStream.on('data', (evt) => {
419
517
  if (evt.type === 'CODE_FIX') {
420
- resolve(evt.payload.content);
518
+ resolve(evt.payload.content.files);
421
519
  }
422
520
  });
423
521
  this.out.on('aborted', () => {
424
522
  fixStream.abort();
425
523
  });
426
524
  fixStream.on('error', (err) => {
525
+ console.log("error", err);
427
526
  reject(err);
428
527
  });
429
528
  await fixStream.waitForDone();
@@ -455,7 +554,7 @@ class StormCodegen {
455
554
  created: Date.now(),
456
555
  payload: {
457
556
  filename: file.filename,
458
- path: (0, path_1.join)(basePath, file.filename),
557
+ path: (0, path_2.join)(basePath, file.filename),
459
558
  content: file.content,
460
559
  blockName: aiName,
461
560
  blockRef: ref,
@@ -475,7 +574,7 @@ class StormCodegen {
475
574
  created: Date.now(),
476
575
  payload: {
477
576
  filename: filename,
478
- path: (0, path_1.join)(basePath, filename),
577
+ path: (0, path_2.join)(basePath, filename),
479
578
  content: content,
480
579
  blockName,
481
580
  blockRef: ref,
@@ -111,14 +111,28 @@ export interface StormEventCodeFix {
111
111
  reason: string;
112
112
  created: number;
113
113
  payload: {
114
- filename: string;
115
- content: string;
114
+ content: {
115
+ files: StormEventErrorDetailsFile[];
116
+ };
116
117
  };
117
118
  }
118
119
  export interface StormEventErrorClassifierInfo {
119
120
  error: string;
120
121
  filename: string;
121
- potentialFix: string;
122
+ lineNumber: number;
123
+ column: number;
124
+ }
125
+ export interface StormEventErrorDetailsFile {
126
+ filename: string;
127
+ content: string;
128
+ }
129
+ export interface StormEventErrorDetails {
130
+ type: 'ERROR_DETAILS';
131
+ reason: string;
132
+ created: number;
133
+ payload: {
134
+ files: string[];
135
+ };
122
136
  }
123
137
  export interface ScreenTemplate {
124
138
  name: string;
@@ -218,4 +232,4 @@ export interface StormEventPhases {
218
232
  phaseType: StormEventPhaseType;
219
233
  };
220
234
  }
221
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady | StormEventPhases;
235
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases;
@@ -11,6 +11,7 @@ declare class StormClient {
11
11
  createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
12
12
  createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
13
13
  createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
14
+ createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
14
15
  }
15
16
  export declare const stormClient: StormClient;
16
17
  export {};
@@ -100,5 +100,11 @@ class StormClient {
100
100
  prompt,
101
101
  });
102
102
  }
103
+ createErrorDetails(prompt, history, conversationId) {
104
+ return this.send('/v2/code/errordetails', {
105
+ conversationId: conversationId,
106
+ prompt,
107
+ });
108
+ }
103
109
  }
104
110
  exports.stormClient = new StormClient();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.52.0",
3
+ "version": "0.52.1",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
5
 
6
- import { Definition } from '@kapeta/local-cluster-config';
6
+ import {Definition} from '@kapeta/local-cluster-config';
7
7
  import {
8
8
  AIFileTypes,
9
9
  BlockCodeGenerator,
@@ -13,22 +13,36 @@ import {
13
13
  GeneratedResult,
14
14
  MODE_CREATE_ONLY,
15
15
  } from '@kapeta/codegen';
16
- import { BlockDefinition } from '@kapeta/schemas';
17
- import { codeGeneratorManager } from '../codeGeneratorManager';
18
- import { STORM_ID, stormClient } from './stormClient';
19
- import { StormEvent, StormEventFileChunk, StormEventFileDone, StormEventFileLogical } from './events';
20
- import { BlockDefinitionInfo, StormEventParser } from './event-parser';
21
- import { StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
22
- import { KapetaURI, parseKapetaUri } from '@kapeta/nodejs-utils';
23
- import { writeFile } from 'fs/promises';
24
- import path, { join } from 'path';
16
+ import {BlockDefinition} from '@kapeta/schemas';
17
+ import {codeGeneratorManager} from '../codeGeneratorManager';
18
+ import {STORM_ID, stormClient} from './stormClient';
19
+ import {
20
+ StormEvent,
21
+ StormEventErrorDetailsFile,
22
+ StormEventFileChunk,
23
+ StormEventFileDone,
24
+ StormEventFileLogical
25
+ } from './events';
26
+ import {BlockDefinitionInfo, StormEventParser} from './event-parser';
27
+ import {ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream} from './stream';
28
+ import {KapetaURI, parseKapetaUri} from '@kapeta/nodejs-utils';
29
+ import {writeFile} from 'fs/promises';
30
+ import path from 'path';
31
+ import Path, {join} from 'path';
25
32
  import os from 'node:os';
26
- import { readFileSync, writeFileSync } from 'fs';
27
- import Path from 'path';
33
+ import {readFileSync, writeFileSync} from 'fs';
28
34
  import YAML from "yaml";
35
+ import * as fs from "node:fs";
36
+ import {v4 as uuidv4} from 'uuid';
29
37
 
30
38
  type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
31
39
 
40
+ interface ErrorClassification {
41
+ error: string;
42
+ lineNumber: number;
43
+ column: number;
44
+ }
45
+
32
46
  const SIMULATED_DELAY = 1000;
33
47
 
34
48
  class SimulatedFileDelay {
@@ -321,7 +335,7 @@ export class StormCodegen {
321
335
  (file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
322
336
  );
323
337
 
324
- // Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
338
+ // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
325
339
  const serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
326
340
  if (serviceFiles.length > 0) {
327
341
  await this.processTemplates(
@@ -366,8 +380,9 @@ export class StormCodegen {
366
380
  type: AIFileTypes.WEB_SCREEN,
367
381
  };
368
382
  });
369
- const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
370
383
  allFiles.push(...screenFilesConverted);
384
+
385
+ const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
371
386
  const codeGenerator = new BlockCodeGenerator(block.content as BlockDefinition);
372
387
  await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
373
388
 
@@ -389,7 +404,7 @@ export class StormCodegen {
389
404
  codeGenerator: CodeGenerator,
390
405
  basePath: string,
391
406
  filesToBeFixed: StormFileInfo[],
392
- knownFiles: StormFileInfo[]
407
+ allFiles: StormFileInfo[]
393
408
  ) {
394
409
  let attempts = 0;
395
410
  let validCode = false;
@@ -405,46 +420,18 @@ export class StormCodegen {
405
420
 
406
421
  if (result && !result.valid) {
407
422
  console.debug('Validation error:', result);
408
- const errorStream = await stormClient.createErrorClassification(result.error, []);
409
- const fixes = new Map<string, Promise<string>>();
410
-
411
- this.out.on('aborted', () => {
412
- errorStream.abort();
413
- });
414
-
415
- errorStream.on('data', (evt) => {
416
- if (evt.type === 'ERROR_CLASSIFIER') {
417
- // find the file that caused the error
418
- // strip base path from event file name, if it exists sometimes the AI sends the full path
419
- const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
420
- const file = filesToBeFixed.find((f) => f.filename === eventFileName);
421
- if (!file) {
422
- console.log(
423
- `Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`
424
- );
425
- }
426
- // read the content of the file
427
- const content = readFileSync(join(basePath, eventFileName), 'utf8');
428
- const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
429
- .map((e) => e.filename)
430
- .join('\n')}\n---\n${content}`;
431
- //console.log(`trying to fix the code in ${eventFileName}`);
432
- //console.debug(`with the fix:\n${fix}`);
433
- const code = this.codeFix(fix);
434
- fixes.set(join(basePath, eventFileName), code);
435
- }
436
- });
437
-
438
- await errorStream.waitForDone();
439
- for (const [filename, codePromise] of fixes) {
440
- const code = await codePromise;
441
- writeFileSync(filename, code);
423
+
424
+ const errors = await this.classifyErrors(result.error, basePath);
425
+ for (const [filename, fileErrors] of errors.entries()) {
426
+ // todo: only try to fix file if it is part of filesToBeFixed
427
+ await this.tryToFixFile(basePath, filename, fileErrors, allFiles, codeGenerator);
442
428
  }
443
429
  }
444
430
  } catch (e) {
445
431
  console.error('Error:', e);
446
432
  }
447
433
  }
434
+
448
435
  if (validCode) {
449
436
  console.log(`Validation successful after ${attempts} attempts`);
450
437
  } else {
@@ -452,6 +439,152 @@ export class StormCodegen {
452
439
  }
453
440
  }
454
441
 
442
+ private async tryToFixFile(basePath: string, filename: string, fileErrors: ErrorClassification[], allFiles: StormFileInfo[], codeGenerator: CodeGenerator) {
443
+ console.log(`Processing ${filename}`);
444
+ const language = await codeGenerator.language();
445
+ const relevantFiles = allFiles.filter(file => file.type != AIFileTypes.IGNORE);
446
+
447
+ for (let attempts = 1; attempts <= 5; attempts++) {
448
+ if (fileErrors.length == 0) {
449
+ console.log(`No more errors for ${filename}`);
450
+ return;
451
+ }
452
+
453
+ console.log(`Errors in ${filename} - requesting error details`);
454
+ const filesForContext = await this.getErrorDetailsForFile(basePath, filename, fileErrors[0], relevantFiles, language);
455
+ console.log(`Get error details for ${filename} requesting code fixes`);
456
+
457
+ const fix = this.createFixRequestForFile(basePath, filename, fileErrors[0], filesForContext, relevantFiles, language);
458
+ const codeFixFiles = await this.codeFix(fix);
459
+ console.log(`Got fixed code for ${filename}`);
460
+ for (const codeFixFile of codeFixFiles) {
461
+ const filePath = codeFixFile.filename.indexOf(basePath) > -1 ? codeFixFile.filename : join(basePath, codeFixFile.filename);
462
+ const existing = readFileSync(filePath);
463
+ if (existing.toString().replace(/(\r\n|\r|\n)+$/, '') == codeFixFile.content.replace(/(\r\n|\r|\n)+$/, '')) {
464
+ console.log(`${filename} not changed by gemini`);
465
+ continue;
466
+ }
467
+ writeFileSync(filePath, codeFixFile.content);
468
+ }
469
+
470
+ const result = await codeGenerator.validateForTarget(basePath);
471
+ if (result && result.valid) {
472
+ return;
473
+ }
474
+
475
+ const errors = await this.classifyErrors(result.error, basePath);
476
+ fileErrors = errors.get(filename) ?? [];
477
+ }
478
+ }
479
+
480
+ private async classifyErrors(errors: string, basePath: string): Promise<Map<string, ErrorClassification[]>> {
481
+ const errorStream = await stormClient.createErrorClassification(errors, []);
482
+ const fixes = new Map<string, ErrorClassification[]>();
483
+
484
+ this.out.on('aborted', () => {
485
+ errorStream.abort();
486
+ });
487
+
488
+ errorStream.on('data', (evt) => {
489
+ if (evt.type === 'ERROR_CLASSIFIER') {
490
+ const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
491
+ const fix = { error: evt.payload.error, lineNumber: evt.payload.lineNumber, column: evt.payload.column };
492
+
493
+ let existingFixes = fixes.get(eventFileName);
494
+ if (existingFixes) {
495
+ existingFixes.push(fix);
496
+ } else {
497
+ fixes.set(eventFileName, [fix]);
498
+ }
499
+ }
500
+ });
501
+
502
+ await errorStream.waitForDone();
503
+
504
+ return fixes;
505
+ }
506
+
507
+ private async getErrorDetailsForFile(basePath: string, filename: string, error: ErrorClassification, allFiles: StormFileInfo[], language: string): Promise<string[]> {
508
+ const filePath = filename.indexOf(basePath) > -1 ? filename : join(basePath, filename); // to compensate when compiler returns absolute path
509
+ return new Promise<string[]>(async (resolve, reject) => {
510
+ const request = {
511
+ "language": language,
512
+ "sourceFile": {
513
+ "filename": filename,
514
+ "content": readFileSync(filePath, 'utf8')
515
+ },
516
+ "error": error,
517
+ "projectFiles": allFiles.map(f => f.filename)
518
+ };
519
+
520
+ const detailsStream = await stormClient.createErrorDetails(JSON.stringify(request), []);
521
+ detailsStream.on('data', (evt) => {
522
+ if (evt.type === 'ERROR_DETAILS') {
523
+ resolve(evt.payload.files);
524
+ }
525
+ });
526
+ this.out.on('aborted', () => {
527
+ detailsStream.abort();
528
+ });
529
+ detailsStream.on('error', (err) => {
530
+ console.log("error", err);
531
+ reject(err);
532
+ });
533
+ await detailsStream.waitForDone();
534
+ });
535
+ }
536
+
537
+ private createFixRequestForFile(basePath: string, filename: string, error: ErrorClassification, filesForContext: string[], allFiles: StormFileInfo[], language: string): string {
538
+ const files = new Set(filesForContext);
539
+ files.add(filename);
540
+
541
+ const requestedFiles = Array.from(files).flatMap(file => {
542
+ if (fs.existsSync(file)) {
543
+ return file;
544
+ }
545
+
546
+ // file does not exist - look for similar
547
+ const candidateName = file.split('/').pop();
548
+ return allFiles
549
+ .filter(file => file.filename.split('/').pop() === candidateName)
550
+ .map(f => f.filename);
551
+ });
552
+
553
+ const filePath = filename.indexOf(basePath) > -1 ? filename : join(basePath, filename);
554
+ const content = readFileSync(filePath, 'utf8');
555
+ const affectedLine = this.getErrorLine(error, content);
556
+
557
+ const fixRequest = {
558
+ "language": language,
559
+ "filename": filename,
560
+ "errors": error.error,
561
+ "affectedLine": affectedLine,
562
+ "projectFiles": requestedFiles.map(filename => {
563
+ const filePath = filename.indexOf(basePath) > -1 ? filename : join(basePath, filename);
564
+ const content = readFileSync(filePath, 'utf8');
565
+ return { filename: filename, content: content };
566
+ }),
567
+ "conversationId": this.conversationId,
568
+ "sessionId": uuidv4()
569
+ };
570
+
571
+ return JSON.stringify(fixRequest);
572
+ }
573
+
574
+ private getErrorLine(
575
+ errorDetails: ErrorClassification,
576
+ sourceCode: string
577
+ ): string {
578
+ const lines = sourceCode.split('\n');
579
+ const errorLine = lines[errorDetails.lineNumber - 1];
580
+
581
+ if (!errorLine) {
582
+ return "Error: Line number out of range.";
583
+ }
584
+
585
+ return errorLine;
586
+ }
587
+
455
588
  removePrefix(prefix: string, str: string): string {
456
589
  if (str.startsWith(prefix)) {
457
590
  return str.slice(prefix.length);
@@ -462,18 +595,19 @@ export class StormCodegen {
462
595
  /**
463
596
  * Sends the code to the AI for a fix
464
597
  */
465
- private async codeFix(fix: string): Promise<string> {
466
- return new Promise<string>(async (resolve, reject) => {
467
- const fixStream = await stormClient.createCodeFix(fix, []);
598
+ private async codeFix(fix: string, history?: ConversationItem[]): Promise<StormEventErrorDetailsFile[]> {
599
+ return new Promise<StormEventErrorDetailsFile[]>(async (resolve, reject) => {
600
+ const fixStream = await stormClient.createCodeFix(fix, history);
468
601
  fixStream.on('data', (evt) => {
469
602
  if (evt.type === 'CODE_FIX') {
470
- resolve(evt.payload.content);
603
+ resolve(evt.payload.content.files);
471
604
  }
472
605
  });
473
606
  this.out.on('aborted', () => {
474
607
  fixStream.abort();
475
608
  });
476
609
  fixStream.on('error', (err) => {
610
+ console.log("error", err);
477
611
  reject(err);
478
612
  });
479
613
  await fixStream.waitForDone();
@@ -139,14 +139,31 @@ export interface StormEventCodeFix {
139
139
  reason: string;
140
140
  created: number;
141
141
  payload: {
142
- filename: string;
143
- content: string;
142
+ content: {
143
+ files: StormEventErrorDetailsFile[]
144
+ }
144
145
  };
145
146
  }
147
+
146
148
  export interface StormEventErrorClassifierInfo {
147
149
  error: string;
148
150
  filename: string;
149
- potentialFix: string;
151
+ lineNumber: number;
152
+ column: number;
153
+ }
154
+
155
+ export interface StormEventErrorDetailsFile {
156
+ filename: string;
157
+ content: string;
158
+ }
159
+
160
+ export interface StormEventErrorDetails {
161
+ type: 'ERROR_DETAILS';
162
+ reason: string;
163
+ created: number;
164
+ payload: {
165
+ files: string[]
166
+ };
150
167
  }
151
168
 
152
169
  export interface ScreenTemplate {
@@ -280,5 +297,6 @@ export type StormEvent =
280
297
  | StormEventDefinitionChange
281
298
  | StormEventErrorClassifier
282
299
  | StormEventCodeFix
300
+ | StormEventErrorDetails
283
301
  | StormEventBlockReady
284
302
  | StormEventPhases;
@@ -125,12 +125,20 @@ class StormClient {
125
125
  prompt,
126
126
  });
127
127
  }
128
+
128
129
  public createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string) {
129
130
  return this.send('/v2/code/fix', {
130
131
  conversationId: conversationId,
131
132
  prompt,
132
133
  });
133
134
  }
135
+
136
+ public createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string) {
137
+ return this.send('/v2/code/errordetails', {
138
+ conversationId: conversationId,
139
+ prompt,
140
+ });
141
+ }
134
142
  }
135
143
 
136
144
  export const stormClient = new StormClient();