@kapeta/local-cluster-service 0.48.4 → 0.49.0

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.
Files changed (35) hide show
  1. package/.vscode/launch.json +1 -1
  2. package/CHANGELOG.md +14 -0
  3. package/dist/cjs/src/assetManager.d.ts +1 -1
  4. package/dist/cjs/src/assetManager.js +5 -3
  5. package/dist/cjs/src/filesystem/routes.js +10 -0
  6. package/dist/cjs/src/storm/codegen.d.ts +9 -1
  7. package/dist/cjs/src/storm/codegen.js +111 -8
  8. package/dist/cjs/src/storm/event-parser.d.ts +1 -1
  9. package/dist/cjs/src/storm/event-parser.js +3 -1
  10. package/dist/cjs/src/storm/events.d.ts +32 -1
  11. package/dist/cjs/src/storm/routes.js +26 -1
  12. package/dist/cjs/src/storm/stormClient.d.ts +3 -1
  13. package/dist/cjs/src/storm/stormClient.js +12 -0
  14. package/dist/cjs/src/storm/stream.d.ts +6 -0
  15. package/dist/esm/src/assetManager.d.ts +1 -1
  16. package/dist/esm/src/assetManager.js +5 -3
  17. package/dist/esm/src/filesystem/routes.js +10 -0
  18. package/dist/esm/src/storm/codegen.d.ts +9 -1
  19. package/dist/esm/src/storm/codegen.js +111 -8
  20. package/dist/esm/src/storm/event-parser.d.ts +1 -1
  21. package/dist/esm/src/storm/event-parser.js +3 -1
  22. package/dist/esm/src/storm/events.d.ts +32 -1
  23. package/dist/esm/src/storm/routes.js +26 -1
  24. package/dist/esm/src/storm/stormClient.d.ts +3 -1
  25. package/dist/esm/src/storm/stormClient.js +12 -0
  26. package/dist/esm/src/storm/stream.d.ts +6 -0
  27. package/package.json +1 -1
  28. package/src/assetManager.ts +6 -4
  29. package/src/filesystem/routes.ts +10 -0
  30. package/src/storm/codegen.ts +132 -12
  31. package/src/storm/event-parser.ts +4 -2
  32. package/src/storm/events.ts +38 -1
  33. package/src/storm/routes.ts +39 -2
  34. package/src/storm/stormClient.ts +13 -1
  35. package/src/storm/stream.ts +7 -0
@@ -36,25 +36,31 @@ const codeGeneratorManager_1 = require("../codeGeneratorManager");
36
36
  const stormClient_1 = require("./stormClient");
37
37
  const event_parser_1 = require("./event-parser");
38
38
  const stream_1 = require("./stream");
39
+ const nodejs_utils_1 = require("@kapeta/nodejs-utils");
39
40
  const promises_1 = require("fs/promises");
40
41
  const path_1 = __importStar(require("path"));
41
42
  const node_os_1 = __importDefault(require("node:os"));
