openkbs 0.0.66 → 0.0.70

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.
Files changed (63) hide show
  1. package/README.md +1 -0
  2. package/elastic/README.md +1 -1
  3. package/elastic/functions.md +5 -5
  4. package/elastic/pulse.md +2 -2
  5. package/package.json +2 -2
  6. package/scripts/deploy.js +68 -0
  7. package/src/actions.js +59 -0
  8. package/src/index.js +8 -6
  9. package/templates/.claude/skills/openkbs/SKILL.md +37 -8
  10. package/templates/.claude/skills/openkbs/examples/monitoring-bot/README.md +55 -0
  11. package/templates/.claude/skills/openkbs/examples/monitoring-bot/app/instructions.txt +40 -0
  12. package/templates/.claude/skills/openkbs/examples/monitoring-bot/app/settings.json +41 -0
  13. package/templates/.claude/skills/openkbs/examples/monitoring-bot/openkbs.json +3 -0
  14. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/actions.js +141 -0
  15. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/handler.js +32 -0
  16. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/memoryHelpers.js +91 -0
  17. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onCronjob.js +105 -0
  18. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onPublicAPIRequest.js +165 -0
  19. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onRequest.js +2 -0
  20. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onResponse.js +2 -0
  21. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Frontend/contentRender.js +74 -0
  22. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Frontend/contentRender.json +3 -0
  23. package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/auth/index.mjs +228 -0
  24. package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/auth/package.json +7 -0
  25. package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/posts/index.mjs +287 -0
  26. package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/posts/package.json +10 -0
  27. package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/settings.json +4 -0
  28. package/templates/.claude/skills/openkbs/examples/nodejs-demo/openkbs.json +16 -0
  29. package/templates/.claude/skills/openkbs/examples/nodejs-demo/site/index.html +658 -0
  30. package/templates/.claude/skills/openkbs/examples/nodejs-demo/site/settings.json +4 -0
  31. package/templates/.claude/skills/openkbs/patterns/cronjob-batch-processing.md +278 -0
  32. package/templates/.claude/skills/openkbs/patterns/cronjob-monitoring.md +341 -0
  33. package/templates/.claude/skills/openkbs/patterns/file-upload.md +205 -0
  34. package/templates/.claude/skills/openkbs/patterns/image-generation.md +139 -0
  35. package/templates/.claude/skills/openkbs/patterns/memory-system.md +264 -0
  36. package/templates/.claude/skills/openkbs/patterns/public-api-item-proxy.md +254 -0
  37. package/templates/.claude/skills/openkbs/patterns/scheduled-tasks.md +157 -0
  38. package/templates/.claude/skills/openkbs/patterns/telegram-webhook.md +424 -0
  39. package/templates/.claude/skills/openkbs/patterns/telegram.md +222 -0
  40. package/templates/.claude/skills/openkbs/patterns/vectordb-archive.md +231 -0
  41. package/templates/.claude/skills/openkbs/patterns/video-generation.md +145 -0
  42. package/templates/.claude/skills/openkbs/patterns/web-publishing.md +257 -0
  43. package/templates/.claude/skills/openkbs/reference/backend-sdk.md +13 -2
  44. package/templates/.claude/skills/openkbs/reference/elastic-services.md +61 -29
  45. package/templates/platform/README.md +35 -0
  46. package/templates/platform/agents/assistant/app/icon.png +0 -0
  47. package/templates/platform/agents/assistant/app/instructions.txt +13 -0
  48. package/templates/platform/agents/assistant/app/settings.json +13 -0
  49. package/templates/platform/agents/assistant/src/Events/actions.js +50 -0
  50. package/templates/platform/agents/assistant/src/Events/handler.js +54 -0
  51. package/templates/platform/agents/assistant/src/Events/onRequest.js +3 -0
  52. package/templates/platform/agents/assistant/src/Events/onRequest.json +5 -0
  53. package/templates/platform/agents/assistant/src/Events/onResponse.js +3 -0
  54. package/templates/platform/agents/assistant/src/Events/onResponse.json +5 -0
  55. package/templates/platform/agents/assistant/src/Frontend/contentRender.js +27 -0
  56. package/templates/platform/agents/assistant/src/Frontend/contentRender.json +10 -0
  57. package/templates/platform/functions/api/index.mjs +63 -0
  58. package/templates/platform/functions/api/package.json +4 -0
  59. package/templates/platform/openkbs.json +19 -0
  60. package/templates/platform/site/index.html +75 -0
  61. package/version.json +3 -3
  62. package/templates/.claude/skills/openkbs/examples/ai-copywriter-agent/scripts/run_job.js +0 -26
  63. package/templates/.claude/skills/openkbs/examples/ai-copywriter-agent/scripts/utils/agent_client.js +0 -265
