@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 +7 -0
- package/dist/cjs/src/storm/page-utils.js +2 -1
- package/dist/cjs/src/storm/routes.js +51 -1
- package/dist/cjs/src/storm/stormClient.d.ts +3 -0
- package/dist/cjs/src/storm/stormClient.js +20 -1
- package/dist/cjs/src/stormService.d.ts +19 -0
- package/dist/cjs/src/stormService.js +127 -0
- package/dist/esm/src/storm/page-utils.js +2 -1
- package/dist/esm/src/storm/routes.js +51 -1
- package/dist/esm/src/storm/stormClient.d.ts +3 -0
- package/dist/esm/src/storm/stormClient.js +20 -1
- package/dist/esm/src/stormService.d.ts +19 -0
- package/dist/esm/src/stormService.js +127 -0
- package/package.json +3 -3
- package/src/storm/page-utils.ts +3 -1
- package/src/storm/routes.ts +62 -1
- package/src/storm/stormClient.ts +25 -2
- package/src/stormService.ts +129 -0
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.
|
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": "^
|
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",
|
package/src/storm/page-utils.ts
CHANGED
@@ -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) {
|
package/src/storm/routes.ts
CHANGED
@@ -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
|
|
package/src/storm/stormClient.ts
CHANGED
@@ -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,
|
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();
|