aimeat 1.2.6 → 1.3.1

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 (128) hide show
  1. package/dist/.env.example +4 -0
  2. package/dist/locales/en.json +74 -2
  3. package/dist/locales/fi.json +74 -2
  4. package/dist/public/css/views/capabilities.css +211 -0
  5. package/dist/public/llms-template.txt +152 -89
  6. package/dist/public/spa.html +1 -0
  7. package/dist/public/views/admin/capabilities-tab.js +114 -0
  8. package/dist/public/views/admin.js +2 -0
  9. package/dist/public/views/aimeat-os.js +194 -34
  10. package/dist/public/views/profile/capabilities-tab.js +252 -0
  11. package/dist/public/views/profile/extensions-tab.js +404 -50
  12. package/dist/public/views/profile/landing-page.js +2 -0
  13. package/dist/public/views/profile.js +2 -0
  14. package/dist/src/config.d.ts +11 -0
  15. package/dist/src/config.d.ts.map +1 -1
  16. package/dist/src/config.js +11 -0
  17. package/dist/src/config.js.map +1 -1
  18. package/dist/src/mcp/apps.d.ts +18 -0
  19. package/dist/src/mcp/apps.d.ts.map +1 -0
  20. package/dist/src/mcp/apps.js +241 -0
  21. package/dist/src/mcp/apps.js.map +1 -0
  22. package/dist/src/mcp/capabilities.d.ts +12 -0
  23. package/dist/src/mcp/capabilities.d.ts.map +1 -0
  24. package/dist/src/mcp/capabilities.js +187 -0
  25. package/dist/src/mcp/capabilities.js.map +1 -0
  26. package/dist/src/mcp/cortex.d.ts +17 -0
  27. package/dist/src/mcp/cortex.d.ts.map +1 -0
  28. package/dist/src/mcp/cortex.js +237 -0
  29. package/dist/src/mcp/cortex.js.map +1 -0
  30. package/dist/src/mcp/extensions.d.ts +4 -3
  31. package/dist/src/mcp/extensions.d.ts.map +1 -1
  32. package/dist/src/mcp/extensions.js +233 -3
  33. package/dist/src/mcp/extensions.js.map +1 -1
  34. package/dist/src/mcp/index.d.ts.map +1 -1
  35. package/dist/src/mcp/index.js +6 -0
  36. package/dist/src/mcp/index.js.map +1 -1
  37. package/dist/src/routes/admin-capabilities.d.ts +11 -0
  38. package/dist/src/routes/admin-capabilities.d.ts.map +1 -0
  39. package/dist/src/routes/admin-capabilities.js +54 -0
  40. package/dist/src/routes/admin-capabilities.js.map +1 -0
  41. package/dist/src/routes/capabilities.d.ts +11 -0
  42. package/dist/src/routes/capabilities.d.ts.map +1 -0
  43. package/dist/src/routes/capabilities.js +289 -0
  44. package/dist/src/routes/capabilities.js.map +1 -0
  45. package/dist/src/routes/cortex.d.ts.map +1 -1
  46. package/dist/src/routes/cortex.js +2 -0
  47. package/dist/src/routes/cortex.js.map +1 -1
  48. package/dist/src/routes/extensions.d.ts.map +1 -1
  49. package/dist/src/routes/extensions.js +9 -0
  50. package/dist/src/routes/extensions.js.map +1 -1
  51. package/dist/src/routes/lib-capabilities.d.ts +9 -0
  52. package/dist/src/routes/lib-capabilities.d.ts.map +1 -0
  53. package/dist/src/routes/lib-capabilities.js +190 -0
  54. package/dist/src/routes/lib-capabilities.js.map +1 -0
  55. package/dist/src/routes/libs.d.ts.map +1 -1
  56. package/dist/src/routes/libs.js +13 -0
  57. package/dist/src/routes/libs.js.map +1 -1
  58. package/dist/src/routes/verification.d.ts +10 -1
  59. package/dist/src/routes/verification.d.ts.map +1 -1
  60. package/dist/src/routes/verification.js +174 -24
  61. package/dist/src/routes/verification.js.map +1 -1
  62. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  63. package/dist/src/server-bootstrap/routes-loader.js +35 -2
  64. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  65. package/dist/src/services/capability-aggregator.d.ts +8 -0
  66. package/dist/src/services/capability-aggregator.d.ts.map +1 -0
  67. package/dist/src/services/capability-aggregator.js +239 -0
  68. package/dist/src/services/capability-aggregator.js.map +1 -0
  69. package/dist/src/services/capability-invoke.d.ts +20 -0
  70. package/dist/src/services/capability-invoke.d.ts.map +1 -0
  71. package/dist/src/services/capability-invoke.js +95 -0
  72. package/dist/src/services/capability-invoke.js.map +1 -0
  73. package/dist/src/services/config-schema.d.ts.map +1 -1
  74. package/dist/src/services/config-schema.js +4 -0
  75. package/dist/src/services/config-schema.js.map +1 -1
  76. package/dist/src/services/core-jobs.d.ts.map +1 -1
  77. package/dist/src/services/core-jobs.js +16 -0
  78. package/dist/src/services/core-jobs.js.map +1 -1
  79. package/dist/src/services/did-document.d.ts +25 -0
  80. package/dist/src/services/did-document.d.ts.map +1 -0
  81. package/dist/src/services/did-document.js +31 -0
  82. package/dist/src/services/did-document.js.map +1 -0
  83. package/dist/src/services/eudiw.d.ts +11 -1
  84. package/dist/src/services/eudiw.d.ts.map +1 -1
  85. package/dist/src/services/eudiw.js +43 -48
  86. package/dist/src/services/eudiw.js.map +1 -1
  87. package/dist/src/services/extension-runtime.d.ts.map +1 -1
  88. package/dist/src/services/extension-runtime.js +8 -5
  89. package/dist/src/services/extension-runtime.js.map +1 -1
  90. package/dist/src/services/job-seeding.d.ts.map +1 -1
  91. package/dist/src/services/job-seeding.js +4 -0
  92. package/dist/src/services/job-seeding.js.map +1 -1
  93. package/dist/src/services/oidc-client.d.ts +32 -0
  94. package/dist/src/services/oidc-client.d.ts.map +1 -0
  95. package/dist/src/services/oidc-client.js +67 -0
  96. package/dist/src/services/oidc-client.js.map +1 -0
  97. package/dist/src/services/sd-jwt.d.ts +21 -0
  98. package/dist/src/services/sd-jwt.d.ts.map +1 -0
  99. package/dist/src/services/sd-jwt.js +87 -0
  100. package/dist/src/services/sd-jwt.js.map +1 -0
  101. package/dist/src/services/vc-issuer.d.ts +20 -1
  102. package/dist/src/services/vc-issuer.d.ts.map +1 -1
  103. package/dist/src/services/vc-issuer.js +75 -15
  104. package/dist/src/services/vc-issuer.js.map +1 -1
  105. package/dist/src/storage/interface.d.ts +118 -1
  106. package/dist/src/storage/interface.d.ts.map +1 -1
  107. package/dist/src/storage/providers/mongodb/index.d.ts +37 -1
  108. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  109. package/dist/src/storage/providers/mongodb/index.js +282 -0
  110. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  111. package/dist/src/storage/providers/sqlite/index.d.ts +37 -1
  112. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  113. package/dist/src/storage/providers/sqlite/index.js +241 -0
  114. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  115. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  116. package/dist/src/storage/providers/sqlite/schema.js +77 -0
  117. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  118. package/dist/src/storage/repositories/capability.repository.d.ts +35 -0
  119. package/dist/src/storage/repositories/capability.repository.d.ts.map +1 -0
  120. package/dist/src/storage/repositories/capability.repository.js +2 -0
  121. package/dist/src/storage/repositories/capability.repository.js.map +1 -0
  122. package/dist/src/storage/repositories/index.d.ts +1 -0
  123. package/dist/src/storage/repositories/index.d.ts.map +1 -1
  124. package/dist/src/storage/repositories/node.repository.d.ts +5 -1
  125. package/dist/src/storage/repositories/node.repository.d.ts.map +1 -1
  126. package/dist/static/app-catalog.html +185 -29
  127. package/package.json +6 -1
  128. package/prisma/schema.prisma +86 -0
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * @file extensions-tab.js
3
- * @description Profile tab for managing Cortex (YAML manifest) and V8 (sandboxed JS)
3
+ * @description Profile tab for managing Cortex (YAML manifest) and Server (sandboxed JS)
4
4
  * extensions. Lists installed extensions, shows detail views with component
