@vertesia/tools-sdk 0.80.0-dev-20251118 → 0.80.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.
Files changed (61) hide show
  1. package/README.md +122 -0
  2. package/lib/cjs/InteractionCollection.js +118 -0
  3. package/lib/cjs/InteractionCollection.js.map +1 -1
  4. package/lib/cjs/SkillCollection.js +318 -0
  5. package/lib/cjs/SkillCollection.js.map +1 -0
  6. package/lib/cjs/ToolCollection.js +98 -0
  7. package/lib/cjs/ToolCollection.js.map +1 -1
  8. package/lib/cjs/copy-assets.js +84 -0
  9. package/lib/cjs/copy-assets.js.map +1 -0
  10. package/lib/cjs/index.js +6 -1
  11. package/lib/cjs/index.js.map +1 -1
  12. package/lib/cjs/server.js +327 -0
  13. package/lib/cjs/server.js.map +1 -0
  14. package/lib/cjs/site/styles.js +621 -0
  15. package/lib/cjs/site/styles.js.map +1 -0
  16. package/lib/cjs/site/templates.js +932 -0
  17. package/lib/cjs/site/templates.js.map +1 -0
  18. package/lib/esm/InteractionCollection.js +83 -0
  19. package/lib/esm/InteractionCollection.js.map +1 -1
  20. package/lib/esm/SkillCollection.js +311 -0
  21. package/lib/esm/SkillCollection.js.map +1 -0
  22. package/lib/esm/ToolCollection.js +64 -0
  23. package/lib/esm/ToolCollection.js.map +1 -1
  24. package/lib/esm/copy-assets.js +81 -0
  25. package/lib/esm/copy-assets.js.map +1 -0
  26. package/lib/esm/index.js +4 -0
  27. package/lib/esm/index.js.map +1 -1
  28. package/lib/esm/server.js +323 -0
  29. package/lib/esm/server.js.map +1 -0
  30. package/lib/esm/site/styles.js +618 -0
  31. package/lib/esm/site/styles.js.map +1 -0
  32. package/lib/esm/site/templates.js +920 -0
  33. package/lib/esm/site/templates.js.map +1 -0
  34. package/lib/types/InteractionCollection.d.ts +29 -0
  35. package/lib/types/InteractionCollection.d.ts.map +1 -1
  36. package/lib/types/SkillCollection.d.ts +111 -0
  37. package/lib/types/SkillCollection.d.ts.map +1 -0
  38. package/lib/types/ToolCollection.d.ts +18 -0
  39. package/lib/types/ToolCollection.d.ts.map +1 -1
  40. package/lib/types/copy-assets.d.ts +14 -0
  41. package/lib/types/copy-assets.d.ts.map +1 -0
  42. package/lib/types/index.d.ts +4 -0
  43. package/lib/types/index.d.ts.map +1 -1
  44. package/lib/types/server.d.ts +72 -0
  45. package/lib/types/server.d.ts.map +1 -0
  46. package/lib/types/site/styles.d.ts +5 -0
  47. package/lib/types/site/styles.d.ts.map +1 -0
  48. package/lib/types/site/templates.d.ts +54 -0
  49. package/lib/types/site/templates.d.ts.map +1 -0
  50. package/lib/types/types.d.ts +152 -0
  51. package/lib/types/types.d.ts.map +1 -1
  52. package/package.json +55 -42
  53. package/src/InteractionCollection.ts +90 -0
  54. package/src/SkillCollection.ts +389 -0
  55. package/src/ToolCollection.ts +68 -0
  56. package/src/copy-assets.ts +104 -0
  57. package/src/index.ts +4 -0
  58. package/src/server.ts +444 -0
  59. package/src/site/styles.ts +617 -0
  60. package/src/site/templates.ts +956 -0
  61. package/src/types.ts +162 -0