@@ -0,0 +1,231 @@
1
+ # VectorDB Archive Pattern
2
+
3
+ Complete working code for archiving items to long-term memory with semantic search.
4
+
5
+ ## Concept
6
+
7
+ - **Priority items** (memory_, agent_) - Fast, auto-injected, limited capacity
8
+ - **Archive items** - Unlimited, semantic search, for long-term storage
9
+
10
+ When priority storage fills up, archive old items to VectorDB.
11
+
12
+ ## actions.js
13
+
14
+ ```javascript
15
+ // Archive items to long-term memory (VectorDB)
16
+ [/<archiveItems>([\s\S]*?)<\/archiveItems>/s, async (match) => {
17
+ try {
18
+ const content = match[1].trim();
19
+ const itemIds = JSON.parse(content);
20
+
21
+ if (!Array.isArray(itemIds) || itemIds.length === 0) {
22
+ throw new Error('Must provide an array of itemIds to archive');
23
+ }
24
+
25
+ const results = [];
26
+ const embeddingModel = 'text-embedding-3-large';
27
+ const embeddingDimension = 3072;
28
+ const timestamp = Date.now();
29
+
30
+ for (const itemId of itemIds) {
31
+ try {
32
+ // 1. Fetch the original item
33
+ const originalItem = await openkbs.getItem(itemId);
34
+ if (!originalItem?.item?.body) {
35
+ results.push({ itemId, status: 'error', error: 'Item not found' });
36
+ continue;
37
+ }
38
+
39
+ const body = originalItem.item.body;
40
+ const originalItemType = itemId.split('_')[0];
41
+
42
+ // 2. Build embedding text based on item type
43
+ let embeddingText = '';
44
+ if (originalItemType === 'memory') {
45
+ embeddingText = `${itemId}: ${typeof body.value === 'string' ? body.value : JSON.stringify(body.value)}`;
46
+ } else if (originalItemType === 'telegram') {
47
+ const date = body.date ? new Date(body.date * 1000).toISOString() : '';
48
+ embeddingText = `[${date}] ${body.from || 'Unknown'}: ${body.text || ''}`;
49
+ } else {
50
+ embeddingText = `${itemId}: ${JSON.stringify(body)}`;
51
+ }
52
+
53
+ // 3. Create embeddings
54
+ const { embeddings, totalTokens } = await openkbs.createEmbeddings(embeddingText, embeddingModel);
55
+
56
+ // 4. Create archive item with timestamp for uniqueness
57
+ const archiveItemId = `archive_${timestamp}_${itemId}`;
58
+ const archiveBody = {
59
+ originalItemId: itemId,
60
+ originalItemType: originalItemType,
61
+ content: body,
62
+ archivedAt: new Date().toISOString()
63
+ };
64
+
65
+ await openkbs.items({
66
+ action: 'createItem',
67
+ itemType: 'archive',
68
+ itemId: archiveItemId,
69
+ attributes: [
70
+ { attrType: 'itemId', attrName: 'itemId', encrypted: false },
71
+ { attrType: 'body', attrName: 'body', encrypted: true }
72
+ ],
73
+ item: { body: await openkbs.encrypt(JSON.stringify(archiveBody)) },
74
+ totalTokens,
75
+ embeddings: embeddings ? embeddings.slice(0, embeddingDimension) : undefined,
76
+ embeddingModel,
77
+ embeddingDimension
78
+ });
79
+
80
+ // 5. Delete original item from priority storage
81
+ await openkbs.deleteItem(itemId);
82
+
83
+ results.push({
84
+ itemId,
85
+ archiveItemId,
86
+ status: 'success',
87
+ tokens: totalTokens
88
+ });
89
+
90
+ } catch (e) {
91
+ results.push({ itemId, status: 'error', error: e.message });
92
+ }
93
+ }
94
+
95
+ const successCount = results.filter(r => r.status === 'success').length;
96
+ const errorCount = results.filter(r => r.status === 'error').length;
97
+
98
+ return {
99
+ type: "ITEMS_ARCHIVED",
100
+ summary: `Archived ${successCount} of ${itemIds.length} items (${errorCount} errors)`,
101
+ results,
102
+ _meta_actions: ["REQUEST_CHAT_MODEL"]
103
+ };
104
+ } catch (e) {
105
+ return {
106
+ type: "ARCHIVE_ERROR",
107
+ error: e.message,
108
+ _meta_actions: ["REQUEST_CHAT_MODEL"]
109
+ };
110
+ }
111
+ }],
112
+
113
+ // Search long-term archive memory (VectorDB semantic search)
114
+ [/<searchArchive>([\s\S]*?)<\/searchArchive>/s, async (match) => {
115
+ try {
116
+ const content = match[1].trim();
117
+ const data = JSON.parse(content);
118
+
119
+ if (!data.query) {
120
+ throw new Error('Must provide a "query" for semantic search');
121
+ }
122
+
123
+ const topK = data.topK || 10;
124
+ const minScore = data.minScore || 0;
125
+
126
+ // Call VectorDB search
127
+ const searchResult = await openkbs.items({
128
+ action: 'searchVectorDBItems',
129
+ queryText: data.query,
130
+ topK: topK,
131
+ minScore: minScore
132
+ });
133
+
134
+ // Format and decrypt results
135
+ const formattedResults = [];
136
+
137
+ for (const item of (searchResult?.items || [])) {
138
+ try {
139
+ let parsed = null;
140
+ if (item.body) {
141
+ const decryptedBody = await openkbs.decrypt(item.body);
142
+ parsed = JSON.parse(decryptedBody);
143
+ }
144
+
145
+ formattedResults.push({
146
+ archiveItemId: item.itemId,
147
+ originalItemId: parsed?.originalItemId,
148
+ originalItemType: parsed?.originalItemType,
149
+ content: parsed?.content,
150
+ archivedAt: parsed?.archivedAt,
151
+ score: item.score
152
+ });
153
+ } catch (e) {
154
+ formattedResults.push({
155
+ archiveItemId: item.itemId,
156
+ score: item.score,
157
+ error: 'Failed to decrypt: ' + e.message
158
+ });
159
+ }
160
+ }
161
+
162
+ return {
163
+ type: "ARCHIVE_SEARCH_RESULTS",
164
+ query: data.query,
165
+ count: formattedResults.length,
166
+ results: formattedResults,
167
+ _meta_actions: ["REQUEST_CHAT_MODEL"]
168
+ };
169
+ } catch (e) {
170
+ return {
171
+ type: "ARCHIVE_SEARCH_ERROR",
172
+ error: e.message,
173
+ _meta_actions: ["REQUEST_CHAT_MODEL"]
174
+ };
175
+ }
176
+ }]
177
+ ```
178
+
179
+ ## settings.json Configuration
180
+
181
+ ```json
182
+ {
183
+ "embeddingModel": "text-embedding-3-large",
184
+ "embeddingDimension": 3072,
185
+ "searchEngine": "VectorDB",
186
+ "itemTypes": {
187
+ "archive": {
188
+ "attributes": [
189
+ { "attrName": "itemId", "attrType": "itemId", "encrypted": false },
190
+ { "attrName": "body", "attrType": "body", "encrypted": true }
191
+ ]
192
+ }
193
+ },
194
+ "options": {
195
+ "vectorDBMaxTokens": 25000,
196
+ "vectorDBTopK": 30,
197
+ "vectorDBMinScore": 90
198
+ }
199
+ }
200
+ ```
201
+
202
+ ## instructions.txt (LLM prompt)
203
+
204
+ ```
205
+ Long-term Memory:
206
+
207
+ Archive items from priority storage:
208
+ <archiveItems>["memory_old_item1", "memory_old_item2"]</archiveItems>
209
+
210
+ Search archived memories semantically:
211
+ <searchArchive>{"query": "what did user say about project X", "topK": 10}</searchArchive>
212
+
213
+ Use archive when:
214
+ - Priority storage is filling up
215
+ - You need to remember things long-term
216
+ - You want semantic search over past conversations
217
+ ```
218
+
219
+ ## Key Points
220
+
221
+ 1. **Two-tier storage**:
222
+ - Priority items: Fast, limited, auto-injected into context
223
+ - Archive items: Unlimited, semantic search, manual retrieval
224
+
225
+ 2. **Archive flow**: Fetch → Create embeddings → Store in VectorDB → Delete original
226
+
227
+ 3. **Embedding model** must match settings.json configuration
228
+
229
+ 4. **Encrypted storage** - Archive body is encrypted, decrypted on retrieval
230
+
231
+ 5. **Score-based search** - Results sorted by semantic similarity score
@@ -0,0 +1,145 @@
1
+ # Video Generation Pattern
2
+
3
+ Complete working code for AI video generation with async polling.
4
+
5
+ ## actions.js
6
+
7
+ ```javascript
8
+ // Create video - returns pending status, requires polling
9
+ [/<createAIVideo>([\s\S]*?)<\/createAIVideo>/s, async (match) => {
10
+ try {
11
+ const data = JSON.parse(match[1].trim());
12
+
13
+ const videoModel = data.model || 'sora-2';
14
+ const prompt = data.prompt;
15
+
16
+ // Validate seconds (4, 8, or 12 only)
17
+ const validSeconds = [4, 8, 12];
18
+ const seconds = validSeconds.includes(data.seconds) ? data.seconds :
19
+ validSeconds.reduce((prev, curr) =>
20
+ Math.abs(curr - data.seconds) < Math.abs(prev - data.seconds) ? curr : prev);
21
+
22
+ const params = {
23
+ video_model: videoModel,
24
+ seconds: seconds
25
+ };
26
+
27
+ // Reference image OR size (not both)
28
+ if (data.input_reference_url) {
29
+ params.input_reference_url = data.input_reference_url;
30
+ } else {
31
+ const validSizes = ['720x1280', '1280x720'];
32
+ params.size = validSizes.includes(data.size) ? data.size : '1280x720';
33
+ }
34
+
35
+ const videoData = await openkbs.generateVideo(prompt, params);
36
+
37
+ // Video generation is async - check status
38
+ if (videoData?.[0]?.status === 'pending') {
39
+ return {
40
+ type: 'VIDEO_PENDING',
41
+ data: {
42
+ videoId: videoData[0].video_id,
43
+ message: 'Video generation in progress. Use continueVideoPolling to check status.'
44
+ },
45
+ ...meta
46
+ };
47
+ }
48
+
49
+ // Immediate completion (rare)
50
+ if (videoData?.[0]?.video_url) {
51
+ return {
52
+ type: 'CHAT_VIDEO',
53
+ data: { videoUrl: videoData[0].video_url },
54
+ ...meta
55
+ };
56
+ }
57
+
58
+ return { error: 'Video generation failed - no response', ...meta };
59
+ } catch (error) {
60
+ return { error: error.message || 'Video creation failed', ...meta };
61
+ }
62
+ }],
63
+
64
+ // Poll for video completion
65
+ [/<continueVideoPolling>([\s\S]*?)<\/continueVideoPolling>/s, async (match) => {
66
+ try {
67
+ const data = JSON.parse(match[1].trim());
68
+ const videoId = data.videoId;
69
+
70
+ const videoData = await openkbs.checkVideoStatus(videoId);
71
+
72
+ if (videoData?.[0]?.status === 'completed' && videoData[0].video_url) {
73
+ return {
74
+ type: 'CHAT_VIDEO',
75
+ data: { videoUrl: videoData[0].video_url },
76
+ ...meta
77
+ };
78
+ }
79
+
80
+ if (videoData?.[0]?.status === 'pending') {
81
+ return {
82
+ type: 'VIDEO_PENDING',
83
+ data: {
84
+ videoId: videoId,
85
+ message: 'Video still generating. Continue polling.'
86
+ },
87
+ ...meta
88
+ };
89
+ }
90
+
91
+ if (videoData?.[0]?.status === 'failed') {
92
+ return { error: 'Video generation failed', ...meta };
93
+ }
94
+
95
+ return { error: 'Unable to get video status', ...meta };
96
+ } catch (error) {
97
+ return { error: error.message || 'Failed to check video status', ...meta };
98
+ }
99
+ }]
100
+ ```
101
+
102
+ ## contentRender.js
103
+
104
+ ```javascript
105
+ // In isVisualResult function
106
+ const isVisualResult = (r) => {
107
+ return (r?.type === 'CHAT_VIDEO' && r?.data?.videoUrl) ||
108
+ r?.type === 'VIDEO_PENDING';
109
+ };
110
+
111
+ // In renderVisualResults function
112
+ if (item?.type === 'CHAT_VIDEO' && item?.data?.videoUrl) {
113
+ return (
114
+ <div key={`vid-${idx}`} style={{ flex: '1 1 100%' }}>
115
+ <video
116
+ src={item.data.videoUrl}
117
+ controls
118
+ style={{ width: '100%', maxWidth: 600, borderRadius: 8 }}
119
+ />
120
+ </div>
121
+ );
122
+ }
123
+ ```
124
+
125
+ ## instructions.txt (LLM prompt)
126
+
127
+ ```
128
+ To generate a video:
129
+ <createAIVideo>{"prompt": "scene description", "model": "sora-2", "seconds": 8, "size": "1280x720"}</createAIVideo>
130
+
131
+ Video generation takes time. When you receive VIDEO_PENDING, poll with:
132
+ <continueVideoPolling>{"videoId": "the-video-id"}</continueVideoPolling>
133
+
134
+ Models: sora-2 (standard), sora-2-pro (higher quality)
135
+ Duration: 4, 8, or 12 seconds
136
+ Size: 1280x720 (landscape), 720x1280 (portrait)
137
+ Optional: input_reference_url for image-to-video
138
+ ```
139
+
140
+ ## Key Points
141
+
142
+ 1. **Async by nature** - Video generation returns `VIDEO_PENDING`, not immediate result
143
+ 2. **Polling required** - LLM must call `continueVideoPolling` to check completion
144
+ 3. **CHAT_VIDEO is core magic** - Renders as video player automatically
145
+ 4. **Two commands** - Create and poll are separate for clean flow
@@ -0,0 +1,257 @@
1
+ # Web Publishing Pattern
2
+
3
+ Complete working code for publishing HTML pages to KB storage.
4
+
5
+ ## actions.js
6
+
7
+ ```javascript
8
+ // Publish HTML webpage
9
+ [/<publishWebPage>([\s\S]*?)<\/publishWebPage>/s, async (match) => {
10
+ try {
11
+ let htmlContent = match[1].trim();
12
+
13
+ // Extract HTML if wrapped in markdown code block
14
+ const codeBlockMatch = htmlContent.match(/```html\s*([\s\S]*?)\s*```/);
15
+ if (codeBlockMatch) {
16
+ htmlContent = codeBlockMatch[1].trim();
17
+ }
18
+
19
+ // Validate HTML content
20
+ if (!htmlContent || !htmlContent.includes('<html')) {
21
+ return {
22
+ type: "PUBLISH_ERROR",
23
+ error: "No valid HTML content found. Must include <html> tag.",
24
+ _meta_actions: ["REQUEST_CHAT_MODEL"]
25
+ };
26
+ }
27
+
28
+ // Fix charset to UTF-8
29
+ htmlContent = htmlContent.replace(
30
+ /<meta\s+charset=["']?[^"'>]*["']?\s*\/?>/gi,
31
+ '<meta charset="UTF-8">'
32
+ );
33
+
34
+ // Extract title for filename
35
+ const titleMatch = htmlContent.match(/<title>(.*?)<\/title>/i);
36
+ const title = titleMatch ? titleMatch[1].trim() : 'page';
37
+
38
+ // Generate filename from title
39
+ const timestamp = Date.now();
40
+ const baseName = title
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9]+/g, '_')
43
+ .replace(/^_+|_+$/g, '') || 'page';
44
+ const filename = `${baseName}_${timestamp}.html`;
45
+
46
+ // Upload embedded images (optional - for CDN URLs)
47
+ const cdnUrlRegex = /https:\/\/cdn\.example\.com\/[^\s"'<>]+/g;
48
+ const cdnUrls = [...new Set(htmlContent.match(cdnUrlRegex) || [])];
49
+
50
+ for (const cdnUrl of cdnUrls) {
51
+ try {
52
+ // Download and re-upload to permanent storage
53
+ const fileResponse = await axios.get(cdnUrl, { responseType: 'arraybuffer' });
54
+ const fileName = cdnUrl.split('/').pop();
55
+ const fileType = fileResponse.headers['content-type'] || 'image/jpeg';
56
+
57
+ const presigned = await openkbs.kb({
58
+ action: 'createPresignedURL',
59
+ namespace: 'files',
60
+ fileName: fileName,
61
+ fileType: fileType,
62
+ presignedOperation: 'putObject'
63
+ });
64
+
65
+ await axios.put(presigned, fileResponse.data, {
66
+ headers: { 'Content-Type': fileType, 'Content-Length': fileResponse.data.length }
67
+ });
68
+
69
+ const permanentUrl = `https://yourdomain.com/media/${fileName}`;
70
+ htmlContent = htmlContent.split(cdnUrl).join(permanentUrl);
71
+ } catch (err) {
72
+ // Continue if image upload fails
73
+ }
74
+ }
75
+
76
+ // Get presigned URL for HTML upload
77
+ const presignedUrl = await openkbs.kb({
78
+ action: 'createPresignedURL',
79
+ namespace: 'files',
80
+ fileName: filename,
81
+ fileType: 'text/html',
82
+ presignedOperation: 'putObject'
83
+ });
84
+
85
+ // Upload HTML as UTF-8 Buffer
86
+ const htmlBuffer = Buffer.from(htmlContent, 'utf8');
87
+ await axios.put(presignedUrl, htmlBuffer, {
88
+ headers: {
89
+ 'Content-Type': 'text/html',
90
+ 'Content-Length': htmlBuffer.length
91
+ }
92
+ });
93
+
94
+ const publicUrl = `https://yourdomain.com/media/${filename}`;
95
+
96
+ return {
97
+ type: "WEBSITE_PUBLISHED",
98
+ filename: filename,
99
+ url: publicUrl,
100
+ size: htmlBuffer.length,
101
+ title: title,
102
+ message: `Website published at ${publicUrl}`,
103
+ _meta_actions: ["REQUEST_CHAT_MODEL"]
104
+ };
105
+
106
+ } catch (e) {
107
+ return {
108
+ type: "PUBLISH_ERROR",
109
+ error: e.message,
110
+ _meta_actions: ["REQUEST_CHAT_MODEL"]
111
+ };
112
+ }
113
+ }]
114
+ ```
115
+
116
+ ## Helper: Web Publishing Guide
117
+
118
+ Create `webPublishingGuide.js`:
119
+
120
+ ```javascript
121
+ export const webPublishingGuide = `
122
+ # Web Publishing Guide
123
+
124
+ ## HTML Structure
125
+ - Always include <!DOCTYPE html>
126
+ - Use <meta charset="UTF-8">
127
+ - Set viewport for mobile: <meta name="viewport" content="width=device-width, initial-scale=1.0">
128
+
129
+ ## Responsive Design
130
+ - Use flexbox or CSS grid
131
+ - Mobile-first approach
132
+ - Test at 320px, 768px, 1024px widths
133
+
134
+ ## Images
135
+ - Use CDN URLs that will be auto-converted to permanent storage
136
+ - Add alt text for accessibility
137
+ - Use lazy loading: loading="lazy"
138
+
139
+ ## SEO
140
+ - Unique, descriptive <title>
141
+ - Meta description
142
+ - Semantic HTML (header, main, footer, article)
143
+ `;
144
+
145
+ // Command to read guide
146
+ [/<getWebPublishingGuide\s*\/>/s, async () => {
147
+ return {
148
+ type: 'WEB_PUBLISHING_GUIDE',
149
+ content: webPublishingGuide,
150
+ ...meta
151
+ };
152
+ }]
153
+ ```
154
+
155
+ ## Example: Complete Landing Page
156
+
157
+ ```html
158
+ <!DOCTYPE html>
159
+ <html lang="en">
160
+ <head>
161
+ <meta charset="UTF-8">
162
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
163
+ <title>Product Launch</title>
164
+ <style>
165
+ * { margin: 0; padding: 0; box-sizing: border-box; }
166
+ body {
167
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
168
+ line-height: 1.6;
169
+ color: #333;
170
+ }
171
+ .hero {
172
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
173
+ color: white;
174
+ padding: 80px 20px;
175
+ text-align: center;
176
+ }
177
+ .hero h1 { font-size: 2.5rem; margin-bottom: 1rem; }
178
+ .hero p { font-size: 1.2rem; opacity: 0.9; max-width: 600px; margin: 0 auto; }
179
+ .cta {
180
+ display: inline-block;
181
+ margin-top: 2rem;
182
+ padding: 15px 40px;
183
+ background: white;
184
+ color: #667eea;
185
+ text-decoration: none;
186
+ border-radius: 30px;
187
+ font-weight: 600;
188
+ }
189
+ .features {
190
+ display: grid;
191
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
192
+ gap: 30px;
193
+ padding: 60px 20px;
194
+ max-width: 1200px;
195
+ margin: 0 auto;
196
+ }
197
+ .feature {
198
+ text-align: center;
199
+ padding: 20px;
200
+ }
201
+ .feature h3 { margin-bottom: 0.5rem; }
202
+ </style>
203
+ </head>
204
+ <body>
205
+ <section class="hero">
206
+ <h1>Your Product Name</h1>
207
+ <p>A compelling description of what makes your product amazing.</p>
208
+ <a href="#" class="cta">Get Started</a>
209
+ </section>
210
+ <section class="features">
211
+ <div class="feature">
212
+ <h3>Feature One</h3>
213
+ <p>Description of the first key feature.</p>
214
+ </div>
215
+ <div class="feature">
216
+ <h3>Feature Two</h3>
217
+ <p>Description of the second key feature.</p>
218
+ </div>
219
+ <div class="feature">
220
+ <h3>Feature Three</h3>
221
+ <p>Description of the third key feature.</p>
222
+ </div>
223
+ </section>
224
+ </body>
225
+ </html>
226
+ ```
227
+
228
+ ## instructions.txt (LLM prompt)
229
+
230
+ ```
231
+ Web Publishing:
232
+
233
+ Publish an HTML page:
234
+ <publishWebPage>
235
+ <!DOCTYPE html>
236
+ <html lang="en">
237
+ ...full HTML content...
238
+ </html>
239
+ </publishWebPage>
240
+
241
+ Get publishing guidelines:
242
+ <getWebPublishingGuide/>
243
+
244
+ Best practices:
245
+ - Always include DOCTYPE and charset
246
+ - Use inline CSS (no external stylesheets)
247
+ - Images from CDN are auto-converted to permanent URLs
248
+ - Title becomes filename (e.g., "My Page" → my_page_1234567890.html)
249
+ ```
250
+
251
+ ## Key Points
252
+
253
+ 1. **Self-contained HTML**: Include all CSS inline, no external dependencies
254
+ 2. **UTF-8 encoding**: Always set charset, upload as Buffer
255
+ 3. **Auto-filename**: Generated from `<title>` with timestamp
256
+ 4. **Image processing**: CDN URLs auto-converted to permanent storage
257
+ 5. **CDN delivery**: Pages served via CloudFront at `/media/`
@@ -357,8 +357,9 @@ await axios.put(presignedUrl, fileBuffer, {
357
357
  }
358
358
  });
359
359
 
360
- // Public URL pattern
361
- const publicUrl = `https://web.file.vpc1.us/files/${openkbs.kbId}/${fileName}`;
360
+ // Public URL pattern (varies by deployment)
361
+ // For whitelabel: https://yourdomain.file.vpc1.us/files/kbId/filename
362
+ // Generic: https://file.openkbs.com/files/kbId/filename
362
363
  ```
363
364
 
364
365
  ## VectorDB (Semantic Search)
@@ -418,6 +419,16 @@ const rates = await openkbs.getExchangeRates('USD');
418
419
  // Returns: { EUR: 0.92, GBP: 0.79, ... }
419
420
  ```
420
421
 
422
+ ## Global Objects
423
+
424
+ These are available globally in all backend handlers (no import needed):
425
+
426
+ ```javascript
427
+ openkbs // OpenKBS SDK (this document)
428
+ axios // HTTP client (axios.get, axios.post, etc.)
429
+ crypto // Node.js crypto module
430
+ ```
431
+
421
432
  ## Properties
422
433
 
423
434
  ```javascript