5
5
  * breakdowns, action testing, instance management, and install/uninstall flows.
6
6
  * @structure
7
7
  * - buildCortexPrompt() — generates AI scaffolding prompt for extension creation
8
8
  * - ExtensionsTab() — main tab component (default export)
9
9
  * - Cortex detail view — manifest components, prompts, libs, schemas, ontologies
10
- * - V8 detail view — actions, test runner, instances, endpoint info
10
+ * - Server extension detail view — actions, test runner, instances, endpoint info
11
11
  * - Grid view — cards for installed + bundled extensions
12
12
  * - Install modal — upload/paste manifest + libs
13
13
  * @usage Loaded as a lazy tab in profile.js via dynamic import.
14
14
  * @version-history
15
- * v1.0.0 — 2026-03-10 — Initial implementation with Cortex + V8 extension management
15
+ * v1.0.0 — 2026-03-10 — Initial implementation with Cortex + Server extension management
16
16
  * v1.1.0 — 2026-03-17 — Refactor: replace all inline style="" attributes with CSS classes
17
17
  */
18
18
  import { h } from 'preact';
@@ -79,6 +79,65 @@ spec:
79
79
  The "components" array can contain any mix of the 7 component types:
80
80
  schema, prompt, action, board-template, ontology, seed-data, lib.
81
81
 
82
+ Required fields (installation fails without these):
83
+ - apiVersion: must be "cortex.aimeat.org/v1"
84
+ - kind: must be "Extension"
85
+ - metadata.name, metadata.namespace: both mandatory strings
86
+ - spec.version: mandatory semver string
87
+ - spec.components: mandatory array, each component needs at least "type"
88
+ - Schema components require: name, key_pattern
89
+ - Lib components require: name, filename
90
+
91
+ ────────────────────────────────────────────
92
+ COMPONENT EXAMPLES
93
+ ────────────────────────────────────────────
94
+
95
+ Schema (locks memory keys to a JSON Schema):
96
+ - type: schema
97
+ name: my-item
98
+ key_pattern: "myext.items.*"
99
+ apply_to: prefix
100
+ schema:
101
+ type: object
102
+ properties:
103
+ id: { type: string, description: "Unique ID" }
104
+ name: { type: string, description: "Item name" }
105
+ value: { type: number, description: "Numeric value" }
106
+ required: [id, name]
107
+
108
+ Schema properties must have full definitions with types and descriptions.
109
+
110
+ Lib (JavaScript library loaded by apps):
111
+ - type: lib
112
+ name: my-lib.js
113
+ filename: my-lib.js
114
+ exports: [getData, setData, search]
115
+ api_surface: |
116
+ AIMEAT.myExt.getData({ id }) — Get item by ID. Returns { id: string, name: string, value: number } or null
117
+ AIMEAT.myExt.setData({ id, name, value }) — Save item. Returns saved item { id, name, value, createdAt }
118
+ AIMEAT.myExt.search({ query }) — Search items. Returns Item[]
119
+
120
+ api_surface must describe every function with parameter names, types, and return shapes.
121
+
122
+ Prompt (AI instructions for using this cortex):
123
+ - type: prompt
124
+ name: my-assistant
125
+ content: |
126
+ Available API:
127
+ AIMEAT.myExt.getData({ id }) — Returns { id, name, value }
128
+ ...
129
+ variables:
130
+ - metadata.name
131
+ - node_url
132
+
133
+ Seed data (initial entries written on activation):
134
+ - type: seed-data
135
+ entries:
136
+ - key: myext.index
137
+ value: []
138
+
139
+ Seed-data keys must not collide with schema key_patterns (e.g. use "myext.index" not "myext.items.index").
140
+
82
141
  ────────────────────────────────────────────
