@kapeta/local-cluster-service 0.53.5 → 0.54.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/.eslintrc.cjs CHANGED
@@ -6,6 +6,7 @@ module.exports = {
6
6
  rules: {
7
7
  '@typescript-eslint/no-explicit-any': 'off',
8
8
  '@typescript-eslint/no-non-null-assertion': 'off',
9
+ '@typescript-eslint/no-use-before-define': 'off',
9
10
  '@typescript-eslint/no-unsafe-assignment': 'off',
10
11
  '@typescript-eslint/no-unsafe-member-access': 'off',
11
12
  '@typescript-eslint/no-unsafe-return': 'off',
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.54.1](https://github.com/kapetacom/local-cluster-service/compare/v0.54.0...v0.54.1) (2024-06-19)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * handle screen events by adding a meta-file to context + routes ([#180](https://github.com/kapetacom/local-cluster-service/issues/180)) ([69d67f8](https://github.com/kapetacom/local-cluster-service/commit/69d67f84fd7a373d49f5c20ff9057d8b2baa620a))
7
+
8
+ # [0.54.0](https://github.com/kapetacom/local-cluster-service/compare/v0.53.5...v0.54.0) (2024-06-17)
9
+
10
+
11
+ ### Features
12
+
13
+ * add support for split UI routes + screen context in AI service ([#178](https://github.com/kapetacom/local-cluster-service/issues/178)) ([4330a72](https://github.com/kapetacom/local-cluster-service/commit/4330a72eddb5fe82f9e7a9c2af412d47091cf9b0))
14
+
1
15
  ## [0.53.5](https://github.com/kapetacom/local-cluster-service/compare/v0.53.4...v0.53.5) (2024-06-17)
2
16
 
3
17
 
@@ -44,7 +44,6 @@ const path_2 = __importStar(require("path"));
44
44
  const node_os_1 = __importDefault(require("node:os"));
45
45
  const fs_1 = require("fs");
46
46
  const yaml_1 = __importDefault(require("yaml"));
47
- const fs = __importStar(require("node:fs"));
48
47
  const SIMULATED_DELAY = 1000;
49
48
  const ENABLE_SIMULATED_DELAY = false;
50
49
  class SimulatedFileDelay {
@@ -239,7 +238,9 @@ class StormCodegen {
239
238
  return;
240
239
  }
241
240
  const blockUri = (0, nodejs_utils_1.parseKapetaUri)(block.uri);
242
- const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
241
+ const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE &&
242
+ file.type !== codegen_1.AIFileTypes.WEB_SCREEN &&
243
+ file.type !== codegen_1.AIFileTypes.WEB_ROUTER);
243
244
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
244
245
  const screenFiles = [];
245
246
  let filteredEvents = [];
@@ -249,39 +250,110 @@ class StormCodegen {
249
250
  filteredEvents = [];
250
251
  }
251
252
  }
252
- if (uiTemplates.length > 0) {
253
- const uiStream = await stormClient_1.stormClient.createUIImplementation({
253
+ const screenEvents = [];
254
+ const getScreenEventsFile = () => ({
255
+ content: JSON.stringify(screenEvents),
256
+ filename: '<screens>.json',
257
+ type: codegen_1.AIFileTypes.CONFIG,
258
+ mode: codegen_1.MODE_WRITE_NEVER,
259
+ permissions: '0644',
260
+ });
261
+ const uiEvents = [];
262
+ // generate screens
263
+ if (uiTemplates.length) {
264
+ const screenStream = await stormClient_1.stormClient.listScreens({
254
265
  events: filteredEvents,
255
266
  templates: uiTemplates,
256
267
  context: relevantFiles,
257
268
  blockName: block.aiName,
258
269
  prompt: this.userPrompt,
259
270
  });
260
- uiStream.on('data', (evt) => {
261
- const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
262
- if (uiFile != undefined) {
263
- screenFiles.push(uiFile);
271
+ screenStream.on('data', (evt) => {
272
+ if (evt.type === 'SCREEN') {
273
+ screenEvents.push(evt);
264
274
  }
275
+ this.handleUiOutput(blockUri, block.aiName, evt);
265
276
  });
266
277
  this.out.on('aborted', () => {
267
- uiStream.abort();
278
+ screenStream.abort();
268
279
  });
269
- await uiStream.waitForDone();
280
+ await screenStream.waitForDone();
281
+ // screenfiles
282
+ const screenTemplates = screenEvents
283
+ .map((screenEvent) => ({
284
+ ...uiTemplates.find((template) => template.filename.endsWith(screenEvent.payload.template)),
285
+ filename: screenEvent.payload.filename,
286
+ }))
287
+ .filter((tpl) => !!tpl.content);
288
+ await Promise.all(screenTemplates.map(async (template) => {
289
+ const payload = {
290
+ events: filteredEvents,
291
+ blockName: block.aiName,
292
+ filename: template.filename,
293
+ template: template,
294
+ context: relevantFiles.concat([getScreenEventsFile()]),
295
+ prompt: this.userPrompt,
296
+ };
297
+ const uiStream = await stormClient_1.stormClient.createUIImplementation(payload);
298
+ uiStream.on('data', (evt) => {
299
+ const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
300
+ if (uiFile != undefined) {
301
+ screenFiles.push(uiFile);
302
+ }
303
+ uiEvents.push(evt);
304
+ });
305
+ this.out.on('aborted', () => {
306
+ uiStream.abort();
307
+ });
308
+ await uiStream.waitForDone();
309
+ }));
270
310
  }
271
311
  if (this.isAborted()) {
272
312
  return;
273
313
  }
314
+ const basePath = this.getBasePath(block.content.metadata.name);
315
+ const screenFilesConverted = screenFiles.map((screenFile) => {
316
+ return {
317
+ filename: screenFile.payload.filename,
318
+ content: screenFile.payload.content,
319
+ mode: codegen_1.MODE_CREATE_ONLY,
320
+ permissions: '0644',
321
+ type: codegen_1.AIFileTypes.WEB_SCREEN,
322
+ };
323
+ });
324
+ allFiles.push(...screenFilesConverted);
325
+ const webRouters = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_ROUTER);
326
+ await Promise.all(webRouters.map(async (webRouter) => {
327
+ const payload = {
328
+ filename: webRouter.filename,
329
+ template: webRouter,
330
+ context: screenFilesConverted.concat([getScreenEventsFile()]),
331
+ prompt: this.userPrompt,
332
+ };
333
+ const stream = await stormClient_1.stormClient.generateCode(payload);
334
+ stream.on('data', (evt) => {
335
+ this.handleTemplateFileOutput(blockUri, block.aiName, webRouter, evt);
336
+ });
337
+ this.out.on('aborted', () => {
338
+ stream.abort();
339
+ });
340
+ await stream.waitForDone();
341
+ }));
274
342
  // Gather the context files for implementation. These will be all be passed to the AI
275
- const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
343
+ const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN, codegen_1.AIFileTypes.WEB_ROUTER].includes(file.type));
276
344
  // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
277
345
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
278
346
  if (serviceFiles.length > 0) {
279
347
  await this.processTemplates(blockUri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
280
348
  }
281
- const basePath = this.getBasePath(block.content.metadata.name);
282
349
  if (this.isAborted()) {
283
350
  return;
284
351
  }
352
+ for (const screenFile of screenFilesConverted) {
353
+ // this.emitFile(blockUri, block.aiName, screenFile.filename, screenFile.content);
354
+ const filePath = (0, path_2.join)(basePath, screenFile.filename);
355
+ await (0, promises_1.writeFile)(filePath, screenFile.content);
356
+ }
285
357
  for (const serviceFile of serviceFiles) {
286
358
  const filePath = (0, path_2.join)(basePath, serviceFile.filename);
287
359
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
@@ -290,22 +362,13 @@ class StormCodegen {
290
362
  const filePath = (0, path_2.join)(basePath, serviceFile.filename);
291
363
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
292
364
  }
365
+ // Write again after modifications
366
+ for (const webRouterFile of webRouters) {
367
+ const filePath = (0, path_2.join)(basePath, webRouterFile.filename);
368
+ await (0, promises_1.writeFile)(filePath, webRouterFile.content);
369
+ }
293
370
  const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
294
371
  await (0, promises_1.writeFile)(kapetaYmlPath, yaml_1.default.stringify(block.content));
295
- for (const screenFile of screenFiles) {
296
- const filePath = (0, path_2.join)(basePath, screenFile.payload.filename);
297
- await (0, promises_1.writeFile)(filePath, screenFile.payload.content);
298
- }
299
- const screenFilesConverted = screenFiles.map((screenFile) => {
300
- return {
301
- filename: screenFile.payload.filename,
302
- content: screenFile.payload.content,
303
- mode: codegen_1.MODE_CREATE_ONLY,
304
- permissions: '0644',
305
- type: codegen_1.AIFileTypes.WEB_SCREEN,
306
- };
307
- });
308
- allFiles.push(...screenFilesConverted);
309
372
  const blockRef = block.uri;
310
373
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.QA);
311
374
  const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
@@ -466,7 +529,7 @@ class StormCodegen {
466
529
  const files = new Set(filesForContext);
467
530
  files.add(filename);
468
531
  const requestedFiles = Array.from(files).flatMap((file) => {
469
- if (fs.existsSync(file)) {
532
+ if ((0, fs_1.existsSync)(file)) {
470
533
  return file;
471
534
  }
472
535
  // file does not exist - look for similar
@@ -551,7 +614,7 @@ class StormCodegen {
551
614
  // They will need to be implemented by the AI
552
615
  return;
553
616
  }
554
- if (file.type === codegen_1.AIFileTypes.WEB_SCREEN) {
617
+ if ([codegen_1.AIFileTypes.WEB_ROUTER, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type)) {
555
618
  // Don't send the web screen files to the stream yet
556
619
  // They will need to be implemented by the AI
557
620
  return;
@@ -159,6 +159,7 @@ export interface StormEventScreen {
159
159
  template: string;
160
160
  description: string;
161
161
  url: string;
162
+ filename: string;
162
163
  };
163
164
  }
164
165
  export interface StormEventScreenCandidate {
@@ -1,4 +1,4 @@
1
- import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
1
+ import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
2
  export declare const STORM_ID = "storm";
3
3
  export declare const ConversationIdHeader = "Conversation-Id";
4
4
  declare class StormClient {
@@ -7,11 +7,13 @@ declare class StormClient {
7
7
  private createOptions;
8
8
  private send;
9
9
  createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
10
+ listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
10
11
  createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
11
12
  createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
12
13
  createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
13
14
  createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
14
15
  createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
16
+ generateCode(prompt: StormFileImplementationPrompt, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
15
17
  }
16
18
  export declare const stormClient: StormClient;
17
19
  export {};
@@ -80,6 +80,12 @@ class StormClient {
80
80
  conversationId,
81
81
  });
82
82
  }
83
+ listScreens(prompt, conversationId) {
84
+ return this.send('/v2/ui/list', {
85
+ prompt,
86
+ conversationId,
87
+ });
88
+ }
83
89
  createUIImplementation(prompt, conversationId) {
84
90
  return this.send('/v2/ui/merge', {
85
91
  prompt,
@@ -110,5 +116,11 @@ class StormClient {
110
116
  prompt,
111
117
  });
112
118
  }
119
+ generateCode(prompt, history, conversationId) {
120
+ return this.send('/v2/code/generate', {
121
+ conversationId: conversationId,
122
+ prompt,
123
+ });
124
+ }
113
125
  }
114
126
  exports.stormClient = new StormClient();
@@ -50,6 +50,14 @@ export interface StormFileImplementationPrompt {
50
50
  prompt: string;
51
51
  }
52
52
  export interface StormUIImplementationPrompt {
53
+ events: StormEvent[];
54
+ template: StormFileInfo;
55
+ filename: string;
56
+ context: StormFileInfo[];
57
+ blockName: string;
58
+ prompt: string;
59
+ }
60
+ export interface StormUIListPrompt {
53
61
  events: StormEvent[];
54
62
  templates: StormFileInfo[];
55
63
  context: StormFileInfo[];
@@ -44,7 +44,6 @@ const path_2 = __importStar(require("path"));
44
44
  const node_os_1 = __importDefault(require("node:os"));
45
45
  const fs_1 = require("fs");
46
46
  const yaml_1 = __importDefault(require("yaml"));
47
- const fs = __importStar(require("node:fs"));
48
47
  const SIMULATED_DELAY = 1000;
49
48
  const ENABLE_SIMULATED_DELAY = false;
50
49
  class SimulatedFileDelay {
@@ -239,7 +238,9 @@ class StormCodegen {
239
238
  return;
240
239
  }
241
240
  const blockUri = (0, nodejs_utils_1.parseKapetaUri)(block.uri);
242
- const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
241
+ const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE &&
242
+ file.type !== codegen_1.AIFileTypes.WEB_SCREEN &&
243
+ file.type !== codegen_1.AIFileTypes.WEB_ROUTER);
243
244
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
244
245
  const screenFiles = [];
245
246
  let filteredEvents = [];
@@ -249,39 +250,110 @@ class StormCodegen {
249
250
  filteredEvents = [];
250
251
  }
251
252
  }
252
- if (uiTemplates.length > 0) {
253
- const uiStream = await stormClient_1.stormClient.createUIImplementation({
253
+ const screenEvents = [];
254
+ const getScreenEventsFile = () => ({
255
+ content: JSON.stringify(screenEvents),
256
+ filename: '<screens>.json',
257
+ type: codegen_1.AIFileTypes.CONFIG,
258
+ mode: codegen_1.MODE_WRITE_NEVER,
259
+ permissions: '0644',
260
+ });
261
+ const uiEvents = [];
262
+ // generate screens
263
+ if (uiTemplates.length) {
264
+ const screenStream = await stormClient_1.stormClient.listScreens({
254
265
  events: filteredEvents,
255
266
  templates: uiTemplates,
256
267
  context: relevantFiles,
257
268
  blockName: block.aiName,
258
269
  prompt: this.userPrompt,
259
270
  });
260
- uiStream.on('data', (evt) => {
261
- const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
262
- if (uiFile != undefined) {
263
- screenFiles.push(uiFile);
271
+ screenStream.on('data', (evt) => {
272
+ if (evt.type === 'SCREEN') {
273
+ screenEvents.push(evt);
264
274
  }
275
+ this.handleUiOutput(blockUri, block.aiName, evt);
265
276
  });
266
277
  this.out.on('aborted', () => {
267
- uiStream.abort();
278
+ screenStream.abort();
268
279
  });
269
- await uiStream.waitForDone();
280
+ await screenStream.waitForDone();
281
+ // screenfiles
282
+ const screenTemplates = screenEvents
283
+ .map((screenEvent) => ({
284
+ ...uiTemplates.find((template) => template.filename.endsWith(screenEvent.payload.template)),
285
+ filename: screenEvent.payload.filename,
286
+ }))
287
+ .filter((tpl) => !!tpl.content);
288
+ await Promise.all(screenTemplates.map(async (template) => {
289
+ const payload = {
290
+ events: filteredEvents,
291
+ blockName: block.aiName,
292
+ filename: template.filename,
293
+ template: template,
294
+ context: relevantFiles.concat([getScreenEventsFile()]),
295
+ prompt: this.userPrompt,
296
+ };
297
+ const uiStream = await stormClient_1.stormClient.createUIImplementation(payload);
298
+ uiStream.on('data', (evt) => {
299
+ const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
300
+ if (uiFile != undefined) {
301
+ screenFiles.push(uiFile);
302
+ }
303
+ uiEvents.push(evt);
304
+ });
305
+ this.out.on('aborted', () => {
306
+ uiStream.abort();
307
+ });
308
+ await uiStream.waitForDone();
309
+ }));
270
310
  }
271
311
  if (this.isAborted()) {
272
312
  return;
273
313
  }
314
+ const basePath = this.getBasePath(block.content.metadata.name);
315
+ const screenFilesConverted = screenFiles.map((screenFile) => {
316
+ return {
317
+ filename: screenFile.payload.filename,
318
+ content: screenFile.payload.content,
319
+ mode: codegen_1.MODE_CREATE_ONLY,
320
+ permissions: '0644',
321
+ type: codegen_1.AIFileTypes.WEB_SCREEN,
322
+ };
323
+ });
324
+ allFiles.push(...screenFilesConverted);
325
+ const webRouters = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_ROUTER);
326
+ await Promise.all(webRouters.map(async (webRouter) => {
327
+ const payload = {
328
+ filename: webRouter.filename,
329
+ template: webRouter,
330
+ context: screenFilesConverted.concat([getScreenEventsFile()]),
331
+ prompt: this.userPrompt,
332
+ };
333
+ const stream = await stormClient_1.stormClient.generateCode(payload);
334
+ stream.on('data', (evt) => {
335
+ this.handleTemplateFileOutput(blockUri, block.aiName, webRouter, evt);
336
+ });
337
+ this.out.on('aborted', () => {
338
+ stream.abort();
339
+ });
340
+ await stream.waitForDone();
341
+ }));
274
342
  // Gather the context files for implementation. These will be all be passed to the AI
275
- const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type));
343
+ const contextFiles = relevantFiles.filter((file) => ![codegen_1.AIFileTypes.SERVICE, codegen_1.AIFileTypes.WEB_SCREEN, codegen_1.AIFileTypes.WEB_ROUTER].includes(file.type));
276
344
  // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
277
345
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
278
346
  if (serviceFiles.length > 0) {
279
347
  await this.processTemplates(blockUri, block.aiName, stormClient_1.stormClient.createServiceImplementation.bind(stormClient_1.stormClient), serviceFiles, contextFiles);
280
348
  }
281
- const basePath = this.getBasePath(block.content.metadata.name);
282
349
  if (this.isAborted()) {
283
350
  return;
284
351
  }
352
+ for (const screenFile of screenFilesConverted) {
353
+ // this.emitFile(blockUri, block.aiName, screenFile.filename, screenFile.content);
354
+ const filePath = (0, path_2.join)(basePath, screenFile.filename);
355
+ await (0, promises_1.writeFile)(filePath, screenFile.content);
356
+ }
285
357
  for (const serviceFile of serviceFiles) {
286
358
  const filePath = (0, path_2.join)(basePath, serviceFile.filename);
287
359
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
@@ -290,22 +362,13 @@ class StormCodegen {
290
362
  const filePath = (0, path_2.join)(basePath, serviceFile.filename);
291
363
  await (0, promises_1.writeFile)(filePath, serviceFile.content);
292
364
  }
365
+ // Write again after modifications
366
+ for (const webRouterFile of webRouters) {
367
+ const filePath = (0, path_2.join)(basePath, webRouterFile.filename);
368
+ await (0, promises_1.writeFile)(filePath, webRouterFile.content);
369
+ }
293
370
  const kapetaYmlPath = (0, path_2.join)(basePath, 'kapeta.yml');
294
371
  await (0, promises_1.writeFile)(kapetaYmlPath, yaml_1.default.stringify(block.content));
295
- for (const screenFile of screenFiles) {
296
- const filePath = (0, path_2.join)(basePath, screenFile.payload.filename);
297
- await (0, promises_1.writeFile)(filePath, screenFile.payload.content);
298
- }
299
- const screenFilesConverted = screenFiles.map((screenFile) => {
300
- return {
301
- filename: screenFile.payload.filename,
302
- content: screenFile.payload.content,
303
- mode: codegen_1.MODE_CREATE_ONLY,
304
- permissions: '0644',
305
- type: codegen_1.AIFileTypes.WEB_SCREEN,
306
- };
307
- });
308
- allFiles.push(...screenFilesConverted);
309
372
  const blockRef = block.uri;
310
373
  this.emitBlockStatus(blockUri, block.aiName, events_1.StormEventBlockStatusType.QA);
311
374
  const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
@@ -466,7 +529,7 @@ class StormCodegen {
466
529
  const files = new Set(filesForContext);
467
530
  files.add(filename);
468
531
  const requestedFiles = Array.from(files).flatMap((file) => {
469
- if (fs.existsSync(file)) {
532
+ if ((0, fs_1.existsSync)(file)) {
470
533
  return file;
471
534
  }
472
535
  // file does not exist - look for similar
@@ -551,7 +614,7 @@ class StormCodegen {
551
614
  // They will need to be implemented by the AI
552
615
  return;
553
616
  }
554
- if (file.type === codegen_1.AIFileTypes.WEB_SCREEN) {
617
+ if ([codegen_1.AIFileTypes.WEB_ROUTER, codegen_1.AIFileTypes.WEB_SCREEN].includes(file.type)) {
555
618
  // Don't send the web screen files to the stream yet
556
619
  // They will need to be implemented by the AI
557
620
  return;
@@ -159,6 +159,7 @@ export interface StormEventScreen {
159
159
  template: string;
160
160
  description: string;
161
161
  url: string;
162
+ filename: string;
162
163
  };
163
164
  }
164
165
  export interface StormEventScreenCandidate {
@@ -1,4 +1,4 @@
1
- import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt } from './stream';
1
+ import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
2
  export declare const STORM_ID = "storm";
3
3
  export declare const ConversationIdHeader = "Conversation-Id";
4
4
  declare class StormClient {
@@ -7,11 +7,13 @@ declare class StormClient {
7
7
  private createOptions;
8
8
  private send;
9
9
  createMetadata(prompt: string, conversationId?: string): Promise<StormStream>;
10
+ listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
10
11
  createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string): Promise<StormStream>;
11
12
  createServiceImplementation(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
12
13
  createErrorClassification(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
13
14
  createCodeFix(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
14
15
  createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
16
+ generateCode(prompt: StormFileImplementationPrompt, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
15
17
  }
16
18
  export declare const stormClient: StormClient;
17
19
  export {};
@@ -80,6 +80,12 @@ class StormClient {
80
80
  conversationId,
81
81
  });
82
82
  }
83
+ listScreens(prompt, conversationId) {
84
+ return this.send('/v2/ui/list', {
85
+ prompt,
86
+ conversationId,
87
+ });
88
+ }
83
89
  createUIImplementation(prompt, conversationId) {
84
90
  return this.send('/v2/ui/merge', {
85
91
  prompt,
@@ -110,5 +116,11 @@ class StormClient {
110
116
  prompt,
111
117
  });
112
118
  }
119
+ generateCode(prompt, history, conversationId) {
120
+ return this.send('/v2/code/generate', {
121
+ conversationId: conversationId,
122
+ prompt,
123
+ });
124
+ }
113
125
  }
114
126
  exports.stormClient = new StormClient();
@@ -50,6 +50,14 @@ export interface StormFileImplementationPrompt {
50
50
  prompt: string;
51
51
  }
52
52
  export interface StormUIImplementationPrompt {
53
+ events: StormEvent[];
54
+ template: StormFileInfo;
55
+ filename: string;
56
+ context: StormFileInfo[];
57
+ blockName: string;
58
+ prompt: string;
59
+ }
60
+ export interface StormUIListPrompt {
53
61
  events: StormEvent[];
54
62
  templates: StormFileInfo[];
55
63
  context: StormFileInfo[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.53.5",
3
+ "version": "0.54.1",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "homepage": "https://github.com/kapetacom/local-cluster-service#readme",
52
52
  "dependencies": {
53
- "@kapeta/codegen": "^1.6.0",
53
+ "@kapeta/codegen": "^1.6.1",
54
54
  "@kapeta/config-mapper": "^1.2.2",
55
55
  "@kapeta/kaplang-core": "^1.17.2",
56
56
  "@kapeta/local-cluster-config": "^0.4.2",
@@ -12,7 +12,9 @@ import {
12
12
  GeneratedFile,
13
13
  GeneratedResult,
14
14
  MODE_CREATE_ONLY,
15
+ MODE_WRITE_NEVER,
15
16
  } from '@kapeta/codegen';
17
+
16
18
  import { BlockDefinition } from '@kapeta/schemas';
17
19
  import { codeGeneratorManager } from '../codeGeneratorManager';
18
20
  import { STORM_ID, stormClient } from './stormClient';
@@ -23,6 +25,7 @@ import {
23
25
  StormEventFileChunk,
24
26
  StormEventFileDone,
25
27
  StormEventFileLogical,
28
+ StormEventScreen,
26
29
  } from './events';
27
30
  import { BlockDefinitionInfo, StormEventParser } from './event-parser';
28
31
  import { ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
@@ -31,11 +34,14 @@ import { writeFile } from 'fs/promises';
31
34
  import path from 'path';
32
35
  import Path, { join } from 'path';
33
36
  import os from 'node:os';
34
- import { readFileSync, writeFileSync } from 'fs';
37
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
35
38
  import YAML from 'yaml';
36
- import * as fs from 'node:fs';
39
+ import assert from 'assert';
37
40
 
38
- type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
41
+ type ImplementationGenerator<T = StormFileImplementationPrompt> = (
42
+ prompt: T,
43
+ conversationId?: string
44
+ ) => Promise<StormStream>;
39
45
 
40
46
  interface ErrorClassification {
41
47
  error: string;
@@ -266,9 +272,12 @@ export class StormCodegen {
266
272
  const blockUri = parseKapetaUri(block.uri);
267
273
 
268
274
  const relevantFiles: StormFileInfo[] = allFiles.filter(
269
- (file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
275
+ (file) =>
276
+ file.type !== AIFileTypes.IGNORE &&
277
+ file.type !== AIFileTypes.WEB_SCREEN &&
278
+ file.type !== AIFileTypes.WEB_ROUTER
270
279
  );
271
- const uiTemplates: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.WEB_SCREEN);
280
+ const uiTemplates = allFiles.filter((file) => file.type === AIFileTypes.WEB_SCREEN);
272
281
  const screenFiles: StormEventFileDone[] = [];
273
282
  let filteredEvents = [] as StormEvent[];
274
283
  for (const event of this.events) {
@@ -277,36 +286,120 @@ export class StormCodegen {
277
286
  filteredEvents = [];
278
287
  }
279
288
  }
280
- if (uiTemplates.length > 0) {
281
- const uiStream = await stormClient.createUIImplementation({
289
+
290
+ const screenEvents: StormEventScreen[] = [];
291
+ const getScreenEventsFile = () => ({
292
+ content: JSON.stringify(screenEvents),
293
+ filename: '<screens>.json',
294
+ type: AIFileTypes.CONFIG,
295
+ mode: MODE_WRITE_NEVER,
296
+ permissions: '0644',
297
+ });
298
+ const uiEvents = [];
299
+
300
+ // generate screens
301
+ if (uiTemplates.length) {
302
+ const screenStream = await stormClient.listScreens({
282
303
  events: filteredEvents,
283
304
  templates: uiTemplates,
284
305
  context: relevantFiles,
285
306
  blockName: block.aiName,
286
307
  prompt: this.userPrompt,
287
308
  });
288
-
289
- uiStream.on('data', (evt) => {
290
- const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
291
- if (uiFile != undefined) {
292
- screenFiles.push(uiFile);
309
+ screenStream.on('data', (evt) => {
310
+ if (evt.type === 'SCREEN') {
311
+ screenEvents.push(evt);
293
312
  }
313
+ this.handleUiOutput(blockUri, block.aiName, evt);
294
314
  });
295
315
 
296
316
  this.out.on('aborted', () => {
297
- uiStream.abort();
317
+ screenStream.abort();
298
318
  });
299
319
 
300
- await uiStream.waitForDone();
320
+ await screenStream.waitForDone();
321
+
322
+ // screenfiles
323
+ const screenTemplates = screenEvents
324
+ .map((screenEvent) => ({
325
+ ...uiTemplates.find((template) => template.filename.endsWith(screenEvent.payload.template)),
326
+ filename: screenEvent.payload.filename,
327
+ }))
328
+ .filter((tpl): tpl is StormFileInfo => !!tpl.content);
329
+
330
+ await Promise.all(
331
+ screenTemplates.map(async (template) => {
332
+ const payload = {
333
+ events: filteredEvents,
334
+ blockName: block.aiName,
335
+ filename: template.filename,
336
+ template: template,
337
+ context: relevantFiles.concat([getScreenEventsFile()]),
338
+ prompt: this.userPrompt,
339
+ };
340
+
341
+ const uiStream = await stormClient.createUIImplementation(payload);
342
+
343
+ uiStream.on('data', (evt) => {
344
+ const uiFile = this.handleUiOutput(blockUri, block.aiName, evt);
345
+ if (uiFile != undefined) {
346
+ screenFiles.push(uiFile);
347
+ }
348
+ uiEvents.push(evt);
349
+ });
350
+
351
+ this.out.on('aborted', () => {
352
+ uiStream.abort();
353
+ });
354
+
355
+ await uiStream.waitForDone();
356
+ })
357
+ );
301
358
  }
302
359
 
303
360
  if (this.isAborted()) {
304
361
  return;
305
362
  }
363
+ const basePath = this.getBasePath(block.content.metadata.name);
364
+
365
+ const screenFilesConverted = screenFiles.map((screenFile) => {
366
+ return {
367
+ filename: screenFile.payload.filename,
368
+ content: screenFile.payload.content,
369
+ mode: MODE_CREATE_ONLY,
370
+ permissions: '0644',
371
+ type: AIFileTypes.WEB_SCREEN,
372
+ };
373
+ });
374
+ allFiles.push(...screenFilesConverted);
375
+
376
+ const webRouters = allFiles.filter((file) => file.type === AIFileTypes.WEB_ROUTER);
377
+ await Promise.all(
378
+ webRouters.map(async (webRouter) => {
379
+ const payload = {
380
+ filename: webRouter.filename,
381
+ template: webRouter,
382
+ context: screenFilesConverted.concat([getScreenEventsFile()]),
383
+ prompt: this.userPrompt,
384
+ };
385
+
386
+ const stream = await stormClient.generateCode(payload);
387
+
388
+ stream.on('data', (evt) => {
389
+ this.handleTemplateFileOutput(blockUri, block.aiName, webRouter, evt);
390
+ });
391
+
392
+ this.out.on('aborted', () => {
393
+ stream.abort();
394
+ });
395
+
396
+ await stream.waitForDone();
397
+ })
398
+ );
306
399
 
307
400
  // Gather the context files for implementation. These will be all be passed to the AI
308
401
  const contextFiles: StormFileInfo[] = relevantFiles.filter(
309
- (file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
402
+ (file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN, AIFileTypes.WEB_ROUTER].includes(file.type)
310
403
  );
311
404
 
312
405
  // Send the service and UI templates to the AI. These will be sent one-by-one in addition to the context files
@@ -321,12 +414,16 @@ export class StormCodegen {
321
414
  );
322
415
  }
323
416
 
324
- const basePath = this.getBasePath(block.content.metadata.name);
325
-
326
417
  if (this.isAborted()) {
327
418
  return;
328
419
  }
329
420
 
421
+ for (const screenFile of screenFilesConverted) {
422
+ // this.emitFile(blockUri, block.aiName, screenFile.filename, screenFile.content);
423
+ const filePath = join(basePath, screenFile.filename);
424
+ await writeFile(filePath, screenFile.content);
425
+ }
426
+
330
427
  for (const serviceFile of serviceFiles) {
331
428
  const filePath = join(basePath, serviceFile.filename);
332
429
  await writeFile(filePath, serviceFile.content);
@@ -337,30 +434,21 @@ export class StormCodegen {
337
434
  await writeFile(filePath, serviceFile.content);
338
435
  }
339
436
 
340
- const kapetaYmlPath = join(basePath, 'kapeta.yml');
341
- await writeFile(kapetaYmlPath, YAML.stringify(block.content as BlockDefinition));
342
-
343
- for (const screenFile of screenFiles) {
344
- const filePath = join(basePath, screenFile.payload.filename);
345
- await writeFile(filePath, screenFile.payload.content);
437
+ // Write again after modifications
438
+ for (const webRouterFile of webRouters) {
439
+ const filePath = join(basePath, webRouterFile.filename);
440
+ await writeFile(filePath, webRouterFile.content);
346
441
  }
347
442
 
348
- const screenFilesConverted = screenFiles.map((screenFile) => {
349
- return {
350
- filename: screenFile.payload.filename,
351
- content: screenFile.payload.content,
352
- mode: MODE_CREATE_ONLY,
353
- permissions: '0644',
354
- type: AIFileTypes.WEB_SCREEN,
355
- };
356
- });
357
- allFiles.push(...screenFilesConverted);
443
+ const kapetaYmlPath = join(basePath, 'kapeta.yml');
444
+ await writeFile(kapetaYmlPath, YAML.stringify(block.content));
445
+
358
446
  const blockRef = block.uri;
359
447
 
360
448
  this.emitBlockStatus(blockUri, block.aiName, StormEventBlockStatusType.QA);
361
449
 
362
450
  const filesToBeFixed = serviceFiles.concat(contextFiles).concat(screenFilesConverted);
363
- const codeGenerator = new BlockCodeGenerator(block.content as BlockDefinition);
451
+ const codeGenerator = new BlockCodeGenerator(block.content);
364
452
 
365
453
  this.emitBlockStatus(blockUri, block.aiName, StormEventBlockStatusType.BUILDING);
366
454
  await this.verifyAndFixCode(blockUri, block.aiName, codeGenerator, basePath, filesToBeFixed, allFiles);
@@ -594,7 +682,7 @@ export class StormCodegen {
594
682
  files.add(filename);
595
683
 
596
684
  const requestedFiles = Array.from(files).flatMap((file) => {
597
- if (fs.existsSync(file)) {
685
+ if (existsSync(file)) {
598
686
  return file;
599
687
  }
600
688
 
@@ -696,7 +784,7 @@ export class StormCodegen {
696
784
  return;
697
785
  }
698
786
 
699
- if (file.type === AIFileTypes.WEB_SCREEN) {
787
+ if ([AIFileTypes.WEB_ROUTER, AIFileTypes.WEB_SCREEN].includes(file.type)) {
700
788
  // Don't send the web screen files to the stream yet
701
789
  // They will need to be implemented by the AI
702
790
  return;
@@ -819,6 +907,7 @@ export class StormCodegen {
819
907
  type,
820
908
  };
821
909
  });
910
+
822
911
  return allFiles;
823
912
  }
824
913
 
@@ -193,6 +193,7 @@ export interface StormEventScreen {
193
193
  template: string;
194
194
  description: string;
195
195
  url: string;
196
+ filename: string;
196
197
  };
197
198
  }
198
199
 
@@ -12,6 +12,7 @@ import {
12
12
  StormFileImplementationPrompt,
13
13
  StormStream,
14
14
  StormUIImplementationPrompt,
15
+ StormUIListPrompt,
15
16
  } from './stream';
16
17
  import { getRawAsset } from 'node:sea';
17
18
 
@@ -112,8 +113,15 @@ class StormClient {
112
113
  });
113
114
  }
114
115
 
116
+ public listScreens(prompt: StormUIListPrompt, conversationId?: string) {
117
+ return this.send('/v2/ui/list', {
118
+ prompt,
119
+ conversationId,
120
+ });
121
+ }
122
+
115
123
  public createUIImplementation(prompt: StormUIImplementationPrompt, conversationId?: string) {
116
- return this.send<StormUIImplementationPrompt>('/v2/ui/merge', {
124
+ return this.send('/v2/ui/merge', {
117
125
  prompt,
118
126
  conversationId,
119
127
  });
@@ -146,6 +154,13 @@ class StormClient {
146
154
  prompt,
147
155
  });
148
156
  }
157
+
158
+ public generateCode(prompt: StormFileImplementationPrompt, history?: ConversationItem[], conversationId?: string) {
159
+ return this.send('/v2/code/generate', {
160
+ conversationId: conversationId,
161
+ prompt,
162
+ });
163
+ }
149
164
  }
150
165
 
151
166
  export const stormClient = new StormClient();
@@ -117,6 +117,15 @@ export interface StormFileImplementationPrompt {
117
117
  }
118
118
 
119
119
  export interface StormUIImplementationPrompt {
120
+ events: StormEvent[];
121
+ template: StormFileInfo;
122
+ filename: string;
123
+ context: StormFileInfo[];
124
+ blockName: string;
125
+ prompt: string;
126
+ }
127
+
128
+ export interface StormUIListPrompt {
120
129
  events: StormEvent[];
121
130
  templates: StormFileInfo[];
122
131
  context: StormFileInfo[];