@kapeta/local-cluster-service 0.48.5 → 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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.49.0](https://github.com/kapetacom/local-cluster-service/compare/v0.48.5...v0.49.0) (2024-06-05)
2
+
3
+
4
+ ### Features
5
+
6
+ * Implement ability to save ai generated plan ([#161](https://github.com/kapetacom/local-cluster-service/issues/161)) ([8223757](https://github.com/kapetacom/local-cluster-service/commit/8223757ff90ee6b5e1b7f171fc724d9877051df2))
7
+
1
8
  ## [0.48.5](https://github.com/kapetacom/local-cluster-service/compare/v0.48.4...v0.48.5) (2024-06-04)
2
9
 
3
10
 
@@ -28,7 +28,7 @@ declare class AssetManager {
28
28
  getPlan(ref: string, noCache?: boolean): Promise<Plan>;
29
29
  getBlockInstance(systemId: string, instanceId: string): Promise<BlockInstance>;
30
30
  getAsset(ref: string, noCache?: boolean, autoFetch?: boolean): Promise<EnrichedAsset | undefined>;
31
- createAsset(path: string, yaml: BlockDefinition, sourceOfChange?: SourceOfChange): Promise<EnrichedAsset[]>;
31
+ createAsset(path: string, yaml: BlockDefinition, sourceOfChange?: SourceOfChange, codegen?: boolean): Promise<EnrichedAsset[]>;
32
32
  updateAsset(ref: string, yaml: Definition, sourceOfChange?: SourceOfChange): Promise<void>;
33
33
  importFile(filePath: string): Promise<EnrichedAsset[]>;
34
34
  unregisterAsset(ref: string): Promise<void>;
@@ -142,7 +142,7 @@ class AssetManager {
142
142
  }
143
143
  return undefined;
144
144
  }
145
- async createAsset(path, yaml, sourceOfChange = 'filesystem') {
145
+ async createAsset(path, yaml, sourceOfChange = 'filesystem', codegen = true) {
146
146
  if (await fs_extra_1.default.pathExists(path)) {
147
147
  throw new Error('File already exists: ' + path);
148
148
  }
@@ -159,9 +159,11 @@ class AssetManager {
159
159
  cacheManager_1.cacheManager.set(key, a, CACHE_TTL);
160
160
  });
161
161
  definitionsManager_1.definitionsManager.clearCache();
162
- console.log(`Created asset at: ${path}`);
163
162
  const ref = `kapeta://${yaml.metadata.name}:local`;
164
- await this.maybeGenerateCode(ref, path, yaml);
163
+ console.log(`Created asset ${ref} at: ${path}`);
164
+ if (codegen) {
165
+ await this.maybeGenerateCode(ref, path, yaml);
166
+ }
165
167
  return asset;
166
168
  }
167
169
  async updateAsset(ref, yaml, sourceOfChange = 'filesystem') {
@@ -11,6 +11,7 @@ const express_promise_router_1 = __importDefault(require("express-promise-router
11
11
  const stringBody_1 = require("../middleware/stringBody");
12
12
  const filesystemManager_1 = require("../filesystemManager");
13
13
  const cors_1 = require("../middleware/cors");
14
+ const fs_extra_1 = __importDefault(require("fs-extra"));
14
15
  let router = (0, express_promise_router_1.default)();
15
16
  router.use('/', cors_1.corsHandler);
16
17
  router.get('/root', (req, res) => {
@@ -81,6 +82,15 @@ router.get('/readfile', async (req, res) => {
81
82
  res.status(400).send({ error: '' + err });
82
83
  }
83
84
  });
85
+ router.get('/exists', async (req, res) => {
86
+ let pathArg = req.query.path;
87
+ try {
88
+ res.send(await fs_extra_1.default.pathExists(pathArg));
89
+ }
90
+ catch (err) {
91
+ res.status(400).send({ error: '' + err });
92
+ }
93
+ });
84
94
  router.put('/mkdir', async (req, res) => {
85
95
  let pathArg = req.query.path;
86
96
  try {
@@ -11,7 +11,8 @@ export declare class StormCodegen {
11
11
  private readonly out;
12
12
  private readonly events;
13
13
  private readonly tmpDir;
14
- constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]);
14
+ private readonly conversationId;
15
+ constructor(conversationId: string, userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]);
15
16
  process(): Promise<void>;
16
17
  getStream(): StormStream;
17
18
  private handleTemplateFileOutput;
@@ -36,26 +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"));
42
43
  const fs_1 = require("fs");
44
+ const path_2 = __importDefault(require("path"));
43
45
  class StormCodegen {
44
46
  userPrompt;
45
47
  blocks;
46
48
  out = new stream_1.StormStream();
47
49
  events;
48
50
  tmpDir;
49
- constructor(userPrompt, blocks, events) {
51
+ conversationId;
52
+ constructor(conversationId, userPrompt, blocks, events) {
50
53
  this.userPrompt = userPrompt;
51
54
  this.blocks = blocks;
52
55
  this.events = events;
53
- this.tmpDir = node_os_1.default.tmpdir();
56
+ this.tmpDir = path_2.default.join(node_os_1.default.tmpdir(), conversationId);
57
+ this.conversationId = conversationId;
54
58
  }
55
59
  async process() {
56
- for (const block of this.blocks) {
57
- await this.processBlockCode(block);
58
- }
60
+ const promises = this.blocks.map((block) => {
61
+ return this.processBlockCode(block);
62
+ });
63
+ await Promise.all(promises);
59
64
  this.out.end();
60
65
  }
61
66
  getStream() {
@@ -119,7 +124,7 @@ class StormCodegen {
119
124
  }
120
125
  const allFiles = this.toStormFiles(generatedResult);
121
126
  // Send all the non-ai files to the stream
122
- this.emitFiles(block.uri, block.aiName, allFiles);
127
+ this.emitFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
123
128
  const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
124
129
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
125
130
  if (uiTemplates.length > 0) {
@@ -131,7 +136,7 @@ class StormCodegen {
131
136
  prompt: this.userPrompt,
132
137
  });
133
138
  uiStream.on('data', (evt) => {
134
- this.handleUiOutput(block.uri, block.aiName, evt);
139
+ this.handleUiOutput((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, evt);
135
140
  });
136
141
  await uiStream.waitForDone();
137
142
  }
@@ -140,7 +145,7 @@ class StormCodegen {
140
145
  // Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
141
146
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
142
147
  if (serviceFiles.length > 0) {
143
- 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);
144
149
  }
145
150
  const basePath = this.getBasePath(block.content.metadata.name);
146
151
  for (const serviceFile of serviceFiles) {
@@ -158,6 +163,18 @@ class StormCodegen {
158
163
  const filesToBeFixed = serviceFiles.concat(contextFiles);
159
164
  const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
160
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
+ });
161
178
  }
162
179
  async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, knownFiles) {
163
180
  let attempts = 0;
@@ -186,7 +203,9 @@ class StormCodegen {
186
203
  }
187
204
  // read the content of the file
188
205
  const content = (0, fs_1.readFileSync)((0, path_1.join)(basePath, eventFileName), 'utf8');
189
- const fix = `${evt.payload.potentialFix}\n---\n${knownFiles.map(e => e.filename).join("\n")}\n---\n${content}`;
206
+ const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
207
+ .map((e) => e.filename)
208
+ .join('\n')}\n---\n${content}`;
190
209
  console.log(`trying to fix the code in ${eventFileName}`);
191
210
  console.debug(`with the fix:\n${fix}`);
192
211
  const code = this.codeFix(fix);
@@ -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),
@@ -163,6 +163,17 @@ export interface StormEventFile {
163
163
  instanceId: string;
164
164
  };
165
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
+ }
166
177
  export interface StormEventDone {
167
178
  type: 'DONE';
168
179
  created: number;
@@ -173,4 +184,4 @@ export interface StormEventDefinitionChange {
173
184
  created: number;
174
185
  payload: StormDefinitions;
175
186
  }
176
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix;
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',
@@ -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
  }
@@ -28,7 +28,7 @@ declare class AssetManager {
28
28
  getPlan(ref: string, noCache?: boolean): Promise<Plan>;
29
29
  getBlockInstance(systemId: string, instanceId: string): Promise<BlockInstance>;
30
30
  getAsset(ref: string, noCache?: boolean, autoFetch?: boolean): Promise<EnrichedAsset | undefined>;
31
- createAsset(path: string, yaml: BlockDefinition, sourceOfChange?: SourceOfChange): Promise<EnrichedAsset[]>;
31
+ createAsset(path: string, yaml: BlockDefinition, sourceOfChange?: SourceOfChange, codegen?: boolean): Promise<EnrichedAsset[]>;
32
32
  updateAsset(ref: string, yaml: Definition, sourceOfChange?: SourceOfChange): Promise<void>;
33
33
  importFile(filePath: string): Promise<EnrichedAsset[]>;
34
34
  unregisterAsset(ref: string): Promise<void>;
@@ -142,7 +142,7 @@ class AssetManager {
142
142
  }
143
143
  return undefined;
144
144
  }
145
- async createAsset(path, yaml, sourceOfChange = 'filesystem') {
145
+ async createAsset(path, yaml, sourceOfChange = 'filesystem', codegen = true) {
146
146
  if (await fs_extra_1.default.pathExists(path)) {
147
147
  throw new Error('File already exists: ' + path);
148
148
  }
@@ -159,9 +159,11 @@ class AssetManager {
159
159
  cacheManager_1.cacheManager.set(key, a, CACHE_TTL);
160
160
  });
161
161
  definitionsManager_1.definitionsManager.clearCache();
162
- console.log(`Created asset at: ${path}`);
163
162
  const ref = `kapeta://${yaml.metadata.name}:local`;
164
- await this.maybeGenerateCode(ref, path, yaml);
163
+ console.log(`Created asset ${ref} at: ${path}`);
164
+ if (codegen) {
165
+ await this.maybeGenerateCode(ref, path, yaml);
166
+ }
165
167
  return asset;
166
168
  }
167
169
  async updateAsset(ref, yaml, sourceOfChange = 'filesystem') {
@@ -11,6 +11,7 @@ const express_promise_router_1 = __importDefault(require("express-promise-router
11
11
  const stringBody_1 = require("../middleware/stringBody");
12
12
  const filesystemManager_1 = require("../filesystemManager");
13
13
  const cors_1 = require("../middleware/cors");
14
+ const fs_extra_1 = __importDefault(require("fs-extra"));
14
15
  let router = (0, express_promise_router_1.default)();
15
16
  router.use('/', cors_1.corsHandler);
16
17
  router.get('/root', (req, res) => {
@@ -81,6 +82,15 @@ router.get('/readfile', async (req, res) => {
81
82
  res.status(400).send({ error: '' + err });
82
83
  }
83
84
  });
85
+ router.get('/exists', async (req, res) => {
86
+ let pathArg = req.query.path;
87
+ try {
88
+ res.send(await fs_extra_1.default.pathExists(pathArg));
89
+ }
90
+ catch (err) {
91
+ res.status(400).send({ error: '' + err });
92
+ }
93
+ });
84
94
  router.put('/mkdir', async (req, res) => {
85
95
  let pathArg = req.query.path;
86
96
  try {
@@ -11,7 +11,8 @@ export declare class StormCodegen {
11
11
  private readonly out;
12
12
  private readonly events;
13
13
  private readonly tmpDir;
14
- constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]);
14
+ private readonly conversationId;
15
+ constructor(conversationId: string, userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]);
15
16
  process(): Promise<void>;
16
17
  getStream(): StormStream;
17
18
  private handleTemplateFileOutput;
@@ -36,26 +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"));
42
43
  const fs_1 = require("fs");
44
+ const path_2 = __importDefault(require("path"));
43
45
  class StormCodegen {
44
46
  userPrompt;
45
47
  blocks;
46
48
  out = new stream_1.StormStream();
47
49
  events;
48
50
  tmpDir;
49
- constructor(userPrompt, blocks, events) {
51
+ conversationId;
52
+ constructor(conversationId, userPrompt, blocks, events) {
50
53
  this.userPrompt = userPrompt;
51
54
  this.blocks = blocks;
52
55
  this.events = events;
53
- this.tmpDir = node_os_1.default.tmpdir();
56
+ this.tmpDir = path_2.default.join(node_os_1.default.tmpdir(), conversationId);
57
+ this.conversationId = conversationId;
54
58
  }
55
59
  async process() {
56
- for (const block of this.blocks) {
57
- await this.processBlockCode(block);
58
- }
60
+ const promises = this.blocks.map((block) => {
61
+ return this.processBlockCode(block);
62
+ });
63
+ await Promise.all(promises);
59
64
  this.out.end();
60
65
  }
61
66
  getStream() {
@@ -119,7 +124,7 @@ class StormCodegen {
119
124
  }
120
125
  const allFiles = this.toStormFiles(generatedResult);
121
126
  // Send all the non-ai files to the stream
122
- this.emitFiles(block.uri, block.aiName, allFiles);
127
+ this.emitFiles((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, allFiles);
123
128
  const relevantFiles = allFiles.filter((file) => file.type !== codegen_1.AIFileTypes.IGNORE && file.type !== codegen_1.AIFileTypes.WEB_SCREEN);
124
129
  const uiTemplates = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.WEB_SCREEN);
125
130
  if (uiTemplates.length > 0) {
@@ -131,7 +136,7 @@ class StormCodegen {
131
136
  prompt: this.userPrompt,
132
137
  });
133
138
  uiStream.on('data', (evt) => {
134
- this.handleUiOutput(block.uri, block.aiName, evt);
139
+ this.handleUiOutput((0, nodejs_utils_1.parseKapetaUri)(block.uri), block.aiName, evt);
135
140
  });
136
141
  await uiStream.waitForDone();
137
142
  }
@@ -140,7 +145,7 @@ class StormCodegen {
140
145
  // Send the service and UI templates to the AI. These will be send one-by-one in addition to the context files
141
146
  const serviceFiles = allFiles.filter((file) => file.type === codegen_1.AIFileTypes.SERVICE);
142
147
  if (serviceFiles.length > 0) {
143
- 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);
144
149
  }
145
150
  const basePath = this.getBasePath(block.content.metadata.name);
146
151
  for (const serviceFile of serviceFiles) {
@@ -158,6 +163,18 @@ class StormCodegen {
158
163
  const filesToBeFixed = serviceFiles.concat(contextFiles);
159
164
  const codeGenerator = new codegen_1.BlockCodeGenerator(block.content);
160
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
+ });
161
178
  }
162
179
  async verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, knownFiles) {
163
180
  let attempts = 0;
@@ -186,7 +203,9 @@ class StormCodegen {
186
203
  }
187
204
  // read the content of the file
188
205
  const content = (0, fs_1.readFileSync)((0, path_1.join)(basePath, eventFileName), 'utf8');
189
- const fix = `${evt.payload.potentialFix}\n---\n${knownFiles.map(e => e.filename).join("\n")}\n---\n${content}`;
206
+ const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
207
+ .map((e) => e.filename)
208
+ .join('\n')}\n---\n${content}`;
190
209
  console.log(`trying to fix the code in ${eventFileName}`);
191
210
  console.debug(`with the fix:\n${fix}`);
192
211
  const code = this.codeFix(fix);
@@ -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),
@@ -163,6 +163,17 @@ export interface StormEventFile {
163
163
  instanceId: string;
164
164
  };
165
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
+ }
166
177
  export interface StormEventDone {
167
178
  type: 'DONE';
168
179
  created: number;
@@ -173,4 +184,4 @@ export interface StormEventDefinitionChange {
173
184
  created: number;
174
185
  payload: StormDefinitions;
175
186
  }
176
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFile | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix;
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',
@@ -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.5",
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,18 +4,26 @@
4
4
  */
5
5
 
6
6
  import { Definition } from '@kapeta/local-cluster-config';
7
- import { AIFileTypes, BlockCodeGenerator, CodeGenerator, 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';
18
25
  import { readFile, readFileSync, writeFileSync } from 'fs';
26
+ import Path from 'path';
19
27
 
20
28
  type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
21
29
 
@@ -25,19 +33,21 @@ export class StormCodegen {
25
33
  private readonly out = new StormStream();
26
34
  private readonly events: StormEvent[];
27
35
  private readonly tmpDir: string;
36
+ private readonly conversationId: string;
28
37
 
29
- constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
38
+ constructor(conversationId: string, userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
30
39
  this.userPrompt = userPrompt;
31
40
  this.blocks = blocks;
32
41
  this.events = events;
33
- this.tmpDir = os.tmpdir();
42
+ this.tmpDir = Path.join(os.tmpdir(), conversationId);
43
+ this.conversationId = conversationId;
34
44
  }
35
45
 
36
46
  public async process() {
37
- for (const block of this.blocks) {
38
- await this.processBlockCode(block);
39
- }
40
-
47
+ const promises = this.blocks.map((block) => {
48
+ return this.processBlockCode(block);
49
+ });
50
+ await Promise.all(promises);
41
51
  this.out.end();
42
52
  }
43
53
 
@@ -109,7 +119,7 @@ export class StormCodegen {
109
119
  const allFiles = this.toStormFiles(generatedResult);
110
120
 
111
121
  // Send all the non-ai files to the stream
112
- this.emitFiles(block.uri, block.aiName, allFiles);
122
+ this.emitFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
113
123
 
114
124
  const relevantFiles: StormFileInfo[] = allFiles.filter(
115
125
  (file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
@@ -125,7 +135,7 @@ export class StormCodegen {
125
135
  });
126
136
 
127
137
  uiStream.on('data', (evt) => {
128
- this.handleUiOutput(block.uri, block.aiName, evt);
138
+ this.handleUiOutput(parseKapetaUri(block.uri), block.aiName, evt);
129
139
  });
130
140
 
131
141
  await uiStream.waitForDone();
@@ -140,7 +150,7 @@ export class StormCodegen {
140
150
  const serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
141
151
  if (serviceFiles.length > 0) {
142
152
  await this.processTemplates(
143
- block.uri,
153
+ parseKapetaUri(block.uri),
144
154
  block.aiName,
145
155
  stormClient.createServiceImplementation.bind(stormClient),
146
156
  serviceFiles,
@@ -168,18 +178,36 @@ export class StormCodegen {
168
178
  const filesToBeFixed = serviceFiles.concat(contextFiles);
169
179
  const codeGenerator = new BlockCodeGenerator(block.content as BlockDefinition);
170
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);
171
194
  }
172
195
 
173
- private async verifyAndFixCode(codeGenerator: CodeGenerator, basePath: string, filesToBeFixed: StormFileInfo[], knownFiles: StormFileInfo[]) {
196
+ private async verifyAndFixCode(
197
+ codeGenerator: CodeGenerator,
198
+ basePath: string,
199
+ filesToBeFixed: StormFileInfo[],
200
+ knownFiles: StormFileInfo[]
201
+ ) {
174
202
  let attempts = 0;
175
203
  let validCode = false;
176
204
  for (let i = 0; i <= 3; i++) {
177
205
  attempts++;
178
206
  try {
179
- console.log(`Validating the code in ${basePath} attempt #${attempts}`)
207
+ console.log(`Validating the code in ${basePath} attempt #${attempts}`);
180
208
  const result = await codeGenerator.validateForTarget(basePath);
181
209
  if (result && result.valid) {
182
- validCode = true;
210
+ validCode = true;
183
211
  break;
184
212
  }
185
213
 
@@ -192,16 +220,20 @@ export class StormCodegen {
192
220
  if (evt.type === 'ERROR_CLASSIFIER') {
193
221
  // find the file that caused the error
194
222
  // strip base path from event file name, if it exists sometimes the AI sends the full path
195
- const eventFileName = this.removePrefix(basePath+'/', evt.payload.filename);
223
+ const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
196
224
  const file = filesToBeFixed.find((f) => f.filename === eventFileName);
197
- if(!file) {
198
- 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`);
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
+ );
199
229
  }
200
230
  // read the content of the file
201
231
  const content = readFileSync(join(basePath, eventFileName), 'utf8');
202
- const fix = `${evt.payload.potentialFix}\n---\n${knownFiles.map(e => e.filename).join("\n")}\n---\n${content}`;
232
+ const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
233
+ .map((e) => e.filename)
234
+ .join('\n')}\n---\n${content}`;
203
235
  console.log(`trying to fix the code in ${eventFileName}`);
204
- console.debug(`with the fix:\n${fix}`)
236
+ console.debug(`with the fix:\n${fix}`);
205
237
  const code = this.codeFix(fix);
206
238
  fixes.set(join(basePath, eventFileName), code);
207
239
  }
@@ -226,7 +258,7 @@ export class StormCodegen {
226
258
 
227
259
  removePrefix(prefix: string, str: string): string {
228
260
  if (str.startsWith(prefix)) {
229
- return str.slice(prefix.length);
261
+ return str.slice(prefix.length);
230
262
  }
231
263
  return str;
232
264
  }
@@ -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),
@@ -144,9 +144,9 @@ export interface StormEventCodeFix {
144
144
  };
145
145
  }
146
146
  export interface StormEventErrorClassifierInfo {
147
- error: string,
148
- filename: string,
149
- potentialFix: string
147
+ error: string;
148
+ filename: string;
149
+ potentialFix: string;
150
150
  }
151
151
 
152
152
  export interface ScreenTemplate {
@@ -196,6 +196,18 @@ export interface StormEventFile {
196
196
  };
197
197
  }
198
198
 
199
+ export interface StormEventBlockReady {
200
+ type: 'BLOCK_READY';
201
+ reason: string;
202
+ created: number;
203
+ payload: {
204
+ path: string;
205
+ blockName: string;
206
+ blockRef: string;
207
+ instanceId: string;
208
+ };
209
+ }
210
+
199
211
  export interface StormEventDone {
200
212
  type: 'DONE';
201
213
  created: number;
@@ -223,4 +235,5 @@ export type StormEvent =
223
235
  | StormEventDone
224
236
  | StormEventDefinitionChange
225
237
  | StormEventErrorClassifier
226
- | StormEventCodeFix;
238
+ | StormEventCodeFix
239
+ | StormEventBlockReady;
@@ -4,15 +4,19 @@
4
4
  */
5
5
 
6
6
  import Router from 'express-promise-router';
7
+ import FS from 'fs-extra';
7
8
  import { Response } from 'express';
8
9
  import { corsHandler } from '../middleware/cors';
9
10
  import { stringBody } from '../middleware/stringBody';
10
11
  import { KapetaBodyRequest } from '../types';
11
- import { StormContextRequest, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
12
+ import { StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
12
13
  import { ConversationIdHeader, stormClient } from './stormClient';
13
14
  import { StormEvent } from './events';
14
15
  import { resolveOptions, StormDefinitions, StormEventParser } from './event-parser';
15
16
  import { StormCodegen } from './codegen';
17
+ import { assetManager } from '../assetManager';
18
+ import Path from 'path';
19
+ import { normalizeKapetaUri } from '@kapeta/nodejs-utils';
16
20
 
17
21
  const router = Router();
18
22
 
@@ -62,7 +66,12 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
62
66
  sendDefinitions(res, result);
63
67
 
64
68
  if (!req.query.skipCodegen) {
65
- const stormCodegen = new StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
69
+ const stormCodegen = new StormCodegen(
70
+ metaStream.getConversationId(),
71
+ aiRequest.prompt,
72
+ result.blocks,
73
+ eventParser.getEvents()
74
+ );
66
75
 
67
76
  const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
68
77
 
@@ -78,6 +87,34 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
78
87
  }
79
88
  });
80
89
 
90
+ router.post('/block/create', async (req: KapetaBodyRequest, res: Response) => {
91
+ const createRequest: StormCreateBlockRequest = JSON.parse(req.stringBody ?? '{}');
92
+
93
+ try {
94
+ const ymlPath = Path.join(createRequest.newPath, 'kapeta.yml');
95
+
96
+ console.log('Creating block at', ymlPath);
97
+
98
+ const [asset] = await assetManager.createAsset(ymlPath, createRequest.definition);
99
+
100
+ if (await FS.pathExists(createRequest.tmpPath)) {
101
+ console.log('Moving block from', createRequest.tmpPath, 'to', createRequest.newPath);
102
+
103
+ await FS.move(createRequest.tmpPath, createRequest.newPath, {
104
+ overwrite: true,
105
+ });
106
+
107
+ console.log('Updating asset', asset.ref);
108
+
109
+ res.send(await assetManager.updateAsset(asset.ref, createRequest.definition));
110
+ } else {
111
+ res.send(asset);
112
+ }
113
+ } catch (err: any) {
114
+ res.status(500).send({ error: err.message });
115
+ }
116
+ });
117
+
81
118
  function sendDefinitions(res: Response, result: StormDefinitions) {
82
119
  sendEvent(res, {
83
120
  type: 'DEFINITION_CHANGE',
@@ -5,6 +5,7 @@
5
5
  import { EventEmitter } from 'node:events';
6
6
  import { StormEvent } from './events';
7
7
  import { AIFileTypes, GeneratedFile } from '@kapeta/codegen';
8
+ import { BlockDefinition } from '@kapeta/schemas';
8
9
 
9
10
  export class StormStream extends EventEmitter {
10
11
  private conversationId: string = '';
@@ -73,6 +74,12 @@ export interface StormContextRequest<T = string> {
73
74
  prompt: T;
74
75
  }
75
76
 
77
+ export interface StormCreateBlockRequest {
78
+ definition: BlockDefinition;
79
+ tmpPath: string;
80
+ newPath: string;
81
+ }
82
+
76
83
  export interface StormFileInfo extends GeneratedFile {
77
84
  type: AIFileTypes;
78
85
  }