@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 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.stringBody = Buffer.concat(body).toString();
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
- absolute: true,
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.stringBody = Buffer.concat(body).toString();
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
- absolute: true,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.74.2",
3
+ "version": "0.75.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -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.stringBody = Buffer.concat(body).toString();
18
+ req.body = Buffer.concat(body);
19
+ req.stringBody = req.body.toString();
19
20
  next();
20
21
  });
21
22
  }
@@ -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) {
@@ -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
- absolute: true,
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: { id: string; description: string; title: string; url?: string }[] = [];
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 as string, 'utf8');
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();