@kapeta/local-cluster-service 0.58.6 → 0.60.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,17 @@
1
+ # [0.60.0](https://github.com/kapetacom/local-cluster-service/compare/v0.59.0...v0.60.0) (2024-08-05)
2
+
3
+
4
+ ### Features
5
+
6
+ * Host ui pages on a temp server so we can have it on its own server ([#208](https://github.com/kapetacom/local-cluster-service/issues/208)) ([31ba4b8](https://github.com/kapetacom/local-cluster-service/commit/31ba4b8142c4618cc347037004f8fed87c69c6b7))
7
+
8
+ # [0.59.0](https://github.com/kapetacom/local-cluster-service/compare/v0.58.6...v0.59.0) (2024-08-01)
9
+
10
+
11
+ ### Features
12
+
13
+ * Convert PAGE events to PAGE_URL ([#207](https://github.com/kapetacom/local-cluster-service/issues/207)) ([9fddeec](https://github.com/kapetacom/local-cluster-service/commit/9fddeec4a055a02796d80987ae5b9ca39d7dd9ee))
14
+
1
15
  ## [0.58.6](https://github.com/kapetacom/local-cluster-service/compare/v0.58.5...v0.58.6) (2024-08-01)
2
16
 
3
17
 
@@ -14,9 +14,8 @@ declare class ClusterService {
14
14
  _findClusterServicePort(): Promise<void>;
15
15
  /**
16
16
  * Gets next available port
17
- * @return {Promise<number>}
18
17
  */
19
- getNextAvailablePort(): Promise<number>;
18
+ getNextAvailablePort(startPort?: number): Promise<number>;
20
19
  _checkIfPortIsUsed(port: number, host?: string): Promise<unknown>;
21
20
  /**
22
21
  * The port of this local cluster service itself
@@ -48,14 +48,23 @@ class ClusterService {
48
48
  }
49
49
  /**
50
50
  * Gets next available port
51
- * @return {Promise<number>}
52
51
  */
53
- async getNextAvailablePort() {
52
+ async getNextAvailablePort(startPort = -1) {
53
+ let receivedStartPort = startPort > 0;
54
+ if (!receivedStartPort) {
55
+ startPort = this._currentPort;
56
+ }
54
57
  while (true) {
55
- while (this._reservedPorts.indexOf(this._currentPort) > -1) {
56
- this._currentPort++;
58
+ while (this._reservedPorts.indexOf(startPort) > -1) {
59
+ startPort++;
60
+ if (!receivedStartPort) {
61
+ this._currentPort = startPort;
62
+ }
63
+ }
64
+ const nextPort = startPort++;
65
+ if (!receivedStartPort) {
66
+ this._currentPort = startPort;
57
67
  }
58
- const nextPort = this._currentPort++;
59
68
  const isUsed = await this._checkIfPortIsUsed(nextPort);
60
69
  if (!isUsed) {
61
70
  return nextPort;
@@ -0,0 +1,11 @@
1
+ import { StormEventPage } from './events';
2
+ export declare class UIServer {
3
+ private readonly express;
4
+ private readonly systemId;
5
+ private port;
6
+ private server;
7
+ constructor(systemId: string);
8
+ start(): Promise<void>;
9
+ close(): void;
10
+ resolveUrl(screenData: StormEventPage): string;
11
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UIServer = void 0;
7
+ /**
8
+ * Copyright 2023 Kapeta Inc.
9
+ * SPDX-License-Identifier: BUSL-1.1
10
+ */
11
+ const express_1 = __importDefault(require("express"));
12
+ const page_utils_1 = require("./page-utils");
13
+ const clusterService_1 = require("../clusterService");
14
+ class UIServer {
15
+ express;
16
+ systemId;
17
+ port = 50000;
18
+ server;
19
+ constructor(systemId) {
20
+ this.systemId = systemId;
21
+ this.express = (0, express_1.default)();
22
+ }
23
+ async start() {
24
+ this.port = await clusterService_1.clusterService.getNextAvailablePort(this.port);
25
+ this.express.all('/*', async (req, res) => {
26
+ (0, page_utils_1.readPageFromDisk)(this.systemId, req.params[0], req.method, res);
27
+ });
28
+ return new Promise((resolve) => {
29
+ this.server = this.express.listen(this.port, () => {
30
+ console.log(`UI Server started on port ${this.port}`);
31
+ resolve();
32
+ });
33
+ });
34
+ }
35
+ close() {
36
+ if (this.server) {
37
+ console.log('UI Server closed on port: %s', this.port);
38
+ this.server.close();
39
+ this.server = undefined;
40
+ }
41
+ }
42
+ resolveUrl(screenData) {
43
+ const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
44
+ return `http://localhost:${this.port}${path}`;
45
+ }
46
+ }
47
+ exports.UIServer = UIServer;
@@ -272,12 +272,14 @@ export interface StormEventPhases {
272
272
  }
273
273
  export interface Page {
274
274
  name: string;
275
+ filename: string;
275
276
  title: string;
276
277
  description: string;
277
278
  content: string;
278
279
  path: string;
279
280
  method: string;
280
281
  conversationId: string;
282
+ prompt: string;
281
283
  }
282
284
  export interface StormEventPage {
283
285
  type: 'PAGE';
@@ -285,6 +287,23 @@ export interface StormEventPage {
285
287
  created: number;
286
288
  payload: Page;
287
289
  }
290
+ export interface StormEventPageUrl {
291
+ type: 'PAGE_URL';
292
+ reason: string;
293
+ created: number;
294
+ payload: {
295
+ id: string;
296
+ name: string;
297
+ filename: string;
298
+ title: string;
299
+ description: string;
300
+ path: string;
301
+ url: string;
302
+ method: string;
303
+ conversationId: string;
304
+ prompt: string;
305
+ };
306
+ }
288
307
  export interface UserJourneyScreen {
289
308
  name: string;
290
309
  title: string;
@@ -304,5 +323,5 @@ export interface StormEventUserJourney {
304
323
  created: number;
305
324
  payload: UserJourney;
306
325
  }
307
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage;
326
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage | StormEventPageUrl;
308
327
  export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import { StormEventPage } from './events';
6
+ import { Response } from 'express';
7
+ export declare const SystemIdHeader = "System-Id";
8
+ export declare function writePageToDisk(systemId: string, event: StormEventPage): Promise<{
9
+ path: string;
10
+ }>;
11
+ export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
12
+ export declare function readPageFromDisk(systemId: string, path: string, method: string, res: Response): void;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.writePageToDisk = exports.SystemIdHeader = void 0;
7
+ const node_os_1 = __importDefault(require("node:os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ exports.SystemIdHeader = 'System-Id';
11
+ async function writePageToDisk(systemId, event) {
12
+ const path = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, event.payload.path, event.payload.method.toLowerCase(), 'index.html');
13
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(path));
14
+ await fs_extra_1.default.writeFile(path, event.payload.content);
15
+ console.log(`Page written to disk: ${event.payload.title} > ${path}`);
16
+ return {
17
+ path,
18
+ };
19
+ }
20
+ exports.writePageToDisk = writePageToDisk;
21
+ function readPageFromDiskAsString(systemId, path, method) {
22
+ const filePath = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
23
+ if (!fs_extra_1.default.existsSync(filePath)) {
24
+ return null;
25
+ }
26
+ return fs_extra_1.default.readFileSync(filePath, 'utf8');
27
+ }
28
+ exports.readPageFromDiskAsString = readPageFromDiskAsString;
29
+ function readPageFromDisk(systemId, path, method, res) {
30
+ const filePath = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
31
+ if (!fs_extra_1.default.existsSync(filePath)) {
32
+ res.status(404).send('Page not found');
33
+ return;
34
+ }
35
+ res.type(filePath.split('.').pop());
36
+ const content = fs_extra_1.default.readFileSync(filePath, 'utf8');
37
+ res.write(content);
38
+ res.end();
39
+ }
40
+ exports.readPageFromDisk = readPageFromDisk;
@@ -20,14 +20,48 @@ const codegen_1 = require("./codegen");
20
20
  const assetManager_1 = require("../assetManager");
21
21
  const node_uuid_1 = __importDefault(require("node-uuid"));
22
22
  const PromiseQueue_1 = require("./PromiseQueue");
23
+ const page_utils_1 = require("./page-utils");
24
+ const UIServer_1 = require("./UIServer");
25
+ const UI_SERVERS = {};
23
26
  const router = (0, express_promise_router_1.default)();
24
27
  router.use('/', cors_1.corsHandler);
25
28
  router.use('/', stringBody_1.stringBody);
26
- router.post('/:handle/ui/screen', async (req, res) => {
27
- const handle = req.params.handle;
29
+ function convertPageEvent(screenData, innerConversationId, mainConversationId) {
30
+ if (screenData.type === 'PAGE') {
31
+ const server = UI_SERVERS[mainConversationId];
32
+ if (!server) {
33
+ console.warn('No server found for conversation', mainConversationId);
34
+ }
35
+ screenData.payload.conversationId = innerConversationId;
36
+ return {
37
+ type: 'PAGE_URL',
38
+ reason: screenData.reason,
39
+ created: screenData.created,
40
+ payload: {
41
+ id: node_uuid_1.default.v4(),
42
+ name: screenData.payload.name,
43
+ title: screenData.payload.title,
44
+ filename: screenData.payload.filename,
45
+ description: screenData.payload.description,
46
+ prompt: screenData.payload.prompt,
47
+ path: screenData.payload.path,
48
+ url: server ? server.resolveUrl(screenData) : '',
49
+ method: screenData.payload.method,
50
+ conversationId: innerConversationId,
51
+ },
52
+ };
53
+ }
54
+ return screenData;
55
+ }
56
+ router.all('/ui/:systemId/serve/:method/*', async (req, res) => {
57
+ (0, page_utils_1.readPageFromDisk)(req.params.systemId, req.params[0], req.params.method, res);
58
+ });
59
+ router.post('/ui/screen', async (req, res) => {
28
60
  try {
29
61
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
62
+ const systemId = req.headers[page_utils_1.SystemIdHeader.toLowerCase()];
30
63
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
64
+ aiRequest.storage_prefix = systemId ? systemId + '_' : 'mock_';
31
65
  const screenStream = await stormClient_1.stormClient.createUIPage(aiRequest, conversationId);
32
66
  onRequestAborted(req, res, () => {
33
67
  screenStream.abort();
@@ -35,11 +69,21 @@ router.post('/:handle/ui/screen', async (req, res) => {
35
69
  res.set('Content-Type', 'application/x-ndjson');
36
70
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
37
71
  res.set(stormClient_1.ConversationIdHeader, screenStream.getConversationId());
72
+ const promises = [];
38
73
  screenStream.on('data', (data) => {
39
- console.log('Processing screen event', data);
74
+ switch (data.type) {
75
+ case 'PAGE':
76
+ console.log('Processing page event', data);
77
+ data.payload.conversationId = screenStream.getConversationId();
78
+ if (systemId) {
79
+ promises.push(sendPageEvent(systemId, data, res));
80
+ }
81
+ break;
82
+ }
40
83
  sendEvent(res, data);
41
84
  });
42
85
  await waitForStormStream(screenStream);
86
+ await Promise.allSettled(promises);
43
87
  sendDone(res);
44
88
  }
45
89
  catch (err) {
@@ -49,6 +93,18 @@ router.post('/:handle/ui/screen', async (req, res) => {
49
93
  }
50
94
  }
51
95
  });
96
+ router.delete('/:handle/ui', async (req, res) => {
97
+ const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
98
+ if (!conversationId) {
99
+ res.status(400).send('Missing conversation id');
100
+ return;
101
+ }
102
+ const server = UI_SERVERS[conversationId];
103
+ if (server) {
104
+ server.close();
105
+ delete UI_SERVERS[conversationId];
106
+ }
107
+ });
52
108
  router.post('/:handle/ui', async (req, res) => {
53
109
  const handle = req.params.handle;
54
110
  try {
@@ -62,10 +118,13 @@ router.post('/:handle/ui', async (req, res) => {
62
118
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
63
119
  res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
64
120
  const promises = {};
65
- const queue = new PromiseQueue_1.PromiseQueue(10);
121
+ const queue = new PromiseQueue_1.PromiseQueue(5);
66
122
  onRequestAborted(req, res, () => {
67
123
  queue.cancel();
68
124
  });
125
+ const systemId = userJourneysStream.getConversationId();
126
+ UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
127
+ await UI_SERVERS[systemId].start();
69
128
  userJourneysStream.on('data', async (data) => {
70
129
  try {
71
130
  console.log('Processing user journey event', data);
@@ -91,15 +150,20 @@ router.post('/:handle/ui', async (req, res) => {
91
150
  name: screen.name,
92
151
  title: screen.title,
93
152
  filename: screen.filename,
153
+ storage_prefix: userJourneysStream.getConversationId() + '_',
94
154
  }, innerConversationId);
155
+ const promises = [];
95
156
  screenStream.on('data', (screenData) => {
96
157
  if (screenData.type === 'PAGE') {
97
158
  screenData.payload.conversationId = innerConversationId;
159
+ promises.push(sendPageEvent(userJourneysStream.getConversationId(), screenData, res));
160
+ }
161
+ else {
162
+ sendEvent(res, screenData);
98
163
  }
99
- sendEvent(res, screenData);
100
164
  });
101
165
  screenStream.on('end', () => {
102
- resolve();
166
+ Promise.allSettled(promises).finally(resolve);
103
167
  });
104
168
  }
105
169
  catch (e) {
@@ -131,16 +195,45 @@ router.post('/ui/edit', async (req, res) => {
131
195
  try {
132
196
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
133
197
  const aiRequest = JSON.parse(req.stringBody ?? '{}');
134
- const editStream = await stormClient_1.stormClient.editPages(aiRequest.prompt, conversationId);
198
+ const pages = aiRequest.prompt.pages
199
+ .map((page) => {
200
+ const content = (0, page_utils_1.readPageFromDiskAsString)(conversationId, page.path, page.method);
201
+ if (!content) {
202
+ console.warn('Page not found', page);
203
+ return undefined;
204
+ }
205
+ return {
206
+ filename: page.filename,
207
+ path: page.path,
208
+ method: page.method,
209
+ title: page.title,
210
+ conversationId: page.conversationId,
211
+ prompt: page.prompt,
212
+ name: page.name,
213
+ description: page.description,
214
+ content,
215
+ };
216
+ })
217
+ .filter((page) => !!page);
218
+ const editStream = await stormClient_1.stormClient.editPages({
219
+ ...aiRequest.prompt,
220
+ pages,
221
+ }, conversationId);
135
222
  onRequestAborted(req, res, () => {
136
223
  editStream.abort();
137
224
  });
138
225
  res.set('Content-Type', 'application/x-ndjson');
139
226
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
140
227
  res.set(stormClient_1.ConversationIdHeader, editStream.getConversationId());
228
+ const promises = [];
141
229
  editStream.on('data', (data) => {
142
230
  try {
143
- sendEvent(res, data);
231
+ if (data.type === 'PAGE') {
232
+ promises.push(sendPageEvent(editStream.getConversationId(), data, res));
233
+ }
234
+ else {
235
+ sendEvent(res, data);
236
+ }
144
237
  }
145
238
  catch (e) {
146
239
  console.error('Failed to process event', e);
@@ -150,6 +243,7 @@ router.post('/ui/edit', async (req, res) => {
150
243
  if (editStream.isAborted()) {
151
244
  return;
152
245
  }
246
+ await Promise.all(promises);
153
247
  sendDone(res);
154
248
  }
155
249
  catch (err) {
@@ -351,4 +445,13 @@ function onRequestAborted(req, res, onAborted) {
351
445
  onAborted();
352
446
  });
353
447
  }
448
+ function sendPageEvent(mainConversationId, data, res) {
449
+ return (0, page_utils_1.writePageToDisk)(mainConversationId, data)
450
+ .catch((err) => {
451
+ console.error('Failed to write page to disk', err);
452
+ })
453
+ .then(() => {
454
+ sendEvent(res, convertPageEvent(data, data.payload.conversationId, mainConversationId));
455
+ });
456
+ }
354
457
  exports.default = router;
@@ -1,4 +1,5 @@
1
1
  import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
+ import { Page, StormEventPageUrl } from './events';
2
3
  export declare const STORM_ID = "storm";
3
4
  export declare const ConversationIdHeader = "Conversation-Id";
4
5
  export interface UIPagePrompt {
@@ -9,14 +10,18 @@ export interface UIPagePrompt {
9
10
  path: string;
10
11
  method: string;
11
12
  description: string;
13
+ storage_prefix: string;
12
14
  }
13
15
  export interface UIPageEditPrompt {
14
16
  planDescription: string;
15
17
  blockDescription: string;
16
- pages: {
17
- filename: string;
18
- content: string;
19
- }[];
18
+ pages: Page[];
19
+ prompt: string;
20
+ }
21
+ export interface UIPageEditRequest {
22
+ planDescription: string;
23
+ blockDescription: string;
24
+ pages: StormEventPageUrl['payload'][];
20
25
  prompt: string;
21
26
  }
22
27
  declare class StormClient {
@@ -14,9 +14,8 @@ declare class ClusterService {
14
14
  _findClusterServicePort(): Promise<void>;
15
15
  /**
16
16
  * Gets next available port
17
- * @return {Promise<number>}
18
17
  */
19
- getNextAvailablePort(): Promise<number>;
18
+ getNextAvailablePort(startPort?: number): Promise<number>;
20
19
  _checkIfPortIsUsed(port: number, host?: string): Promise<unknown>;
21
20
  /**
22
21
  * The port of this local cluster service itself
@@ -48,14 +48,23 @@ class ClusterService {
48
48
  }
49
49
  /**
50
50
  * Gets next available port
51
- * @return {Promise<number>}
52
51
  */
53
- async getNextAvailablePort() {
52
+ async getNextAvailablePort(startPort = -1) {
53
+ let receivedStartPort = startPort > 0;
54
+ if (!receivedStartPort) {
55
+ startPort = this._currentPort;
56
+ }
54
57
  while (true) {
55
- while (this._reservedPorts.indexOf(this._currentPort) > -1) {
56
- this._currentPort++;
58
+ while (this._reservedPorts.indexOf(startPort) > -1) {
59
+ startPort++;
60
+ if (!receivedStartPort) {
61
+ this._currentPort = startPort;
62
+ }
63
+ }
64
+ const nextPort = startPort++;
65
+ if (!receivedStartPort) {
66
+ this._currentPort = startPort;
57
67
  }
58
- const nextPort = this._currentPort++;
59
68
  const isUsed = await this._checkIfPortIsUsed(nextPort);
60
69
  if (!isUsed) {
61
70
  return nextPort;
@@ -0,0 +1,11 @@
1
+ import { StormEventPage } from './events';
2
+ export declare class UIServer {
3
+ private readonly express;
4
+ private readonly systemId;
5
+ private port;
6
+ private server;
7
+ constructor(systemId: string);
8
+ start(): Promise<void>;
9
+ close(): void;
10
+ resolveUrl(screenData: StormEventPage): string;
11
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UIServer = void 0;
7
+ /**
8
+ * Copyright 2023 Kapeta Inc.
9
+ * SPDX-License-Identifier: BUSL-1.1
10
+ */
11
+ const express_1 = __importDefault(require("express"));
12
+ const page_utils_1 = require("./page-utils");
13
+ const clusterService_1 = require("../clusterService");
14
+ class UIServer {
15
+ express;
16
+ systemId;
17
+ port = 50000;
18
+ server;
19
+ constructor(systemId) {
20
+ this.systemId = systemId;
21
+ this.express = (0, express_1.default)();
22
+ }
23
+ async start() {
24
+ this.port = await clusterService_1.clusterService.getNextAvailablePort(this.port);
25
+ this.express.all('/*', async (req, res) => {
26
+ (0, page_utils_1.readPageFromDisk)(this.systemId, req.params[0], req.method, res);
27
+ });
28
+ return new Promise((resolve) => {
29
+ this.server = this.express.listen(this.port, () => {
30
+ console.log(`UI Server started on port ${this.port}`);
31
+ resolve();
32
+ });
33
+ });
34
+ }
35
+ close() {
36
+ if (this.server) {
37
+ console.log('UI Server closed on port: %s', this.port);
38
+ this.server.close();
39
+ this.server = undefined;
40
+ }
41
+ }
42
+ resolveUrl(screenData) {
43
+ const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
44
+ return `http://localhost:${this.port}${path}`;
45
+ }
46
+ }
47
+ exports.UIServer = UIServer;
@@ -272,12 +272,14 @@ export interface StormEventPhases {
272
272
  }
273
273
  export interface Page {
274
274
  name: string;
275
+ filename: string;
275
276
  title: string;
276
277
  description: string;
277
278
  content: string;
278
279
  path: string;
279
280
  method: string;
280
281
  conversationId: string;
282
+ prompt: string;
281
283
  }
282
284
  export interface StormEventPage {
283
285
  type: 'PAGE';
@@ -285,6 +287,23 @@ export interface StormEventPage {
285
287
  created: number;
286
288
  payload: Page;
287
289
  }
290
+ export interface StormEventPageUrl {
291
+ type: 'PAGE_URL';
292
+ reason: string;
293
+ created: number;
294
+ payload: {
295
+ id: string;
296
+ name: string;
297
+ filename: string;
298
+ title: string;
299
+ description: string;
300
+ path: string;
301
+ url: string;
302
+ method: string;
303
+ conversationId: string;
304
+ prompt: string;
305
+ };
306
+ }
288
307
  export interface UserJourneyScreen {
289
308
  name: string;
290
309
  title: string;
@@ -304,5 +323,5 @@ export interface StormEventUserJourney {
304
323
  created: number;
305
324
  payload: UserJourney;
306
325
  }
307
- export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage;
326
+ export type StormEvent = StormEventCreateBlock | StormEventCreateConnection | StormEventCreatePlanProperties | StormEventInvalidResponse | StormEventPlanRetry | StormEventCreateDSL | StormEventCreateDSLResource | StormEventError | StormEventScreen | StormEventScreenCandidate | StormEventFileLogical | StormEventFileState | StormEventFileDone | StormEventFileFailed | StormEventFileChunk | StormEventDone | StormEventDefinitionChange | StormEventErrorClassifier | StormEventCodeFix | StormEventErrorDetails | StormEventBlockReady | StormEventPhases | StormEventBlockStatus | StormEventCreateDSLRetry | StormEventUserJourney | StormEventPage | StormEventPageUrl;
308
327
  export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import { StormEventPage } from './events';
6
+ import { Response } from 'express';
7
+ export declare const SystemIdHeader = "System-Id";
8
+ export declare function writePageToDisk(systemId: string, event: StormEventPage): Promise<{
9
+ path: string;
10
+ }>;
11
+ export declare function readPageFromDiskAsString(systemId: string, path: string, method: string): string | null;
12
+ export declare function readPageFromDisk(systemId: string, path: string, method: string, res: Response): void;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.writePageToDisk = exports.SystemIdHeader = void 0;
7
+ const node_os_1 = __importDefault(require("node:os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ exports.SystemIdHeader = 'System-Id';
11
+ async function writePageToDisk(systemId, event) {
12
+ const path = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, event.payload.path, event.payload.method.toLowerCase(), 'index.html');
13
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(path));
14
+ await fs_extra_1.default.writeFile(path, event.payload.content);
15
+ console.log(`Page written to disk: ${event.payload.title} > ${path}`);
16
+ return {
17
+ path,
18
+ };
19
+ }
20
+ exports.writePageToDisk = writePageToDisk;
21
+ function readPageFromDiskAsString(systemId, path, method) {
22
+ const filePath = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
23
+ if (!fs_extra_1.default.existsSync(filePath)) {
24
+ return null;
25
+ }
26
+ return fs_extra_1.default.readFileSync(filePath, 'utf8');
27
+ }
28
+ exports.readPageFromDiskAsString = readPageFromDiskAsString;
29
+ function readPageFromDisk(systemId, path, method, res) {
30
+ const filePath = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId, path, method.toLowerCase(), 'index.html');
31
+ if (!fs_extra_1.default.existsSync(filePath)) {
32
+ res.status(404).send('Page not found');
33
+ return;
34
+ }
35
+ res.type(filePath.split('.').pop());
36
+ const content = fs_extra_1.default.readFileSync(filePath, 'utf8');
37
+ res.write(content);
38
+ res.end();
39
+ }
40
+ exports.readPageFromDisk = readPageFromDisk;