43
+ const fs_1 = require("fs");
44
+ const path_2 = __importDefault(require("path"));
42
45
  class StormCodegen {
43
46
  userPrompt;
44
47
  blocks;
45
48
  out = new stream_1.StormStream();
46
49
  events;
47
50
  tmpDir;
48
- constructor(userPrompt, blocks, events) {
51
+ conversationId;
52
+ constructor(conversationId, userPrompt, blocks, events) {
49
53
  this.userPrompt = userPrompt;
50
54
  this.blocks = blocks;
51
55
  this.events = events;
52
- this.tmpDir = node_os_1.default.tmpdir();
56
+ this.tmpDir = path_2.default.join(node_os_1.default.tmpdir(), conversationId);
57
+ this.conversationId = conversationId;
53
58
  }
54
59
  async process() {
55
- for (const block of this.blocks) {
56
- await this.processBlockCode(block);
57
- }
60
+ const promises = this.blocks.map((block) => {
61
+ return this.processBlockCode(block);
62
+ });
63
+ await Promise.all(promises);
58
64
  this.out.end();
59
65
  }
60
66
  getStream() {
@@ -118,7 +124,7 @@ class StormCodegen {
118
124
  }
119
125
  const allFiles = this.toStormFiles(generatedResult);
120
126
  // Send all the non-ai files to the stream
121
- this.emitFiles(block.uri, block.aiName, allFiles);
127
+ this.emitFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
122
128
  const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
123
129
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
124
130
  if (uiTemplates.length > 0) {
@@ -130,7 +136,7 @@ class StormCodegen {
130
136
  prompt: this.userPrompt,
131
137
  });
132
138
  uiStream.on('data', (evt) => {
133
- this.handleUiOutput(block.uri, block.aiName, evt);
139
+ this.handleUiOutput((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, evt);
134
140
  });
135
141
  await uiStream.waitForDone();
136
142
  }
@@ -139,7 +145,7 @@ class StormCodegen {
139
145
  // Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
140
146
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
141
147
  if (serviceFiles.length > 0) {
142
- await this.processTemplates(block.uri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
148
+ await this.processTemplates((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
143
149
  }
144
150
  const basePath = this.getBasePath(block.content.metadata.name);
145
151
  for (const serviceFile of serviceFiles) {
@@ -154,6 +160,103 @@ class StormCodegen {
154
160
  const filePath = (0, path_1.join)(basePath, uiFile.filename);
155
161
  await (0, promises_1.writeFile)(filePath, uiFile.content);
156
162
  }
163
+ const filesToBeFixed = serviceFiles.concat(contextFiles);
164
+ const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
165
+ await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
166
+ const blockRef = block.uri;
167
+ this.out.emit('data', {
168
+ type: 'BLOCK_READY',
169
+ reason: 'Block ready',
170
+ created: Date.now(),
171
+ payload: {
172
+ path: basePath,
173
+ blockName: block.aiName,
174
+ blockRef,
175
+ instanceId: event_parser_1.StormEventParser.toInstanceIdFromRef(blockRef),
176
+ },
177
+ });
178
+ }
179
+ async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, knownFiles) {
180
+ let attempts = 0;
181
+ let validCode = false;
182
+ for (let i = 0; i <= 3; i++) {
183
+ attempts++;
184
+ try {
185
+ console.log(`Validating the code in ${basePath} attempt #${attempts}`);
186
+ const result = await codeGenerator.validateForTarget(basePath);
187
+ if (result && result.valid) {
188
+ validCode = true;
189
+ break;
190
+ }
191
+ if (result && !result.valid) {
192
+ console.debug('Validation error:', result);
193
+ const errorStream = await stormClient_1.stormClient.createErrorClassification(result.error, []);
194
+ const fixes = new Map();
195
+ errorStream.on('data', (evt) => {
196
+ if (evt.type === 'ERROR_CLASSIFIER') {
197
+ // find the file that caused the error
198
+ // strip base path from event file name, if it exists sometimes the AI sends the full path
199
+ const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
200
+ const file = filesToBeFixed.find((f) => f.filename === eventFileName);
201
+ if (!file) {
202
+ 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`);
203
+ }
204
+ // read the content of the file
205
+ const content = (0, fs_1.readFileSync)((0, path_1.join)(basePath, eventFileName), 'utf8');
206
+ const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
207
+ .map((e) => e.filename)
208
+ .join('\n')}\n---\n${content}`;
209
+ console.log(`trying to fix the code in ${eventFileName}`);
210
+ console.debug(`with the fix:\n${fix}`);
211
+ const code = this.codeFix(fix);
212
+ fixes.set((0, path_1.join)(basePath, eventFileName), code);
213
+ }
214
+ });
215
+ await errorStream.waitForDone();
216
+ for (const [filename, codePromise] of fixes) {
217
+ const code = await codePromise;
218
+ (0, fs_1.writeFileSync)(filename, code);
219
+ }
220
+ }
221
+ }
222
+ catch (e) {
223
+ console.error('Error:', e);
224
+ }
225
+ }
226
+ if (validCode) {
227
+ console.log(`Validation successful after ${attempts} attempts`);
228
+ }
229
+ else {
230
+ console.error(`Validation failed for ${basePath} after ${attempts} attempts`);
231
+ }
232
+ }
233
+ removePrefix(prefix, str) {
234
+ if (str.startsWith(prefix)) {
235
+ return str.slice(prefix.length);
236
+ }
237
+ return str;
238
+ }
239
+ async writeToFile(fileName, code) {
240
+ console.log(`writing the fixed code to ${fileName}`);
241
+ const resolvedCode = await code;
242
+ (0, fs_1.writeFileSync)(fileName, resolvedCode);
243
+ }
244
+ /**
245
+ * Sends the code to the AI for a fix
246
+ */
247
+ async codeFix(fix) {
248
+ return new Promise(async (resolve, reject) => {
249
+ const fixStream = await stormClient_1.stormClient.createCodeFix(fix, []);
250
+ fixStream.on('data', (evt) => {
251
+ if (evt.type === 'CODE_FIX') {
252
+ resolve(evt.payload.content);
253
+ }
254
+ });
255
+ fixStream.on('error', (err) => {
256
+ reject(err);
257
+ });
258
+ await fixStream.waitForDone();
259
+ });
157
260
  }
158
261
  /**
159
262
  * Emits the text-based files to the stream
@@ -6,7 +6,7 @@ import { StormEvent } from './events';
6
6
  import { BlockDefinition, Plan } from '@kapeta/schemas';
7
7
  import { KapetaURI } from '@kapeta/nodejs-utils';
8
8
  export interface BlockDefinitionInfo {
9
- uri: KapetaURI;
9
+ uri: string;
10
10
  content: BlockDefinition;
11
11
  aiName: string;
12
12
  }
@@ -315,6 +315,8 @@ class StormEventParser {
315
315
  name: planRef.fullName,
316
316
  title: this.planName,
317
317
  description: this.planDescription,
318
+ structure: 'mono',
319
+ visibility: 'private',
318
320
  },
319
321
  spec: {
320
322
  blocks,
@@ -331,7 +333,7 @@ class StormEventParser {
331
333
  Object.entries(this.blocks).forEach(([, blockInfo]) => {
332
334
  const blockRef = StormEventParser.toRef(handle, blockInfo.name);
333
335
  const blockDefinitionInfo = {
334
- uri: blockRef,
336
+ uri: blockRef.toNormalizedString(),
335
337
  aiName: blockInfo.name,
336
338
  content: {
337
339
  kind: this.toBlockKind(blockInfo.type),
@@ -100,6 +100,26 @@ export interface StormEventError {
100
100
  error: string;
101
101
  };
102
102
  }
103
+ export interface StormEventErrorClassifier {
104
+ type: 'ERROR_CLASSIFIER';
105
+ reason: string;
106
+ created: number;
107
+ payload: StormEventErrorClassifierInfo;
108
+ }
109
+ export interface StormEventCodeFix {
110
+ type: 'CODE_FIX';
111
+ reason: string;
112
+ created: number;
113
+ payload: {
114
+ filename: string;
115
+ content: string;
116
+ };
117
+ }
118
+ export interface StormEventErrorClassifierInfo {
119
+ error: string;
120
+ filename: string;
121
+ potentialFix: string;
122
+ }
103
123
  export interface ScreenTemplate {
104
124
  name: string;
105
125
  template: string;
@@ -143,6 +163,17 @@ export interface StormEventFile {
143
163
  instanceId: string;
144
164
  };
145
165
  }
166
+ export interface StormEventBlockReady {
167
+ type: 'BLOCK_READY';
168
+ reason: string;
169
+ created: number;
170
+ payload: {
171
+ path: string;
172
+ blockName: string;
173
+ blockRef: string;
174
+ instanceId: string;
175
+ };
176
+ }
146
177
  export interface StormEventDone {
147
178
  type: 'DONE';
148
179
  created: number;
@@ -153,4 +184,4 @@ export interface StormEventDefinitionChange {
153
184
  created: number;
154
185
  payload: StormDefinitions;
155
186
  }
156
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange;
187
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventBlockReady;
@@ -8,11 +8,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  const express_promise_router_1 = __importDefault(require("express-promise-router"));
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
12
  const cors_1 = require("../middleware/cors");
12
13
  const stringBody_1 = require("../middleware/stringBody");
13
14
  const stormClient_1 = require("./stormClient");
14
15
  const event_parser_1 = require("./event-parser");
15
16
  const codegen_1 = require("./codegen");
17
+ const assetManager_1 = require("../assetManager");
18
+ const path_1 = __importDefault(require("path"));
16
19
  const router = (0, express_promise_router_1.default)();
17
20
  router.use('/', cors_1.corsHandler);
18
21
  router.use('/', stringBody_1.stringBody);
@@ -47,7 +50,7 @@ router.post('/:handle/all', async (req, res) => {
47
50
  const result = eventParser.toResult(handle);
48
51
  sendDefinitions(res, result);
49
52
  if (!req.query.skipCodegen) {
50
- const stormCodegen = new codegen_1.StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
53
+ const stormCodegen = new codegen_1.StormCodegen(metaStream.getConversationId(), aiRequest.prompt, result.blocks, eventParser.getEvents());
51
54
  const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
52
55
  await stormCodegen.process();
53
56
  await codegenPromise;
@@ -59,6 +62,28 @@ router.post('/:handle/all', async (req, res) => {
59
62
  res.end();
60
63
  }
61
64
  });
65
+ router.post('/block/create', async (req, res) => {
66
+ const createRequest = JSON.parse(req.stringBody ?? '{}');
67
+ try {
68
+ const ymlPath = path_1.default.join(createRequest.newPath, 'kapeta.yml');
69
+ console.log('Creating block at', ymlPath);
70
+ const [asset] = await assetManager_1.assetManager.createAsset(ymlPath, createRequest.definition);
71
+ if (await fs_extra_1.default.pathExists(createRequest.tmpPath)) {
72
+ console.log('Moving block from', createRequest.tmpPath, 'to', createRequest.newPath);
73
+ await fs_extra_1.default.move(createRequest.tmpPath, createRequest.newPath, {
74
+ overwrite: true,
75
+ });
76
+ console.log('Updating asset', asset.ref);
77
+ res.send(await assetManager_1.assetManager.updateAsset(asset.ref, createRequest.definition));
78
+ }
79
+ else {
80
+ res.send(asset);
81
+ }
82
+ }
83
+ catch (err) {
84
+ res.status(500).send({ error: err.message });
85
+ }
86
+ });
62
87
  function sendDefinitions(res, result) {
63
88
  sendEvent(res, {
64
89
  type: 'DEFINITION_CHANGE',
@@ -1,4 +1,4 @@
1
- import { StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
1
+ import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
2
2
  export declare const STORM_ID = "storm";
3
3
  export declare const ConversationIdHeader = "Conversation-Id";
4
4
  declare class StormClient {
@@ -9,6 +9,8 @@ declare class StormClient {
9
9
  createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
10
10
  createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
11
11
  createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
12
+ createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
13
+ createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
12
14
  }
13
15
  export declare const stormClient: StormClient;
14
16
  export {};
@@ -83,5 +83,17 @@ class StormClient {
83
83
  conversationId,
84
84
  });
85
85
  }
86
+ createErrorClassification(prompt, history, conversationId) {
87
+ return this.send('/v2/code/errorclassifier', {
88
+ conversationId: conversationId,
89
+ prompt,
90
+ });
91
+ }
92
+ createCodeFix(prompt, history, conversationId) {
93
+ return this.send('/v2/code/fix', {
94
+ conversationId: conversationId,
95
+ prompt,
96
+ });
97
+ }
86
98
  }
87
99
  exports.stormClient = new StormClient();
@@ -6,6 +6,7 @@
6
6
  import { EventEmitter } from 'node:events';
7
7
  import { StormEvent } from './events';
8
8
  import { AIFileTypes, GeneratedFile } from '@kapeta/codegen';
9
+ import { BlockDefinition } from '@kapeta/schemas';
9
10
  export declare class StormStream extends EventEmitter {
10
11
  private conversationId;
11
12
  private lines;
@@ -29,6 +30,11 @@ export interface StormContextRequest<T = string> {
29
30
  conversationId?: string;
30
31
  prompt: T;
31
32
  }
33
+ export interface StormCreateBlockRequest {
34
+ definition: BlockDefinition;
35
+ tmpPath: string;
36
+ newPath: string;
37
+ }
32
38
  export interface StormFileInfo extends GeneratedFile {
33
39
  type: AIFileTypes;
34
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.48.4",
3
+ "version": "0.49.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -179,7 +179,8 @@ class AssetManager {
179
179
  async createAsset(
180
180
  path: string,
181
181
  yaml: BlockDefinition,
182
- sourceOfChange: SourceOfChange = 'filesystem'
182
+ sourceOfChange: SourceOfChange = 'filesystem',
183
+ codegen: boolean = true
183
184
  ): Promise<EnrichedAsset[]> {
184
185
  if (await FS.pathExists(path)) {
185
186
  throw new Error('File already exists: ' + path);
@@ -199,11 +200,12 @@ class AssetManager {
199
200
  });
200
201
 
201
202
  definitionsManager.clearCache();
202
- console.log(`Created asset at: ${path}`);
203
-
204
203
  const ref = `kapeta://${yaml.metadata.name}:local`;
204
+ console.log(`Created asset ${ref} at: ${path}`);
205
205
 
206
- await this.maybeGenerateCode(ref, path, yaml);
206
+ if (codegen) {
207
+ await this.maybeGenerateCode(ref, path, yaml);
208
+ }
207
209
 
208
210
  return asset;
209
211
  }
@@ -8,6 +8,7 @@ import { stringBody, StringBodyRequest } from '../middleware/stringBody';
8
8
  import { filesystemManager } from '../filesystemManager';
9
9
  import { corsHandler } from '../middleware/cors';
10
10
  import { NextFunction, Request, Response } from 'express';
11
+ import FS from 'fs-extra';
11
12
 
12
13
  let router = Router();
13
14
 
@@ -99,6 +100,15 @@ router.get('/readfile', async (req: Request, res: Response) => {
99
100
  }
100
101
  });
101
102
 
103
+ router.get('/exists', async (req: Request, res: Response) => {
104
+ let pathArg = req.query.path as string;
105
+ try {
106
+ res.send(await FS.pathExists(pathArg));
107
+ } catch (err) {
108
+ res.status(400).send({ error: '' + err });
109
+ }
110
+ });
111
+
102
112
  router.put('/mkdir', async (req: Request, res: Response) => {
103
113
  let pathArg = req.query.path as string;
104
114
  try {
@@ -4,17 +4,26 @@
4
4
  */
5
5
 
6
6
  import { Definition } from '@kapeta/local-cluster-config';
7
- import { AIFileTypes, BlockCodeGenerator, CodeWriter, GeneratedFile, GeneratedResult } from '@kapeta/codegen';
7
+ import {
8
+ AIFileTypes,
9
+ BlockCodeGenerator,
10
+ CodeGenerator,
11
+ CodeWriter,
12
+ GeneratedFile,
13
+ GeneratedResult,
14
+ } from '@kapeta/codegen';
8
15
  import { BlockDefinition } from '@kapeta/schemas';
9
16
  import { codeGeneratorManager } from '../codeGeneratorManager';
10
17
  import { STORM_ID, stormClient } from './stormClient';
11
18
  import { StormEvent, StormEventFile } from './events';
12
19
  import { BlockDefinitionInfo, StormEventParser } from './event-parser';
13
- import { ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
14
- import { KapetaURI } from '@kapeta/nodejs-utils';
20
+ import { StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
21
+ import { KapetaURI, parseKapetaUri } from '@kapeta/nodejs-utils';
15
22
  import { writeFile } from 'fs/promises';
16
23
  import path, { join } from 'path';
17
24
  import os from 'node:os';
25
+ import { readFile, readFileSync, writeFileSync } from 'fs';
26
+ import Path from 'path';
18
27
 
19
28
  type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
20
29
 
@@ -24,19 +33,21 @@ export class StormCodegen {
24
33
  private readonly out = new StormStream();
25
34
  private readonly events: StormEvent[];
26
35
  private readonly tmpDir: string;
36
+ private readonly conversationId: string;
27
37
 
28
- constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
38
+ constructor(conversationId: string, userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
29
39
  this.userPrompt = userPrompt;
30
40
  this.blocks = blocks;
31
41
  this.events = events;
32
- this.tmpDir = os.tmpdir();
42
+ this.tmpDir = Path.join(os.tmpdir(), conversationId);
43
+ this.conversationId = conversationId;
33
44
  }
34
45
 
35
46
  public async process() {
36
- for (const block of this.blocks) {
37
- await this.processBlockCode(block);
38
- }
39
-
47
+ const promises = this.blocks.map((block) => {
48
+ return this.processBlockCode(block);
49
+ });
50
+ await Promise.all(promises);
40
51
  this.out.end();
41
52
  }
42
53
 
@@ -108,7 +119,7 @@ export class StormCodegen {
108
119
  const allFiles = this.toStormFiles(generatedResult);
109
120
 
110
121
  // Send all the non-ai files to the stream
111
- this.emitFiles(block.uri, block.aiName, allFiles);
122
+ this.emitFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
112
123
 
113
124
  const relevantFiles: StormFileInfo[] = allFiles.filter(
114
125
  (file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
@@ -124,7 +135,7 @@ export class StormCodegen {
124
135
  });
125
136
 
126
137
  uiStream.on('data', (evt) => {
127
- this.handleUiOutput(block.uri, block.aiName, evt);
138
+ this.handleUiOutput(parseKapetaUri(block.uri), block.aiName, evt);
128
139
  });
129
140
 
130
141
  await uiStream.waitForDone();
@@ -139,7 +150,7 @@ export class StormCodegen {
139
150
  const serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
140
151
  if (serviceFiles.length > 0) {
141
152
  await this.processTemplates(
142
- block.uri,
153
+ parseKapetaUri(block.uri),
143
154
  block.aiName,
144
155
  stormClient.createServiceImplementation.bind(stormClient),
145
156
  serviceFiles,
@@ -163,6 +174,115 @@ export class StormCodegen {
163
174
  const filePath = join(basePath, uiFile.filename);
164
175
  await writeFile(filePath, uiFile.content);
165
176
  }
177
+
178
+ const filesToBeFixed = serviceFiles.concat(contextFiles);
179
+ const codeGenerator = new BlockCodeGenerator(block.content as BlockDefinition);
180
+ await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
181
+
182
+ const blockRef = block.uri;
183
+ this.out.emit('data', {
184
+ type: 'BLOCK_READY',
185
+ reason: 'Block ready',
186
+ created: Date.now(),
187
+ payload: {
188
+ path: basePath,
189
+ blockName: block.aiName,
190
+ blockRef,
191
+ instanceId: StormEventParser.toInstanceIdFromRef(blockRef),
192
+ },
193
+ } satisfies StormEvent);
194
+ }
195
+
196
+ private async verifyAndFixCode(
197
+ codeGenerator: CodeGenerator,
198
+ basePath: string,
199
+ filesToBeFixed: StormFileInfo[],
200
+ knownFiles: StormFileInfo[]
201
+ ) {
202
+ let attempts = 0;
203
+ let validCode = false;
204
+ for (let i = 0; i <= 3; i++) {
205
+ attempts++;
206
+ try {
207
+ console.log(`Validating the code in ${basePath} attempt #${attempts}`);
208
+ const result = await codeGenerator.validateForTarget(basePath);
209
+ if (result && result.valid) {
210
+ validCode = true;
211
+ break;
212
+ }
213
+
214
+ if (result && !result.valid) {
215
+ console.debug('Validation error:', result);
216
+ const errorStream = await stormClient.createErrorClassification(result.error, []);
217
+ const fixes = new Map<string, Promise<string>>();
218
+
219
+ errorStream.on('data', (evt) => {
220
+ if (evt.type === 'ERROR_CLASSIFIER') {
221
+ // find the file that caused the error
222
+ // strip base path from event file name, if it exists sometimes the AI sends the full path
223
+ const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
224
+ const file = filesToBeFixed.find((f) => f.filename === eventFileName);
225
+ if (!file) {
226
+ console.log(
227
+ `Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`
228
+ );
229
+ }
230
+ // read the content of the file
231
+ const content = readFileSync(join(basePath, eventFileName), 'utf8');
232
+ const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
233
+ .map((e) => e.filename)
234
+ .join('\n')}\n---\n${content}`;
235
+ console.log(`trying to fix the code in ${eventFileName}`);
236
+ console.debug(`with the fix:\n${fix}`);
237
+ const code = this.codeFix(fix);
238
+ fixes.set(join(basePath, eventFileName), code);
239
+ }
240
+ });
241
+
242
+ await errorStream.waitForDone();
243
+ for (const [filename, codePromise] of fixes) {
244
+ const code = await codePromise;
245
+ writeFileSync(filename, code);
246
+ }
247
+ }
248
+ } catch (e) {
249
+ console.error('Error:', e);
250
+ }
251
+ }
252
+ if (validCode) {
253
+ console.log(`Validation successful after ${attempts} attempts`);
254
+ } else {
255
+ console.error(`Validation failed for ${basePath} after ${attempts} attempts`);
256
+ }
257
+ }
258
+
259
+ removePrefix(prefix: string, str: string): string {
260
+ if (str.startsWith(prefix)) {
261
+ return str.slice(prefix.length);
262
+ }
263
+ return str;
264
+ }
265
+ async writeToFile(fileName: string, code: Promise<string>) {
266
+ console.log(`writing the fixed code to ${fileName}`);
267
+ const resolvedCode = await code;
268
+ writeFileSync(fileName, resolvedCode);
269
+ }
270
+ /**
271
+ * Sends the code to the AI for a fix
272
+ */
273
+ private async codeFix(fix: string): Promise<string> {
274
+ return new Promise<string>(async (resolve, reject) => {
275
+ const fixStream = await stormClient.createCodeFix(fix, []);
276
+ fixStream.on('data', (evt) => {
277
+ if (evt.type === 'CODE_FIX') {
278
+ resolve(evt.payload.content);
279
+ }
280
+ });
281
+ fixStream.on('error', (err) => {
282
+ reject(err);
283
+ });
284
+ await fixStream.waitForDone();
285
+ });
166
286
  }
167
287
 
168
288
  /**
@@ -29,7 +29,7 @@ import { v5 as uuid } from 'uuid';
29
29
  import { definitionsManager } from '../definitionsManager';
30
30
 
31
31
  export interface BlockDefinitionInfo {
32
- uri: KapetaURI;
32
+ uri: string;
33
33
  content: BlockDefinition;
34
34
  aiName: string;
35
35
  }
@@ -454,6 +454,8 @@ export class StormEventParser {
454
454
  name: planRef.fullName,
455
455
  title: this.planName,
456
456
  description: this.planDescription,
457
+ structure: 'mono',
458
+ visibility: 'private',
457
459
  },
458
460
  spec: {
459
461
  blocks,
@@ -474,7 +476,7 @@ export class StormEventParser {
474
476
  const blockRef = StormEventParser.toRef(handle, blockInfo.name);
475
477
 
476
478
  const blockDefinitionInfo: BlockDefinitionInfo = {
477
- uri: blockRef,
479
+ uri: blockRef.toNormalizedString(),
478
480
  aiName: blockInfo.name,
479
481
  content: {
480
482
  kind: this.toBlockKind(blockInfo.type),