@kapeta/local-cluster-service 0.52.0 → 0.52.2

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.
@@ -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;
@@ -121,7 +121,6 @@ router.post('/block/create', async (req, res) => {
121
121
  res.send(asset);
122
122
  }
123
123
  catch (err) {
124
- console.error('err');
125
124
  res.status(500).send({ error: err.message });
126
125
  }
127
126
  });
@@ -161,26 +160,14 @@ function sendError(err, res) {
161
160
  }
162
161
  }
163
162
  function waitForStormStream(result) {
164
- return new Promise((resolve, reject) => {
165
- result.on('error', (err) => {
166
- reject(err);
167
- });
168
- result.on('end', () => {
169
- resolve();
170
- });
171
- });
163
+ return result.waitForDone();
172
164
  }
173
165
  function streamStormPartialResponse(result, res) {
174
- return new Promise((resolve, reject) => {
166
+ return new Promise((resolve) => {
175
167
  result.on('data', (data) => {
176
168
  sendEvent(res, data);
177
169
  });
178
- result.on('error', (err) => {
179
- reject(err);
180
- });
181
- result.on('end', () => {
182
- resolve();
183
- });
170
+ resolve(result.waitForDone());
184
171
  });
185
172
  }
186
173
  function sendEvent(res, evt) {
@@ -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();
@@ -44,12 +44,18 @@ class StormStream extends node_events_1.EventEmitter {
44
44
  }
45
45
  waitForDone() {
46
46
  return new Promise((resolve, reject) => {
47
- this.on('error', (err) => {
47
+ const errorHandler = (err) => {
48
+ this.removeListener('error', errorHandler);
49
+ this.removeListener('end', endHandler);
48
50
  reject(err);
49
- });
50
- this.on('end', () => {
51
+ };
52
+ const endHandler = () => {
53
+ this.removeListener('error', errorHandler);
54
+ this.removeListener('end', endHandler);
51
55
  resolve();
52
- });
56
+ };
57
+ this.once('error', errorHandler);
58
+ this.once('end', endHandler);
53
59
  });
54
60
  }
55
61
  abort() {
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.2",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {