@kapeta/local-cluster-service 0.76.4 → 0.77.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.77.0](https://github.com/kapetacom/local-cluster-service/compare/v0.76.5...v0.77.0) (2024-10-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * Enable page agent to distinguish beween global and local edits ([1e24653](https://github.com/kapetacom/local-cluster-service/commit/1e246536272f9a141acf2233eb96fdec2a30c709))
7
+
8
+ ## [0.76.5](https://github.com/kapetacom/local-cluster-service/compare/v0.76.4...v0.76.5) (2024-10-03)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Poor mans security ([#268](https://github.com/kapetacom/local-cluster-service/issues/268)) ([dcc85ec](https://github.com/kapetacom/local-cluster-service/commit/dcc85ecdf9971c3033eaf43e65a9b262de269821))
14
+
1
15
  ## [0.76.4](https://github.com/kapetacom/local-cluster-service/compare/v0.76.3...v0.76.4) (2024-10-01)
2
16
 
3
17
 
@@ -41,9 +41,11 @@ export declare class PageQueue extends EventEmitter {
41
41
  addUiShell(uiShell: UIShell): void;
42
42
  setUiTheme(theme: string): void;
43
43
  private hasPrompt;
44
- addPrompt(initialPrompt: InitialPrompt, conversationId?: string, overwrite?: boolean): Promise<void>;
45
- private getPrefix;
46
- private wrapPagePrompt;
44
+ addPrompt(initialPrompt: InitialPrompt, conversationId?: string, overwrite?: boolean, globalEdit?: boolean): Promise<void>;
45
+ /**
46
+ * Get the existing pages
47
+ */
48
+ private getExistingPages;
47
49
  private processPageEventWithReferences;
48
50
  cancel(): void;
49
51
  wait(): Promise<void>;
@@ -87,7 +87,9 @@ class PageQueue extends node_events_1.EventEmitter {
87
87
  }
88
88
  return false;
89
89
  }
90
- addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false) {
90
+ addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false,
91
+ // Set globalEdit to true when the same prompt is being sent to multiple pages
92
+ globalEdit = false) {
91
93
  if (!overwrite && this.hasPrompt(initialPrompt.path)) {
92
94
  //console.log('Ignoring duplicate prompt', initialPrompt.path);
93
95
  return Promise.resolve();
@@ -96,39 +98,26 @@ class PageQueue extends node_events_1.EventEmitter {
96
98
  const prompt = {
97
99
  ...initialPrompt,
98
100
  shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
99
- prompt: this.wrapPagePrompt(initialPrompt.path, initialPrompt.prompt),
101
+ prompt: initialPrompt.prompt,
100
102
  theme: this.theme,
103
+ global_edit: globalEdit,
104
+ system_prompt: this.systemPrompt,
105
+ existing_pages: this.getExistingPages(initialPrompt.path),
106
+ existing_images: Object.entries(this.images).map(([path, description]) => ({ path, description })),
101
107
  };
102
108
  this.references.set(prompt.path, true);
103
109
  this.pages.set(prompt.path, prompt.description);
104
110
  return this.queue.add(() => this.generate(prompt, conversationId));
105
111
  }
106
- getPrefix() {
107
- let promptPrefix = '';
108
- if (this.systemPrompt) {
109
- promptPrefix = `For a system with this description: ${this.systemPrompt}\n`;
110
- }
111
- return promptPrefix;
112
- }
113
- wrapPagePrompt(pagePath, prompt) {
114
- const promptPrefix = this.getPrefix();
115
- let promptPostfix = '';
116
- if (this.pages.size > 0) {
117
- promptPostfix = `\nThe following pages are already implemented:\n`;
118
- this.pages.forEach((description, path) => {
119
- if (pagePath === path) {
120
- return;
121
- }
122
- promptPostfix += `- PAGE: '${path}' -> ${description}.\n`;
123
- });
124
- }
125
- if (this.images.size > 0) {
126
- promptPostfix += `\nThe following images already exist:\n`;
127
- this.images.forEach((description, path) => {
128
- promptPostfix += `- IMAGE: '${path}' -> ${description}.\n`;
129
- });
130
- }
131
- return promptPrefix + prompt + promptPostfix;
112
+ /**
113
+ * Get the existing pages
114
+ */
115
+ getExistingPages(excludePath) {
116
+ return (Object.entries(this.pages)
117
+ // Possibly exclude one page. This is useful when we don't want to include the page
118
+ // we're currently editing.
119
+ .filter(([path]) => (excludePath ? path !== excludePath : true))
120
+ .map(([path, description]) => ({ path, description })));
132
121
  }
133
122
  async processPageEventWithReferences(event) {
134
123
  try {
@@ -176,9 +165,17 @@ class PageQueue extends node_events_1.EventEmitter {
176
165
  path: normalizedPath,
177
166
  method: 'GET',
178
167
  storage_prefix: this.systemId + '_',
179
- prompt: `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
180
- `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
168
+ prompt: `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}`,
181
169
  description: reference.description,
170
+ referenced_from: {
171
+ path: event.payload.path,
172
+ content: event.payload.content,
173
+ },
174
+ existing_pages: this.getExistingPages(),
175
+ existing_images: Object.entries(this.images).map(([path, description]) => ({
176
+ path,
177
+ description,
178
+ })),
182
179
  // Only used for matching
183
180
  filename: reference.name + '.ref.html',
184
181
  theme: this.theme,
@@ -226,6 +226,9 @@ router.delete('/ui/serve/:systemId', async (req, res) => {
226
226
  }
227
227
  res.status(200).json({ status: 'ok' });
228
228
  });
229
+ /**
230
+ * Edit a single page
231
+ */
229
232
  router.post('/:handle/ui/screen', async (req, res) => {
230
233
  try {
231
234
  const handle = req.params.handle;
@@ -540,6 +543,9 @@ router.post('/:handle/ui', async (req, res) => {
540
543
  }
541
544
  }
542
545
  });
546
+ /**
547
+ * Edit all pages
548
+ */
543
549
  router.post('/:handle/ui/edit', async (req, res) => {
544
550
  try {
545
551
  const handle = req.params.handle;
@@ -586,7 +592,8 @@ router.post('/:handle/ui/edit', async (req, res) => {
586
592
  filename: page.filename,
587
593
  prompt: aiRequest.prompt.prompt.prompt,
588
594
  storage_prefix: storagePrefix,
589
- }, page.conversationId, true);
595
+ }, page.conversationId, true, true // this is a global edit
596
+ );
590
597
  }
591
598
  }));
592
599
  await queue.wait();
@@ -28,6 +28,20 @@ export interface UIPagePrompt {
28
28
  storage_prefix: string;
29
29
  shell_page?: string;
30
30
  theme?: string;
31
+ global_edit?: boolean;
32
+ system_prompt?: string;
33
+ existing_pages?: {
34
+ path: string;
35
+ description: string;
36
+ }[];
37
+ existing_images?: {
38
+ path: string;
39
+ description: string;
40
+ }[];
41
+ referenced_from?: {
42
+ path: string;
43
+ content: string;
44
+ };
31
45
  }
32
46
  export interface UIPageSamplePrompt extends UIPagePrompt {
33
47
  variantId: string;
@@ -61,6 +75,7 @@ export declare class StormClient {
61
75
  private readonly _baseUrl;
62
76
  private readonly _systemId;
63
77
  private readonly _handle;
78
+ private readonly _sharedSecret;
64
79
  constructor(handle: string, systemId?: string);
65
80
  private createOptions;
66
81
  private send;
@@ -91,4 +106,5 @@ export declare class StormClient {
91
106
  deleteUIPageConversation(conversationId: string): Promise<string>;
92
107
  downloadSystem(handle: string, conversationId: string): Promise<Buffer>;
93
108
  uploadSystem(handle: string, conversationId: string, buffer: Buffer): Promise<Response>;
109
+ private getSharedSecretHeader;
94
110
  }
@@ -23,14 +23,17 @@ class StormClient {
23
23
  _baseUrl;
24
24
  _systemId;
25
25
  _handle;
26
+ _sharedSecret;
26
27
  constructor(handle, systemId) {
27
28
  this._baseUrl = (0, utils_1.getRemoteUrl)('ai-service', 'https://ai.kapeta.com');
28
29
  this._systemId = systemId || '';
29
30
  this._handle = handle;
31
+ this._sharedSecret = process.env.SHARED_SECRET || '@keep-this-super-secret!';
30
32
  }
31
33
  async createOptions(path, method, body) {
32
34
  const url = `${this._baseUrl}${path}`;
33
35
  const headers = {
36
+ ...this.getSharedSecretHeader(),
34
37
  'Content-Type': 'application/json',
35
38
  };
36
39
  const api = new nodejs_api_client_1.KapetaAPI();
@@ -149,7 +152,7 @@ class StormClient {
149
152
  async replaceMockWithAPICall(prompt) {
150
153
  const u = `${this._baseUrl}/v2/ui/implement-api-clients-all`;
151
154
  try {
152
- const headers = {};
155
+ const headers = this.getSharedSecretHeader();
153
156
  headers[exports.HandleHeader] = this._handle;
154
157
  headers[exports.ConversationIdHeader] = this._systemId;
155
158
  headers[exports.SystemIdHeader] = this._systemId;
@@ -176,12 +179,13 @@ class StormClient {
176
179
  body: JSON.stringify({
177
180
  pages: pages,
178
181
  }),
182
+ headers: this.getSharedSecretHeader(),
179
183
  });
180
184
  return await response.text();
181
185
  }
182
186
  async createSimpleBackend(handle, systemId, input) {
183
187
  const u = `${this._baseUrl}/v2/create-simple-backend/${handle}/${systemId}`;
184
- const headers = {};
188
+ const headers = this.getSharedSecretHeader();
185
189
  headers[exports.HandleHeader] = this._handle;
186
190
  headers[exports.ConversationIdHeader] = this._systemId;
187
191
  headers[exports.SystemIdHeader] = this._systemId;
@@ -267,7 +271,9 @@ class StormClient {
267
271
  }
268
272
  async downloadSystem(handle, conversationId) {
269
273
  const u = `${this._baseUrl}/v2/systems/${handle}/${conversationId}/download`;
270
- const response = await fetch(u);
274
+ const response = await fetch(u, {
275
+ headers: this.getSharedSecretHeader(),
276
+ });
271
277
  if (!response.ok) {
272
278
  throw new Error(`Failed to download system: ${response.status}`);
273
279
  }
@@ -279,10 +285,16 @@ class StormClient {
279
285
  method: 'PUT',
280
286
  body: buffer,
281
287
  headers: {
288
+ ...this.getSharedSecretHeader(),
282
289
  'content-type': 'application/zip',
283
290
  },
284
291
  });
285
292
  return response;
286
293
  }
294
+ getSharedSecretHeader() {
295
+ return {
296
+ SharedSecret: this._sharedSecret,
297
+ };
298
+ }
287
299
  }
288
300
  exports.StormClient = StormClient;
@@ -41,9 +41,11 @@ export declare class PageQueue extends EventEmitter {
41
41
  addUiShell(uiShell: UIShell): void;
42
42
  setUiTheme(theme: string): void;
43
43
  private hasPrompt;
44
- addPrompt(initialPrompt: InitialPrompt, conversationId?: string, overwrite?: boolean): Promise<void>;
45
- private getPrefix;
46
- private wrapPagePrompt;
44
+ addPrompt(initialPrompt: InitialPrompt, conversationId?: string, overwrite?: boolean, globalEdit?: boolean): Promise<void>;
45
+ /**
46
+ * Get the existing pages
47
+ */
48
+ private getExistingPages;
47
49
  private processPageEventWithReferences;
48
50
  cancel(): void;
49
51
  wait(): Promise<void>;
@@ -87,7 +87,9 @@ class PageQueue extends node_events_1.EventEmitter {
87
87
  }
88
88
  return false;
89
89
  }
90
- addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false) {
90
+ addPrompt(initialPrompt, conversationId = node_uuid_1.default.v4(), overwrite = false,
91
+ // Set globalEdit to true when the same prompt is being sent to multiple pages
92
+ globalEdit = false) {
91
93
  if (!overwrite && this.hasPrompt(initialPrompt.path)) {
92
94
  //console.log('Ignoring duplicate prompt', initialPrompt.path);
93
95
  return Promise.resolve();
@@ -96,39 +98,26 @@ class PageQueue extends node_events_1.EventEmitter {
96
98
  const prompt = {
97
99
  ...initialPrompt,
98
100
  shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
99
- prompt: this.wrapPagePrompt(initialPrompt.path, initialPrompt.prompt),
101
+ prompt: initialPrompt.prompt,
100
102
  theme: this.theme,
103
+ global_edit: globalEdit,
104
+ system_prompt: this.systemPrompt,
105
+ existing_pages: this.getExistingPages(initialPrompt.path),
106
+ existing_images: Object.entries(this.images).map(([path, description]) => ({ path, description })),
101
107
  };
102
108
  this.references.set(prompt.path, true);
103
109
  this.pages.set(prompt.path, prompt.description);
104
110
  return this.queue.add(() => this.generate(prompt, conversationId));
105
111
  }
106
- getPrefix() {
107
- let promptPrefix = '';
108
- if (this.systemPrompt) {
109
- promptPrefix = `For a system with this description: ${this.systemPrompt}\n`;
110
- }
111
- return promptPrefix;
112
- }
113
- wrapPagePrompt(pagePath, prompt) {
114
- const promptPrefix = this.getPrefix();
115
- let promptPostfix = '';
116
- if (this.pages.size > 0) {
117
- promptPostfix = `\nThe following pages are already implemented:\n`;
118
- this.pages.forEach((description, path) => {
119
- if (pagePath === path) {
120
- return;
121
- }
122
- promptPostfix += `- PAGE: '${path}' -> ${description}.\n`;
123
- });
124
- }
125
- if (this.images.size > 0) {
126
- promptPostfix += `\nThe following images already exist:\n`;
127
- this.images.forEach((description, path) => {
128
- promptPostfix += `- IMAGE: '${path}' -> ${description}.\n`;
129
- });
130
- }
131
- return promptPrefix + prompt + promptPostfix;
112
+ /**
113
+ * Get the existing pages
114
+ */
115
+ getExistingPages(excludePath) {
116
+ return (Object.entries(this.pages)
117
+ // Possibly exclude one page. This is useful when we don't want to include the page
118
+ // we're currently editing.
119
+ .filter(([path]) => (excludePath ? path !== excludePath : true))
120
+ .map(([path, description]) => ({ path, description })));
132
121
  }
133
122
  async processPageEventWithReferences(event) {
134
123
  try {
@@ -176,9 +165,17 @@ class PageQueue extends node_events_1.EventEmitter {
176
165
  path: normalizedPath,
177
166
  method: 'GET',
178
167
  storage_prefix: this.systemId + '_',
179
- prompt: `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
180
- `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
168
+ prompt: `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}`,
181
169
  description: reference.description,
170
+ referenced_from: {
171
+ path: event.payload.path,
172
+ content: event.payload.content,
173
+ },
174
+ existing_pages: this.getExistingPages(),
175
+ existing_images: Object.entries(this.images).map(([path, description]) => ({
176
+ path,
177
+ description,
178
+ })),
182
179
  // Only used for matching
183
180
  filename: reference.name + '.ref.html',
184
181
  theme: this.theme,
@@ -226,6 +226,9 @@ router.delete('/ui/serve/:systemId', async (req, res) => {
226
226
  }
227
227
  res.status(200).json({ status: 'ok' });
228
228
  });
229
+ /**
230
+ * Edit a single page
231
+ */
229
232
  router.post('/:handle/ui/screen', async (req, res) => {
230
233
  try {
231
234
  const handle = req.params.handle;
@@ -540,6 +543,9 @@ router.post('/:handle/ui', async (req, res) => {
540
543
  }
541
544
  }
542
545
  });
546
+ /**
547
+ * Edit all pages
548
+ */
543
549
  router.post('/:handle/ui/edit', async (req, res) => {
544
550
  try {
545
551
  const handle = req.params.handle;
@@ -586,7 +592,8 @@ router.post('/:handle/ui/edit', async (req, res) => {
586
592
  filename: page.filename,
587
593
  prompt: aiRequest.prompt.prompt.prompt,
588
594
  storage_prefix: storagePrefix,
589
- }, page.conversationId, true);
595
+ }, page.conversationId, true, true // this is a global edit
596
+ );
590
597
  }
591
598
  }));
592
599
  await queue.wait();
@@ -28,6 +28,20 @@ export interface UIPagePrompt {
28
28
  storage_prefix: string;
29
29
  shell_page?: string;
30
30
  theme?: string;
31
+ global_edit?: boolean;
32
+ system_prompt?: string;
33
+ existing_pages?: {
34
+ path: string;
35
+ description: string;
36
+ }[];
37
+ existing_images?: {
38
+ path: string;
39
+ description: string;
40
+ }[];
41
+ referenced_from?: {
42
+ path: string;
43
+ content: string;
44
+ };
31
45
  }
32
46
  export interface UIPageSamplePrompt extends UIPagePrompt {
33
47
  variantId: string;
@@ -61,6 +75,7 @@ export declare class StormClient {
61
75
  private readonly _baseUrl;
62
76
  private readonly _systemId;
63
77
  private readonly _handle;
78
+ private readonly _sharedSecret;
64
79
  constructor(handle: string, systemId?: string);
65
80
  private createOptions;
66
81
  private send;
@@ -91,4 +106,5 @@ export declare class StormClient {
91
106
  deleteUIPageConversation(conversationId: string): Promise<string>;
92
107
  downloadSystem(handle: string, conversationId: string): Promise<Buffer>;
93
108
  uploadSystem(handle: string, conversationId: string, buffer: Buffer): Promise<Response>;
109
+ private getSharedSecretHeader;
94
110
  }
@@ -23,14 +23,17 @@ class StormClient {
23
23
  _baseUrl;
24
24
  _systemId;
25
25
  _handle;
26
+ _sharedSecret;
26
27
  constructor(handle, systemId) {
27
28
  this._baseUrl = (0, utils_1.getRemoteUrl)('ai-service', 'https://ai.kapeta.com');
28
29
  this._systemId = systemId || '';
29
30
  this._handle = handle;
31
+ this._sharedSecret = process.env.SHARED_SECRET || '@keep-this-super-secret!';
30
32
  }
31
33
  async createOptions(path, method, body) {
32
34
  const url = `${this._baseUrl}${path}`;
33
35
  const headers = {
36
+ ...this.getSharedSecretHeader(),
34
37
  'Content-Type': 'application/json',
35
38
  };
36
39
  const api = new nodejs_api_client_1.KapetaAPI();
@@ -149,7 +152,7 @@ class StormClient {
149
152
  async replaceMockWithAPICall(prompt) {
150
153
  const u = `${this._baseUrl}/v2/ui/implement-api-clients-all`;
151
154
  try {
152
- const headers = {};
155
+ const headers = this.getSharedSecretHeader();
153
156
  headers[exports.HandleHeader] = this._handle;
154
157
  headers[exports.ConversationIdHeader] = this._systemId;
155
158
  headers[exports.SystemIdHeader] = this._systemId;
@@ -176,12 +179,13 @@ class StormClient {
176
179
  body: JSON.stringify({
177
180
  pages: pages,
178
181
  }),
182
+ headers: this.getSharedSecretHeader(),
179
183
  });
180
184
  return await response.text();
181
185
  }
182
186
  async createSimpleBackend(handle, systemId, input) {
183
187
  const u = `${this._baseUrl}/v2/create-simple-backend/${handle}/${systemId}`;
184
- const headers = {};
188
+ const headers = this.getSharedSecretHeader();
185
189
  headers[exports.HandleHeader] = this._handle;
186
190
  headers[exports.ConversationIdHeader] = this._systemId;
187
191
  headers[exports.SystemIdHeader] = this._systemId;
@@ -267,7 +271,9 @@ class StormClient {
267
271
  }
268
272
  async downloadSystem(handle, conversationId) {
269
273
  const u = `${this._baseUrl}/v2/systems/${handle}/${conversationId}/download`;
270
- const response = await fetch(u);
274
+ const response = await fetch(u, {
275
+ headers: this.getSharedSecretHeader(),
276
+ });
271
277
  if (!response.ok) {
272
278
  throw new Error(`Failed to download system: ${response.status}`);
273
279
  }
@@ -279,10 +285,16 @@ class StormClient {
279
285
  method: 'PUT',
280
286
  body: buffer,
281
287
  headers: {
288
+ ...this.getSharedSecretHeader(),
282
289
  'content-type': 'application/zip',
283
290
  },
284
291
  });
285
292
  return response;
286
293
  }
294
+ getSharedSecretHeader() {
295
+ return {
296
+ SharedSecret: this._sharedSecret,
297
+ };
298
+ }
287
299
  }
288
300
  exports.StormClient = StormClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.76.4",
3
+ "version": "0.77.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -95,7 +95,13 @@ export class PageQueue extends EventEmitter {
95
95
  return false;
96
96
  }
97
97
 
98
- public addPrompt(initialPrompt: InitialPrompt, conversationId: string = uuid.v4(), overwrite: boolean = false) {
98
+ public addPrompt(
99
+ initialPrompt: InitialPrompt,
100
+ conversationId: string = uuid.v4(),
101
+ overwrite: boolean = false,
102
+ // Set globalEdit to true when the same prompt is being sent to multiple pages
103
+ globalEdit: boolean = false
104
+ ) {
99
105
  if (!overwrite && this.hasPrompt(initialPrompt.path)) {
100
106
  //console.log('Ignoring duplicate prompt', initialPrompt.path);
101
107
  return Promise.resolve();
@@ -105,8 +111,12 @@ export class PageQueue extends EventEmitter {
105
111
  const prompt: UIPagePrompt = {
106
112
  ...initialPrompt,
107
113
  shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
108
- prompt: this.wrapPagePrompt(initialPrompt.path, initialPrompt.prompt),
114
+ prompt: initialPrompt.prompt,
109
115
  theme: this.theme,
116
+ global_edit: globalEdit,
117
+ system_prompt: this.systemPrompt,
118
+ existing_pages: this.getExistingPages(initialPrompt.path),
119
+ existing_images: Object.entries(this.images).map(([path, description]) => ({ path, description })),
110
120
  };
111
121
 
112
122
  this.references.set(prompt.path, true);
@@ -114,36 +124,18 @@ export class PageQueue extends EventEmitter {
114
124
 
115
125
  return this.queue.add<void>(() => this.generate(prompt, conversationId));
116
126
  }
117
- private getPrefix(): string {
118
- let promptPrefix = '';
119
- if (this.systemPrompt) {
120
- promptPrefix = `For a system with this description: ${this.systemPrompt}\n`;
121
- }
122
- return promptPrefix;
123
- }
124
-
125
- private wrapPagePrompt(pagePath: string, prompt: string): string {
126
- const promptPrefix = this.getPrefix();
127
- let promptPostfix = '';
128
-
129
- if (this.pages.size > 0) {
130
- promptPostfix = `\nThe following pages are already implemented:\n`;
131
- this.pages.forEach((description, path) => {
132
- if (pagePath === path) {
133
- return;
134
- }
135
- promptPostfix += `- PAGE: '${path}' -> ${description}.\n`;
136
- });
137
- }
138
-
139
- if (this.images.size > 0) {
140
- promptPostfix += `\nThe following images already exist:\n`;
141
- this.images.forEach((description, path) => {
142
- promptPostfix += `- IMAGE: '${path}' -> ${description}.\n`;
143
- });
144
- }
145
127
 
146
- return promptPrefix + prompt + promptPostfix;
128
+ /**
129
+ * Get the existing pages
130
+ */
131
+ private getExistingPages(excludePath?: string) {
132
+ return (
133
+ Object.entries(this.pages)
134
+ // Possibly exclude one page. This is useful when we don't want to include the page
135
+ // we're currently editing.
136
+ .filter(([path]) => (excludePath ? path !== excludePath : true))
137
+ .map(([path, description]) => ({ path, description }))
138
+ );
147
139
  }
148
140
 
149
141
  private async processPageEventWithReferences(event: StormEventPage) {
@@ -198,10 +190,17 @@ export class PageQueue extends EventEmitter {
198
190
  path: normalizedPath,
199
191
  method: 'GET',
200
192
  storage_prefix: this.systemId + '_',
201
- prompt:
202
- `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
203
- `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
193
+ prompt: `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}`,
204
194
  description: reference.description,
195
+ referenced_from: {
196
+ path: event.payload.path,
197
+ content: event.payload.content,
198
+ },
199
+ existing_pages: this.getExistingPages(),
200
+ existing_images: Object.entries(this.images).map(([path, description]) => ({
201
+ path,
202
+ description,
203
+ })),
205
204
  // Only used for matching
206
205
  filename: reference.name + '.ref.html',
207
206
  theme: this.theme,
@@ -293,6 +293,9 @@ router.delete('/ui/serve/:systemId', async (req: KapetaBodyRequest, res: Respons
293
293
  res.status(200).json({ status: 'ok' });
294
294
  });
295
295
 
296
+ /**
297
+ * Edit a single page
298
+ */
296
299
  router.post('/:handle/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
297
300
  try {
298
301
  const handle = req.params.handle as string;
@@ -673,6 +676,9 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
673
676
  }
674
677
  });
675
678
 
679
+ /**
680
+ * Edit all pages
681
+ */
676
682
  router.post('/:handle/ui/edit', async (req: KapetaBodyRequest, res: Response) => {
677
683
  try {
678
684
  const handle = req.params.handle as string;
@@ -732,7 +738,8 @@ router.post('/:handle/ui/edit', async (req: KapetaBodyRequest, res: Response) =>
732
738
  storage_prefix: storagePrefix,
733
739
  },
734
740
  page.conversationId,
735
- true
741
+ true,
742
+ true // this is a global edit
736
743
  );
737
744
  }
738
745
  })
@@ -51,6 +51,25 @@ export interface UIPagePrompt {
51
51
  shell_page?: string;
52
52
  // contents of theme.md
53
53
  theme?: string;
54
+ // whether this prompt is for a global edit
55
+ global_edit?: boolean;
56
+ // The prompt that was used to start the conversation (user prompt or improved prompt)
57
+ system_prompt?: string;
58
+ // The pages we have already created
59
+ existing_pages?: {
60
+ path: string;
61
+ description: string;
62
+ }[];
63
+ // The images we have already created
64
+ existing_images?: {
65
+ path: string;
66
+ description: string;
67
+ }[];
68
+ // The page that referenced this page
69
+ referenced_from?: {
70
+ path: string;
71
+ content: string;
72
+ };
54
73
  }
55
74
 
56
75
  export interface UIPageSamplePrompt extends UIPagePrompt {
@@ -91,10 +110,12 @@ export class StormClient {
91
110
  private readonly _baseUrl: string;
92
111
  private readonly _systemId: string;
93
112
  private readonly _handle: string;
113
+ private readonly _sharedSecret: string;
94
114
  constructor(handle: string, systemId?: string) {
95
115
  this._baseUrl = getRemoteUrl('ai-service', 'https://ai.kapeta.com');
96
116
  this._systemId = systemId || '';
97
117
  this._handle = handle;
118
+ this._sharedSecret = process.env.SHARED_SECRET || '@keep-this-super-secret!';
98
119
  }
99
120
 
100
121
  private async createOptions(
@@ -104,6 +125,7 @@ export class StormClient {
104
125
  ): Promise<RequestInit & { url: string }> {
105
126
  const url = `${this._baseUrl}${path}`;
106
127
  const headers: { [k: string]: string } = {
128
+ ...this.getSharedSecretHeader(),
107
129
  'Content-Type': 'application/json',
108
130
  };
109
131
  const api = new KapetaAPI();
@@ -253,7 +275,7 @@ export class StormClient {
253
275
  public async replaceMockWithAPICall(prompt: ImplementAPIClients): Promise<HTMLPage[]> {
254
276
  const u = `${this._baseUrl}/v2/ui/implement-api-clients-all`;
255
277
  try {
256
- const headers: { [key: string]: any } = {};
278
+ const headers: { [key: string]: any } = this.getSharedSecretHeader();
257
279
  headers[HandleHeader] = this._handle;
258
280
  headers[ConversationIdHeader] = this._systemId;
259
281
  headers[SystemIdHeader] = this._systemId;
@@ -283,6 +305,7 @@ export class StormClient {
283
305
  body: JSON.stringify({
284
306
  pages: pages,
285
307
  }),
308
+ headers: this.getSharedSecretHeader(),
286
309
  });
287
310
  return await response.text();
288
311
  }
@@ -290,7 +313,7 @@ export class StormClient {
290
313
  public async createSimpleBackend(handle: string, systemId: string, input: CreateSimpleBackendRequest) {
291
314
  const u = `${this._baseUrl}/v2/create-simple-backend/${handle}/${systemId}`;
292
315
 
293
- const headers: { [key: string]: any } = {};
316
+ const headers: { [key: string]: any } = this.getSharedSecretHeader();
294
317
  headers[HandleHeader] = this._handle;
295
318
  headers[ConversationIdHeader] = this._systemId;
296
319
  headers[SystemIdHeader] = this._systemId;
@@ -391,7 +414,9 @@ export class StormClient {
391
414
 
392
415
  async downloadSystem(handle: string, conversationId: string) {
393
416
  const u = `${this._baseUrl}/v2/systems/${handle}/${conversationId}/download`;
394
- const response = await fetch(u);
417
+ const response = await fetch(u, {
418
+ headers: this.getSharedSecretHeader(),
419
+ });
395
420
  if (!response.ok) {
396
421
  throw new Error(`Failed to download system: ${response.status}`);
397
422
  }
@@ -404,10 +429,17 @@ export class StormClient {
404
429
  method: 'PUT',
405
430
  body: buffer,
406
431
  headers: {
432
+ ...this.getSharedSecretHeader(),
407
433
  'content-type': 'application/zip',
408
434
  },
409
435
  });
410
436
 
411
437
  return response;
412
438
  }
439
+
440
+ private getSharedSecretHeader() {
441
+ return {
442
+ SharedSecret: this._sharedSecret,
443
+ };
444
+ }
413
445
  }