@@ -0,0 +1,956 @@
1
+ import type { InteractionCollection } from "../InteractionCollection.js";
2
+ import type { SkillCollection } from "../SkillCollection.js";
3
+ import type { ToolCollection } from "../ToolCollection.js";
4
+ import type { ICollection, SkillDefinition, Tool } from "../types.js";
5
+ import { baseStyles } from "./styles.js";
6
+
7
+ type MCPProviderMeta = {
8
+ name: string;
9
+ description?: string;
10
+ };
11
+
12
+ /**
13
+ * Default icon SVG for collections without a custom icon
14
+ */
15
+ const defaultIcon = /*html*/`
16
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
17
+ <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
18
+ </svg>`;
19
+
20
+ const skillIcon = /*html*/`
21
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
22
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
23
+ </svg>`;
24
+
25
+ /**
26
+ * Extended styles for detail pages
27
+ */
28
+ const detailStyles = /*css*/`
29
+ ${baseStyles}
30
+
31
+ .nav {
32
+ margin-bottom: 1.5rem;
33
+ }
34
+
35
+ .nav a {
36
+ color: #6b7280;
37
+ text-decoration: none;
38
+ display: inline-flex;
39
+ align-items: center;
40
+ gap: 0.5rem;
41
+ font-size: 0.875rem;
42
+ }
43
+
44
+ .nav a:hover {
45
+ color: #2563eb;
46
+ }
47
+
48
+ .nav svg {
49
+ width: 16px;
50
+ height: 16px;
51
+ }
52
+
53
+ .detail-card {
54
+ background: white;
55
+ border-radius: 12px;
56
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
57
+ margin-bottom: 1.5rem;
58
+ overflow: hidden;
59
+ }
60
+
61
+ .detail-header {
62
+ padding: 1.5rem;
63
+ border-bottom: 1px solid #e5e7eb;
64
+ display: flex;
65
+ justify-content: space-between;
66
+ align-items: flex-start;
67
+ }
68
+
69
+ .detail-title {
70
+ font-size: 1.25rem;
71
+ font-weight: 600;
72
+ color: #111827;
73
+ margin: 0 0 0.25rem 0;
74
+ font-family: ui-monospace, monospace;
75
+ }
76
+
77
+ .detail-desc {
78
+ color: #6b7280;
79
+ font-size: 0.95rem;
80
+ margin: 0;
81
+ }
82
+
83
+ .detail-badges {
84
+ display: flex;
85
+ gap: 0.5rem;
86
+ flex-wrap: wrap;
87
+ }
88
+
89
+ .detail-body {
90
+ padding: 1.5rem;
91
+ }
92
+
93
+ .detail-section {
94
+ margin-bottom: 1.5rem;
95
+ }
96
+
97
+ .detail-section:last-child {
98
+ margin-bottom: 0;
99
+ }
100
+
101
+ .detail-section-title {
102
+ font-size: 0.75rem;
103
+ font-weight: 600;
104
+ text-transform: uppercase;
105
+ letter-spacing: 0.05em;
106
+ color: #9ca3af;
107
+ margin: 0 0 0.75rem 0;
108
+ }
109
+
110
+ .schema-block {
111
+ background: #1f2937;
112
+ color: #e5e7eb;
113
+ padding: 1rem;
114
+ border-radius: 8px;
115
+ font-family: ui-monospace, monospace;
116
+ font-size: 0.8rem;
117
+ overflow-x: auto;
118
+ white-space: pre;
119
+ line-height: 1.5;
120
+ }
121
+
122
+ .schema-block .key { color: #93c5fd; }
123
+ .schema-block .string { color: #86efac; }
124
+ .schema-block .number { color: #fcd34d; }
125
+ .schema-block .boolean { color: #f9a8d4; }
126
+ .schema-block .null { color: #9ca3af; }
127
+
128
+ .info-grid {
129
+ display: grid;
130
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
131
+ gap: 1rem;
132
+ }
133
+
134
+ .info-item {
135
+ background: #f9fafb;
136
+ padding: 1rem;
137
+ border-radius: 8px;
138
+ }
139
+
140
+ .info-label {
141
+ font-size: 0.75rem;
142
+ font-weight: 600;
143
+ text-transform: uppercase;
144
+ letter-spacing: 0.05em;
145
+ color: #9ca3af;
146
+ margin-bottom: 0.25rem;
147
+ }
148
+
149
+ .info-value {
150
+ font-size: 0.95rem;
151
+ color: #111827;
152
+ }
153
+
154
+ .info-value code {
155
+ background: #e5e7eb;
156
+ padding: 0.125rem 0.375rem;
157
+ border-radius: 4px;
158
+ font-family: ui-monospace, monospace;
159
+ font-size: 0.85rem;
160
+ }
161
+
162
+ .package-list {
163
+ display: flex;
164
+ flex-wrap: wrap;
165
+ gap: 0.5rem;
166
+ }
167
+
168
+ .package-tag {
169
+ background: #dbeafe;
170
+ color: #1e40af;
171
+ padding: 0.25rem 0.75rem;
172
+ border-radius: 9999px;
173
+ font-size: 0.8rem;
174
+ font-family: ui-monospace, monospace;
175
+ }
176
+
177
+ .script-list {
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: 0.5rem;
181
+ }
182
+
183
+ .script-item {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 0.75rem;
187
+ padding: 0.75rem 1rem;
188
+ background: #f9fafb;
189
+ border-radius: 8px;
190
+ }
191
+
192
+ .script-icon {
193
+ width: 20px;
194
+ height: 20px;
195
+ color: #6b7280;
196
+ }
197
+
198
+ .script-name {
199
+ font-family: ui-monospace, monospace;
200
+ font-size: 0.9rem;
201
+ color: #111827;
202
+ }
203
+
204
+ .keyword-list {
205
+ display: flex;
206
+ flex-wrap: wrap;
207
+ gap: 0.5rem;
208
+ }
209
+
210
+ .keyword-tag {
211
+ background: #fef3c7;
212
+ color: #92400e;
213
+ padding: 0.25rem 0.75rem;
214
+ border-radius: 9999px;
215
+ font-size: 0.8rem;
216
+ }
217
+
218
+ .instructions-preview {
219
+ background: #f9fafb;
220
+ border: 1px solid #e5e7eb;
221
+ border-radius: 8px;
222
+ padding: 1rem;
223
+ max-height: 300px;
224
+ overflow-y: auto;
225
+ font-size: 0.875rem;
226
+ line-height: 1.6;
227
+ color: #374151;
228
+ white-space: pre-wrap;
229
+ }
230
+
231
+ .endpoint-box {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 0.75rem;
235
+ background: #f3f4f6;
236
+ padding: 0.75rem 1rem;
237
+ border-radius: 8px;
238
+ margin-top: 0.5rem;
239
+ }
240
+
241
+ .endpoint-box code {
242
+ flex: 1;
243
+ font-family: ui-monospace, monospace;
244
+ font-size: 0.875rem;
245
+ color: #1f2937;
246
+ }
247
+
248
+ .copy-btn {
249
+ background: #e5e7eb;
250
+ border: none;
251
+ padding: 0.5rem;
252
+ border-radius: 6px;
253
+ cursor: pointer;
254
+ color: #6b7280;
255
+ transition: all 0.15s;
256
+ }
257
+
258
+ .copy-btn:hover {
259
+ background: #d1d5db;
260
+ color: #374151;
261
+ }
262
+
263
+ .copy-btn svg {
264
+ width: 16px;
265
+ height: 16px;
266
+ }
267
+
268
+ .empty-state {
269
+ text-align: center;
270
+ padding: 3rem;
271
+ color: #9ca3af;
272
+ }
273
+
274
+ .tool-type-badge {
275
+ background: #6366f1;
276
+ color: white;
277
+ }
278
+
279
+ .skill-type-badge {
280
+ background: #10b981;
281
+ color: white;
282
+ }
283
+
284
+ @media (prefers-color-scheme: dark) {
285
+ .nav a {
286
+ color: #9ca3af;
287
+ }
288
+
289
+ .nav a:hover {
290
+ color: #60a5fa;
291
+ }
292
+
293
+ .detail-card {
294
+ background: rgba(15, 23, 42, 0.96);
295
+ box-shadow:
296
+ 0 18px 40px rgba(15, 23, 42, 0.9),
297
+ 0 0 0 1px rgba(15, 23, 42, 0.9);
298
+ }
299
+
300
+ .detail-header {
301
+ border-bottom-color: rgba(55, 65, 81, 0.9);
302
+ }
303
+
304
+ .detail-title {
305
+ color: #e5e7eb;
306
+ }
307
+
308
+ .detail-desc {
309
+ color: #9ca3af;
310
+ }
311
+
312
+ .detail-section-title {
313
+ color: #9ca3af;
314
+ }
315
+
316
+ .info-item {
317
+ background: rgba(15, 23, 42, 0.9);
318
+ }
319
+
320
+ .info-value {
321
+ color: #e5e7eb;
322
+ }
323
+
324
+ .info-value code {
325
+ background: rgba(31, 41, 55, 0.9);
326
+ color: #e5e7eb;
327
+ }
328
+
329
+ .script-item {
330
+ background: rgba(15, 23, 42, 0.9);
331
+ }
332
+
333
+ .script-name {
334
+ color: #e5e7eb;
335
+ }
336
+
337
+ .keyword-tag {
338
+ background: rgba(250, 204, 21, 0.12);
339
+ color: #facc15;
340
+ }
341
+
342
+ .instructions-preview {
343
+ background: rgba(15, 23, 42, 0.9);
344
+ border-color: rgba(55, 65, 81, 0.9);
345
+ color: #e5e7eb;
346
+ }
347
+
348
+ .endpoint-box {
349
+ background: rgba(31, 41, 55, 0.95);
350
+ }
351
+
352
+ .endpoint-box code {
353
+ color: #e5e7eb;
354
+ }
355
+
356
+ .copy-btn {
357
+ background: rgba(55, 65, 81, 0.95);
358
+ color: #e5e7eb;
359
+ }
360
+
361
+ .copy-btn:hover {
362
+ background: rgba(75, 85, 99, 0.98);
363
+ color: #f9fafb;
364
+ }
365
+
366
+ .empty-state {
367
+ color: #9ca3af;
368
+ }
369
+ }
370
+ `;
371
+
372
+ /**
373
+ * Syntax highlight JSON
374
+ */
375
+ function highlightJson(obj: unknown): string {
376
+ const json = JSON.stringify(obj, null, 2);
377
+ return json
378
+ .replace(/"([^"]+)":/g, '<span class="key">"$1"</span>:')
379
+ .replace(/: "([^"]*)"([,\n])/g, ': <span class="string">"$1"</span>$2')
380
+ .replace(/: (\d+)([,\n])/g, ': <span class="number">$1</span>$2')
381
+ .replace(/: (true|false)([,\n])/g, ': <span class="boolean">$1</span>$2')
382
+ .replace(/: (null)([,\n])/g, ': <span class="null">$1</span>$2');
383
+ }
384
+
385
+ /**
386
+ * Back navigation arrow
387
+ */
388
+ const backArrow = /*html*/`
389
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
390
+ <path d="M19 12H5M12 19l-7-7 7-7"/>
391
+ </svg>`;
392
+
393
+ /**
394
+ * Copy icon
395
+ */
396
+ const copyIcon = /*html*/`
397
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
398
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
399
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
400
+ </svg>`;
401
+
402
+ /**
403
+ * File icon
404
+ */
405
+ const fileIcon = /*html*/`
406
+ <svg class="script-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
407
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
408
+ <polyline points="14 2 14 8 20 8"></polyline>
409
+ </svg>`;
410
+
411
+ /**
412
+ * Render a collection card for the index page
413
+ */
414
+ export function collectionCard(collection: ICollection, pathPrefix: string, meta?: string): string {
415
+ return /*html*/`
416
+ <a class="card" href="/${pathPrefix}/${collection.name}" data-collection-type="${pathPrefix}" data-collection-name="${collection.name}">
417
+ <div class="card-icon">${collection.icon || defaultIcon}</div>
418
+ <div class="card-title">${collection.title || collection.name}</div>
419
+ <div class="card-desc">${collection.description || ''}</div>
420
+ ${meta ? `<div class="card-meta">${meta}</div>` : ''}
421
+ </a>`;
422
+ }
423
+
424
+ /**
425
+ * Render a tool card (simple version for lists)
426
+ */
427
+ export function toolCard(tool: Tool<Record<string, unknown>>): string {
428
+ return /*html*/`
429
+ <div class="item-card">
430
+ <div class="item-name">${tool.name}</div>
431
+ <div class="item-desc">${tool.description || ''}</div>
432
+ ${tool.input_schema ? /*html*/`
433
+ <div class="item-schema">${JSON.stringify(tool.input_schema, null, 2)}</div>
434
+ ` : ''}
435
+ </div>`;
436
+ }
437
+
438
+ /**
439
+ * Render an MCP provider card
440
+ */
441
+ export function mcpProviderCard(provider: MCPProviderMeta): string {
442
+ return /*html*/`
443
+ <a class="card" href="/api/mcp/${provider.name}">
444
+ <div class="card-title">${provider.name}</div>
445
+ <div class="card-desc">${provider.description || ''}</div>
446
+ </a>`;
447
+ }
448
+
449
+ /**
450
+ * Render a detailed tool card
451
+ */
452
+ export function toolDetailCard(tool: Tool<Record<string, unknown>>, collectionName: string): string {
453
+ const schema = tool.input_schema;
454
+ const properties = (schema as Record<string, unknown>)?.properties as Record<string, unknown> | undefined;
455
+ const required = (schema as Record<string, unknown>)?.required as string[] | undefined;
456
+
457
+ return /*html*/`
458
+ <div class="detail-card">
459
+ <div class="detail-header">
460
+ <div>
461
+ <h3 class="detail-title">${tool.name}</h3>
462
+ <p class="detail-desc">${tool.description || 'No description'}</p>
463
+ </div>
464
+ <div class="detail-badges">
465
+ <span class="badge tool-type-badge">Tool</span>
466
+ </div>
467
+ </div>
468
+ <div class="detail-body">
469
+ <div class="detail-section">
470
+ <h4 class="detail-section-title">Endpoint</h4>
471
+ <div class="endpoint-box">
472
+ <code>POST /api/tools/${collectionName}</code>
473
+ <button class="copy-btn" onclick="navigator.clipboard.writeText('/api/tools/${collectionName}')" title="Copy">
474
+ ${copyIcon}
475
+ </button>
476
+ </div>
477
+ </div>
478
+
479
+ ${schema ? /*html*/`
480
+ <div class="detail-section">
481
+ <h4 class="detail-section-title">Input Schema</h4>
482
+ ${properties ? /*html*/`
483
+ <div class="info-grid" style="margin-bottom: 1rem;">
484
+ ${Object.entries(properties).map(([key, value]) => {
485
+ const prop = value as Record<string, unknown>;
486
+ const isRequired = required?.includes(key);
487
+ return /*html*/`
488
+ <div class="info-item">
489
+ <div class="info-label">${key}${isRequired ? ' *' : ''}</div>
490
+ <div class="info-value">
491
+ <code>${prop.type || 'any'}</code>
492
+ ${prop.description ? `<br><span style="color: #6b7280; font-size: 0.85rem;">${prop.description}</span>` : ''}
493
+ </div>
494
+ </div>`;
495
+ }).join('')}
496
+ </div>
497
+ ` : ''}
498
+ <details>
499
+ <summary style="cursor: pointer; color: #6b7280; font-size: 0.85rem;">View full schema</summary>
500
+ <div class="schema-block" style="margin-top: 0.75rem;">${highlightJson(schema)}</div>
501
+ </details>
502
+ </div>
503
+ ` : /*html*/`
504
+ <div class="detail-section">
505
+ <div class="empty-state">No input schema defined</div>
506
+ </div>
507
+ `}
508
+ </div>
509
+ </div>`;
510
+ }
511
+
512
+ /**
513
+ * Render a skill card (simple version for lists)
514
+ */
515
+ export function skillCard(skill: SkillDefinition): string {
516
+ return /*html*/`
517
+ <div class="item-card skill">
518
+ <div class="item-name">${skill.name}</div>
519
+ <div class="item-desc">${skill.description || ''}</div>
520
+ <div class="item-meta">
521
+ <span class="badge ${skill.execution?.language || ''}">${skill.content_type === 'jst' ? 'Dynamic' : 'Static'}</span>
522
+ ${skill.execution?.language ? `<span class="badge ${skill.execution.language}">${skill.execution.language}</span>` : ''}
523
+ ${skill.scripts?.length ? `<span class="badge">${skill.scripts.length} script${skill.scripts.length > 1 ? 's' : ''}</span>` : ''}
524
+ </div>
525
+ </div>`;
526
+ }
527
+
528
+ /**
529
+ * Render a detailed skill card
530
+ */
531
+ export function skillDetailCard(skill: SkillDefinition): string {
532
+ const hasKeywords = skill.context_triggers?.keywords?.length;
533
+ const hasPackages = skill.execution?.packages?.length;
534
+ const hasScripts = skill.scripts?.length;
535
+
536
+ return /*html*/`
537
+ <div class="detail-card">
538
+ <div class="detail-header">
539
+ <div>
540
+ <h3 class="detail-title">${skill.name}</h3>
541
+ <p class="detail-desc">${skill.description || 'No description'}</p>
542
+ </div>
543
+ <div class="detail-badges">
544
+ <span class="badge skill-type-badge">Skill</span>
545
+ ${skill.execution?.language ? `<span class="badge ${skill.execution.language}">${skill.execution.language}</span>` : ''}
546
+ </div>
547
+ </div>
548
+ <div class="detail-body">
549
+ <div class="info-grid">
550
+ <div class="info-item">
551
+ <div class="info-label">Content Type</div>
552
+ <div class="info-value">${skill.content_type === 'jst' ? 'Dynamic (JST Template)' : 'Static (Markdown)'}</div>
553
+ </div>
554
+ ${skill.execution?.language ? /*html*/`
555
+ <div class="info-item">
556
+ <div class="info-label">Language</div>
557
+ <div class="info-value"><code>${skill.execution.language}</code></div>
558
+ </div>
559
+ ` : ''}
560
+ </div>
561
+
562
+ ${hasKeywords ? /*html*/`
563
+ <div class="detail-section">
564
+ <h4 class="detail-section-title">Trigger Keywords</h4>
565
+ <div class="keyword-list">
566
+ ${skill.context_triggers?.keywords?.map(kw => `<span class="keyword-tag">${kw}</span>`).join('')}
567
+ </div>
568
+ </div>
569
+ ` : ''}
570
+
571
+ ${hasPackages ? /*html*/`
572
+ <div class="detail-section">
573
+ <h4 class="detail-section-title">Required Packages</h4>
574
+ <div class="package-list">
575
+ ${skill.execution?.packages?.map(pkg => `<span class="package-tag">${pkg}</span>`).join('')}
576
+ </div>
577
+ </div>
578
+ ` : ''}
579
+
580
+ ${hasScripts ? /*html*/`
581
+ <div class="detail-section">
582
+ <h4 class="detail-section-title">Bundled Scripts</h4>
583
+ <div class="script-list">
584
+ ${skill.scripts?.map(script => /*html*/`
585
+ <div class="script-item">
586
+ ${fileIcon}
587
+ <span class="script-name">${script.name}</span>
588
+ </div>
589
+ `).join('')}
590
+ </div>
591
+ </div>
592
+ ` : ''}
593
+
594
+ <div class="detail-section">
595
+ <h4 class="detail-section-title">Instructions Preview</h4>
596
+ <div class="instructions-preview">${escapeHtml(skill.instructions.slice(0, 1000))}${skill.instructions.length > 1000 ? '...' : ''}</div>
597
+ </div>
598
+ </div>
599
+ </div>`;
600
+ }
601
+
602
+ /**
603
+ * Escape HTML for safe rendering
604
+ */
605
+ function escapeHtml(str: string): string {
606
+ return str
607
+ .replace(/&/g, '&amp;')
608
+ .replace(/</g, '&lt;')
609
+ .replace(/>/g, '&gt;')
610
+ .replace(/"/g, '&quot;')
611
+ .replace(/'/g, '&#039;');
612
+ }
613
+
614
+ /**
615
+ * Derive simple initials from a title for use in the hero avatar.
616
+ */
617
+ function getInitials(title: string): string {
618
+ const words = title.trim().split(/\s+/).filter(Boolean);
619
+ if (!words.length) return "TS";
620
+ const initials = words
621
+ .slice(0, 2)
622
+ .map((word) => word.charAt(0).toUpperCase())
623
+ .join("");
624
+ return initials || "TS";
625
+ }
626
+
627
+ /**
628
+ * Render the main index page
629
+ *
630
+ * Note: The fourth argument is backward compatible:
631
+ * - If a string is passed, it is treated as the title.
632
+ * - If an array is passed, it is treated as MCP providers and the fifth argument (if any) is the title.
633
+ */
634
+ export function indexPage(
635
+ tools: ToolCollection[],
636
+ skills: SkillCollection[],
637
+ interactions: InteractionCollection[],
638
+ mcpProvidersOrTitle?: MCPProviderMeta[] | string,
639
+ titleParam?: string
640
+ ): string {
641
+ let mcpProviders: MCPProviderMeta[] = [];
642
+ let title = "Tools Server";
643
+
644
+ if (Array.isArray(mcpProvidersOrTitle)) {
645
+ mcpProviders = mcpProvidersOrTitle;
646
+ if (typeof titleParam === "string" && titleParam.length > 0) {
647
+ title = titleParam;
648
+ }
649
+ } else if (typeof mcpProvidersOrTitle === "string" && mcpProvidersOrTitle.length > 0) {
650
+ title = mcpProvidersOrTitle;
651
+ } else if (typeof titleParam === "string" && titleParam.length > 0) {
652
+ title = titleParam;
653
+ }
654
+
655
+ return /*html*/`
656
+ <!DOCTYPE html>
657
+ <html lang="en">
658
+ <head>
659
+ <meta charset="UTF-8">
660
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
661
+ <title>${title}</title>
662
+ <style>${baseStyles}</style>
663
+ </head>
664
+ <body>
665
+ <div class="page">
666
+ <header class="hero">
667
+ <div class="hero-main">
668
+ <div class="hero-logo">
669
+ <span class="hero-logo-initial">${escapeHtml(getInitials(title))}</span>
670
+ </div>
671
+ <div class="hero-meta">
672
+ <p class="hero-eyebrow">Tools Server</p>
673
+ <h1 class="hero-title">${title}</h1>
674
+ <p class="hero-tagline">
675
+ Discover the tools, skills, and interactions exposed by this server.
676
+ </p>
677
+ <div class="hero-summary">
678
+ ${tools.length ? /*html*/`<span><dot></dot> ${tools.length} tool collection${tools.length !== 1 ? 's' : ''}</span>` : ''}
679
+ ${skills.length ? /*html*/`<span><dot></dot> ${skills.length} skill collection${skills.length !== 1 ? 's' : ''}</span>` : ''}
680
+ ${interactions.length ? /*html*/`<span><dot></dot> ${interactions.length} interaction collection${interactions.length !== 1 ? 's' : ''}</span>` : ''}
681
+ ${mcpProviders.length ? /*html*/`<span><dot></dot> ${mcpProviders.length} MCP provider${mcpProviders.length !== 1 ? 's' : ''}</span>` : ''}
682
+ </div>
683
+ </div>
684
+ </div>
685
+ <aside class="hero-panel">
686
+ <div class="hero-panel-label">Base endpoint</div>
687
+ <div class="hero-panel-endpoint"><code>/api</code></div>
688
+ <p class="hero-panel-hint">
689
+ Use <strong>POST /api/tools/&lt;collection&gt;</strong> or
690
+ <strong>POST /api/skills/&lt;collection&gt;</strong> to call these from your apps or agents.
691
+ </p>
692
+ </aside>
693
+ </header>
694
+
695
+ <div class="search-bar">
696
+ <input
697
+ type="search"
698
+ id="collection-search"
699
+ class="search-input"
700
+ placeholder="Search tools, skills, interactions..."
701
+ aria-label="Search collections"
702
+ autocomplete="off"
703
+ />
704
+ <p class="search-hint">
705
+ Filter collections by name or description. Search runs locally in your browser.
706
+ </p>
707
+ <p id="search-empty" class="search-empty" style="display: none;">
708
+ No collections match this search.
709
+ </p>
710
+ </div>
711
+
712
+ ${tools.length > 0 ? /*html*/`
713
+ <section data-section="tools">
714
+ <div class="section-header">
715
+ <h2>Tool Collections</h2>
716
+ <p class="section-subtitle">Remote tools available to agents via Vertesia.</p>
717
+ </div>
718
+ <div class="card-grid">
719
+ ${tools.map(t => collectionCard(t, 'tools')).join('')}
720
+ </div>
721
+ </section>
722
+ ` : ''}
723
+
724
+ ${skills.length > 0 ? /*html*/`
725
+ <section data-section="skills">
726
+ <hr>
727
+ <div class="section-header">
728
+ <h2>Skill Collections</h2>
729
+ <p class="section-subtitle">Reusable instructions and scripts packaged as tools.</p>
730
+ </div>
731
+ <div class="card-grid">
732
+ ${skills.map(s => {
733
+ const count = Array.from(s).length;
734
+ return collectionCard(s, 'skills', `${count} skill${count !== 1 ? 's' : ''}`);
735
+ }).join('')}
736
+ </div>
737
+ </section>
738
+ ` : ''}
739
+
740
+ ${interactions.length > 0 ? /*html*/`
741
+ <section data-section="interactions">
742
+ <hr>
743
+ <div class="section-header">
744
+ <h2>Interaction Collections</h2>
745
+ <p class="section-subtitle">Conversation blueprints surfaced in the Vertesia UI.</p>
746
+ </div>
747
+ <div class="card-grid">
748
+ ${interactions.map(i => collectionCard(i, 'interactions')).join('')}
749
+ </div>
750
+ </section>
751
+ ` : ''}
752
+
753
+ ${mcpProviders.length > 0 ? /*html*/`
754
+ <section data-section="mcp">
755
+ <hr>
756
+ <div class="section-header">
757
+ <h2>MCP Providers</h2>
758
+ <p class="section-subtitle">Remote MCP servers available through this tools server.</p>
759
+ </div>
760
+ <div class="card-grid">
761
+ ${mcpProviders.map(p => mcpProviderCard(p)).join('')}
762
+ </div>
763
+ </section>
764
+ ` : ''}
765
+ </div>
766
+ <script>
767
+ (function () {
768
+ var input = document.getElementById('collection-search');
769
+ if (!input) return;
770
+
771
+ var cards = Array.prototype.slice.call(document.querySelectorAll('.card'));
772
+ if (!cards.length) return;
773
+
774
+ var sections = Array.prototype.slice.call(document.querySelectorAll('[data-section]'));
775
+ var emptyState = document.getElementById('search-empty');
776
+
777
+ function normalize(value) {
778
+ return (value || '').toString().toLowerCase();
779
+ }
780
+
781
+ function update(query) {
782
+ var q = normalize(query).trim();
783
+ var anyVisible = false;
784
+
785
+ cards.forEach(function (card) {
786
+ var text = normalize(card.textContent);
787
+ var match = !q || text.indexOf(q) !== -1;
788
+ card.style.display = match ? '' : 'none';
789
+ if (match) anyVisible = true;
790
+ });
791
+
792
+ sections.forEach(function (section) {
793
+ var visibleCards = section.querySelectorAll('.card');
794
+ var hasVisible = false;
795
+ for (var i = 0; i < visibleCards.length; i++) {
796
+ var style = window.getComputedStyle(visibleCards[i]);
797
+ if (style.display !== 'none') {
798
+ hasVisible = true;
799
+ break;
800
+ }
801
+ }
802
+ section.style.display = hasVisible ? '' : 'none';
803
+ });
804
+
805
+ if (emptyState) {
806
+ emptyState.style.display = q && !anyVisible ? '' : 'none';
807
+ }
808
+ }
809
+
810
+ input.addEventListener('input', function () {
811
+ update(input.value);
812
+ });
813
+ }());
814
+ </script>
815
+ </body>
816
+ </html>`;
817
+ }
818
+
819
+ /**
820
+ * Render a tool collection detail page
821
+ */
822
+ export function toolCollectionPage(collection: ToolCollection): string {
823
+ const toolsArray = Array.from(collection);
824
+ return /*html*/`
825
+ <!DOCTYPE html>
826
+ <html lang="en">
827
+ <head>
828
+ <meta charset="UTF-8">
829
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
830
+ <title>${collection.title || collection.name} - Tools</title>
831
+ <style>${detailStyles}</style>
832
+ </head>
833
+ <body>
834
+ <nav class="nav">
835
+ <a href="/">${backArrow} Back to all collections</a>
836
+ </nav>
837
+
838
+ <div class="header">
839
+ <div class="header-icon">${collection.icon || defaultIcon}</div>
840
+ <div>
841
+ <h1>${collection.title || collection.name}</h1>
842
+ <p style="color: #6b7280; margin: 0.25rem 0 0 0;">${collection.description || ''}</p>
843
+ <div class="endpoint-box">
844
+ <code>/api/tools/${collection.name}</code>
845
+ <button class="copy-btn" onclick="navigator.clipboard.writeText(window.location.origin + '/api/tools/${collection.name}')" title="Copy endpoint URL">
846
+ ${copyIcon}
847
+ </button>
848
+ </div>
849
+ </div>
850
+ </div>
851
+
852
+ <h2>${toolsArray.length} Tool${toolsArray.length !== 1 ? 's' : ''}</h2>
853
+
854
+ ${toolsArray.length > 0 ?
855
+ toolsArray.map(tool => toolDetailCard(tool, collection.name)).join('') :
856
+ '<div class="empty-state">No tools in this collection</div>'
857
+ }
858
+ </body>
859
+ </html>`;
860
+ }
861
+
862
+ /**
863
+ * Render a skill collection detail page
864
+ */
865
+ export function skillCollectionPage(collection: SkillCollection): string {
866
+ const skillsArray = Array.from(collection);
867
+ return /*html*/`
868
+ <!DOCTYPE html>
869
+ <html lang="en">
870
+ <head>
871
+ <meta charset="UTF-8">
872
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
873
+ <title>${collection.title || collection.name} - Skills</title>
874
+ <style>${detailStyles}</style>
875
+ </head>
876
+ <body>
877
+ <nav class="nav">
878
+ <a href="/">${backArrow} Back to all collections</a>
879
+ </nav>
880
+
881
+ <div class="header">
882
+ <div class="header-icon">${collection.icon || skillIcon}</div>
883
+ <div>
884
+ <h1>${collection.title || collection.name}</h1>
885
+ <p style="color: #6b7280; margin: 0.25rem 0 0 0;">${collection.description || ''}</p>
886
+ <div class="endpoint-box">
887
+ <code>/api/skills/${collection.name}</code>
888
+ <button class="copy-btn" onclick="navigator.clipboard.writeText(window.location.origin + '/api/skills/${collection.name}')" title="Copy endpoint URL">
889
+ ${copyIcon}
890
+ </button>
891
+ </div>
892
+ </div>
893
+ </div>
894
+
895
+ <h2>${skillsArray.length} Skill${skillsArray.length !== 1 ? 's' : ''}</h2>
896
+
897
+ ${skillsArray.length > 0 ?
898
+ skillsArray.map(skill => skillDetailCard(skill)).join('') :
899
+ '<div class="empty-state">No skills in this collection</div>'
900
+ }
901
+ </body>
902
+ </html>`;
903
+ }
904
+
905
+ /**
906
+ * Render an interaction collection detail page
907
+ */
908
+ export function interactionCollectionPage(collection: InteractionCollection): string {
909
+ return /*html*/`
910
+ <!DOCTYPE html>
911
+ <html lang="en">
912
+ <head>
913
+ <meta charset="UTF-8">
914
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
915
+ <title>${collection.title || collection.name} - Interactions</title>
916
+ <style>${detailStyles}</style>
917
+ </head>
918
+ <body>
919
+ <nav class="nav">
920
+ <a href="/">${backArrow} Back to all collections</a>
921
+ </nav>
922
+
923
+ <div class="header">
924
+ <div class="header-icon">${collection.icon || defaultIcon}</div>
925
+ <div>
926
+ <h1>${collection.title || collection.name}</h1>
927
+ <p style="color: #6b7280; margin: 0.25rem 0 0 0;">${collection.description || ''}</p>
928
+ <div class="endpoint-box">
929
+ <code>/api/interactions/${collection.name}</code>
930
+ <button class="copy-btn" onclick="navigator.clipboard.writeText(window.location.origin + '/api/interactions/${collection.name}')" title="Copy endpoint URL">
931
+ ${copyIcon}
932
+ </button>
933
+ </div>
934
+ </div>
935
+ </div>
936
+
937
+ <h2>${collection.interactions.length} Interaction${collection.interactions.length !== 1 ? 's' : ''}</h2>
938
+
939
+ <div class="item-list">
940
+ ${collection.interactions.map(inter => /*html*/`
941
+ <div class="detail-card">
942
+ <div class="detail-header">
943
+ <div>
944
+ <h3 class="detail-title">${inter.name}</h3>
945
+ <p class="detail-desc">${inter.description || 'No description'}</p>
946
+ </div>
947
+ <div class="detail-badges">
948
+ ${inter.tags?.map(tag => `<span class="badge">${tag}</span>`).join('') || ''}
949
+ </div>
950
+ </div>
951
+ </div>
952
+ `).join('')}
953
+ </div>
954
+ </body>
955
+ </html>`;
956
+ }