@kapeta/local-cluster-service 0.71.6 → 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,17 @@
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
+
8
+ # [0.72.0](https://github.com/kapetacom/local-cluster-service/compare/v0.71.6...v0.72.0) (2024-09-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * Add redirect endpoint for opening conversation ([#258](https://github.com/kapetacom/local-cluster-service/issues/258)) ([2b400ac](https://github.com/kapetacom/local-cluster-service/commit/2b400ac0066813a9e18c105628743650646c83c6))
14
+
1
15
  ## [0.71.6](https://github.com/kapetacom/local-cluster-service/compare/v0.71.5...v0.71.6) (2024-09-19)
2
16
 
3
17
 
@@ -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,8 +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");
27
+ const utils_2 = require("../utils/utils");
28
+ const stormService_1 = __importDefault(require("../stormService"));
29
+ const PageGenerator_1 = require("./PageGenerator");
28
30
  const UI_SERVERS = {};
29
31
  const router = (0, express_promise_router_1.default)();
30
32
  router.use('/', cors_1.corsHandler);
@@ -69,6 +71,43 @@ router.post('/ui/serve/:systemId', async (req, res) => {
69
71
  }
70
72
  res.status(200).send({ status: 'running', url: svr.getUrl(), resetUrl: svr.resolveUrlFromPath('/_reset') });
71
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
+ });
72
111
  router.post('/ui/create-system/:handle/:systemId', async (req, res) => {
73
112
  const systemId = req.params.systemId;
74
113
  const srcDir = (0, page_utils_1.getSystemBaseDir)(systemId);
@@ -141,6 +180,18 @@ router.post('/ui/create-system-simple/:handle/:systemId', async (req, res) => {
141
180
  }
142
181
  }
143
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
+ });
144
195
  router.delete('/ui/serve/:systemId', async (req, res) => {
145
196
  const systemId = req.params.systemId;
146
197
  if (!systemId) {
@@ -544,6 +595,12 @@ router.post('/ui/get-vote', async (req, res) => {
544
595
  router.post('/:handle/all', async (req, res) => {
545
596
  await handleAll(req, res);
546
597
  });
598
+ router.get('/conversations/:agentName/:systemId/redirect', async (req, res) => {
599
+ const aiService = (0, utils_2.getRemoteUrl)('ai-service', 'https://ai.kapeta.com');
600
+ const agentName = req.params.agentName;
601
+ const systemId = req.params.systemId;
602
+ res.redirect(aiService + `/v2/conversations/${encodeURIComponent(agentName)}/${encodeURIComponent(systemId)}`);
603
+ });
547
604
  async function handleAll(req, res) {
548
605
  const handle = req.params.handle;
549
606
  const systemId = req.query.systemId ?? undefined;
@@ -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,8 +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");
27
+ const utils_2 = require("../utils/utils");
28
+ const stormService_1 = __importDefault(require("../stormService"));
29
+ const PageGenerator_1 = require("./PageGenerator");
28
30
  const UI_SERVERS = {};
29
31
  const router = (0, express_promise_router_1.default)();
30
32
  router.use('/', cors_1.corsHandler);
@@ -69,6 +71,43 @@ router.post('/ui/serve/:systemId', async (req, res) => {
69
71
  }
70
72
  res.status(200).send({ status: 'running', url: svr.getUrl(), resetUrl: svr.resolveUrlFromPath('/_reset') });
71
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
+ });
72
111
  router.post('/ui/create-system/:handle/:systemId', async (req, res) => {
73
112
  const systemId = req.params.systemId;
74
113
  const srcDir = (0, page_utils_1.getSystemBaseDir)(systemId);
@@ -141,6 +180,18 @@ router.post('/ui/create-system-simple/:handle/:systemId', async (req, res) => {
141
180
  }
142
181
  }
143
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
+ });
144
195
  router.delete('/ui/serve/:systemId', async (req, res) => {
145
196
  const systemId = req.params.systemId;
146
197
  if (!systemId) {
@@ -544,6 +595,12 @@ router.post('/ui/get-vote', async (req, res) => {
544
595
  router.post('/:handle/all', async (req, res) => {
545
596
  await handleAll(req, res);
546
597
  });
598
+ router.get('/conversations/:agentName/:systemId/redirect', async (req, res) => {
599
+ const aiService = (0, utils_2.getRemoteUrl)('ai-service', 'https://ai.kapeta.com');
600
+ const agentName = req.params.agentName;
601
+ const systemId = req.params.systemId;
602
+ res.redirect(aiService + `/v2/conversations/${encodeURIComponent(agentName)}/${encodeURIComponent(systemId)}`);
603
+ });
547
604
  async function handleAll(req, res) {
548
605
  const handle = req.params.handle;
549
606
  const systemId = req.query.systemId ?? undefined;
@@ -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.71.6",
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,8 +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';
53
+ import { getRemoteUrl } from '../utils/utils';
54
+ import stormService from '../stormService';
55
+ import { PageQueue } from './PageGenerator';
54
56
 
55
57
  const UI_SERVERS: { [key: string]: UIServer } = {};
56
58
  const router = Router();
@@ -104,6 +106,50 @@ router.post('/ui/serve/:systemId', async (req: KapetaBodyRequest, res: Response)
104
106
  res.status(200).send({ status: 'running', url: svr.getUrl(), resetUrl: svr.resolveUrlFromPath('/_reset') });
105
107
  });
106
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
+
107
153
  router.post('/ui/create-system/:handle/:systemId', async (req: KapetaBodyRequest, res: Response) => {
108
154
  const systemId = req.params.systemId as string;
109
155
  const srcDir = getSystemBaseDir(systemId);
@@ -196,6 +242,20 @@ router.post('/ui/create-system-simple/:handle/:systemId', async (req: KapetaBody
196
242
  }
197
243
  });
198
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
+
199
259
  router.delete('/ui/serve/:systemId', async (req: KapetaBodyRequest, res: Response) => {
200
260
  const systemId = req.params.systemId as string | undefined;
201
261
  if (!systemId) {
@@ -370,6 +430,7 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
370
430
 
371
431
  router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
372
432
  const handle = req.params.handle as string;
433
+
373
434
  try {
374
435
  const outerConversationId =
375
436
  (req.headers[ConversationIdHeader.toLowerCase()] as string | undefined) || randomUUID();
@@ -679,6 +740,14 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
679
740
  await handleAll(req, res);
680
741
  });
681
742
 
743
+ router.get('/conversations/:agentName/:systemId/redirect', async (req: KapetaBodyRequest, res: Response) => {
744
+ const aiService = getRemoteUrl('ai-service', 'https://ai.kapeta.com');
745
+ const agentName = req.params.agentName as string;
746
+ const systemId = req.params.systemId as string;
747
+
748
+ res.redirect(aiService + `/v2/conversations/${encodeURIComponent(agentName)}/${encodeURIComponent(systemId)}`);
749
+ });
750
+
682
751
  async function handleAll(req: KapetaBodyRequest, res: Response) {
683
752
  const handle = req.params.handle as string;
684
753
  const systemId = (req.query.systemId as string) ?? undefined;
@@ -920,6 +989,7 @@ function sendEvent(res: Response, evt: StormEvent) {
920
989
  if (res.closed) {
921
990
  return;
922
991
  }
992
+
923
993
  res.write(JSON.stringify(evt) + '\n');
924
994
  }
925
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();