@kapeta/local-cluster-service 0.74.2 → 0.75.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/middleware/stringBody.js +2 -1
- package/dist/cjs/src/storm/routes.js +16 -0
- package/dist/cjs/src/stormService.d.ts +7 -0
- package/dist/cjs/src/stormService.js +23 -2
- package/dist/esm/src/middleware/stringBody.js +2 -1
- package/dist/esm/src/storm/routes.js +16 -0
- package/dist/esm/src/stormService.d.ts +7 -0
- package/dist/esm/src/stormService.js +23 -2
- package/package.json +1 -1
- package/src/middleware/stringBody.ts +2 -1
- package/src/storm/routes.ts +18 -1
- package/src/stormService.ts +36 -4
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# [0.75.0](https://github.com/kapetacom/local-cluster-service/compare/v0.74.2...v0.75.0) (2024-09-30)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* add thumbnail support and timestamps to AI systems [CORE-3532] ([#262](https://github.com/kapetacom/local-cluster-service/issues/262)) ([31a3cad](https://github.com/kapetacom/local-cluster-service/commit/31a3cadba0d5034d3497b98616468e12ce3f586b))
|
7
|
+
|
1
8
|
## [0.74.2](https://github.com/kapetacom/local-cluster-service/compare/v0.74.1...v0.74.2) (2024-09-27)
|
2
9
|
|
3
10
|
|
@@ -11,7 +11,8 @@ function stringBody(req, res, next) {
|
|
11
11
|
req.on('data', (chunk) => {
|
12
12
|
body.push(chunk);
|
13
13
|
}).on('end', () => {
|
14
|
-
req.
|
14
|
+
req.body = Buffer.concat(body);
|
15
|
+
req.stringBody = req.body.toString();
|
15
16
|
next();
|
16
17
|
});
|
17
18
|
}
|
@@ -196,6 +196,22 @@ router.post('/ui/systems/:handle/:systemId/upload', async (req, res) => {
|
|
196
196
|
await stormService_1.default.uploadConversation(handle, systemId);
|
197
197
|
res.send({ ok: true });
|
198
198
|
});
|
199
|
+
router.put('/ui/systems/:handle/:systemId/thumbnail', async (req, res) => {
|
200
|
+
const systemId = req.params.systemId;
|
201
|
+
await stormService_1.default.saveThumbnail(systemId, req.body);
|
202
|
+
res.send({ ok: true });
|
203
|
+
});
|
204
|
+
router.get('/ui/systems/:handle/:systemId/thumbnail.png', async (req, res) => {
|
205
|
+
const systemId = req.params.systemId;
|
206
|
+
const thumbnail = await stormService_1.default.getThumbnail(systemId);
|
207
|
+
if (thumbnail) {
|
208
|
+
res.set('Content-Type', 'image/png');
|
209
|
+
res.send(thumbnail);
|
210
|
+
}
|
211
|
+
else {
|
212
|
+
res.status(404).send({ error: 'No thumbnail found' });
|
213
|
+
}
|
214
|
+
});
|
199
215
|
router.delete('/ui/serve/:systemId', async (req, res) => {
|
200
216
|
const systemId = req.params.systemId;
|
201
217
|
if (!systemId) {
|
@@ -1,13 +1,18 @@
|
|
1
|
+
/// <reference types="node" />
|
1
2
|
import { StormEvent } from './storm/events';
|
2
3
|
export declare class StormService {
|
3
4
|
private getConversationFile;
|
4
5
|
private getConversationTarball;
|
6
|
+
private getThumbnailFile;
|
5
7
|
listRemoteConversations(): Promise<never[]>;
|
6
8
|
listLocalConversations(): Promise<{
|
7
9
|
id: string;
|
8
10
|
description: string;
|
9
11
|
title: string;
|
10
12
|
url?: string | undefined;
|
13
|
+
lastModified?: number | undefined;
|
14
|
+
createdAt?: number | undefined;
|
15
|
+
thumbnail?: string | undefined;
|
11
16
|
}[]>;
|
12
17
|
getConversation(conversationId: string): Promise<string>;
|
13
18
|
saveConversation(conversationId: string, events: StormEvent[]): Promise<void>;
|
@@ -15,6 +20,8 @@ export declare class StormService {
|
|
15
20
|
deleteConversation(conversationId: string): Promise<void>;
|
16
21
|
uploadConversation(handle: string, systemId: string): Promise<void>;
|
17
22
|
installProjectById(handle: string, systemId: string): Promise<void>;
|
23
|
+
saveThumbnail(systemId: string, thumbnail: Buffer): Promise<void>;
|
24
|
+
getThumbnail(systemId: string): Promise<Buffer | null>;
|
18
25
|
}
|
19
26
|
declare const _default: StormService;
|
20
27
|
export default _default;
|
@@ -41,6 +41,9 @@ class StormService {
|
|
41
41
|
getConversationTarball(conversationId) {
|
42
42
|
return path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems', conversationId, 'system.tar.gz');
|
43
43
|
}
|
44
|
+
getThumbnailFile(conversationId) {
|
45
|
+
return path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems', conversationId, 'thumbnail.png');
|
46
|
+
}
|
44
47
|
async listRemoteConversations() {
|
45
48
|
// i.e. conversations from org / user on registry
|
46
49
|
return [];
|
@@ -49,13 +52,16 @@ class StormService {
|
|
49
52
|
const systemsFolder = path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems');
|
50
53
|
const eventFiles = await (0, glob_1.glob)('*/events.ndjson', {
|
51
54
|
cwd: systemsFolder,
|
52
|
-
|
55
|
+
stat: true,
|
56
|
+
withFileTypes: true,
|
53
57
|
});
|
54
58
|
// Returns list of UUIDs - probably want to make it more useful than that
|
55
59
|
const conversations = [];
|
60
|
+
// Sort by modification time, newest first
|
61
|
+
eventFiles.sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0));
|
56
62
|
for (const file of eventFiles) {
|
57
63
|
try {
|
58
|
-
const nldContents = await promises_1.default.readFile(file, 'utf8');
|
64
|
+
const nldContents = await promises_1.default.readFile(file.fullpath(), 'utf8');
|
59
65
|
const events = nldContents.split('\n').map((e) => JSON.parse(e));
|
60
66
|
// find the shell and get the title tag
|
61
67
|
const shellEvent = events.find((e) => e.type === 'AI' && e.event.type === 'UI_SHELL')?.event;
|
@@ -86,6 +92,9 @@ class StormService {
|
|
86
92
|
description: initialPrompt,
|
87
93
|
title: title || 'New system',
|
88
94
|
url,
|
95
|
+
lastModified: file.mtimeMs,
|
96
|
+
createdAt: file.birthtimeMs,
|
97
|
+
thumbnail: (0, fs_1.existsSync)(this.getThumbnailFile(id)) ? `thumbnail.png?v=${file.mtimeMs}` : undefined,
|
89
98
|
});
|
90
99
|
}
|
91
100
|
catch (e) {
|
@@ -145,6 +154,18 @@ class StormService {
|
|
145
154
|
});
|
146
155
|
await promises_1.default.unlink(tarballFile);
|
147
156
|
}
|
157
|
+
async saveThumbnail(systemId, thumbnail) {
|
158
|
+
const thumbnailFile = this.getThumbnailFile(systemId);
|
159
|
+
await promises_1.default.mkdir(path_1.default.dirname(thumbnailFile), { recursive: true });
|
160
|
+
await promises_1.default.writeFile(thumbnailFile, thumbnail);
|
161
|
+
}
|
162
|
+
async getThumbnail(systemId) {
|
163
|
+
const thumbnailFile = this.getThumbnailFile(systemId);
|
164
|
+
if ((0, fs_1.existsSync)(thumbnailFile)) {
|
165
|
+
return promises_1.default.readFile(thumbnailFile);
|
166
|
+
}
|
167
|
+
return null;
|
168
|
+
}
|
148
169
|
}
|
149
170
|
exports.StormService = StormService;
|
150
171
|
exports.default = new StormService();
|
@@ -11,7 +11,8 @@ function stringBody(req, res, next) {
|
|
11
11
|
req.on('data', (chunk) => {
|
12
12
|
body.push(chunk);
|
13
13
|
}).on('end', () => {
|
14
|
-
req.
|
14
|
+
req.body = Buffer.concat(body);
|
15
|
+
req.stringBody = req.body.toString();
|
15
16
|
next();
|
16
17
|
});
|
17
18
|
}
|
@@ -196,6 +196,22 @@ router.post('/ui/systems/:handle/:systemId/upload', async (req, res) => {
|
|
196
196
|
await stormService_1.default.uploadConversation(handle, systemId);
|
197
197
|
res.send({ ok: true });
|
198
198
|
});
|
199
|
+
router.put('/ui/systems/:handle/:systemId/thumbnail', async (req, res) => {
|
200
|
+
const systemId = req.params.systemId;
|
201
|
+
await stormService_1.default.saveThumbnail(systemId, req.body);
|
202
|
+
res.send({ ok: true });
|
203
|
+
});
|
204
|
+
router.get('/ui/systems/:handle/:systemId/thumbnail.png', async (req, res) => {
|
205
|
+
const systemId = req.params.systemId;
|
206
|
+
const thumbnail = await stormService_1.default.getThumbnail(systemId);
|
207
|
+
if (thumbnail) {
|
208
|
+
res.set('Content-Type', 'image/png');
|
209
|
+
res.send(thumbnail);
|
210
|
+
}
|
211
|
+
else {
|
212
|
+
res.status(404).send({ error: 'No thumbnail found' });
|
213
|
+
}
|
214
|
+
});
|
199
215
|
router.delete('/ui/serve/:systemId', async (req, res) => {
|
200
216
|
const systemId = req.params.systemId;
|
201
217
|
if (!systemId) {
|
@@ -1,13 +1,18 @@
|
|
1
|
+
/// <reference types="node" />
|
1
2
|
import { StormEvent } from './storm/events';
|
2
3
|
export declare class StormService {
|
3
4
|
private getConversationFile;
|
4
5
|
private getConversationTarball;
|
6
|
+
private getThumbnailFile;
|
5
7
|
listRemoteConversations(): Promise<never[]>;
|
6
8
|
listLocalConversations(): Promise<{
|
7
9
|
id: string;
|
8
10
|
description: string;
|
9
11
|
title: string;
|
10
12
|
url?: string | undefined;
|
13
|
+
lastModified?: number | undefined;
|
14
|
+
createdAt?: number | undefined;
|
15
|
+
thumbnail?: string | undefined;
|
11
16
|
}[]>;
|
12
17
|
getConversation(conversationId: string): Promise<string>;
|
13
18
|
saveConversation(conversationId: string, events: StormEvent[]): Promise<void>;
|
@@ -15,6 +20,8 @@ export declare class StormService {
|
|
15
20
|
deleteConversation(conversationId: string): Promise<void>;
|
16
21
|
uploadConversation(handle: string, systemId: string): Promise<void>;
|
17
22
|
installProjectById(handle: string, systemId: string): Promise<void>;
|
23
|
+
saveThumbnail(systemId: string, thumbnail: Buffer): Promise<void>;
|
24
|
+
getThumbnail(systemId: string): Promise<Buffer | null>;
|
18
25
|
}
|
19
26
|
declare const _default: StormService;
|
20
27
|
export default _default;
|
@@ -41,6 +41,9 @@ class StormService {
|
|
41
41
|
getConversationTarball(conversationId) {
|
42
42
|
return path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems', conversationId, 'system.tar.gz');
|
43
43
|
}
|
44
|
+
getThumbnailFile(conversationId) {
|
45
|
+
return path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems', conversationId, 'thumbnail.png');
|
46
|
+
}
|
44
47
|
async listRemoteConversations() {
|
45
48
|
// i.e. conversations from org / user on registry
|
46
49
|
return [];
|
@@ -49,13 +52,16 @@ class StormService {
|
|
49
52
|
const systemsFolder = path_1.default.join(filesystemManager_1.filesystemManager.getProjectRootFolder(), 'ai-systems');
|
50
53
|
const eventFiles = await (0, glob_1.glob)('*/events.ndjson', {
|
51
54
|
cwd: systemsFolder,
|
52
|
-
|
55
|
+
stat: true,
|
56
|
+
withFileTypes: true,
|
53
57
|
});
|
54
58
|
// Returns list of UUIDs - probably want to make it more useful than that
|
55
59
|
const conversations = [];
|
60
|
+
// Sort by modification time, newest first
|
61
|
+
eventFiles.sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0));
|
56
62
|
for (const file of eventFiles) {
|
57
63
|
try {
|
58
|
-
const nldContents = await promises_1.default.readFile(file, 'utf8');
|
64
|
+
const nldContents = await promises_1.default.readFile(file.fullpath(), 'utf8');
|
59
65
|
const events = nldContents.split('\n').map((e) => JSON.parse(e));
|
60
66
|
// find the shell and get the title tag
|
61
67
|
const shellEvent = events.find((e) => e.type === 'AI' && e.event.type === 'UI_SHELL')?.event;
|
@@ -86,6 +92,9 @@ class StormService {
|
|
86
92
|
description: initialPrompt,
|
87
93
|
title: title || 'New system',
|
88
94
|
url,
|
95
|
+
lastModified: file.mtimeMs,
|
96
|
+
createdAt: file.birthtimeMs,
|
97
|
+
thumbnail: (0, fs_1.existsSync)(this.getThumbnailFile(id)) ? `thumbnail.png?v=${file.mtimeMs}` : undefined,
|
89
98
|
});
|
90
99
|
}
|
91
100
|
catch (e) {
|
@@ -145,6 +154,18 @@ class StormService {
|
|
145
154
|
});
|
146
155
|
await promises_1.default.unlink(tarballFile);
|
147
156
|
}
|
157
|
+
async saveThumbnail(systemId, thumbnail) {
|
158
|
+
const thumbnailFile = this.getThumbnailFile(systemId);
|
159
|
+
await promises_1.default.mkdir(path_1.default.dirname(thumbnailFile), { recursive: true });
|
160
|
+
await promises_1.default.writeFile(thumbnailFile, thumbnail);
|
161
|
+
}
|
162
|
+
async getThumbnail(systemId) {
|
163
|
+
const thumbnailFile = this.getThumbnailFile(systemId);
|
164
|
+
if ((0, fs_1.existsSync)(thumbnailFile)) {
|
165
|
+
return promises_1.default.readFile(thumbnailFile);
|
166
|
+
}
|
167
|
+
return null;
|
168
|
+
}
|
148
169
|
}
|
149
170
|
exports.StormService = StormService;
|
150
171
|
exports.default = new StormService();
|
package/package.json
CHANGED
@@ -15,7 +15,8 @@ export function stringBody(req: StringBodyRequest, res: Response, next: NextFunc
|
|
15
15
|
req.on('data', (chunk) => {
|
16
16
|
body.push(chunk);
|
17
17
|
}).on('end', () => {
|
18
|
-
req.
|
18
|
+
req.body = Buffer.concat(body);
|
19
|
+
req.stringBody = req.body.toString();
|
19
20
|
next();
|
20
21
|
});
|
21
22
|
}
|
package/src/storm/routes.ts
CHANGED
@@ -162,7 +162,7 @@ router.post('/ui/create-system/:handle/:systemId', async (req: KapetaBodyRequest
|
|
162
162
|
sendEvent(res, createPhaseStartEvent(StormEventPhaseType.IMPLEMENT_APIS));
|
163
163
|
|
164
164
|
const pagesFromDisk = readFilesAndContent(srcDir);
|
165
|
-
const client = new StormClient(systemId)
|
165
|
+
const client = new StormClient(systemId);
|
166
166
|
const pagesWithImplementation = await client.replaceMockWithAPICall({
|
167
167
|
pages: pagesFromDisk,
|
168
168
|
systemId: systemId,
|
@@ -260,6 +260,23 @@ router.post('/ui/systems/:handle/:systemId/upload', async (req: KapetaBodyReques
|
|
260
260
|
res.send({ ok: true });
|
261
261
|
});
|
262
262
|
|
263
|
+
router.put('/ui/systems/:handle/:systemId/thumbnail', async (req: KapetaBodyRequest, res: Response) => {
|
264
|
+
const systemId = req.params.systemId as string;
|
265
|
+
await stormService.saveThumbnail(systemId, req.body as Buffer);
|
266
|
+
res.send({ ok: true });
|
267
|
+
});
|
268
|
+
|
269
|
+
router.get('/ui/systems/:handle/:systemId/thumbnail.png', async (req: KapetaBodyRequest, res: Response) => {
|
270
|
+
const systemId = req.params.systemId as string;
|
271
|
+
const thumbnail = await stormService.getThumbnail(systemId);
|
272
|
+
if (thumbnail) {
|
273
|
+
res.set('Content-Type', 'image/png');
|
274
|
+
res.send(thumbnail);
|
275
|
+
} else {
|
276
|
+
res.status(404).send({ error: 'No thumbnail found' });
|
277
|
+
}
|
278
|
+
});
|
279
|
+
|
263
280
|
router.delete('/ui/serve/:systemId', async (req: KapetaBodyRequest, res: Response) => {
|
264
281
|
const systemId = req.params.systemId as string | undefined;
|
265
282
|
if (!systemId) {
|
package/src/stormService.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import fs from 'fs/promises';
|
2
|
-
import { glob } from 'glob';
|
2
|
+
import { glob, Path } from 'glob';
|
3
3
|
import { filesystemManager } from './filesystemManager';
|
4
4
|
import path from 'path';
|
5
5
|
import { existsSync } from 'fs';
|
@@ -16,6 +16,10 @@ export class StormService {
|
|
16
16
|
return path.join(filesystemManager.getProjectRootFolder()!, 'ai-systems', conversationId, 'system.tar.gz');
|
17
17
|
}
|
18
18
|
|
19
|
+
private getThumbnailFile(conversationId: string) {
|
20
|
+
return path.join(filesystemManager.getProjectRootFolder()!, 'ai-systems', conversationId, 'thumbnail.png');
|
21
|
+
}
|
22
|
+
|
19
23
|
async listRemoteConversations() {
|
20
24
|
// i.e. conversations from org / user on registry
|
21
25
|
return [];
|
@@ -25,13 +29,24 @@ export class StormService {
|
|
25
29
|
const systemsFolder = path.join(filesystemManager.getProjectRootFolder()!, 'ai-systems');
|
26
30
|
const eventFiles = await glob('*/events.ndjson', {
|
27
31
|
cwd: systemsFolder,
|
28
|
-
|
32
|
+
stat: true,
|
33
|
+
withFileTypes: true,
|
29
34
|
});
|
30
35
|
// Returns list of UUIDs - probably want to make it more useful than that
|
31
|
-
const conversations: {
|
36
|
+
const conversations: {
|
37
|
+
id: string;
|
38
|
+
description: string;
|
39
|
+
title: string;
|
40
|
+
url?: string;
|
41
|
+
lastModified?: number;
|
42
|
+
createdAt?: number;
|
43
|
+
thumbnail?: string;
|
44
|
+
}[] = [];
|
45
|
+
// Sort by modification time, newest first
|
46
|
+
eventFiles.sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0));
|
32
47
|
for (const file of eventFiles) {
|
33
48
|
try {
|
34
|
-
const nldContents = await fs.readFile(file
|
49
|
+
const nldContents = await fs.readFile(file.fullpath(), 'utf8');
|
35
50
|
const events = nldContents.split('\n').map((e) => JSON.parse(e)) as {
|
36
51
|
// | { type: 'USER'; event: any } // IS stupid!
|
37
52
|
type: 'AI';
|
@@ -79,6 +94,9 @@ export class StormService {
|
|
79
94
|
description: initialPrompt,
|
80
95
|
title: title || 'New system',
|
81
96
|
url,
|
97
|
+
lastModified: file.mtimeMs,
|
98
|
+
createdAt: file.birthtimeMs,
|
99
|
+
thumbnail: existsSync(this.getThumbnailFile(id)) ? `thumbnail.png?v=${file.mtimeMs}` : undefined,
|
82
100
|
});
|
83
101
|
} catch (e) {
|
84
102
|
console.error('Failed to load conversation at %s', file, e);
|
@@ -148,6 +166,20 @@ export class StormService {
|
|
148
166
|
});
|
149
167
|
await fs.unlink(tarballFile);
|
150
168
|
}
|
169
|
+
|
170
|
+
async saveThumbnail(systemId: string, thumbnail: Buffer) {
|
171
|
+
const thumbnailFile = this.getThumbnailFile(systemId);
|
172
|
+
await fs.mkdir(path.dirname(thumbnailFile), { recursive: true });
|
173
|
+
await fs.writeFile(thumbnailFile, thumbnail);
|
174
|
+
}
|
175
|
+
|
176
|
+
async getThumbnail(systemId: string) {
|
177
|
+
const thumbnailFile = this.getThumbnailFile(systemId);
|
178
|
+
if (existsSync(thumbnailFile)) {
|
179
|
+
return fs.readFile(thumbnailFile);
|
180
|
+
}
|
181
|
+
return null;
|
182
|
+
}
|
151
183
|
}
|
152
184
|
|
153
185
|
export default new StormService();
|