@kapeta/local-cluster-service 0.72.0 → 0.73.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.73.0](https://github.com/kapetacom/local-cluster-service/compare/v0.72.0...v0.73.0) (2024-09-25)
2
+
3
+
4
+ ### Features
5
+
6
+ * add conversation CRUD endpoints and zip upload [CORE-3499] ([#257](https://github.com/kapetacom/local-cluster-service/issues/257)) ([81c1c31](https://github.com/kapetacom/local-cluster-service/commit/81c1c3193e676f1c9e4a802cbf060ca63c8f909d))
7
+
1
8
  # [0.72.0](https://github.com/kapetacom/local-cluster-service/compare/v0.71.6...v0.72.0) (2024-09-24)
2
9
 
3
10
 
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.getSystemBaseImplDir = exports.getSystemBaseDir = exports.hasPageOnDisk = exports.writeImageToDisk = exports.writeAssetToDisk = exports.writePageToDisk = exports.normalizePath = exports.SystemIdHeader = void 0;
7
+ const filesystemManager_1 = require("../filesystemManager");
7
8
  const node_os_1 = __importDefault(require("node:os"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -61,7 +62,7 @@ function hasPageOnDisk(systemId, path, method) {
61
62
  }
62
63
  exports.hasPageOnDisk = hasPageOnDisk;
63
64
  function getSystemBaseDir(systemId) {
64
- return path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId);
65
+ return path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder() || node_os_1.default.tmpdir(), 'ai-systems', systemId);
65
66
  }
66
67
  exports.getSystemBaseDir = getSystemBaseDir;
67
68
  function getSystemBaseImplDir(systemId) {
@@ -23,9 +23,10 @@ const node_uuid_1 = __importDefault(require("node-uuid"));
23
23
  const page_utils_1 = require("./page-utils");
24
24
  const UIServer_1 = require("./UIServer");
25
25
  const crypto_1 = require("crypto");
26
- const PageGenerator_1 = require("./PageGenerator");
27
26
  const utils_1 = require("./utils");
28
27
  const utils_2 = require("../utils/utils");
28
+ const stormService_1 = __importDefault(require("../stormService"));
29
+ const PageGenerator_1 = require("./PageGenerator");
29
30
  const UI_SERVERS = {};
30
31
  const router = (0, express_promise_router_1.default)();
31
32
  router.use('/', cors_1.corsHandler);
@@ -70,6 +71,43 @@ router.post('/ui/serve/:systemId', async (req, res) => {
70
71
  }
71
72
  res.status(200).send({ status: 'running', url: svr.getUrl(), resetUrl: svr.resolveUrlFromPath('/_reset') });
72
73
  });
74
+ router.get('/ui/conversations', async (req, res) => {
75
+ const local = await stormService_1.default.listLocalConversations();
76
+ const remote = await stormService_1.default.listRemoteConversations();
77
+ res.send({
78
+ local,
79
+ remote,
80
+ });
81
+ });
82
+ router.get('/ui/conversations/:systemId', async (req, res) => {
83
+ const systemId = req.params.systemId;
84
+ const eventsFile = (0, page_utils_1.getSystemBaseDir)(systemId) + '/events.ndjson';
85
+ res.set('Content-Type', 'application/x-ndjson');
86
+ res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
87
+ res.set(stormClient_1.ConversationIdHeader, systemId);
88
+ if (!fs_extra_1.default.existsSync(eventsFile)) {
89
+ res.status(404).send({ error: 'No events found' });
90
+ return;
91
+ }
92
+ res.send(fs_extra_1.default.readFileSync(eventsFile));
93
+ });
94
+ router.put('/ui/conversations/:systemId', async (req, res) => {
95
+ const systemId = req.params.systemId;
96
+ const events = req.stringBody ? JSON.parse(req.stringBody) : [];
97
+ await stormService_1.default.saveConversation(systemId, events);
98
+ res.send({ ok: true });
99
+ });
100
+ router.delete('/ui/conversations/:systemId', async (req, res) => {
101
+ const systemId = req.params.systemId;
102
+ await stormService_1.default.deleteConversation(systemId);
103
+ res.send({ ok: true });
104
+ });
105
+ router.post('/ui/conversations/:systemId/append', async (req, res) => {
106
+ const systemId = req.params.systemId;
107
+ const events = req.stringBody ? JSON.parse(req.stringBody) : [];
108
+ await stormService_1.default.appendConversation(systemId, events);
109
+ res.send({ ok: true });
110
+ });
73
111
  router.post('/ui/create-system/:handle/:systemId', async (req, res) => {
74
112
  const systemId = req.params.systemId;
75
113
  const srcDir = (0, page_utils_1.getSystemBaseDir)(systemId);
@@ -142,6 +180,18 @@ router.post('/ui/create-system-simple/:handle/:systemId', async (req, res) => {
142
180
  }
143
181
  }
144
182
  });
183
+ router.post('/ui/systems/:handle/:systemId/download', async (req, res) => {
184
+ const systemId = req.params.systemId;
185
+ const handle = req.params.handle;
186
+ await stormService_1.default.installProjectById(handle, systemId);
187
+ res.send({ ok: true });
188
+ });
189
+ router.post('/ui/systems/:handle/:systemId/upload', async (req, res) => {
190
+ const systemId = req.params.systemId;
191
+ const handle = req.params.handle;
192
+ await stormService_1.default.uploadConversation(handle, systemId);
193
+ res.send({ ok: true });
194
+ });
145
195
  router.delete('/ui/serve/:systemId', async (req, res) => {
146
196
  const systemId = req.params.systemId;
147
197
  if (!systemId) {
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { ConversationItem, CreateSimpleBackendRequest, HTMLPage, ImplementAPIClients, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
3
4
  import { Page, StormEventPageUrl } from './events';
4
5
  export declare const STORM_ID = "storm";
@@ -84,6 +85,8 @@ declare class StormClient {
84
85
  createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
85
86
  generateCode(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
86
87
  deleteUIPageConversation(conversationId: string): Promise<string>;
88
+ downloadSystem(handle: string, conversationId: string): Promise<Buffer>;
89
+ uploadSystem(handle: string, conversationId: string, buffer: Buffer): Promise<Response>;
87
90
  }
88
91
  export declare const stormClient: StormClient;
89
92
  export {};
@@ -139,7 +139,7 @@ class StormClient {
139
139
  method: 'POST',
140
140
  body: JSON.stringify(prompt.pages),
141
141
  });
142
- return await response.json();
142
+ return (await response.json());
143
143
  }
144
144
  async generatePrompt(pages) {
145
145
  const u = `${this._baseUrl}/v2/ui/prompt`;
@@ -232,5 +232,24 @@ class StormClient {
232
232
  const response = await fetch(options.url, options);
233
233
  return response.text();
234
234
  }
235
+ async downloadSystem(handle, conversationId) {
236
+ const u = `${this._baseUrl}/v2/systems/${handle}/${conversationId}/download`;
237
+ const response = await fetch(u);
238
+ if (!response.ok) {
239
+ throw new Error(`Failed to download system: ${response.status}`);
240
+ }
241
+ return Buffer.from(await response.arrayBuffer());
242
+ }
243
+ async uploadSystem(handle, conversationId, buffer) {
244
+ const u = `${this._baseUrl}/v2/systems/${handle}/${conversationId}`;
245
+ const response = await fetch(u, {
246
+ method: 'PUT',
247
+ body: buffer,
248
+ headers: {
249
+ 'content-type': 'application/zip',
250
+ },
251
+ });
252
+ return response;
253
+ }
235
254
  }
236
255
  exports.stormClient = new StormClient();
@@ -0,0 +1,19 @@
1
+ import { StormEvent } from './storm/events';
2
+ export declare class StormService {
3
+ private getConversationFile;
4
+ private getConversationTarball;
5
+ listRemoteConversations(): Promise<never[]>;
6
+ listLocalConversations(): Promise<{
7
+ id: string;
8
+ description: string;
9
+ title: string;
10
+ }[]>;
11
+ getConversation(conversationId: string): Promise<string>;
12
+ saveConversation(conversationId: string, events: StormEvent[]): Promise<void>;
13
+ appendConversation(conversationId: string, events: StormEvent[]): Promise<void>;
14
+ deleteConversation(conversationId: string): Promise<void>;
15
+ uploadConversation(handle: string, conversationId: string): Promise<void>;
16
+ installProjectById(handle: string, conversationId: string): Promise<void>;
17
+ }
18
+ declare const _default: StormService;
19
+ export default _default;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.StormService = void 0;
30
+ const promises_1 = __importDefault(require("fs/promises"));
31
+ const glob_1 = require("glob");
32
+ const filesystemManager_1 = require("./filesystemManager");
33
+ const path_1 = __importDefault(require("path"));
34
+ const fs_1 = require("fs");
35
+ const tar = __importStar(require("tar"));
36
+ const stormClient_1 = require("./storm/stormClient");
37
+ class StormService {
38
+ getConversationFile(conversationId) {
39
+ return path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems', conversationId, 'events.ndjson');
40
+ }
41
+ getConversationTarball(conversationId) {
42
+ return path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems', conversationId, 'system.tar.gz');
43
+ }
44
+ async listRemoteConversations() {
45
+ // i.e. conversations from org / user on registry
46
+ return [];
47
+ }
48
+ async listLocalConversations() {
49
+ const systemsFolder = path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems');
50
+ const eventFiles = await (0, glob_1.glob)('*/events.ndjson', {
51
+ cwd: systemsFolder,
52
+ absolute: true,
53
+ });
54
+ // Returns list of UUIDs - probably want to make it more useful than that
55
+ const conversations = [];
56
+ for (const file of eventFiles) {
57
+ const nldContents = await promises_1.default.readFile(file, 'utf8');
58
+ const events = nldContents.split('\n').map((e) => JSON.parse(e));
59
+ // find the shell and get the title tag
60
+ const shellEvent = events.find((e) => e.type === 'AI' && e.event.type === 'UI_SHELL')?.event;
61
+ const html = shellEvent?.payload.content;
62
+ const title = html?.match(/<title>(.*?)<\/title>/)?.[1];
63
+ const id = events.find((e) => e.type === 'AI')?.systemId;
64
+ const initialPrompt = events.find((e) => e.type === 'AI' && e.event.type === 'PROMPT_IMPROVE')?.event?.payload?.prompt || events[0].text;
65
+ if (!id) {
66
+ continue;
67
+ }
68
+ conversations.push({
69
+ id,
70
+ description: initialPrompt,
71
+ title: title || 'New system',
72
+ });
73
+ }
74
+ return conversations;
75
+ }
76
+ async getConversation(conversationId) {
77
+ const conversationFile = this.getConversationFile(conversationId);
78
+ if (!(0, fs_1.existsSync)(conversationFile)) {
79
+ throw new Error('Conversation not found');
80
+ }
81
+ return promises_1.default.readFile(conversationFile, 'utf8');
82
+ }
83
+ async saveConversation(conversationId, events) {
84
+ const conversationFile = this.getConversationFile(conversationId);
85
+ await promises_1.default.mkdir(path_1.default.dirname(conversationFile), { recursive: true });
86
+ await promises_1.default.writeFile(conversationFile, events.map((e) => JSON.stringify(e)).join('\n'));
87
+ }
88
+ async appendConversation(conversationId, events) {
89
+ const conversationFile = this.getConversationFile(conversationId);
90
+ await promises_1.default.appendFile(conversationFile, events.map((e) => JSON.stringify(e)).join('\n'));
91
+ }
92
+ async deleteConversation(conversationId) {
93
+ const conversationFile = this.getConversationFile(conversationId);
94
+ await promises_1.default.unlink(conversationFile);
95
+ // if it matches a UUID, it's safe to delete the directory too
96
+ const conversationDir = path_1.default.dirname(conversationFile);
97
+ if (conversationDir.match(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {
98
+ await promises_1.default.rm(conversationDir, { recursive: true, force: true });
99
+ }
100
+ }
101
+ async uploadConversation(handle, conversationId) {
102
+ const tarballFile = this.getConversationTarball(conversationId);
103
+ const destDir = path_1.default.dirname(tarballFile);
104
+ const tarballName = path_1.default.basename(tarballFile);
105
+ await tar.create({
106
+ file: tarballFile,
107
+ cwd: destDir,
108
+ gzip: true,
109
+ filter: (entry) => !entry.includes(tarballName),
110
+ }, ['.']);
111
+ await stormClient_1.stormClient.uploadSystem(handle, conversationId, await promises_1.default.readFile(tarballFile));
112
+ }
113
+ async installProjectById(handle, conversationId) {
114
+ const tarballFile = this.getConversationTarball(conversationId);
115
+ const destDir = path_1.default.dirname(tarballFile);
116
+ const buffer = await stormClient_1.stormClient.downloadSystem(handle, conversationId);
117
+ await promises_1.default.mkdir(destDir, { recursive: true });
118
+ await promises_1.default.writeFile(tarballFile, buffer);
119
+ await tar.extract({
120
+ file: tarballFile,
121
+ cwd: destDir,
122
+ });
123
+ await promises_1.default.unlink(tarballFile);
124
+ }
125
+ }
126
+ exports.StormService = StormService;
127
+ exports.default = new StormService();
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.getSystemBaseImplDir = exports.getSystemBaseDir = exports.hasPageOnDisk = exports.writeImageToDisk = exports.writeAssetToDisk = exports.writePageToDisk = exports.normalizePath = exports.SystemIdHeader = void 0;
7
+ const filesystemManager_1 = require("../filesystemManager");
7
8
  const node_os_1 = __importDefault(require("node:os"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -61,7 +62,7 @@ function hasPageOnDisk(systemId, path, method) {
61
62
  }
62
63
  exports.hasPageOnDisk = hasPageOnDisk;
63
64
  function getSystemBaseDir(systemId) {
64
- return path_1.default.join(node_os_1.default.tmpdir(), 'ai-systems', systemId);
65
+ return path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder() || node_os_1.default.tmpdir(), 'ai-systems', systemId);
65
66
  }
66
67
  exports.getSystemBaseDir = getSystemBaseDir;
67
68
  function getSystemBaseImplDir(systemId) {
@@ -23,9 +23,10 @@ const node_uuid_1 = __importDefault(require("node-uuid"));
23
23
  const page_utils_1 = require("./page-utils");
24
24
  const UIServer_1 = require("./UIServer");
25
25
  const crypto_1 = require("crypto");
26
- const PageGenerator_1 = require("./PageGenerator");
27
26
  const utils_1 = require("./utils");
28
27
  const utils_2 = require("../utils/utils");
28
+ const stormService_1 = __importDefault(require("../stormService"));
29
+ const PageGenerator_1 = require("./PageGenerator");
29
30
  const UI_SERVERS = {};
30
31
  const router = (0, express_promise_router_1.default)();
31
32
  router.use('/', cors_1.corsHandler);
@@ -70,6 +71,43 @@ router.post('/ui/serve/:systemId', async (req, res) => {
70
71
  }
71
72
  res.status(200).send({ status: 'running', url: svr.getUrl(), resetUrl: svr.resolveUrlFromPath('/_reset') });
72
73
  });
74
+ router.get('/ui/conversations', async (req, res) => {
75
+ const local = await stormService_1.default.listLocalConversations();
76
+ const remote = await stormService_1.default.listRemoteConversations();
77
+ res.send({
78
+ local,
79
+ remote,
80
+ });
81
+ });
82
+ router.get('/ui/conversations/:systemId', async (req, res) => {
83
+ const systemId = req.params.systemId;
84
+ const eventsFile = (0, page_utils_1.getSystemBaseDir)(systemId) + '/events.ndjson';
85
+ res.set('Content-Type', 'application/x-ndjson');
86
+ res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
87
+ res.set(stormClient_1.ConversationIdHeader, systemId);
88
+ if (!fs_extra_1.default.existsSync(eventsFile)) {
89
+ res.status(404).send({ error: 'No events found' });
90
+ return;
91
+ }
92
+ res.send(fs_extra_1.default.readFileSync(eventsFile));
93
+ });
94
+ router.put('/ui/conversations/:systemId', async (req, res) => {
95
+ const systemId = req.params.systemId;
96
+ const events = req.stringBody ? JSON.parse(req.stringBody) : [];
97
+ await stormService_1.default.saveConversation(systemId, events);
98
+ res.send({ ok: true });
99
+ });
100
+ router.delete('/ui/conversations/:systemId', async (req, res) => {
101
+ const systemId = req.params.systemId;
102
+ await stormService_1.default.deleteConversation(systemId);
103
+ res.send({ ok: true });
104
+ });
105
+ router.post('/ui/conversations/:systemId/append', async (req, res) => {
106
+ const systemId = req.params.systemId;
107
+ const events = req.stringBody ? JSON.parse(req.stringBody) : [];
108
+ await stormService_1.default.appendConversation(systemId, events);
109
+ res.send({ ok: true });
110
+ });
73
111
  router.post('/ui/create-system/:handle/:systemId', async (req, res) => {
74
112
  const systemId = req.params.systemId;
75
113
  const srcDir = (0, page_utils_1.getSystemBaseDir)(systemId);
@@ -142,6 +180,18 @@ router.post('/ui/create-system-simple/:handle/:systemId', async (req, res) => {
142
180
  }
143
181
  }
144
182
  });
183
+ router.post('/ui/systems/:handle/:systemId/download', async (req, res) => {
184
+ const systemId = req.params.systemId;
185
+ const handle = req.params.handle;
186
+ await stormService_1.default.installProjectById(handle, systemId);
187
+ res.send({ ok: true });
188
+ });
189
+ router.post('/ui/systems/:handle/:systemId/upload', async (req, res) => {
190
+ const systemId = req.params.systemId;
191
+ const handle = req.params.handle;
192
+ await stormService_1.default.uploadConversation(handle, systemId);
193
+ res.send({ ok: true });
194
+ });
145
195
  router.delete('/ui/serve/:systemId', async (req, res) => {
146
196
  const systemId = req.params.systemId;
147
197
  if (!systemId) {
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { ConversationItem, CreateSimpleBackendRequest, HTMLPage, ImplementAPIClients, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
3
4
  import { Page, StormEventPageUrl } from './events';
4
5
  export declare const STORM_ID = "storm";
@@ -84,6 +85,8 @@ declare class StormClient {
84
85
  createErrorDetails(prompt: string, history?: ConversationItem[], conversationId?: string): Promise<StormStream>;
85
86
  generateCode(prompt: StormFileImplementationPrompt, conversationId?: string): Promise<StormStream>;
86
87
  deleteUIPageConversation(conversationId: string): Promise<string>;
88
+ downloadSystem(handle: string, conversationId: string): Promise<Buffer>;
89
+ uploadSystem(handle: string, conversationId: string, buffer: Buffer): Promise<Response>;
87
90
  }
88
91
  export declare const stormClient: StormClient;
89
92
  export {};
@@ -139,7 +139,7 @@ class StormClient {
139
139
  method: 'POST',
140
140
  body: JSON.stringify(prompt.pages),
141
141
  });
142
- return await response.json();
142
+ return (await response.json());
143
143
  }
144
144
  async generatePrompt(pages) {
145
145
  const u = `${this._baseUrl}/v2/ui/prompt`;
@@ -232,5 +232,24 @@ class StormClient {
232
232
  const response = await fetch(options.url, options);
233
233
  return response.text();
234
234
  }
235
+ async downloadSystem(handle, conversationId) {
236
+ const u = `${this._baseUrl}/v2/systems/${handle}/${conversationId}/download`;
237
+ const response = await fetch(u);
238
+ if (!response.ok) {
239
+ throw new Error(`Failed to download system: ${response.status}`);
240
+ }
241
+ return Buffer.from(await response.arrayBuffer());
242
+ }
243
+ async uploadSystem(handle, conversationId, buffer) {
244
+ const u = `${this._baseUrl}/v2/systems/${handle}/${conversationId}`;
245
+ const response = await fetch(u, {
246
+ method: 'PUT',
247
+ body: buffer,
248
+ headers: {
249
+ 'content-type': 'application/zip',
250
+ },
251
+ });
252
+ return response;
253
+ }
235
254
  }
236
255
  exports.stormClient = new StormClient();
@@ -0,0 +1,19 @@
1
+ import { StormEvent } from './storm/events';
2
+ export declare class StormService {
3
+ private getConversationFile;
4
+ private getConversationTarball;
5
+ listRemoteConversations(): Promise<never[]>;
6
+ listLocalConversations(): Promise<{
7
+ id: string;
8
+ description: string;
9
+ title: string;
10
+ }[]>;
11
+ getConversation(conversationId: string): Promise<string>;
12
+ saveConversation(conversationId: string, events: StormEvent[]): Promise<void>;
13
+ appendConversation(conversationId: string, events: StormEvent[]): Promise<void>;
14
+ deleteConversation(conversationId: string): Promise<void>;
15
+ uploadConversation(handle: string, conversationId: string): Promise<void>;
16
+ installProjectById(handle: string, conversationId: string): Promise<void>;
17
+ }
18
+ declare const _default: StormService;
19
+ export default _default;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.StormService = void 0;
30
+ const promises_1 = __importDefault(require("fs/promises"));
31
+ const glob_1 = require("glob");
32
+ const filesystemManager_1 = require("./filesystemManager");
33
+ const path_1 = __importDefault(require("path"));
34
+ const fs_1 = require("fs");
35
+ const tar = __importStar(require("tar"));
36
+ const stormClient_1 = require("./storm/stormClient");
37
+ class StormService {
38
+ getConversationFile(conversationId) {
39
+ return path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems', conversationId, 'events.ndjson');
40
+ }
41
+ getConversationTarball(conversationId) {
42
+ return path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems', conversationId, 'system.tar.gz');
43
+ }
44
+ async listRemoteConversations() {
45
+ // i.e. conversations from org / user on registry
46
+ return [];
47
+ }
48
+ async listLocalConversations() {
49
+ const systemsFolder = path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems');
50
+ const eventFiles = await (0, glob_1.glob)('*/events.ndjson', {
51
+ cwd: systemsFolder,
52
+ absolute: true,
53
+ });
54
+ // Returns list of UUIDs - probably want to make it more useful than that
55
+ const conversations = [];
56
+ for (const file of eventFiles) {
57
+ const nldContents = await promises_1.default.readFile(file, 'utf8');
58
+ const events = nldContents.split('\n').map((e) => JSON.parse(e));
59
+ // find the shell and get the title tag
60
+ const shellEvent = events.find((e) => e.type === 'AI' && e.event.type === 'UI_SHELL')?.event;
61
+ const html = shellEvent?.payload.content;
62
+ const title = html?.match(/<title>(.*?)<\/title>/)?.[1];
63
+ const id = events.find((e) => e.type === 'AI')?.systemId;
64
+ const initialPrompt = events.find((e) => e.type === 'AI' && e.event.type === 'PROMPT_IMPROVE')?.event?.payload?.prompt || events[0].text;
65
+ if (!id) {
66
+ continue;
67
+ }
68
+ conversations.push({
69
+ id,
70
+ description: initialPrompt,
71
+ title: title || 'New system',
72
+ });
73
+ }
74
+ return conversations;
75
+ }
76
+ async getConversation(conversationId) {
77
+ const conversationFile = this.getConversationFile(conversationId);
78
+ if (!(0, fs_1.existsSync)(conversationFile)) {
79
+ throw new Error('Conversation not found');
80
+ }
81
+ return promises_1.default.readFile(conversationFile, 'utf8');
82
+ }
83
+ async saveConversation(conversationId, events) {
84
+ const conversationFile = this.getConversationFile(conversationId);
85
+ await promises_1.default.mkdir(path_1.default.dirname(conversationFile), { recursive: true });
86
+ await promises_1.default.writeFile(conversationFile, events.map((e) => JSON.stringify(e)).join('\n'));
87
+ }
88
+ async appendConversation(conversationId, events) {
89
+ const conversationFile = this.getConversationFile(conversationId);
90
+ await promises_1.default.appendFile(conversationFile, events.map((e) => JSON.stringify(e)).join('\n'));
91
+ }
92
+ async deleteConversation(conversationId) {
93
+ const conversationFile = this.getConversationFile(conversationId);
94
+ await promises_1.default.unlink(conversationFile);
95
+ // if it matches a UUID, it's safe to delete the directory too
96
+ const conversationDir = path_1.default.dirname(conversationFile);
97
+ if (conversationDir.match(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {
98
+ await promises_1.default.rm(conversationDir, { recursive: true, force: true });
99
+ }
100
+ }
101
+ async uploadConversation(handle, conversationId) {
102
+ const tarballFile = this.getConversationTarball(conversationId);
103
+ const destDir = path_1.default.dirname(tarballFile);
104
+ const tarballName = path_1.default.basename(tarballFile);
105
+ await tar.create({
106
+ file: tarballFile,
107
+ cwd: destDir,
108
+ gzip: true,
109
+ filter: (entry) => !entry.includes(tarballName),
110
+ }, ['.']);
111
+ await stormClient_1.stormClient.uploadSystem(handle, conversationId, await promises_1.default.readFile(tarballFile));
112
+ }
113
+ async installProjectById(handle, conversationId) {
114
+ const tarballFile = this.getConversationTarball(conversationId);
115
+ const destDir = path_1.default.dirname(tarballFile);
116
+ const buffer = await stormClient_1.stormClient.downloadSystem(handle, conversationId);
117
+ await promises_1.default.mkdir(destDir, { recursive: true });
118
+ await promises_1.default.writeFile(tarballFile, buffer);
119
+ await tar.extract({
120
+ file: tarballFile,
121
+ cwd: destDir,
122
+ });
123
+ await promises_1.default.unlink(tarballFile);
124
+ }
125
+ }
126
+ exports.StormService = StormService;
127
+ exports.default = new StormService();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.72.0",
3
+ "version": "0.73.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -72,7 +72,7 @@
72
72
  "express-promise-router": "^4.1.1",
73
73
  "fetch-retry": "^6.0.0",
74
74
  "fs-extra": "^11.1.0",
75
- "glob": "^7.1.6",
75
+ "glob": "^11.0.0",
76
76
  "gunzip-maybe": "^1.4.2",
77
77
  "js-yaml": "^4.1.0",
78
78
  "lodash": "^4.17.15",
@@ -87,6 +87,7 @@
87
87
  "request": "2.88.2",
88
88
  "socket.io": "^4.5.2",
89
89
  "stream-json": "^1.8.0",
90
+ "tar": "^7.4.3",
90
91
  "tar-stream": "^3.1.6",
91
92
  "typescript": "^5.1.6",
92
93
  "uuid": "^9.0.1",
@@ -99,7 +100,6 @@
99
100
  "@types/async-lock": "^1.4.0",
100
101
  "@types/express": "^4.17.17",
101
102
  "@types/fs-extra": "^11.0.1",
102
- "@types/glob": "^8.1.0",
103
103
  "@types/gunzip-maybe": "^1.4.0",
104
104
  "@types/jest": "^29.5.4",
105
105
  "@types/js-yaml": "^4.0.9",
@@ -3,6 +3,7 @@
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
5
  import { StormEventFileDone, StormEventPage, StormImage } from './events';
6
+ import { filesystemManager } from '../filesystemManager';
6
7
 
7
8
  import { Response } from 'express';
8
9
  import os from 'node:os';
@@ -12,6 +13,7 @@ import FSExtra from 'fs-extra';
12
13
  import { ConversationItem } from './stream';
13
14
  import exp from 'node:constants';
14
15
  import { ImagePrompt } from './PageGenerator';
16
+ import clusterConfiguration from '@kapeta/local-cluster-config';
15
17
 
16
18
  export const SystemIdHeader = 'System-Id';
17
19
 
@@ -76,7 +78,7 @@ export function hasPageOnDisk(systemId: string, path: string, method: string) {
76
78
  }
77
79
 
78
80
  export function getSystemBaseDir(systemId: string) {
79
- return Path.join(os.tmpdir(), 'ai-systems', systemId);
81
+ return Path.join(filesystemManager.getProjectRootFolder() || os.tmpdir(), 'ai-systems', systemId);
80
82
  }
81
83
 
82
84
  export function getSystemBaseImplDir(systemId: string) {
@@ -49,9 +49,10 @@ import {
49
49
  } from './page-utils';
50
50
  import { UIServer } from './UIServer';
51
51
  import { randomUUID } from 'crypto';
52
- import { PageQueue } from './PageGenerator';
53
52
  import { copyDirectory, createFuture, readFilesAndContent } from './utils';
54
53
  import { getRemoteUrl } from '../utils/utils';
54
+ import stormService from '../stormService';
55
+ import { PageQueue } from './PageGenerator';
55
56
 
56
57
  const UI_SERVERS: { [key: string]: UIServer } = {};
57
58
  const router = Router();
@@ -105,6 +106,50 @@ router.post('/ui/serve/:systemId', async (req: KapetaBodyRequest, res: Response)
105
106
  res.status(200).send({ status: 'running', url: svr.getUrl(), resetUrl: svr.resolveUrlFromPath('/_reset') });
106
107
  });
107
108
 
109
+ router.get('/ui/conversations', async (req: KapetaBodyRequest, res: Response) => {
110
+ const local = await stormService.listLocalConversations();
111
+ const remote = await stormService.listRemoteConversations();
112
+ res.send({
113
+ local,
114
+ remote,
115
+ });
116
+ });
117
+
118
+ router.get('/ui/conversations/:systemId', async (req: KapetaBodyRequest, res: Response) => {
119
+ const systemId = req.params.systemId as string;
120
+ const eventsFile = getSystemBaseDir(systemId) + '/events.ndjson';
121
+
122
+ res.set('Content-Type', 'application/x-ndjson');
123
+ res.set('Access-Control-Expose-Headers', ConversationIdHeader);
124
+ res.set(ConversationIdHeader, systemId);
125
+
126
+ if (!FS.existsSync(eventsFile)) {
127
+ res.status(404).send({ error: 'No events found' });
128
+ return;
129
+ }
130
+ res.send(FS.readFileSync(eventsFile));
131
+ });
132
+
133
+ router.put('/ui/conversations/:systemId', async (req: KapetaBodyRequest, res: Response) => {
134
+ const systemId = req.params.systemId as string;
135
+ const events: StormEvent[] = req.stringBody ? JSON.parse(req.stringBody) : [];
136
+ await stormService.saveConversation(systemId, events);
137
+ res.send({ ok: true });
138
+ });
139
+
140
+ router.delete('/ui/conversations/:systemId', async (req: KapetaBodyRequest, res: Response) => {
141
+ const systemId = req.params.systemId as string;
142
+ await stormService.deleteConversation(systemId);
143
+ res.send({ ok: true });
144
+ });
145
+
146
+ router.post('/ui/conversations/:systemId/append', async (req: KapetaBodyRequest, res: Response) => {
147
+ const systemId = req.params.systemId as string;
148
+ const events: StormEvent[] = req.stringBody ? JSON.parse(req.stringBody) : [];
149
+ await stormService.appendConversation(systemId, events);
150
+ res.send({ ok: true });
151
+ });
152
+
108
153
  router.post('/ui/create-system/:handle/:systemId', async (req: KapetaBodyRequest, res: Response) => {
109
154
  const systemId = req.params.systemId as string;
110
155
  const srcDir = getSystemBaseDir(systemId);
@@ -197,6 +242,20 @@ router.post('/ui/create-system-simple/:handle/:systemId', async (req: KapetaBody
197
242
  }
198
243
  });
199
244
 
245
+ router.post('/ui/systems/:handle/:systemId/download', async (req: KapetaBodyRequest, res: Response) => {
246
+ const systemId = req.params.systemId as string;
247
+ const handle = req.params.handle as string;
248
+ await stormService.installProjectById(handle, systemId);
249
+ res.send({ ok: true });
250
+ });
251
+
252
+ router.post('/ui/systems/:handle/:systemId/upload', async (req: KapetaBodyRequest, res: Response) => {
253
+ const systemId = req.params.systemId as string;
254
+ const handle = req.params.handle as string;
255
+ await stormService.uploadConversation(handle, systemId);
256
+ res.send({ ok: true });
257
+ });
258
+
200
259
  router.delete('/ui/serve/:systemId', async (req: KapetaBodyRequest, res: Response) => {
201
260
  const systemId = req.params.systemId as string | undefined;
202
261
  if (!systemId) {
@@ -371,6 +430,7 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
371
430
 
372
431
  router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
373
432
  const handle = req.params.handle as string;
433
+
374
434
  try {
375
435
  const outerConversationId =
376
436
  (req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || randomUUID();
@@ -929,6 +989,7 @@ function sendEvent(res: Response, evt: StormEvent) {
929
989
  if (res.closed) {
930
990
  return;
931
991
  }
992
+
932
993
  res.write(JSON.stringify(evt) + '\n');
933
994
  }
934
995
 
@@ -8,7 +8,8 @@ import readLine from 'node:readline/promises';
8
8
  import { Readable } from 'node:stream';
9
9
  import {
10
10
  ConversationItem,
11
- CreateSimpleBackendRequest, HTMLPage,
11
+ CreateSimpleBackendRequest,
12
+ HTMLPage,
12
13
  ImplementAPIClients,
13
14
  StormContextRequest,
14
15
  StormFileImplementationPrompt,
@@ -245,7 +246,7 @@ class StormClient {
245
246
  method: 'POST',
246
247
  body: JSON.stringify(prompt.pages),
247
248
  });
248
- return await response.json() as HTMLPage[];
249
+ return (await response.json()) as HTMLPage[];
249
250
  }
250
251
 
251
252
  public async generatePrompt(pages: string[]): Promise<string> {
@@ -353,6 +354,28 @@ class StormClient {
353
354
 
354
355
  return response.text();
355
356
  }
357
+
358
+ async downloadSystem(handle: string, conversationId: string) {
359
+ const u = `${this._baseUrl}/v2/systems/${handle}/${conversationId}/download`;
360
+ const response = await fetch(u);
361
+ if (!response.ok) {
362
+ throw new Error(`Failed to download system: ${response.status}`);
363
+ }
364
+ return Buffer.from(await response.arrayBuffer());
365
+ }
366
+
367
+ async uploadSystem(handle: string, conversationId: string, buffer: Buffer) {
368
+ const u = `${this._baseUrl}/v2/systems/${handle}/${conversationId}`;
369
+ const response = await fetch(u, {
370
+ method: 'PUT',
371
+ body: buffer,
372
+ headers: {
373
+ 'content-type': 'application/zip',
374
+ },
375
+ });
376
+
377
+ return response;
378
+ }
356
379
  }
357
380
 
358
381
  export const stormClient = new StormClient();
@@ -0,0 +1,129 @@
1
+ import fs from 'fs/promises';
2
+ import { glob } from 'glob';
3
+ import { filesystemManager } from './filesystemManager';
4
+ import path from 'path';
5
+ import { existsSync } from 'fs';
6
+ import { StormEvent, StormEventPromptImprove, StormEventUIShell } from './storm/events';
7
+ import * as tar from 'tar';
8
+ import { stormClient } from './storm/stormClient';
9
+
10
+ export class StormService {
11
+ private getConversationFile(conversationId: string) {
12
+ return path.join(filesystemManager.getProjectRootFolder()!, 'ai-systems', conversationId, 'events.ndjson');
13
+ }
14
+
15
+ private getConversationTarball(conversationId: string) {
16
+ return path.join(filesystemManager.getProjectRootFolder()!, 'ai-systems', conversationId, 'system.tar.gz');
17
+ }
18
+
19
+ async listRemoteConversations() {
20
+ // i.e. conversations from org / user on registry
21
+ return [];
22
+ }
23
+
24
+ async listLocalConversations() {
25
+ const systemsFolder = path.join(filesystemManager.getProjectRootFolder()!, 'ai-systems');
26
+ const eventFiles = await glob('*/events.ndjson', {
27
+ cwd: systemsFolder,
28
+ absolute: true,
29
+ });
30
+ // Returns list of UUIDs - probably want to make it more useful than that
31
+ const conversations: { id: string; description: string; title: string }[] = [];
32
+ for (const file of eventFiles) {
33
+ const nldContents = await fs.readFile(file as string, 'utf8');
34
+ const events = nldContents.split('\n').map((e) => JSON.parse(e)) as {
35
+ // | { type: 'USER'; event: any } // IS stupid!
36
+ type: 'AI';
37
+ systemId: string;
38
+ event: StormEvent;
39
+ }[];
40
+ // find the shell and get the title tag
41
+ const shellEvent = events.find((e) => e.type === 'AI' && e.event.type === 'UI_SHELL')?.event as
42
+ | StormEventUIShell
43
+ | undefined;
44
+ const html = shellEvent?.payload.content;
45
+ const title = html?.match(/<title>(.*?)<\/title>/)?.[1];
46
+ const id = events.find((e) => e.type === 'AI')?.systemId;
47
+
48
+ const initialPrompt =
49
+ (
50
+ events.find((e) => e.type === 'AI' && e.event.type === 'PROMPT_IMPROVE')?.event as
51
+ | StormEventPromptImprove
52
+ | undefined
53
+ )?.payload?.prompt || (events[0] as any).text;
54
+
55
+ if (!id) {
56
+ continue;
57
+ }
58
+
59
+ conversations.push({
60
+ id,
61
+ description: initialPrompt,
62
+ title: title || 'New system',
63
+ });
64
+ }
65
+
66
+ return conversations;
67
+ }
68
+
69
+ async getConversation(conversationId: string) {
70
+ const conversationFile = this.getConversationFile(conversationId);
71
+ if (!existsSync(conversationFile)) {
72
+ throw new Error('Conversation not found');
73
+ }
74
+ return fs.readFile(conversationFile, 'utf8');
75
+ }
76
+
77
+ async saveConversation(conversationId: string, events: StormEvent[]) {
78
+ const conversationFile = this.getConversationFile(conversationId);
79
+ await fs.mkdir(path.dirname(conversationFile), { recursive: true });
80
+ await fs.writeFile(conversationFile, events.map((e) => JSON.stringify(e)).join('\n'));
81
+ }
82
+
83
+ async appendConversation(conversationId: string, events: StormEvent[]) {
84
+ const conversationFile = this.getConversationFile(conversationId);
85
+ await fs.appendFile(conversationFile, events.map((e) => JSON.stringify(e)).join('\n'));
86
+ }
87
+
88
+ async deleteConversation(conversationId: string) {
89
+ const conversationFile = this.getConversationFile(conversationId);
90
+ await fs.unlink(conversationFile);
91
+
92
+ // if it matches a UUID, it's safe to delete the directory too
93
+ const conversationDir = path.dirname(conversationFile);
94
+ if (conversationDir.match(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {
95
+ await fs.rm(conversationDir, { recursive: true, force: true });
96
+ }
97
+ }
98
+
99
+ async uploadConversation(handle: string, conversationId: string) {
100
+ const tarballFile = this.getConversationTarball(conversationId);
101
+ const destDir = path.dirname(tarballFile);
102
+ const tarballName = path.basename(tarballFile);
103
+ await tar.create(
104
+ {
105
+ file: tarballFile,
106
+ cwd: destDir,
107
+ gzip: true,
108
+ filter: (entry) => !entry.includes(tarballName),
109
+ },
110
+ ['.']
111
+ );
112
+ await stormClient.uploadSystem(handle, conversationId, await fs.readFile(tarballFile));
113
+ }
114
+
115
+ async installProjectById(handle: string, conversationId: string) {
116
+ const tarballFile = this.getConversationTarball(conversationId);
117
+ const destDir = path.dirname(tarballFile);
118
+ const buffer = await stormClient.downloadSystem(handle, conversationId);
119
+ await fs.mkdir(destDir, { recursive: true });
120
+ await fs.writeFile(tarballFile, buffer);
121
+ await tar.extract({
122
+ file: tarballFile,
123
+ cwd: destDir,
124
+ });
125
+ await fs.unlink(tarballFile);
126
+ }
127
+ }
128
+
129
+ export default new StormService();