83
142
  NODE INFO (auto-filled)
84
143
  ────────────────────────────────────────────
@@ -110,6 +169,174 @@ Now ask the user what they want to build! Based on what they describe:
110
169
  To install: go to Profile > Extensions > + Add > paste the YAML manifest > add JS files if any > click Install.`;
111
170
  }
112
171
 
172
+ /* ── Server Extension scaffolding prompt builder ── */
173
+ function buildServerExtensionPrompt(sess) {
174
+ const url = getNodeUrl();
175
+ const owner = sess?.owner || 'user';
176
+ return `You are a Server Extension builder for the AIMEAT protocol.
177
+
178
+ The user wants to create a server-side extension that runs in a secure sandbox.
179
+ Node URL: ${url}
180
+ Owner: ${owner}
181
+
182
+ A server extension is a YAML manifest + JavaScript action scripts. Each action runs
183
+ server-side in a sandboxed isolate with strict resource limits.
184
+
185
+ ────────────────────────────────────────────
186
+ YAML MANIFEST STRUCTURE
187
+ ────────────────────────────────────────────
188
+
189
+ metadata:
190
+ name: my-extension
191
+ version: 1.0.0
192
+ description: "What this extension does"
193
+ author: ${owner}
194
+ license: MIT
195
+
196
+ required_apis:
197
+ - memory # which sandbox APIs the extension needs
198
+
199
+ config: # operator-configurable settings (stored securely)
200
+ api_key:
201
+ type: string
202
+ description: "External API key"
203
+ base_url:
204
+ type: string
205
+ default: "https://api.example.com"
206
+
207
+ actions: # each action is a callable endpoint
208
+ - id: my-action # camelCase, becomes POST /v1/ext/{name}/{id}
209
+ method: POST
210
+ path: /my-action
211
+ script: actions/my-action.js
212
+ description: "What this action does"
213
+ auth: authenticated
214
+ input:
215
+ param1:
216
+ type: string
217
+ required: true
218
+ description: "Input parameter"
219
+ output: # use JSON Schema format for all outputs
220
+ type: object
221
+ properties:
222
+ items:
223
+ type: array
224
+ items:
225
+ type: object
226
+ properties:
227
+ id: { type: integer, description: "Item ID" }
228
+ name: { type: string, description: "Item name" }
229
+ status: { type: string, enum: [active, inactive], description: "Current status" }
230
+ image: { type: string, description: "Image URL" }
231
+ info:
232
+ type: object
233
+ properties:
234
+ count: { type: integer, description: "Total matching items" }
235
+ pages: { type: integer, description: "Total pages" }
236
+ next: { type: string, nullable: true, description: "Next page URL or null" }
237
+ success: { type: boolean }
238
+
239
+ RULES:
240
+ - Each action requires: id, method, path, script. All four are mandatory.
241
+ - The script filename must match the JS file provided during install.
242
+ - Input and output schemas use JSON Schema format (same as OpenAPI 3.1).
243
+ Every object needs \`properties\`, every array needs \`items\`. Use compact
244
+ inline syntax: \`name: { type: string, description: "..." }\` for leaf fields.
245
+
246
+ schedules: # optional cron jobs
247
+ - id: refresh-data
248
+ cron: "*/10 * * * *" # every 10 minutes
249
+ action: refresh
250
+ description: "Periodic data refresh"
251
+
252
+ limits:
253
+ memory_mb: 64
254
+ timeout_ms: 5000
255
+ max_api_calls: 50
256
+
257
+ ────────────────────────────────────────────
258
+ SANDBOX API (available inside action scripts)
259
+ ────────────────────────────────────────────
260
+
261
+ Each action script exports a default async function:
262
+
263
+ export default async function(ctx, input) {
264
+ // ctx.memory — read/write AIMEAT memory
265
+ const data = await ctx.memory.get('my-key');
266
+ await ctx.memory.set('my-key', { value: 'hello' });
267
+ await ctx.memory.delete('my-key');
268
+ const results = await ctx.memory.search('prefix:*');
269
+ const pub = await ctx.memory.getPublic('ext:my-ext', 'public-key');
270
+
271
+ // ctx.fetch — call external APIs (HTTP/HTTPS)
272
+ const resp = await ctx.fetch('https://api.example.com/data', {
273
+ method: 'POST',
274
+ headers: { 'Authorization': 'Bearer ' + ctx.config.api_key },
275
+ body: JSON.stringify({ query: input.query }),
276
+ });
277
+ // resp = { status, ok, text, headers }
278
+ const json = JSON.parse(resp.text);
279
+
280
+ // ctx.config — operator-configured values (from manifest config section)
281
+ const apiKey = ctx.config.api_key;
282
+
283
+ // ctx.caller — who called this action
284
+ // ctx.caller.gaii, ctx.caller.owner, ctx.caller.roles
285
+
286
+ // ctx.wallet.consume(amount, reason) — deduct morsels (optional)
287
+ // ctx.trust.getScore(gaii) — get trust score (optional)
288
+ // ctx.consent.check(grantorGaii, dataPattern) — check consent (optional)
289
+
290
+ // ctx.log(message) — log for debugging
291
+ ctx.log('Processing: ' + input.query);
292
+
293
+ // Return result (must be JSON-serializable object)
294
+ return { result: json.data, processed_at: new Date().toISOString() };
295
+ }
296
+
297
+ SANDBOX ENVIRONMENT:
298
+ Available: JSON, Math, Date, RegExp, Promise, async/await, String, Array,
299
+ Object, Map, Set, encodeURIComponent, decodeURIComponent, parseInt, parseFloat
300
+
301
+ NOT available (will crash):
302
+ URLSearchParams, URL, Buffer, TextEncoder, TextDecoder, Headers,
303
+ Request, Response, FormData, Blob, AbortController, atob, btoa,
304
+ require, import, process, fs, crypto, setTimeout, setInterval,
305
+ console.log (use ctx.log), fetch (use ctx.fetch), window, document
306
+
307
+ URL construction (URLSearchParams is NOT available):
308
+ WRONG: const params = new URLSearchParams(); params.set('name', query);
309
+ RIGHT: const url = base + '?name=' + encodeURIComponent(query) + '&page=' + page;
310
+
311
+ RULES:
312
+ - Scripts use ES module syntax: export default async function(ctx, input) { ... }
313
+ - ctx.fetch returns { status, ok, text, headers } — NOT a Response object
314
+ - Parse JSON manually: JSON.parse(resp.text)
315
+ - Config values are set by the operator, never hardcode secrets
316
+ - Return value must be a plain object (no classes, no functions)
317
+
318
+ ────────────────────────────────────────────
319
+ INSTALL
320
+ ────────────────────────────────────────────
321
+
322
+ Extension API:
323
+ POST ${url}/v1/extensions — install (body: { manifest: "<YAML>", scripts: { "actions/file.js": "<JS>" } })
324
+ POST ${url}/v1/extensions/{name}/activate
325
+ POST ${url}/v1/ext/{name}/{actionId} — call an action
326
+
327
+ Or go to Profile > Extensions > Server Extensions section to install via UI.
328
+
329
+ ────────────────────────────────────────────
330
+ YOUR TASK
331
+ ────────────────────────────────────────────
332
+
333
+ Ask the user what they want to build. Then:
334
+ 1. Design the YAML manifest with actions, config, and schedules as needed
335
+ 2. Write the JavaScript action scripts
336
+ 3. Explain what each action does and what external APIs it calls
337
+ 4. Provide complete YAML + JS files ready to install`;
338
+ }
339
+
113
340
  export default function ExtensionsTab({ session, showToast }) {
114
341
  const { confirm, ConfirmUI } = useConfirm();
115
342
  const NODE_URL = getNodeUrl();
@@ -122,10 +349,13 @@ export default function ExtensionsTab({ session, showToast }) {
122
349
  const [libEntries, setLibEntries] = useState([{filename:'', code:''}]);
123
350
  const [bundledInstalling, setBundledInstalling] = useState(null);
124
351
 
125
- // V8 extension state
126
- const [v8Exts, setV8Exts] = useState(null);
127
- const [v8Detail, setV8Detail] = useState(null);
128
- const [v8Instances, setV8Instances] = useState(null);
352
+ // Server extension state
353
+ const [showSrvInstall, setShowSrvInstall] = useState(false);
354
+ const [srvManifestText, setSrvManifestText] = useState('');
355
+ const [srvScriptEntries, setSrvScriptEntries] = useState([{filename:'', code:''}]);
356
+ const [srvExts, setSrvExts] = useState(null);
357
+ const [srvDetail, setSrvDetail] = useState(null);
358
+ const [srvInstances, setSrvInstances] = useState(null);
129
359
  const [newInstanceId, setNewInstanceId] = useState('');
130
360
  const [testAction, setTestAction] = useState(null); // { actionId }
131
361
  const [testInput, setTestInput] = useState('{}');
@@ -133,12 +363,12 @@ export default function ExtensionsTab({ session, showToast }) {
133
363
  const [testRunning, setTestRunning] = useState(false);
134
364
 
135
365
  useEffect(() => {
136
- if (session) { loadExtensions(); loadV8Extensions(); }
366
+ if (session) { loadExtensions(); loadServerExtensions(); }
137
367
  }, [session]);
138
368
 
139
369
  // Auto-refresh on SSE live updates (extension install/activate/etc)
140
370
  useEffect(() => {
141
- const handler = () => { loadExtensions(); loadV8Extensions(); };
371
+ const handler = () => { loadExtensions(); loadServerExtensions(); };
142
372
  window.addEventListener('aimeat-live-update', handler);
143
373
  return () => window.removeEventListener('aimeat-live-update', handler);
144
374
  }, []);
@@ -150,49 +380,93 @@ export default function ExtensionsTab({ session, showToast }) {
150
380
  } catch { setExtensions([]); }
151
381
  }
152
382
 
153
- async function loadV8Extensions() {
154
- try { setV8Exts(await v8Ext.listV8Extensions()); } catch { setV8Exts([]); }
383
+ function onSrvManifestChange(text) {
384
+ setSrvManifestText(text);
385
+ try {
386
+ const scriptMatches = [...text.matchAll(/script:\s*(.+)/g)];
387
+ if (scriptMatches.length > 0) {
388
+ const filenames = scriptMatches.map(m => m[1].trim().replace(/^["']|["']$/g, ''));
389
+ setSrvScriptEntries(prev => {
390
+ const existing = prev.filter(e => e.code.trim());
391
+ const entries = filenames.map(fn => {
392
+ const found = existing.find(e => e.filename === fn);
393
+ return found || { filename: fn, code: '' };
394
+ });
395
+ return entries.length > 0 ? entries : prev;
396
+ });
397
+ }
398
+ } catch { /* ignore parse errors */ }
399
+ }
400
+
401
+ async function handleSrvInstall() {
402
+ if (!srvManifestText.trim()) { showToast('Manifest is required'); return; }
403
+ try {
404
+ const scripts = {};
405
+ for (const e of srvScriptEntries) {
406
+ if (e.filename && e.code) scripts[e.filename] = e.code;
407
+ }
408
+ const scriptCount = Object.keys(scripts).length;
409
+ if (scriptCount === 0) { showToast('At least one action script is required', true); return; }
410
+ console.log('[SrvInstall] Sending', scriptCount, 'scripts:', Object.keys(scripts).map(k => k + ' (' + scripts[k].length + ' chars)'));
411
+ const sess = getSession();
412
+ const res = await sess.fetch('/v1/extensions', {
413
+ method: 'POST',
414
+ body: JSON.stringify({ manifest: srvManifestText, scripts }),
415
+ });
416
+ if (!res.ok) throw new Error(res.error?.message || 'Install failed');
417
+ showToast('Extension installed!');
418
+ setShowSrvInstall(false);
419
+ setSrvManifestText('');
420
+ setSrvScriptEntries([{filename:'', code:''}]);
421
+ loadServerExtensions();
422
+ } catch (err) {
423
+ showToast('Install failed: ' + (err.message || err), true);
424
+ }
155
425
  }
156
426
 
157
- async function loadV8Detail(name) {
158
- setV8Detail(null);
159
- setV8Instances(null);
427
+ async function loadServerExtensions() {
428
+ try { setSrvExts(await v8Ext.listV8Extensions()); } catch { setSrvExts([]); }
429
+ }
430
+
431
+ async function loadSrvDetail(name) {
432
+ setSrvDetail(null);
433
+ setSrvInstances(null);
160
434
  setNewInstanceId('');
161
435
  try {
162
436
  const ext = await v8Ext.getV8Extension(name);
163
- setV8Detail(ext);
164
- try { setV8Instances(await v8Ext.listInstances(name)); } catch { setV8Instances([]); }
165
- } catch(e) { setV8Detail({ error: e.message }); }
437
+ setSrvDetail(ext);
438
+ try { setSrvInstances(await v8Ext.listInstances(name)); } catch { setSrvInstances([]); }
439
+ } catch(e) { setSrvDetail({ error: e.message }); }
166
440
  }
167
441
 
168
- async function handleV8Activate(name) {
442
+ async function handleSrvActivate(name) {
169
443
  try {
170
444
  const resp = await v8Ext.activateV8Extension(name);
171
445
  if (resp.ok === false) throw new Error(resp.error?.message || 'Failed');
172
446
  showToast(t('profile.v8ext.activated'));
173
- loadV8Extensions();
174
- if (v8Detail?.name === name) loadV8Detail(name);
447
+ loadServerExtensions();
448
+ if (srvDetail?.name === name) loadSrvDetail(name);
175
449
  } catch(e) { showToast(e.message, true); }
176
450
  }
177
451
 
178
- async function handleV8Deactivate(name) {
452
+ async function handleSrvDeactivate(name) {
179
453
  try {
180
454
  const resp = await v8Ext.deactivateV8Extension(name);
181
455
  if (resp.ok === false) throw new Error(resp.error?.message || 'Failed');
182
456
  showToast(t('profile.v8ext.deactivated'));
183
- loadV8Extensions();
184
- if (v8Detail?.name === name) loadV8Detail(name);
457
+ loadServerExtensions();
458
+ if (srvDetail?.name === name) loadSrvDetail(name);
185
459
  } catch(e) { showToast(e.message, true); }
186
460
  }
187
461
 
188
- async function handleV8Delete(name) {
462
+ async function handleSrvDelete(name) {
189
463
  confirm(t('profile.v8ext.confirmDelete'), async () => {
190
464
  try {
191
465
  const resp = await v8Ext.deleteV8Extension(name);
192
466
  if (resp.ok === false) throw new Error(resp.error?.message || 'Failed');
193
467
  showToast(t('profile.v8ext.deleted'));
194
- setV8Detail(null);
195
- loadV8Extensions();
468
+ setSrvDetail(null);
469
+ loadServerExtensions();
196
470
  } catch(e) { showToast(e.message, true); }
197
471
  }, { danger: true });
198
472
  }
@@ -205,7 +479,7 @@ export default function ExtensionsTab({ session, showToast }) {
205
479
  if (resp.ok === false) throw new Error(resp.error?.message || 'Failed');
206
480
  showToast(t('profile.v8ext.instanceCreated'));
207
481
  setNewInstanceId('');
208
- setV8Instances(await v8Ext.listInstances(name));
482
+ setSrvInstances(await v8Ext.listInstances(name));
209
483
  } catch(e) { showToast(e.message, true); }
210
484
  }
211
485
 
@@ -230,7 +504,7 @@ export default function ExtensionsTab({ session, showToast }) {
230
504
  const resp = await v8Ext.deleteInstance(name, instanceId);
231
505
  if (resp.ok === false) throw new Error(resp.error?.message || 'Failed');
232
506
  showToast(t('profile.v8ext.instanceDeleted'));
233
- setV8Instances(await v8Ext.listInstances(name));
507
+ setSrvInstances(await v8Ext.listInstances(name));
234
508
  } catch(e) { showToast(e.message, true); }
235
509
  }, { danger: true });
236
510
  }
@@ -434,16 +708,16 @@ export default function ExtensionsTab({ session, showToast }) {
434
708
  </div>`;
435
709
  }
436
710
 
437
- // ── V8 detail view ──
438
- if (v8Detail) {
439
- if (v8Detail.error) return html`<div><button class="btn-outline" onClick=${() => setV8Detail(null)}>${t('profile.v8ext.back')}</button><div class="empty">Error: ${v8Detail.error}</div></div>`;
440
- const ext = v8Detail;
711
+ // ── Server extension detail view ──
712
+ if (srvDetail) {
713
+ if (srvDetail.error) return html`<div><button class="btn-outline" onClick=${() => setSrvDetail(null)}>${t('profile.v8ext.back')}</button><div class="empty">Error: ${srvDetail.error}</div></div>`;
714
+ const ext = srvDetail;
441
715
  const isActive = ext.status === 'active';
442
716
  const actions = ext.actions || [];
443
717
  const supportsInstances = ext.instances?.supported;
444
718
 
445
719
  return html`<div>
446
- <button class="btn-outline" onClick=${() => setV8Detail(null)}>${t('profile.v8ext.back')}</button>
720
+ <button class="btn-outline" onClick=${() => setSrvDetail(null)}>${t('profile.v8ext.back')}</button>
447
721
  <div class="ext-detail-wrap">
448
722
  <div class="ext-title">
449
723
  ${ext.name}
@@ -468,9 +742,60 @@ export default function ExtensionsTab({ session, showToast }) {
468
742
  <span class="ext-action-desc">${a.description || ''}</span>
469
743
  ${isActive ? html`<button class="btn-outline btn-sm" onClick=${() => {
470
744
  if (testAction?.actionId === a.id) { setTestAction(null); setTestResult(null); }
471
- else { setTestAction({ actionId: a.id }); setTestResult(null); setTestInput('{}'); }
745
+ else {
746
+ setTestAction({ actionId: a.id }); setTestResult(null);
747
+ // Pre-fill test input from inputSchema
748
+ const schema = a.inputSchema || a.input;
749
+ if (schema && typeof schema === 'object') {
750
+ const props = schema.properties || schema;
751
+ const example = {};
752
+ for (const [k, v] of Object.entries(props)) {
753
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
754
+ const spec = v;
755
+ if (spec.type === 'string') example[k] = spec.description ? '<' + spec.description + '>' : '';
756
+ else if (spec.type === 'number') example[k] = 0;
757
+ else if (spec.type === 'boolean') example[k] = false;
758
+ else example[k] = null;
759
+ }
760
+ }
761
+ setTestInput(JSON.stringify(example, null, 2));
762
+ } else { setTestInput('{}'); }
763
+ }
472
764
  }}>${testAction?.actionId === a.id ? t('profile.v8ext.test.close') : t('profile.v8ext.test.btn')}</button>` : null}
473
765
  </div>
766
+ ${(a.inputSchema || a.input) && Object.keys(a.inputSchema || a.input || {}).length > 0 ? html`
767
+ <div style="font-size:.75rem;opacity:.7;padding:2px 0 4px 12px">
768
+ ${(() => {
769
+ const schema = a.inputSchema || a.input || {};
770
+ const props = schema.properties || schema;
771
+ const required = schema.required || [];
772
+ return Object.entries(props).filter(([k]) => k !== 'type' && k !== 'required' && k !== 'properties').map(([k, v]) => {
773
+ const spec = (v && typeof v === 'object') ? v : {};
774
+ const req = required.includes(k);
775
+ return html`<span style="display:inline-block;margin-right:8px"><code>${k}</code>${req ? html`<span style="color:#E8564A">*</span>` : ''}: ${spec.type || '?'}${spec.description ? html` <span style="opacity:.6">— ${spec.description}</span>` : ''}</span>`;
776
+ });
777
+ })()}
778
+ ${(a.outputSchema || a.output) && Object.keys(a.outputSchema || a.output || {}).length > 0 ? html`
779
+ <div style="margin-top:2px;opacity:.6">→ ${(() => {
780
+ const out = a.outputSchema || a.output || {};
781
+ const props = out.properties || out;
782
+ function renderProps(obj, depth) {
783
+ if (!obj || depth > 2) return '';
784
+ return Object.entries(obj).filter(([k]) => k !== 'type' && k !== 'properties' && k !== 'items' && k !== 'required' && k !== 'description' && k !== 'nullable' && k !== 'enum').map(([k, v]) => {
785
+ const spec = (v && typeof v === 'object') ? v : {};
786
+ let typeStr = spec.type || '?';
787
+ let nested = '';
788
+ if (spec.type === 'object' && spec.properties) {
789
+ nested = ' { ' + renderProps(spec.properties, depth + 1) + ' }';
790
+ } else if (spec.type === 'array' && spec.items?.properties) {
791
+ nested = ' [{ ' + renderProps(spec.items.properties, depth + 1) + ' }]';
792
+ }
793
+ return k + ': ' + typeStr + nested;
794
+ }).join(', ');
795
+ }
796
+ return renderProps(props, 0);
797
+ })()}</div>` : null}
798
+ </div>` : null}
474
799
  ${testAction?.actionId === a.id ? html`
475
800
  <div class="ext-test-panel">
476
801
  <div class="ext-test-label">${t('profile.v8ext.test.inputLabel')}</div>
@@ -499,9 +824,9 @@ export default function ExtensionsTab({ session, showToast }) {
499
824
  ${supportsInstances ? html`
500
825
  <div class="ext-detail-section mt-section">
501
826
  <div class="ext-detail-section-title">${t('profile.v8ext.instancesTitle')}</div>
502
- ${!v8Instances ? html`<${Spinner} />` : v8Instances.length === 0
827
+ ${!srvInstances ? html`<${Spinner} />` : srvInstances.length === 0
503
828
  ? html`<div class="ext-no-instances">${t('profile.v8ext.noInstances')}</div>`
504
- : v8Instances.map(inst => html`
829
+ : srvInstances.map(inst => html`
505
830
  <div class="ext-instance-row">
506
831
  <span class="ext-instance-name">${inst.id}</span>
507
832
  <span class="ext-status-dot ${inst.status}"></span>
@@ -525,9 +850,9 @@ export default function ExtensionsTab({ session, showToast }) {
525
850
 
526
851
  <div class="ext-actions-bar">
527
852
  ${isActive
528
- ? html`<button class="btn-outline" onClick=${() => handleV8Deactivate(ext.name)}>${t('profile.v8ext.deactivate')}</button>`
529
- : html`<button class="btn-primary" onClick=${() => handleV8Activate(ext.name)}>${t('profile.v8ext.activate')}</button>`}
530
- <button class="btn-danger-solid" onClick=${() => handleV8Delete(ext.name)}>${t('profile.v8ext.delete')}</button>
853
+ ? html`<button class="btn-outline" onClick=${() => handleSrvDeactivate(ext.name)}>${t('profile.v8ext.deactivate')}</button>`
854
+ : html`<button class="btn-primary" onClick=${() => handleSrvActivate(ext.name)}>${t('profile.v8ext.activate')}</button>`}
855
+ <button class="btn-danger-solid" onClick=${() => handleSrvDelete(ext.name)}>${t('profile.v8ext.delete')}</button>
531
856
  </div>
532
857
  <${ConfirmUI} />
533
858
  </div>`;
@@ -543,18 +868,22 @@ export default function ExtensionsTab({ session, showToast }) {
543
868
  const hasExtensions = allExts.length > 0;
544
869
  const installedNames = allExts.map(e => e.name);
545
870
  const unbundled = BUNDLED.filter(b => !installedNames.includes(b.id));
546
- const hasV8 = v8Exts && v8Exts.length > 0;
871
+ const hasSrv = srvExts && srvExts.length > 0;
547
872
 
548
873
  return html`<div>
549
- ${hasV8 ? html`
550
- <div class="ext-v8-section">
551
- <div class="section-title">${t('profile.v8ext.title')}</div>
552
- <div class="section-desc">${t('profile.v8ext.desc')}</div>
874
+ <div class="ext-v8-section">
875
+ <div class="section-title">${t('profile.v8ext.title')}</div>
876
+ <div class="section-desc">${t('profile.v8ext.desc')}</div>
877
+ <div class="ext-hero-actions">
878
+ <button class="btn-primary" onClick=${() => { const p = buildServerExtensionPrompt(getSession() || {}); copyToClipboard(p); showToast(t('profile.extensions.promptCopied')); }}>${'\u{1F916}'} Create with AI</button>
879
+ <button class="btn-outline" onClick=${() => setShowSrvInstall(true)}>+ Add</button>
880
+ </div>
881
+ ${hasSrv ? html`
553
882
  <div class="ext-grid ext-grid-spaced">
554
- ${v8Exts.map(ext => {
883
+ ${srvExts.map(ext => {
555
884
  const isActive = ext.status === 'active';
556
885
  return html`
557
- <div class="ext-card" onClick=${() => loadV8Detail(ext.name)}>
886
+ <div class="ext-card" onClick=${() => loadSrvDetail(ext.name)}>
558
887
  <div class="ext-card-header">
559
888
  <span class="ext-card-name">${ext.name}</span>
560
889
  <span class="ext-card-version">v${ext.version || '?'}</span>
@@ -567,14 +896,14 @@ export default function ExtensionsTab({ session, showToast }) {
567
896
  <span class="ext-status"><span class="ext-status-dot ${ext.status}"></span> ${isActive ? t('profile.v8ext.statusActive') : t('profile.v8ext.statusInactive')}</span>
568
897
  <span class="ext-card-actions">
569
898
  ${isActive
570
- ? html`<button onClick=${(e) => { e.stopPropagation(); handleV8Deactivate(ext.name); }}>${t('profile.v8ext.deactivate')}</button>`
571
- : html`<button onClick=${(e) => { e.stopPropagation(); handleV8Activate(ext.name); }}>${t('profile.v8ext.activate')}</button>`}
899
+ ? html`<button onClick=${(e) => { e.stopPropagation(); handleSrvDeactivate(ext.name); }}>${t('profile.v8ext.deactivate')}</button>`
900
+ : html`<button onClick=${(e) => { e.stopPropagation(); handleSrvActivate(ext.name); }}>${t('profile.v8ext.activate')}</button>`}
572
901
  </span>
573
902
  </div>
574
903
  </div>`;
575
904
  })}
576
- </div>
577
- </div>` : null}
905
+ </div>` : null}
906
+ </div>
578
907
 
579
908
  <div class="ext-hero">
580
909
  <div class="section-title">${'\u{1F9E9}'} ${t('profile.extensions.title')}</div>
@@ -706,6 +1035,31 @@ export default function ExtensionsTab({ session, showToast }) {
706
1035
  </form>
707
1036
  </div>
708
1037
  </div>` : null}
1038
+
1039
+ ${showSrvInstall ? html`
1040
+ <div class="modal-overlay" onClick=${(e) => { if (e.target === e.currentTarget) setShowSrvInstall(false); }}>
1041
+ <div class="modal ext-modal-narrow">
1042
+ <h3>Install Server Extension</h3>
1043
+ <div class="ext-modal-section">
1044
+ <label>YAML Manifest</label>
1045
+ <textarea rows="12" class="ext-modal-textarea" placeholder="metadata:\n name: my-extension\n version: 1.0.0\n description: ...\n\nactions:\n - id: my-action\n ..." value=${srvManifestText} onInput=${(e) => onSrvManifestChange(e.target.value)}></textarea>
1046
+ </div>
1047
+ <div class="ext-modal-section">
1048
+ <label>Action Scripts (JS files)</label>
1049
+ ${srvScriptEntries.map((entry, i) => html`
1050
+ <div class="ext-lib-entry">
1051
+ <input type="text" placeholder="actions/my-action.js" value=${entry.filename} onInput=${(e) => { const arr = [...srvScriptEntries]; arr[i] = {...arr[i], filename: e.target.value}; setSrvScriptEntries(arr); }} class="ext-lib-entry-input" />
1052
+ <textarea rows="8" placeholder="export default async function(ctx, input) {\n // ...\n return { result: 'ok' };\n}" value=${entry.code} onInput=${(e) => { const arr = [...srvScriptEntries]; arr[i] = {...arr[i], code: e.target.value}; setSrvScriptEntries(arr); }} class="ext-lib-entry-code"></textarea>
1053
+ </div>`)}
1054
+ <button type="button" class="btn-outline ext-add-lib-btn" onClick=${() => setSrvScriptEntries([...srvScriptEntries, {filename:'', code:''}])}>+ Add Script</button>
1055
+ </div>
1056
+ <div class="ext-modal-footer">
1057
+ <button class="btn-outline" onClick=${() => setShowSrvInstall(false)}>Cancel</button>
1058
+ <button class="btn-primary" onClick=${handleSrvInstall}>Install</button>
1059
+ </div>
1060
+ </div>
1061
+ </div>` : null}
1062
+
709
1063
  <${ConfirmUI} />
710
1064
  </div>`;
711
1065
  }
@@ -433,6 +433,7 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
433
433
  <${MenuItem} icon="\u{1F512}" label=${t('profile.tabs.dataWallet')} active=${isOpen('dataWallet')} onClick=${() => open('dataWallet', 'manage-new')} />
434
434
  <${MenuItem} icon="\u{1F514}" label=${t('profile.tabs.notifications')} active=${isOpen('notifications')} onClick=${() => open('notifications', 'manage-new')} />
435
435
  <${MenuItem} icon="\u{1F512}" label=${t('profile.tabs.security')} active=${isOpen('security')} onClick=${() => open('security', 'manage-new')} />
436
+ <${MenuItem} icon="⚡" label=${t('capabilities.tabLabel')} active=${isOpen('capabilities')} onClick=${() => open('capabilities', 'manage-new')} />
436
437
  </div>
437
438
  `}
438
439
  <//>
@@ -468,6 +469,7 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
468
469
  <${MenuItem} icon="\u{1F3ED}" label=${t('profile.foundry.tabLabel')} active=${isOpen('foundry')} onClick=${() => open('foundry', 'build')} />
469
470
  <${MenuItem} icon="\u{1F3AF}" label=${t('profile.calibrator.tabLabel')} active=${isOpen('calibrator')} onClick=${() => open('calibrator', 'build')} />
470
471
  <${MenuItem} icon="\u{1F50C}" label=${t('profile.tabs.extensions')} active=${isOpen('extensions')} onClick=${() => open('extensions', 'build')} />
472
+ <${MenuItem} icon="⚡" label=${t('capabilities.tabLabel')} active=${isOpen('capabilities')} onClick=${() => open('capabilities', 'build')} />
471
473
  <${MenuItem} icon="\u{1F3A8}" label=${t('portfolio.tabLabel')} active=${isOpen('portfolio')} onClick=${() => open('portfolio', 'build')} />
472
474
  ${isExperienced && html`
473
475
  <${MenuItem} icon="\u{1F4E4}" label=${t('profile.landing.ownPackages')} active=${isOpen('packages')} onClick=${() => open('packages', 'build')} />
@@ -44,6 +44,7 @@ import GeneratorTab from './profile/generator-tab.js';
44
44
  import FoundryTab from './profile/foundry-tab.js';
45
45
  import CalibratorTab from './profile/calibrator-tab.js';
46
46
  import PackagesTab from './profile/packages-tab.js';
47
+ import CapabilitiesTab from './profile/capabilities-tab.js';
47
48
 
48
49
  // Each tab has a minTier: 'new' | 'active' | 'experienced'
49
50
  // Tabs with minTier <= current tier are visible in the tab bar.
@@ -62,6 +63,7 @@ const TABS = [
62
63
  { id: 'boards', key: 'profile.tabs.boards', component: BoardsTab, minTier: 'active' },
63
64
  { id: 'apps', key: 'profile.tabs.apps', component: AppsTab, minTier: 'active' },
64
65
  { id: 'extensions', key: 'profile.tabs.extensions', component: ExtensionsTab, minTier: 'active' },
66
+ { id: 'capabilities', key: 'capabilities.tabLabel', component: CapabilitiesTab, minTier: 'active' },
65
67
  { id: 'federation', key: 'profile.tabs.federation', component: FederationTab, minTier: 'experienced' },
66
68
  { id: 'nodes', key: 'profile.tabs.nodes', component: NodesTab, minTier: 'experienced' },
67
69
  { id: 'access', key: 'profile.tabs.access', component: AccessTab, minTier: 'new' },