@kapeta/local-cluster-service 0.67.5 → 0.69.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.69.0](https://github.com/kapetacom/local-cluster-service/compare/v0.68.0...v0.69.0) (2024-09-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * adding create-system api ([d35545a](https://github.com/kapetacom/local-cluster-service/commit/d35545a3f35916116eca2cd116e1e9b923f1a8c2))
7
+
8
+ # [0.68.0](https://github.com/kapetacom/local-cluster-service/compare/v0.67.5...v0.68.0) (2024-09-03)
9
+
10
+
11
+ ### Features
12
+
13
+ * Add fallback page to ui server requests ([45bcf5d](https://github.com/kapetacom/local-cluster-service/commit/45bcf5d596ca359a9ddd63512b705ca840a555c8))
14
+
1
15
  ## [0.67.5](https://github.com/kapetacom/local-cluster-service/compare/v0.67.4...v0.67.5) (2024-09-02)
2
16
 
3
17
 
@@ -115,15 +115,101 @@ exports.readPageFromDiskAsString = readPageFromDiskAsString;
115
115
  function readPageFromDisk(systemId, path, method, res) {
116
116
  const filePath = resolveReadPath(systemId, path, method);
117
117
  if (!filePath || !fs_extra_1.default.existsSync(filePath)) {
118
- res.status(404).send('Page not found');
118
+ if (method === 'HEAD') {
119
+ // For HEAD requests, only return the status and headers
120
+ res.status(202).set('Retry-After', '3').end();
121
+ }
122
+ else {
123
+ // For GET requests, return the fallback HTML with status 202
124
+ res.status(202).set('Retry-After', '3').send(getFallbackHtml(path, method));
125
+ }
119
126
  return;
120
127
  }
121
128
  res.type(filePath.split('.').pop());
122
129
  const content = fs_extra_1.default.readFileSync(filePath);
123
- res.write(content);
124
- res.end();
130
+ if (method === 'HEAD') {
131
+ // For HEAD requests, just end the response after setting headers
132
+ res.status(200).end();
133
+ }
134
+ else {
135
+ // For GET requests, return the full content
136
+ res.write(content);
137
+ res.end();
138
+ }
125
139
  }
126
140
  exports.readPageFromDisk = readPageFromDisk;
141
+ function getFallbackHtml(path, method) {
142
+ return `
143
+ <!DOCTYPE html>
144
+ <html lang="en">
145
+
146
+ <head>
147
+ <meta charset="UTF-8">
148
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
149
+ <title>Page Not Ready</title>
150
+ <style>
151
+ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;600&display=swap');
152
+
153
+ body {
154
+ margin: 0;
155
+ padding: 0;
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ height: 100vh;
160
+ font-family: 'Roboto', sans-serif;
161
+ background: #1E1F20;
162
+ color: white;
163
+ text-align: center;
164
+ }
165
+
166
+ h1 {
167
+ font-size: 2rem;
168
+ font-weight: 600;
169
+ margin-bottom: 1rem;
170
+ }
171
+
172
+ p {
173
+ font-size: 1rem;
174
+ font-weight: 400;
175
+ }
176
+ </style>
177
+ </head>
178
+
179
+ <body>
180
+ <div>
181
+ <h1>Page Not Ready</h1>
182
+ <p>Henrik is still working on this page. Please wait...</p>
183
+ </div>
184
+ <script>
185
+ const checkInterval = 3000;
186
+ function checkPageReady() {
187
+ fetch('${path}', { method: 'HEAD' })
188
+ .then(response => {
189
+ if (response.status === 200) {
190
+ // The page is ready, reload to fetch it
191
+ window.location.reload();
192
+ } else if (response.status === 202) {
193
+ const retryAfter = response.headers.get('Retry-After');
194
+ const retryInterval = retryAfter ? parseInt(retryAfter) * 1000 : 3000;
195
+ setTimeout(checkPageReady, retryInterval);
196
+ } else {
197
+ // Handle other unexpected statuses
198
+ setTimeout(checkPageReady, 3000);
199
+ }
200
+ })
201
+ .catch(error => {
202
+ console.error('Error checking page status:', error);
203
+ setTimeout(checkPageReady, checkInterval);
204
+ });
205
+ }
206
+ setTimeout(checkPageReady, checkInterval);
207
+ </script>
208
+ </body>
209
+
210
+ </html>
211
+ `;
212
+ }
127
213
  function readConversationFromFile(filename) {
128
214
  if (!fs_extra_1.default.existsSync(filename)) {
129
215
  return [];
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  const express_promise_router_1 = __importDefault(require("express-promise-router"));
11
11
  const fs_extra_1 = __importDefault(require("fs-extra"));
12
12
  const path_1 = __importDefault(require("path"));
13
+ const node_os_1 = __importDefault(require("node:os"));
13
14
  const lodash_1 = __importDefault(require("lodash"));
14
15
  const cors_1 = require("../middleware/cors");
15
16
  const stringBody_1 = require("../middleware/stringBody");
@@ -24,6 +25,7 @@ const UIServer_1 = require("./UIServer");
24
25
  const crypto_1 = require("crypto");
25
26
  const PageGenerator_1 = require("./PageGenerator");
26
27
  const PromiseQueue_1 = require("./PromiseQueue");
28
+ const utils_1 = require("./utils");
27
29
  const UI_SERVERS = {};
28
30
  const router = (0, express_promise_router_1.default)();
29
31
  router.use('/', cors_1.corsHandler);
@@ -59,6 +61,20 @@ function convertPageEvent(screenData, innerConversationId, mainConversationId) {
59
61
  router.all('/ui/:systemId/serve/:method/*', async (req, res) => {
60
62
  (0, page_utils_1.readPageFromDisk)(req.params.systemId, req.params[0], req.params.method, res);
61
63
  });
64
+ router.post('/ui/create-system/:systemId', async (req, res) => {
65
+ const systemId = req.params.systemId;
66
+ const srcDir = (0, page_utils_1.getSystemBaseDir)(systemId);
67
+ const destDir = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems-impl', systemId);
68
+ await (0, utils_1.copyDirectory)(srcDir, destDir, async (fileName, content) => {
69
+ const result = await stormClient_1.stormClient.implementAPIClients({
70
+ content: content,
71
+ fileName: fileName,
72
+ });
73
+ return result;
74
+ });
75
+ res.end();
76
+ return;
77
+ });
62
78
  router.post('/ui/screen', async (req, res) => {
63
79
  try {
64
80
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" />
2
- import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
+ import { ConversationItem, ImplementAPIClientsRequest, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
3
3
  import { Page, StormEventPageUrl } from './events';
4
4
  export declare const STORM_ID = "storm";
5
5
  export declare const ConversationIdHeader = "Conversation-Id";
@@ -70,6 +70,7 @@ declare class StormClient {
70
70
  getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string): Promise<{
71
71
  vote: -1 | 0 | 1;
72
72
  }>;
73
+ implementAPIClients(prompt: ImplementAPIClientsRequest): Promise<string>;
73
74
  classifyUIReferences(prompt: string, conversationId?: string): Promise<StormStream>;
74
75
  editPages(prompt: UIPageEditPrompt, conversationId?: string): Promise<StormStream>;
75
76
  listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
@@ -137,6 +137,18 @@ class StormClient {
137
137
  });
138
138
  return response.json();
139
139
  }
140
+ async implementAPIClients(prompt) {
141
+ const u = `${this._baseUrl}/v2/ui/implement-api-clients`;
142
+ const response = await fetch(u, {
143
+ method: 'POST',
144
+ body: JSON.stringify({
145
+ fileName: prompt.fileName,
146
+ content: prompt.content,
147
+ }),
148
+ });
149
+ const data = await response.text();
150
+ return data;
151
+ }
140
152
  classifyUIReferences(prompt, conversationId) {
141
153
  return this.send('/v2/ui/references', {
142
154
  prompt: prompt,
@@ -72,3 +72,7 @@ export interface StormUIListPrompt {
72
72
  blockName: string;
73
73
  prompt: string;
74
74
  }
75
+ export interface ImplementAPIClientsRequest {
76
+ fileName: string;
77
+ content: string;
78
+ }
@@ -0,0 +1 @@
1
+ export declare function copyDirectory(src: string, dest: string, modifyHtml: (fileName: string, content: string) => Promise<string>): Promise<void>;
@@ -0,0 +1,31 @@
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.copyDirectory = void 0;
7
+ /**
8
+ * Copyright 2023 Kapeta Inc.
9
+ * SPDX-License-Identifier: BUSL-1.1
10
+ */
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ const path_1 = __importDefault(require("path"));
13
+ async function copyDirectory(src, dest, modifyHtml) {
14
+ await fs_extra_1.default.promises.mkdir(dest, { recursive: true });
15
+ const entries = await fs_extra_1.default.promises.readdir(src, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ const srcPath = path_1.default.join(src, entry.name);
18
+ const destPath = path_1.default.join(dest, entry.name);
19
+ if (entry.isDirectory()) {
20
+ await copyDirectory(srcPath, destPath, modifyHtml);
21
+ }
22
+ else if (entry.isFile()) {
23
+ let content = await fs_extra_1.default.promises.readFile(srcPath, 'utf-8');
24
+ if (path_1.default.extname(srcPath) === '.html') {
25
+ content = await modifyHtml(srcPath, content);
26
+ }
27
+ await fs_extra_1.default.promises.writeFile(destPath, content, 'utf-8');
28
+ }
29
+ }
30
+ }
31
+ exports.copyDirectory = copyDirectory;
@@ -115,15 +115,101 @@ exports.readPageFromDiskAsString = readPageFromDiskAsString;
115
115
  function readPageFromDisk(systemId, path, method, res) {
116
116
  const filePath = resolveReadPath(systemId, path, method);
117
117
  if (!filePath || !fs_extra_1.default.existsSync(filePath)) {
118
- res.status(404).send('Page not found');
118
+ if (method === 'HEAD') {
119
+ // For HEAD requests, only return the status and headers
120
+ res.status(202).set('Retry-After', '3').end();
121
+ }
122
+ else {
123
+ // For GET requests, return the fallback HTML with status 202
124
+ res.status(202).set('Retry-After', '3').send(getFallbackHtml(path, method));
125
+ }
119
126
  return;
120
127
  }
121
128
  res.type(filePath.split('.').pop());
122
129
  const content = fs_extra_1.default.readFileSync(filePath);
123
- res.write(content);
124
- res.end();
130
+ if (method === 'HEAD') {
131
+ // For HEAD requests, just end the response after setting headers
132
+ res.status(200).end();
133
+ }
134
+ else {
135
+ // For GET requests, return the full content
136
+ res.write(content);
137
+ res.end();
138
+ }
125
139
  }
126
140
  exports.readPageFromDisk = readPageFromDisk;
141
+ function getFallbackHtml(path, method) {
142
+ return `
143
+ <!DOCTYPE html>
144
+ <html lang="en">
145
+
146
+ <head>
147
+ <meta charset="UTF-8">
148
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
149
+ <title>Page Not Ready</title>
150
+ <style>
151
+ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;600&display=swap');
152
+
153
+ body {
154
+ margin: 0;
155
+ padding: 0;
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ height: 100vh;
160
+ font-family: 'Roboto', sans-serif;
161
+ background: #1E1F20;
162
+ color: white;
163
+ text-align: center;
164
+ }
165
+
166
+ h1 {
167
+ font-size: 2rem;
168
+ font-weight: 600;
169
+ margin-bottom: 1rem;
170
+ }
171
+
172
+ p {
173
+ font-size: 1rem;
174
+ font-weight: 400;
175
+ }
176
+ </style>
177
+ </head>
178
+
179
+ <body>
180
+ <div>
181
+ <h1>Page Not Ready</h1>
182
+ <p>Henrik is still working on this page. Please wait...</p>
183
+ </div>
184
+ <script>
185
+ const checkInterval = 3000;
186
+ function checkPageReady() {
187
+ fetch('${path}', { method: 'HEAD' })
188
+ .then(response => {
189
+ if (response.status === 200) {
190
+ // The page is ready, reload to fetch it
191
+ window.location.reload();
192
+ } else if (response.status === 202) {
193
+ const retryAfter = response.headers.get('Retry-After');
194
+ const retryInterval = retryAfter ? parseInt(retryAfter) * 1000 : 3000;
195
+ setTimeout(checkPageReady, retryInterval);
196
+ } else {
197
+ // Handle other unexpected statuses
198
+ setTimeout(checkPageReady, 3000);
199
+ }
200
+ })
201
+ .catch(error => {
202
+ console.error('Error checking page status:', error);
203
+ setTimeout(checkPageReady, checkInterval);
204
+ });
205
+ }
206
+ setTimeout(checkPageReady, checkInterval);
207
+ </script>
208
+ </body>
209
+
210
+ </html>
211
+ `;
212
+ }
127
213
  function readConversationFromFile(filename) {
128
214
  if (!fs_extra_1.default.existsSync(filename)) {
129
215
  return [];
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  const express_promise_router_1 = __importDefault(require("express-promise-router"));
11
11
  const fs_extra_1 = __importDefault(require("fs-extra"));
12
12
  const path_1 = __importDefault(require("path"));
13
+ const node_os_1 = __importDefault(require("node:os"));
13
14
  const lodash_1 = __importDefault(require("lodash"));
14
15
  const cors_1 = require("../middleware/cors");
15
16
  const stringBody_1 = require("../middleware/stringBody");
@@ -24,6 +25,7 @@ const UIServer_1 = require("./UIServer");
24
25
  const crypto_1 = require("crypto");
25
26
  const PageGenerator_1 = require("./PageGenerator");
26
27
  const PromiseQueue_1 = require("./PromiseQueue");
28
+ const utils_1 = require("./utils");
27
29
  const UI_SERVERS = {};
28
30
  const router = (0, express_promise_router_1.default)();
29
31
  router.use('/', cors_1.corsHandler);
@@ -59,6 +61,20 @@ function convertPageEvent(screenData, innerConversationId, mainConversationId) {
59
61
  router.all('/ui/:systemId/serve/:method/*', async (req, res) => {
60
62
  (0, page_utils_1.readPageFromDisk)(req.params.systemId, req.params[0], req.params.method, res);
61
63
  });
64
+ router.post('/ui/create-system/:systemId', async (req, res) => {
65
+ const systemId = req.params.systemId;
66
+ const srcDir = (0, page_utils_1.getSystemBaseDir)(systemId);
67
+ const destDir = path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems-impl', systemId);
68
+ await (0, utils_1.copyDirectory)(srcDir, destDir, async (fileName, content) => {
69
+ const result = await stormClient_1.stormClient.implementAPIClients({
70
+ content: content,
71
+ fileName: fileName,
72
+ });
73
+ return result;
74
+ });
75
+ res.end();
76
+ return;
77
+ });
62
78
  router.post('/ui/screen', async (req, res) => {
63
79
  try {
64
80
  const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" />
2
- import { ConversationItem, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
2
+ import { ConversationItem, ImplementAPIClientsRequest, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
3
3
  import { Page, StormEventPageUrl } from './events';
4
4
  export declare const STORM_ID = "storm";
5
5
  export declare const ConversationIdHeader = "Conversation-Id";
@@ -70,6 +70,7 @@ declare class StormClient {
70
70
  getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string): Promise<{
71
71
  vote: -1 | 0 | 1;
72
72
  }>;
73
+ implementAPIClients(prompt: ImplementAPIClientsRequest): Promise<string>;
73
74
  classifyUIReferences(prompt: string, conversationId?: string): Promise<StormStream>;
74
75
  editPages(prompt: UIPageEditPrompt, conversationId?: string): Promise<StormStream>;
75
76
  listScreens(prompt: StormUIListPrompt, conversationId?: string): Promise<StormStream>;
@@ -137,6 +137,18 @@ class StormClient {
137
137
  });
138
138
  return response.json();
139
139
  }
140
+ async implementAPIClients(prompt) {
141
+ const u = `${this._baseUrl}/v2/ui/implement-api-clients`;
142
+ const response = await fetch(u, {
143
+ method: 'POST',
144
+ body: JSON.stringify({
145
+ fileName: prompt.fileName,
146
+ content: prompt.content,
147
+ }),
148
+ });
149
+ const data = await response.text();
150
+ return data;
151
+ }
140
152
  classifyUIReferences(prompt, conversationId) {
141
153
  return this.send('/v2/ui/references', {
142
154
  prompt: prompt,
@@ -72,3 +72,7 @@ export interface StormUIListPrompt {
72
72
  blockName: string;
73
73
  prompt: string;
74
74
  }
75
+ export interface ImplementAPIClientsRequest {
76
+ fileName: string;
77
+ content: string;
78
+ }
@@ -0,0 +1 @@
1
+ export declare function copyDirectory(src: string, dest: string, modifyHtml: (fileName: string, content: string) => Promise<string>): Promise<void>;
@@ -0,0 +1,31 @@
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.copyDirectory = void 0;
7
+ /**
8
+ * Copyright 2023 Kapeta Inc.
9
+ * SPDX-License-Identifier: BUSL-1.1
10
+ */
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ const path_1 = __importDefault(require("path"));
13
+ async function copyDirectory(src, dest, modifyHtml) {
14
+ await fs_extra_1.default.promises.mkdir(dest, { recursive: true });
15
+ const entries = await fs_extra_1.default.promises.readdir(src, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ const srcPath = path_1.default.join(src, entry.name);
18
+ const destPath = path_1.default.join(dest, entry.name);
19
+ if (entry.isDirectory()) {
20
+ await copyDirectory(srcPath, destPath, modifyHtml);
21
+ }
22
+ else if (entry.isFile()) {
23
+ let content = await fs_extra_1.default.promises.readFile(srcPath, 'utf-8');
24
+ if (path_1.default.extname(srcPath) === '.html') {
25
+ content = await modifyHtml(srcPath, content);
26
+ }
27
+ await fs_extra_1.default.promises.writeFile(destPath, content, 'utf-8');
28
+ }
29
+ }
30
+ }
31
+ exports.copyDirectory = copyDirectory;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.67.5",
3
+ "version": "0.69.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -144,15 +144,101 @@ export function readPageFromDiskAsString(systemId: string, path: string, method:
144
144
  export function readPageFromDisk(systemId: string, path: string, method: string, res: Response) {
145
145
  const filePath = resolveReadPath(systemId, path, method);
146
146
  if (!filePath || !FS.existsSync(filePath)) {
147
- res.status(404).send('Page not found');
147
+ if (method === 'HEAD') {
148
+ // For HEAD requests, only return the status and headers
149
+ res.status(202).set('Retry-After', '3').end();
150
+ } else {
151
+ // For GET requests, return the fallback HTML with status 202
152
+ res.status(202).set('Retry-After', '3').send(getFallbackHtml(path, method));
153
+ }
148
154
  return;
149
155
  }
150
156
 
151
157
  res.type(filePath.split('.').pop() as string);
152
158
 
153
159
  const content = FS.readFileSync(filePath);
154
- res.write(content);
155
- res.end();
160
+
161
+ if (method === 'HEAD') {
162
+ // For HEAD requests, just end the response after setting headers
163
+ res.status(200).end();
164
+ } else {
165
+ // For GET requests, return the full content
166
+ res.write(content);
167
+ res.end();
168
+ }
169
+ }
170
+
171
+ function getFallbackHtml(path: string, method: string): string {
172
+ return `
173
+ <!DOCTYPE html>
174
+ <html lang="en">
175
+
176
+ <head>
177
+ <meta charset="UTF-8">
178
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
179
+ <title>Page Not Ready</title>
180
+ <style>
181
+ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;600&display=swap');
182
+
183
+ body {
184
+ margin: 0;
185
+ padding: 0;
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: center;
189
+ height: 100vh;
190
+ font-family: 'Roboto', sans-serif;
191
+ background: #1E1F20;
192
+ color: white;
193
+ text-align: center;
194
+ }
195
+
196
+ h1 {
197
+ font-size: 2rem;
198
+ font-weight: 600;
199
+ margin-bottom: 1rem;
200
+ }
201
+
202
+ p {
203
+ font-size: 1rem;
204
+ font-weight: 400;
205
+ }
206
+ </style>
207
+ </head>
208
+
209
+ <body>
210
+ <div>
211
+ <h1>Page Not Ready</h1>
212
+ <p>Henrik is still working on this page. Please wait...</p>
213
+ </div>
214
+ <script>
215
+ const checkInterval = 3000;
216
+ function checkPageReady() {
217
+ fetch('${path}', { method: 'HEAD' })
218
+ .then(response => {
219
+ if (response.status === 200) {
220
+ // The page is ready, reload to fetch it
221
+ window.location.reload();
222
+ } else if (response.status === 202) {
223
+ const retryAfter = response.headers.get('Retry-After');
224
+ const retryInterval = retryAfter ? parseInt(retryAfter) * 1000 : 3000;
225
+ setTimeout(checkPageReady, retryInterval);
226
+ } else {
227
+ // Handle other unexpected statuses
228
+ setTimeout(checkPageReady, 3000);
229
+ }
230
+ })
231
+ .catch(error => {
232
+ console.error('Error checking page status:', error);
233
+ setTimeout(checkPageReady, checkInterval);
234
+ });
235
+ }
236
+ setTimeout(checkPageReady, checkInterval);
237
+ </script>
238
+ </body>
239
+
240
+ </html>
241
+ `;
156
242
  }
157
243
 
158
244
  export interface Conversation {
@@ -7,6 +7,7 @@ import Router from 'express-promise-router';
7
7
  import FS from 'fs-extra';
8
8
  import { Response } from 'express';
9
9
  import Path from 'path';
10
+ import os from 'node:os';
10
11
  import _ from 'lodash';
11
12
  import { corsHandler } from '../middleware/cors';
12
13
  import { stringBody } from '../middleware/stringBody';
@@ -34,11 +35,12 @@ import {
34
35
  import { StormCodegen } from './codegen';
35
36
  import { assetManager } from '../assetManager';
36
37
  import uuid from 'node-uuid';
37
- import { readPageFromDisk, SystemIdHeader, writeAssetToDisk, writeImageToDisk, writePageToDisk } from './page-utils';
38
+ import { getSystemBaseDir, readPageFromDisk, resolveReadPath, SystemIdHeader, writeAssetToDisk, writeImageToDisk, writePageToDisk } from './page-utils';
38
39
  import { UIServer } from './UIServer';
39
40
  import { randomUUID } from 'crypto';
40
41
  import { ImagePrompt, PageQueue } from './PageGenerator';
41
42
  import { createFuture } from './PromiseQueue';
43
+ import { copyDirectory } from './utils';
42
44
 
43
45
  const UI_SERVERS: { [key: string]: UIServer } = {};
44
46
  const router = Router();
@@ -81,6 +83,25 @@ router.all('/ui/:systemId/serve/:method/*', async (req: KapetaBodyRequest, res:
81
83
  readPageFromDisk(req.params.systemId, req.params[0], req.params.method, res);
82
84
  });
83
85
 
86
+ router.post('/ui/create-system/:systemId', async (req: KapetaBodyRequest, res: Response) => {
87
+ const systemId = req.params.systemId as string;
88
+ const srcDir = getSystemBaseDir(systemId);
89
+ const destDir = Path.join(os.tmpdir(), 'ai-systems-impl', systemId);
90
+
91
+ await copyDirectory(srcDir, destDir, async (fileName, content) => {
92
+ const result = await stormClient.implementAPIClients({
93
+ content: content,
94
+ fileName: fileName,
95
+ });
96
+ return result;
97
+ });
98
+
99
+ res.end();
100
+ return;
101
+ });
102
+
103
+
104
+
84
105
  router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
85
106
  try {
86
107
  const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
@@ -8,13 +8,13 @@ import readLine from 'node:readline/promises';
8
8
  import { Readable } from 'node:stream';
9
9
  import {
10
10
  ConversationItem,
11
+ ImplementAPIClientsRequest,
11
12
  StormContextRequest,
12
13
  StormFileImplementationPrompt,
13
14
  StormStream,
14
15
  StormUIImplementationPrompt,
15
16
  StormUIListPrompt,
16
17
  } from './stream';
17
- import { getRawAsset } from 'node:sea';
18
18
  import { Page, StormEventPageUrl } from './events';
19
19
 
20
20
  export const STORM_ID = 'storm';
@@ -242,6 +242,19 @@ class StormClient {
242
242
  return response.json() as Promise<{ vote: -1 | 0 | 1 }>;
243
243
  }
244
244
 
245
+ public async implementAPIClients(prompt: ImplementAPIClientsRequest) {
246
+ const u = `${this._baseUrl}/v2/ui/implement-api-clients`;
247
+ const response = await fetch(u, {
248
+ method: 'POST',
249
+ body: JSON.stringify({
250
+ fileName: prompt.fileName,
251
+ content: prompt.content,
252
+ }),
253
+ });
254
+ const data = await response.text();
255
+ return data;
256
+ }
257
+
245
258
  public classifyUIReferences(prompt: string, conversationId?: string) {
246
259
  return this.send('/v2/ui/references', {
247
260
  prompt: prompt,
@@ -142,3 +142,8 @@ export interface StormUIListPrompt {
142
142
  blockName: string;
143
143
  prompt: string;
144
144
  }
145
+
146
+ export interface ImplementAPIClientsRequest {
147
+ fileName: string;
148
+ content: string
149
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import FS from 'fs-extra';
6
+ import Path from 'path';
7
+
8
+ export async function copyDirectory(src: string, dest: string, modifyHtml: (fileName: string, content: string) => Promise<string>): Promise<void> {
9
+ await FS.promises.mkdir(dest, { recursive: true });
10
+ const entries = await FS.promises.readdir(src, { withFileTypes: true });
11
+
12
+ for (const entry of entries) {
13
+ const srcPath = Path.join(src, entry.name);
14
+ const destPath = Path.join(dest, entry.name);
15
+
16
+ if (entry.isDirectory()) {
17
+ await copyDirectory(srcPath, destPath, modifyHtml);
18
+ } else if (entry.isFile()) {
19
+ let content = await FS.promises.readFile(srcPath, 'utf-8');
20
+
21
+ if (Path.extname(srcPath) === '.html') {
22
+ content = await modifyHtml(srcPath, content);
23
+ }
24
+
25
+ await FS.promises.writeFile(destPath, content, 'utf-8');
26
+ }
27
+ }
28
+ }